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 | |
parent | 7599f3bdf2bbb708c95139e9da6421d5969c9a83 (diff) | |
parent | 48fef0c1da52ea1c603355cbf97e722455460e7b (diff) |
Merge branch 'main' into joh/cellUri
Diffstat (limited to 'src/vs/workbench')
841 files changed, 24389 insertions, 11074 deletions
diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts index 2fa247a959b..37afee52e07 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -64,7 +64,6 @@ import './mainThreadWorkspace'; import './mainThreadComments'; import './mainThreadNotebook'; import './mainThreadNotebookKernels'; -import './mainThreadNotebookProxyKernels'; import './mainThreadNotebookDocumentsAndEditors'; import './mainThreadNotebookRenderers'; import './mainThreadInteractive'; diff --git a/src/vs/workbench/api/browser/mainThreadAuthentication.ts b/src/vs/workbench/api/browser/mainThreadAuthentication.ts index b72301f5f28..d48b9079fe7 100644 --- a/src/vs/workbench/api/browser/mainThreadAuthentication.ts +++ b/src/vs/workbench/api/browser/mainThreadAuthentication.ts @@ -70,7 +70,7 @@ export class MainThreadAuthenticationProvider extends Disposable implements IAut quickPick.onDidAccept(() => { const updatedAllowedList = quickPick.items .map(i => (i as TrustedExtensionsQuickPickItem).extension); - this.storageService.store(`${this.id}-${accountName}`, JSON.stringify(updatedAllowedList), StorageScope.GLOBAL, StorageTarget.USER); + this.storageService.store(`${this.id}-${accountName}`, JSON.stringify(updatedAllowedList), StorageScope.APPLICATION, StorageTarget.USER); quickPick.dispose(); }); @@ -116,7 +116,7 @@ export class MainThreadAuthenticationProvider extends Disposable implements IAut const removeSessionPromises = sessions.map(session => this.removeSession(session.id)); await Promise.all(removeSessionPromises); removeAccountUsage(this.storageService, this.id, accountName); - this.storageService.remove(`${this.id}-${accountName}`, StorageScope.GLOBAL); + this.storageService.remove(`${this.id}-${accountName}`, StorageScope.APPLICATION); } } @@ -201,7 +201,7 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu private async setTrustedExtensionAndAccountPreference(providerId: string, accountName: string, extensionId: string, extensionName: string, sessionId: string): Promise<void> { this.authenticationService.updatedAllowedExtension(providerId, accountName, extensionId, extensionName, true); - this.storageService.store(`${extensionName}-${providerId}`, sessionId, StorageScope.GLOBAL, StorageTarget.MACHINE); + this.storageService.store(`${extensionName}-${providerId}`, sessionId, StorageScope.APPLICATION, StorageTarget.MACHINE); } @@ -224,9 +224,9 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu if (!options.forceNewSession && sessions.length) { if (supportsMultipleAccounts) { if (options.clearSessionPreference) { - this.storageService.remove(`${extensionName}-${providerId}`, StorageScope.GLOBAL); + this.storageService.remove(`${extensionName}-${providerId}`, StorageScope.APPLICATION); } else { - const existingSessionPreference = this.storageService.get(`${extensionName}-${providerId}`, StorageScope.GLOBAL); + const existingSessionPreference = this.storageService.get(`${extensionName}-${providerId}`, StorageScope.APPLICATION); if (existingSessionPreference) { const matchingSession = sessions.find(session => session.id === existingSessionPreference); if (matchingSession && this.authenticationService.isAccessAllowed(providerId, matchingSession.account.label, extensionId)) { diff --git a/src/vs/workbench/api/browser/mainThreadBulkEdits.ts b/src/vs/workbench/api/browser/mainThreadBulkEdits.ts index 07a3c45afcc..e3e5d652a16 100644 --- a/src/vs/workbench/api/browser/mainThreadBulkEdits.ts +++ b/src/vs/workbench/api/browser/mainThreadBulkEdits.ts @@ -17,7 +17,7 @@ export function reviveWorkspaceEditDto2(data: IWorkspaceEditDto | undefined): Re } const result: ResourceEdit[] = []; - for (let edit of revive<IWorkspaceEditDto>(data).edits) { + for (const edit of revive<IWorkspaceEditDto>(data).edits) { if (edit._type === WorkspaceEditType.File) { result.push(new ResourceFileEdit(edit.oldUri, edit.newUri, edit.options, edit.metadata)); } else if (edit._type === WorkspaceEditType.Text) { diff --git a/src/vs/workbench/api/browser/mainThreadCommands.ts b/src/vs/workbench/api/browser/mainThreadCommands.ts index dc5df7b422e..0321d2202f5 100644 --- a/src/vs/workbench/api/browser/mainThreadCommands.ts +++ b/src/vs/workbench/api/browser/mainThreadCommands.ts @@ -49,7 +49,7 @@ export class MainThreadCommands implements MainThreadCommandsShape { // print all as markdown const all: string[] = []; - for (let id in result) { + for (const id in result) { all.push('`' + id + '` - ' + _generateMarkdown(result[id])); } console.log(all.join('\n')); @@ -74,6 +74,15 @@ export class MainThreadCommands implements MainThreadCommandsShape { } } + $fireCommandActivationEvent(id: string): void { + const activationEvent = `onCommand:${id}`; + if (!this._extensionService.activationEventIsDone(activationEvent)) { + // this is NOT awaited because we only use it as drive-by-activation + // for commands that are already known inside the extension host + this._extensionService.activateByEvent(activationEvent); + } + } + async $executeCommand<T>(id: string, args: any[] | SerializableObjectWithBuffers<any[]>, retry: boolean): Promise<T | undefined> { if (args instanceof SerializableObjectWithBuffers) { args = args.value; @@ -102,7 +111,7 @@ function _generateMarkdown(description: string | Dto<ICommandHandlerDescription> const parts = [description.description]; parts.push('\n\n'); if (description.args) { - for (let arg of description.args) { + for (const arg of description.args) { parts.push(`* _${arg.name}_ - ${arg.description || ''}\n`); } } diff --git a/src/vs/workbench/api/browser/mainThreadComments.ts b/src/vs/workbench/api/browser/mainThreadComments.ts index 10b7cd6b704..7184b593f3a 100644 --- a/src/vs/workbench/api/browser/mainThreadComments.ts +++ b/src/vs/workbench/api/browser/mainThreadComments.ts @@ -110,11 +110,11 @@ export class MainThreadCommentThread<T> implements languages.CommentThread<T> { set collapsibleState(newState: languages.CommentThreadCollapsibleState | undefined) { this._collapsibleState = newState; - this._onDidChangeCollasibleState.fire(this._collapsibleState); + this._onDidChangeCollapsibleState.fire(this._collapsibleState); } - private readonly _onDidChangeCollasibleState = new Emitter<languages.CommentThreadCollapsibleState | undefined>(); - public onDidChangeCollasibleState = this._onDidChangeCollasibleState.event; + private readonly _onDidChangeCollapsibleState = new Emitter<languages.CommentThreadCollapsibleState | undefined>(); + public onDidChangeCollapsibleState = this._onDidChangeCollapsibleState.event; private _isDisposed: boolean; @@ -172,7 +172,7 @@ export class MainThreadCommentThread<T> implements languages.CommentThread<T> { dispose() { this._isDisposed = true; - this._onDidChangeCollasibleState.dispose(); + this._onDidChangeCollapsibleState.dispose(); this._onDidChangeComments.dispose(); this._onDidChangeInput.dispose(); this._onDidChangeLabel.dispose(); @@ -252,7 +252,7 @@ export class MainThreadCommentController { range: IRange | ICellRange, isTemplate: boolean ): languages.CommentThread<IRange | ICellRange> { - let thread = new MainThreadCommentThread( + const thread = new MainThreadCommentThread( commentThreadHandle, this.handle, extensionId, @@ -286,7 +286,7 @@ export class MainThreadCommentController { threadId: string, resource: UriComponents, changes: CommentThreadChanges): void { - let thread = this.getKnownThread(commentThreadHandle); + const thread = this.getKnownThread(commentThreadHandle); thread.batchUpdate(changes); if (thread.isDocumentCommentThread()) { @@ -306,7 +306,7 @@ export class MainThreadCommentController { } deleteCommentThread(commentThreadHandle: number) { - let thread = this.getKnownThread(commentThreadHandle); + const thread = this.getKnownThread(commentThreadHandle); this._threads.delete(commentThreadHandle); thread.dispose(); @@ -334,10 +334,10 @@ export class MainThreadCommentController { } updateInput(input: string) { - let thread = this.activeCommentThread; + const thread = this.activeCommentThread; if (thread && thread.input) { - let commentInput = thread.input; + const commentInput = thread.input; commentInput.value = input; thread.input = commentInput; } @@ -368,15 +368,15 @@ export class MainThreadCommentController { }; } - let ret: languages.CommentThread<IRange | ICellRange>[] = []; - for (let thread of [...this._threads.keys()]) { + const ret: languages.CommentThread<IRange | ICellRange>[] = []; + for (const thread of [...this._threads.keys()]) { const commentThread = this._threads.get(thread)!; if (commentThread.resource === resource.toString()) { ret.push(commentThread); } } - let commentingRanges = await this._proxy.$provideCommentingRanges(this.handle, resource, token); + const commentingRanges = await this._proxy.$provideCommentingRanges(this.handle, resource, token); return <ICommentInfo>{ owner: this._uniqueId, @@ -398,8 +398,8 @@ export class MainThreadCommentController { }; } - let ret: languages.CommentThread<IRange | ICellRange>[] = []; - for (let thread of [...this._threads.keys()]) { + const ret: languages.CommentThread<IRange | ICellRange>[] = []; + for (const thread of [...this._threads.keys()]) { const commentThread = this._threads.get(thread)!; if (commentThread.resource === resource.toString()) { ret.push(commentThread); @@ -414,7 +414,7 @@ export class MainThreadCommentController { } async getCommentingRanges(resource: URI, token: CancellationToken): Promise<IRange[]> { - let commentingRanges = await this._proxy.$provideCommentingRanges(this.handle, resource, token); + const commentingRanges = await this._proxy.$provideCommentingRanges(this.handle, resource, token); return commentingRanges || []; } @@ -423,8 +423,8 @@ export class MainThreadCommentController { } getAllComments(): MainThreadCommentThread<IRange | ICellRange>[] { - let ret: MainThreadCommentThread<IRange | ICellRange>[] = []; - for (let thread of [...this._threads.keys()]) { + const ret: MainThreadCommentThread<IRange | ICellRange>[] = []; + for (const thread of [...this._threads.keys()]) { ret.push(this._threads.get(thread)!); } @@ -474,8 +474,8 @@ export class MainThreadComments extends Disposable implements MainThreadComments this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostComments); this._register(this._commentService.onDidChangeActiveCommentThread(async thread => { - let handle = (thread as MainThreadCommentThread<IRange | ICellRange>).controllerHandle; - let controller = this._commentControllers.get(handle); + const handle = (thread as MainThreadCommentThread<IRange | ICellRange>).controllerHandle; + const controller = this._commentControllers.get(handle); if (!controller) { return; @@ -517,7 +517,7 @@ export class MainThreadComments extends Disposable implements MainThreadComments } $updateCommentControllerFeatures(handle: number, features: CommentProviderFeatures): void { - let provider = this._commentControllers.get(handle); + const provider = this._commentControllers.get(handle); if (!provider) { return undefined; @@ -534,7 +534,7 @@ export class MainThreadComments extends Disposable implements MainThreadComments extensionId: ExtensionIdentifier, isTemplate: boolean ): languages.CommentThread<IRange | ICellRange> | undefined { - let provider = this._commentControllers.get(handle); + const provider = this._commentControllers.get(handle); if (!provider) { return undefined; @@ -548,7 +548,7 @@ export class MainThreadComments extends Disposable implements MainThreadComments threadId: string, resource: UriComponents, changes: CommentThreadChanges): void { - let provider = this._commentControllers.get(handle); + const provider = this._commentControllers.get(handle); if (!provider) { return undefined; @@ -558,7 +558,7 @@ export class MainThreadComments extends Disposable implements MainThreadComments } $deleteCommentThread(handle: number, commentThreadHandle: number) { - let provider = this._commentControllers.get(handle); + const provider = this._commentControllers.get(handle); if (!provider) { return; @@ -568,7 +568,7 @@ export class MainThreadComments extends Disposable implements MainThreadComments } $updateCommentingRanges(handle: number) { - let provider = this._commentControllers.get(handle); + const provider = this._commentControllers.get(handle); if (!provider) { return; @@ -605,7 +605,7 @@ export class MainThreadComments extends Disposable implements MainThreadComments private setComments() { [...this._commentControllers.keys()].forEach(handle => { - let threads = this._commentControllers.get(handle)!.getAllComments(); + const threads = this._commentControllers.get(handle)!.getAllComments(); if (threads.length) { const providerId = this.getHandler(handle); diff --git a/src/vs/workbench/api/browser/mainThreadDebugService.ts b/src/vs/workbench/api/browser/mainThreadDebugService.ts index 5cefac453b2..c0ec9d1b550 100644 --- a/src/vs/workbench/api/browser/mainThreadDebugService.ts +++ b/src/vs/workbench/api/browser/mainThreadDebugService.ts @@ -125,7 +125,7 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb public $registerBreakpoints(DTOs: Array<ISourceMultiBreakpointDto | IFunctionBreakpointDto | IDataBreakpointDto>): Promise<void> { - for (let dto of DTOs) { + for (const dto of DTOs) { if (dto.type === 'sourceMulti') { const rawbps = dto.lines.map(l => <IBreakpointData>{ @@ -242,9 +242,7 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb public $setDebugSessionName(sessionId: DebugSessionUUID, name: string): void { const session = this.debugService.getModel().getSession(sessionId); - if (session) { - session.setName(name); - } + session?.setName(name); } public $customDebugAdapterRequest(sessionId: DebugSessionUUID, request: string, args: any): Promise<any> { @@ -284,9 +282,7 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb public $appendDebugConsole(value: string): void { // Use warning as severity to get the orange color for messages coming from the debug extension const session = this.debugService.getViewModel().focusedSession; - if (session) { - session.appendToRepl(value, severity.Warning); - } + session?.appendToRepl(value, severity.Warning); } public $acceptDAMessage(handle: number, message: DebugProtocol.ProtocolMessage) { diff --git a/src/vs/workbench/api/browser/mainThreadDecorations.ts b/src/vs/workbench/api/browser/mainThreadDecorations.ts index c3cb860ec9b..76ec7391e01 100644 --- a/src/vs/workbench/api/browser/mainThreadDecorations.ts +++ b/src/vs/workbench/api/browser/mainThreadDecorations.ts @@ -50,7 +50,7 @@ class DecorationRequestsQueue { const requests = this._requests; const resolver = this._resolver; this._proxy.$provideDecorations(this._handle, [...requests.values()], CancellationToken.None).then(data => { - for (let [id, resolve] of resolver) { + for (const [id, resolve] of resolver) { resolve(data[id]); } }); diff --git a/src/vs/workbench/api/browser/mainThreadDiagnostics.ts b/src/vs/workbench/api/browser/mainThreadDiagnostics.ts index 8d59a22b4b1..0fcc79603cc 100644 --- a/src/vs/workbench/api/browser/mainThreadDiagnostics.ts +++ b/src/vs/workbench/api/browser/mainThreadDiagnostics.ts @@ -53,8 +53,8 @@ export class MainThreadDiagnostics implements MainThreadDiagnosticsShape { } $changeMany(owner: string, entries: [UriComponents, IMarkerData[]][]): void { - for (let entry of entries) { - let [uri, markers] = entry; + for (const entry of entries) { + const [uri, markers] = entry; if (markers) { for (const marker of markers) { if (marker.relatedInformation) { diff --git a/src/vs/workbench/api/browser/mainThreadDocuments.ts b/src/vs/workbench/api/browser/mainThreadDocuments.ts index 0e64e46b917..1ef0daed53e 100644 --- a/src/vs/workbench/api/browser/mainThreadDocuments.ts +++ b/src/vs/workbench/api/browser/mainThreadDocuments.ts @@ -50,8 +50,6 @@ export class BoundModelReferenceCollection { add(uri: URI, ref: IReference<any>, length: number = 0): void { // const length = ref.object.textEditorModel.getValueLength(); - let handle: any; - let entry: { uri: URI; length: number; dispose(): void }; const dispose = () => { const idx = this._data.indexOf(entry); if (idx >= 0) { @@ -61,8 +59,8 @@ export class BoundModelReferenceCollection { this._data.splice(idx, 1); } }; - handle = setTimeout(dispose, this._maxAge); - entry = { uri, length, dispose }; + const handle = setTimeout(dispose, this._maxAge); + const entry = { uri, length, dispose }; this._data.push(entry); this._length += length; @@ -189,7 +187,7 @@ export class MainThreadDocuments extends Disposable implements MainThreadDocumen } private _onModelModeChanged(event: { model: ITextModel; oldLanguageId: string }): void { - let { model } = event; + const { model } = event; if (!this._modelTrackers.has(model.uri)) { return; } diff --git a/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts b/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts index 12b5786f94f..01f7703ee43 100644 --- a/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts @@ -5,7 +5,6 @@ import { Event } from 'vs/base/common/event'; import { IDisposable, combinedDisposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { URI } from 'vs/base/common/uri'; import { ICodeEditor, isCodeEditor, isDiffEditor, IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { IEditor } from 'vs/editor/common/editorCommon'; @@ -18,7 +17,7 @@ import { MainThreadDocuments } from 'vs/workbench/api/browser/mainThreadDocument import { MainThreadTextEditor } from 'vs/workbench/api/browser/mainThreadEditor'; import { MainThreadTextEditors } from 'vs/workbench/api/browser/mainThreadEditors'; import { ExtHostContext, ExtHostDocumentsAndEditorsShape, IDocumentsAndEditorsDelta, IModelAddedData, ITextEditorAddData, MainContext } from 'vs/workbench/api/common/extHost.protocol'; -import { BaseTextEditor } from 'vs/workbench/browser/parts/editor/textEditor'; +import { AbstractTextEditor } from 'vs/workbench/browser/parts/editor/textEditor'; import { IEditorPane } from 'vs/workbench/common/editor'; import { EditorGroupColumn, editorGroupToColumn } from 'vs/workbench/services/editor/common/editorGroupColumn'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -254,7 +253,7 @@ class MainThreadDocumentAndEditorStateComputer { private _getActiveEditorFromPanel(): IEditor | undefined { const panel = this._paneCompositeService.getActivePaneComposite(ViewContainerLocation.Panel); - if (panel instanceof BaseTextEditor) { + if (panel instanceof AbstractTextEditor) { const control = panel.getControl(); if (isCodeEditor(control)) { return control; @@ -317,12 +316,11 @@ export class MainThreadDocumentsAndEditors { private _onDelta(delta: DocumentAndEditorStateDelta): void { - let removedDocuments: URI[]; const removedEditors: string[] = []; const addedEditors: MainThreadTextEditor[] = []; // removed models - removedDocuments = delta.removedDocuments.map(m => m.uri); + const removedDocuments = delta.removedDocuments.map(m => m.uri); // added editors for (const apiEditor of delta.addedEditors) { diff --git a/src/vs/workbench/api/browser/mainThreadEditor.ts b/src/vs/workbench/api/browser/mainThreadEditor.ts index edc0ede1509..638fe8aa474 100644 --- a/src/vs/workbench/api/browser/mainThreadEditor.ts +++ b/src/vs/workbench/api/browser/mainThreadEditor.ts @@ -415,7 +415,7 @@ export class MainThreadTextEditor { if (!this._codeEditor) { return; } - this._codeEditor.setDecorations('exthost-api', key, ranges); + this._codeEditor.setDecorationsByType('exthost-api', key, ranges); } public setDecorationsFast(key: string, _ranges: number[]): void { @@ -426,7 +426,7 @@ export class MainThreadTextEditor { for (let i = 0, len = Math.floor(_ranges.length / 4); i < len; i++) { ranges[i] = new Range(_ranges[4 * i], _ranges[4 * i + 1], _ranges[4 * i + 2], _ranges[4 * i + 3]); } - this._codeEditor.setDecorationsFast(key, ranges); + this._codeEditor.setDecorationsByTypeFast(key, ranges); } public revealRange(range: IRange, revealType: TextEditorRevealType): void { diff --git a/src/vs/workbench/api/browser/mainThreadEditorTabs.ts b/src/vs/workbench/api/browser/mainThreadEditorTabs.ts index 10b7782247b..2bcb34d00d1 100644 --- a/src/vs/workbench/api/browser/mainThreadEditorTabs.ts +++ b/src/vs/workbench/api/browser/mainThreadEditorTabs.ts @@ -173,7 +173,7 @@ export class MainThreadEditorTabs implements MainThreadEditorTabsShape { */ private _generateTabId(editor: EditorInput, groupId: number) { let resourceString: string | undefined; - // Properly get the reousrce and account for sideby side editors + // Properly get the resource and account for side by side editors const resource = EditorResourceAccessor.getOriginalUri(editor, { supportSideBySide: SideBySideEditor.BOTH }); if (resource instanceof URI) { resourceString = resource.toString(); @@ -587,7 +587,7 @@ export class MainThreadEditorTabs implements MainThreadEditorTabsShape { } } // Loop over keys of the groups map and call closeEditors - let results: boolean[] = []; + const results: boolean[] = []; for (const [group, editors] of groups) { results.push(await group.closeEditors(editors, { preserveFocus })); } diff --git a/src/vs/workbench/api/browser/mainThreadEditors.ts b/src/vs/workbench/api/browser/mainThreadEditors.ts index 80babfd1c70..c4f4b92a71a 100644 --- a/src/vs/workbench/api/browser/mainThreadEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadEditors.ts @@ -71,7 +71,7 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { }); this._textEditorsListenersMap = Object.create(null); this._toDispose.dispose(); - for (let decorationType in this._registeredDecorationTypes) { + for (const decorationType in this._registeredDecorationTypes) { this._codeEditorService.removeDecorationType(decorationType); } this._registeredDecorationTypes = Object.create(null); @@ -104,7 +104,7 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { private _getTextEditorPositionData(): ITextEditorPositionData { const result: ITextEditorPositionData = Object.create(null); - for (let editorPane of this._editorService.visibleEditorPanes) { + for (const editorPane of this._editorService.visibleEditorPanes) { const id = this._editorLocator.findTextEditorIdFor(editorPane); if (id) { result[id] = editorGroupToColumn(this._editorGroupService, editorPane.group); @@ -156,7 +156,7 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { const mainThreadEditor = this._editorLocator.getEditor(id); if (mainThreadEditor) { const editorPanes = this._editorService.visibleEditorPanes; - for (let editorPane of editorPanes) { + for (const editorPane of editorPanes) { if (mainThreadEditor.matches(editorPane)) { await editorPane.group.closeEditor(editorPane.input); return; diff --git a/src/vs/workbench/api/browser/mainThreadErrors.ts b/src/vs/workbench/api/browser/mainThreadErrors.ts index 36fdac455c6..2d05a6a0e44 100644 --- a/src/vs/workbench/api/browser/mainThreadErrors.ts +++ b/src/vs/workbench/api/browser/mainThreadErrors.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SerializedError, onUnexpectedError } from 'vs/base/common/errors'; +import { SerializedError, onUnexpectedError, ErrorNoTelemetry } from 'vs/base/common/errors'; import { extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { MainContext, MainThreadErrorsShape } from 'vs/workbench/api/common/extHost.protocol'; @@ -17,7 +17,7 @@ export class MainThreadErrors implements MainThreadErrorsShape { $onUnexpectedError(err: any | SerializedError): void { if (err && err.$isError) { const { name, message, stack } = err; - err = new Error(); + err = err.noTelemetry ? new ErrorNoTelemetry() : new Error(); err.message = message; err.name = name; err.stack = stack; diff --git a/src/vs/workbench/api/browser/mainThreadExtensionService.ts b/src/vs/workbench/api/browser/mainThreadExtensionService.ts index 41309ab2bd9..85891b4bd7f 100644 --- a/src/vs/workbench/api/browser/mainThreadExtensionService.ts +++ b/src/vs/workbench/api/browser/mainThreadExtensionService.ts @@ -211,9 +211,6 @@ class ExtensionHostProxy implements IExtensionHostProxy { extensionTestsExecute(): Promise<number> { return this._actual.$extensionTestsExecute(); } - extensionTestsExit(code: number): Promise<void> { - return this._actual.$extensionTestsExit(code); - } activateByEvent(activationEvent: string, activationKind: ActivationKind): Promise<void> { return this._actual.$activateByEvent(activationEvent, activationKind); } diff --git a/src/vs/workbench/api/browser/mainThreadFileSystem.ts b/src/vs/workbench/api/browser/mainThreadFileSystem.ts index bbf3476b101..02e75eae05e 100644 --- a/src/vs/workbench/api/browser/mainThreadFileSystem.ts +++ b/src/vs/workbench/api/browser/mainThreadFileSystem.ts @@ -14,6 +14,9 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { ILogService } from 'vs/platform/log/common/log'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkbenchFileService } from 'vs/workbench/services/files/common/files'; +import { normalizeWatcherPattern } from 'vs/platform/files/common/watcher'; +import { GLOBSTAR } from 'vs/base/common/glob'; +import { rtrim } from 'vs/base/common/strings'; @extHostNamedCustomer(MainContext.MainThreadFileSystem) export class MainThreadFileSystem implements MainThreadFileSystemShape { @@ -34,7 +37,7 @@ export class MainThreadFileSystem implements MainThreadFileSystemShape { const infoProxy = extHostContext.getProxy(ExtHostContext.ExtHostFileSystemInfo); - for (let entry of _fileService.listCapabilities()) { + for (const entry of _fileService.listCapabilities()) { infoProxy.$acceptProviderInfos(URI.from({ scheme: entry.scheme, path: '/dummy' }), entry.capabilities); } this._disposables.add(_fileService.onDidChangeFileSystemProviderRegistrations(e => infoProxy.$acceptProviderInfos(URI.from({ scheme: e.scheme, path: '/dummy' }), e.provider?.capabilities ?? null))); @@ -163,17 +166,34 @@ export class MainThreadFileSystem implements MainThreadFileSystemShape { return this._fileService.activateProvider(scheme); } - $watch(extensionId: string, session: number, resource: UriComponents, opts: IWatchOptions): void { + async $watch(extensionId: string, session: number, resource: UriComponents, unvalidatedOpts: IWatchOptions): Promise<void> { const uri = URI.revive(resource); - const isInsideWorkspace = this._contextService.isInsideWorkspace(uri); + const workspaceFolder = this._contextService.getWorkspaceFolder(uri); + + const opts = { ...unvalidatedOpts }; + + // Convert a recursive watcher to a flat watcher if the path + // turns out to not be a folder. Recursive watching is only + // possible on folders, so we help all file watchers by checking + // early. + if (opts.recursive) { + try { + const stat = await this._fileService.stat(uri); + if (!stat.isDirectory) { + opts.recursive = false; + } + } catch (error) { + this._logService.error(`MainThreadFileSystem#$watch(): failed to stat a resource for file watching (extension: ${extensionId}, path: ${uri.toString(true)}, recursive: ${opts.recursive}, session: ${session}): ${error}`); + } + } // Refuse to watch anything that is already watched via // our workspace watchers in case the request is a // recursive file watcher. // Still allow for non-recursive watch requests as a way - // to bypass configured exlcude rules though + // to bypass configured exclude rules though // (see https://github.com/microsoft/vscode/issues/146066) - if (isInsideWorkspace && opts.recursive) { + if (workspaceFolder && opts.recursive) { this._logService.trace(`MainThreadFileSystem#$watch(): ignoring request to start watching because path is inside workspace (extension: ${extensionId}, path: ${uri.toString(true)}, recursive: ${opts.recursive}, session: ${session})`); return; } @@ -200,7 +220,12 @@ export class MainThreadFileSystem implements MainThreadFileSystemShape { // excluded via `files.watcherExclude`. As such, we configure // to include each configured exclude pattern so that only those // events are reported that are otherwise excluded. - else if (isInsideWorkspace) { + // However, we cannot just use the pattern as is, because a pattern + // such as `bar` for a exclude, will work to exclude any of + // `<workspace path>/bar` but will not work as include for files within + // `bar` unless a suffix of `/**` if added. + // (https://github.com/microsoft/vscode/issues/148245) + else if (workspaceFolder) { const config = this._configurationService.getValue<IFilesConfiguration>(); if (config.files?.watcherExclude) { for (const key in config.files.watcherExclude) { @@ -209,7 +234,8 @@ export class MainThreadFileSystem implements MainThreadFileSystemShape { opts.includes = []; } - opts.includes.push(key); + const includePattern = `${rtrim(key, '/')}/${GLOBSTAR}`; + opts.includes.push(normalizeWatcherPattern(workspaceFolder.uri.fsPath, includePattern)); } } } diff --git a/src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts b/src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts index da8611e4f5b..54a2887e33b 100644 --- a/src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts +++ b/src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts @@ -84,7 +84,7 @@ export class MainThreadFileSystemEventService { } const needsConfirmation = data.edit.edits.some(edit => edit.metadata?.needsConfirmation); - let showPreview = storageService.getBoolean(MainThreadFileSystemEventService.MementoKeyAdditionalEdits, StorageScope.GLOBAL); + let showPreview = storageService.getBoolean(MainThreadFileSystemEventService.MementoKeyAdditionalEdits, StorageScope.PROFILE); if (envService.extensionTestsLocationURI) { // don't show dialog in tests @@ -140,7 +140,7 @@ export class MainThreadFileSystemEventService { } showPreview = answer.choice === 1; if (answer.checkboxChecked /* && answer.choice !== 2 */) { - storageService.store(MainThreadFileSystemEventService.MementoKeyAdditionalEdits, showPreview, StorageScope.GLOBAL, StorageTarget.USER); + storageService.store(MainThreadFileSystemEventService.MementoKeyAdditionalEdits, showPreview, StorageScope.PROFILE, StorageTarget.USER); } } } @@ -190,6 +190,6 @@ registerAction2(class ResetMemento extends Action2 { }); } run(accessor: ServicesAccessor) { - accessor.get(IStorageService).remove(MainThreadFileSystemEventService.MementoKeyAdditionalEdits, StorageScope.GLOBAL); + accessor.get(IStorageService).remove(MainThreadFileSystemEventService.MementoKeyAdditionalEdits, StorageScope.PROFILE); } }); diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 16fada27b5b..bf5de3a19a2 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -5,6 +5,7 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { createStringDataTransferItem, VSDataTransfer } from 'vs/base/common/dataTransfer'; import { CancellationError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { combinedDisposable, Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; @@ -23,14 +24,13 @@ import { ITextModel } from 'vs/editor/common/model'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { decodeSemanticTokensDto } from 'vs/editor/common/services/semanticTokensDto'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; import { DataTransferCache } from 'vs/workbench/api/common/shared/dataTransferCache'; -import { DataTransferConverter } from 'vs/workbench/api/common/shared/dataTransfer'; import * as callh from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; import * as search from 'vs/workbench/contrib/search/common/search'; import * as typeh from 'vs/workbench/contrib/typeHierarchy/common/typeHierarchy'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { ExtHostContext, ExtHostLanguageFeaturesShape, ICallHierarchyItemDto, ICodeActionDto, ICodeActionProviderMetadataDto, IdentifiableInlineCompletion, IdentifiableInlineCompletions, IDocumentFilterDto, IIndentationRuleDto, IInlayHintDto, ILanguageConfigurationDto, ILanguageWordDefinitionDto, ILinkDto, ILocationDto, ILocationLinkDto, IOnEnterRuleDto, IRegExpDto, ISignatureHelpProviderMetadataDto, ISuggestDataDto, ISuggestDataDtoField, ISuggestResultDtoField, ITypeHierarchyItemDto, IWorkspaceSymbolDto, MainContext, MainThreadLanguageFeaturesShape, reviveWorkspaceEditDto } from '../common/extHost.protocol'; -import { IDataTransfer } from 'vs/base/common/dataTransfer'; @extHostNamedCustomer(MainContext.MainThreadLanguageFeatures) export class MainThreadLanguageFeatures extends Disposable implements MainThreadLanguageFeaturesShape { @@ -50,7 +50,7 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread if (this._languageService) { const updateAllWordDefinitions = () => { - let wordDefinitionDtos: ILanguageWordDefinitionDto[] = []; + const wordDefinitionDtos: ILanguageWordDefinitionDto[] = []; for (const languageId of _languageService.getRegisteredLanguageIds()) { const wordDefinition = this._languageConfigurationService.getLanguageConfiguration(languageId).getWordDefinition(); wordDefinitionDtos.push({ @@ -140,9 +140,7 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread } private static _reviveCodeActionDto(data: ReadonlyArray<ICodeActionDto>): languages.CodeAction[] { - if (data) { - data.forEach(code => reviveWorkspaceEditDto(code.edit)); - } + data?.forEach(code => reviveWorkspaceEditDto(code.edit)); return <languages.CodeAction[]>data; } @@ -366,6 +364,49 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread this._registrations.set(handle, this._languageFeaturesService.codeActionProvider.register(selector, provider)); } + // --- copy paste action provider + + $registerPasteEditProvider(handle: number, selector: IDocumentFilterDto[], supportsCopy: boolean, pasteMimeTypes: readonly string[]): void { + const provider: languages.DocumentPasteEditProvider = { + pasteMimeTypes: pasteMimeTypes, + + prepareDocumentPaste: supportsCopy + ? async (model: ITextModel, selections: Selection[], dataTransfer: VSDataTransfer, token: CancellationToken): Promise<VSDataTransfer | undefined> => { + const dataTransferDto = await typeConvert.DataTransfer.toDataTransferDTO(dataTransfer); + if (token.isCancellationRequested) { + return undefined; + } + + const result = await this._proxy.$prepareDocumentPaste(handle, model.uri, selections, dataTransferDto, token); + if (!result) { + return undefined; + } + + const dataTransferOut = new VSDataTransfer(); + result.items.forEach(([type, item]) => { + dataTransferOut.replace(type, createStringDataTransferItem(item.asString)); + }); + return dataTransferOut; + } + : undefined, + + provideDocumentPasteEdits: async (model: ITextModel, selections: Selection[], dataTransfer: VSDataTransfer, token: CancellationToken) => { + const d = await typeConvert.DataTransfer.toDataTransferDTO(dataTransfer); + const result = await this._proxy.$providePasteEdits(handle, model.uri, selections, d, token); + if (!result) { + return undefined; + } + + return { + insertText: result.insertText, + additionalEdit: result.additionalEdit ? reviveWorkspaceEditDto(result.additionalEdit) : undefined, + }; + } + }; + + this._registrations.set(handle, this._languageFeaturesService.documentPasteEditProvider.register(selector, provider)); + } + // --- formatting $registerDocumentFormattingSupport(handle: number, selector: IDocumentFilterDto[], extensionId: ExtensionIdentifier, displayName: string): void { @@ -512,7 +553,7 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread return suggestion; } - let newSuggestion = MainThreadLanguageFeatures._inflateSuggestDto(suggestion.range, result); + const newSuggestion = MainThreadLanguageFeatures._inflateSuggestDto(suggestion.range, result); return mixin(suggestion, newSuggestion, true); }); }; @@ -892,11 +933,18 @@ class MainThreadDocumentOnDropEditProvider implements languages.DocumentOnDropEd private readonly _proxy: ExtHostLanguageFeaturesShape, ) { } - async provideDocumentOnDropEdits(model: ITextModel, position: IPosition, dataTransfer: IDataTransfer, token: CancellationToken): Promise<languages.SnippetTextEdit | null | undefined> { + async provideDocumentOnDropEdits(model: ITextModel, position: IPosition, dataTransfer: VSDataTransfer, token: CancellationToken): Promise<languages.DocumentOnDropEdit | null | undefined> { const request = this.dataTransfers.add(dataTransfer); try { - const dataTransferDto = await DataTransferConverter.toDataTransferDTO(dataTransfer); - return await this._proxy.$provideDocumentOnDropEdits(this.handle, request.id, model.uri, position, dataTransferDto, token); + const dataTransferDto = await typeConvert.DataTransfer.toDataTransferDTO(dataTransfer); + const edit = await this._proxy.$provideDocumentOnDropEdits(this.handle, request.id, model.uri, position, dataTransferDto, token); + if (!edit) { + return undefined; + } + return { + insertText: edit.insertText, + additionalEdit: reviveWorkspaceEditDto(edit.additionalEdit), + }; } finally { request.dispose(); } diff --git a/src/vs/workbench/api/browser/mainThreadLanguages.ts b/src/vs/workbench/api/browser/mainThreadLanguages.ts index e8316e42dfb..fffdbc7f4db 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguages.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguages.ts @@ -10,7 +10,7 @@ import { MainThreadLanguagesShape, MainContext, ExtHostContext, ExtHostLanguages import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { IPosition } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; -import { StandardTokenType } from 'vs/editor/common/languages'; +import { StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { ILanguageStatus, ILanguageStatusService } from 'vs/workbench/services/languageStatus/common/languageStatusService'; import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index 6b522da7c97..d6b554ebb17 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -49,7 +49,7 @@ export class MainThreadNotebooks implements MainThreadNotebookShape { } async $registerNotebookProvider(extension: NotebookExtensionDescription, viewType: string, options: TransientOptions, data: INotebookContributionData | undefined): Promise<void> { - let contentOptions = { ...options }; + const contentOptions = { ...options }; const controller: INotebookContentProvider = { get options() { diff --git a/src/vs/workbench/api/browser/mainThreadNotebookDto.ts b/src/vs/workbench/api/browser/mainThreadNotebookDto.ts index 8f5d53fd444..9c7f5d67f0d 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebookDto.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebookDto.ts @@ -96,6 +96,7 @@ export namespace NotebookDto { if (data.editType === CellExecutionUpdateType.Output) { return { editType: data.editType, + cellHandle: data.cellHandle, append: data.append, outputs: data.outputs.map(fromNotebookOutputDto) }; diff --git a/src/vs/workbench/api/browser/mainThreadNotebookEditors.ts b/src/vs/workbench/api/browser/mainThreadNotebookEditors.ts index 9254a9e2fb4..861c1e680e2 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebookEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebookEditors.ts @@ -7,7 +7,6 @@ import { DisposableStore, dispose } from 'vs/base/common/lifecycle'; import { getNotebookEditorFromEditorPane, INotebookEditor, INotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/notebookEditorService'; import { ExtHostContext, ExtHostNotebookEditorsShape, ICellEditOperationDto, INotebookDocumentShowOptions, INotebookEditorViewColumnInfo, MainThreadNotebookEditorsShape, NotebookEditorRevealType } from '../common/extHost.protocol'; -import { INotebookDecorationRenderOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { ILogService } from 'vs/platform/log/common/log'; import { URI, UriComponents } from 'vs/base/common/uri'; @@ -86,7 +85,7 @@ export class MainThreadNotebookEditors implements MainThreadNotebookEditorsShape private _updateEditorViewColumns(): void { const result: INotebookEditorViewColumnInfo = Object.create(null); - for (let editorPane of this._editorService.visibleEditorPanes) { + for (const editorPane of this._editorService.visibleEditorPanes) { const candidate = getNotebookEditorFromEditorPane(editorPane); if (candidate && this._mainThreadEditors.has(candidate.getId())) { result[candidate.getId()] = editorGroupToColumn(this._editorGroupService, editorPane.group); @@ -133,7 +132,7 @@ export class MainThreadNotebookEditors implements MainThreadNotebookEditorsShape if (notebookEditor) { return notebookEditor.getId(); } else { - throw new Error(`Notebook Editor creation failure for documenet ${resource}`); + throw new Error(`Notebook Editor creation failure for document ${resource}`); } } @@ -165,22 +164,6 @@ export class MainThreadNotebookEditors implements MainThreadNotebookEditorsShape } } - $registerNotebookEditorDecorationType(key: string, options: INotebookDecorationRenderOptions): void { - this._notebookEditorService.registerEditorDecorationType(key, options); - } - - $removeNotebookEditorDecorationType(key: string): void { - this._notebookEditorService.removeEditorDecorationType(key); - } - - $trySetDecorations(id: string, range: ICellRange, key: string): void { - const editor = this._notebookEditorService.getNotebookEditor(id); - if (editor) { - const notebookEditor = editor as INotebookEditor; - notebookEditor.setEditorDecorations(key, range); - } - } - $trySetSelections(id: string, ranges: ICellRange[]): void { const editor = this._notebookEditorService.getNotebookEditor(id); if (!editor) { diff --git a/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts b/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts index a8809c58016..4ebdadf9976 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts @@ -15,13 +15,12 @@ import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/ext import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/notebookEditorService'; import { INotebookCellExecution, INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; -import { IResolvedNotebookKernel, INotebookKernelChangeEvent, INotebookKernelService, NotebookKernelType } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { INotebookKernel, INotebookKernelChangeEvent, INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { SerializableObjectWithBuffers } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import { ExtHostContext, ExtHostNotebookKernelsShape, ICellExecuteUpdateDto, ICellExecutionCompleteDto, INotebookKernelDto2, MainContext, MainThreadNotebookKernelsShape } from '../common/extHost.protocol'; +import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; -abstract class MainThreadKernel implements IResolvedNotebookKernel { - readonly type: NotebookKernelType.Resolved = NotebookKernelType.Resolved; - +abstract class MainThreadKernel implements INotebookKernel { private readonly _onDidChange = new Emitter<INotebookKernelChangeEvent>(); private readonly preloads: { uri: URI; provides: string[] }[]; readonly onDidChange: Event<INotebookKernelChangeEvent> = this._onDidChange.event; @@ -114,6 +113,7 @@ export class MainThreadNotebookKernels implements MainThreadNotebookKernelsShape @ILanguageService private readonly _languageService: ILanguageService, @INotebookKernelService private readonly _notebookKernelService: INotebookKernelService, @INotebookExecutionStateService private readonly _notebookExecutionStateService: INotebookExecutionStateService, + @INotebookService private readonly _notebookService: INotebookService, @INotebookEditorService notebookEditorService: INotebookEditorService ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostNotebookKernels); @@ -136,7 +136,7 @@ export class MainThreadNotebookKernels implements MainThreadNotebookKernelsShape dispose(): void { this._disposables.dispose(); - for (let [, registration] of this._kernels.values()) { + for (const [, registration] of this._kernels.values()) { registration.dispose(); } } @@ -153,7 +153,7 @@ export class MainThreadNotebookKernels implements MainThreadNotebookKernelsShape if (!selected) { return; } - for (let [handle, candidate] of this._kernels) { + for (const [handle, candidate] of this._kernels) { if (candidate[0] === selected) { this._proxy.$acceptKernelMessageFromRenderer(handle, editor.getId(), e.message); break; @@ -248,7 +248,16 @@ export class MainThreadNotebookKernels implements MainThreadNotebookKernelsShape $createExecution(handle: number, controllerId: string, rawUri: UriComponents, cellHandle: number): void { const uri = URI.revive(rawUri); - const execution = this._notebookExecutionStateService.createCellExecution(controllerId, uri, cellHandle); + const notebook = this._notebookService.getNotebookTextModel(uri); + if (!notebook) { + throw new Error(`Notebook not found: ${uri.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}`); + } + const execution = this._notebookExecutionStateService.createCellExecution(uri, cellHandle); execution.confirm(); this._executions.set(handle, execution); } @@ -257,9 +266,7 @@ export class MainThreadNotebookKernels implements MainThreadNotebookKernelsShape const updates = data.value; try { const execution = this._executions.get(handle); - if (execution) { - execution.update(updates.map(NotebookDto.fromCellExecuteUpdateDto)); - } + execution?.update(updates.map(NotebookDto.fromCellExecuteUpdateDto)); } catch (e) { onUnexpectedError(e); } @@ -268,9 +275,7 @@ export class MainThreadNotebookKernels implements MainThreadNotebookKernelsShape $completeExecution(handle: number, data: SerializableObjectWithBuffers<ICellExecutionCompleteDto>): void { try { const execution = this._executions.get(handle); - if (execution) { - execution.complete(NotebookDto.fromCellExecuteCompleteDto(data.value)); - } + execution?.complete(NotebookDto.fromCellExecuteCompleteDto(data.value)); } catch (e) { onUnexpectedError(e); } finally { diff --git a/src/vs/workbench/api/browser/mainThreadNotebookProxyKernels.ts b/src/vs/workbench/api/browser/mainThreadNotebookProxyKernels.ts deleted file mode 100644 index 74e7290161d..00000000000 --- a/src/vs/workbench/api/browser/mainThreadNotebookProxyKernels.ts +++ /dev/null @@ -1,130 +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 { Emitter, Event } from 'vs/base/common/event'; -import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; -import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; -import { INotebookKernelService, INotebookProxyKernel, INotebookProxyKernelChangeEvent, ProxyKernelState, NotebookKernelType } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; -import { ExtHostContext, ExtHostNotebookProxyKernelsShape, INotebookProxyKernelDto, MainContext, MainThreadNotebookProxyKernelsShape } from '../common/extHost.protocol'; -import { onUnexpectedError } from 'vs/base/common/errors'; - -abstract class MainThreadProxyKernel implements INotebookProxyKernel { - readonly type: NotebookKernelType.Proxy = NotebookKernelType.Proxy; - protected readonly _onDidChange = new Emitter<INotebookProxyKernelChangeEvent>(); - readonly onDidChange: Event<INotebookProxyKernelChangeEvent> = this._onDidChange.event; - readonly id: string; - readonly viewType: string; - readonly extension: ExtensionIdentifier; - readonly preloadProvides: string[] = []; - label: string; - description?: string; - detail?: string; - kind?: string; - supportedLanguages: string[] = []; - connectionState: ProxyKernelState; - - constructor(data: INotebookProxyKernelDto) { - this.id = data.id; - this.viewType = data.notebookType; - this.extension = data.extensionId; - - this.label = data.label; - this.description = data.description; - this.detail = data.detail; - this.kind = data.kind; - - this.connectionState = ProxyKernelState.Disconnected; - } - - update(data: Partial<INotebookProxyKernel>) { - const event: INotebookProxyKernelChangeEvent = Object.create(null); - if (data.label !== undefined) { - this.label = data.label; - event.label = true; - } - if (data.description !== undefined) { - this.description = data.description; - event.description = true; - } - if (data.detail !== undefined) { - this.detail = data.detail; - event.detail = true; - } - if (data.kind !== undefined) { - this.kind = data.kind; - event.kind = true; - } - - this._onDidChange.fire(event); - } - - abstract resolveKernel(): Promise<string | null>; -} - -@extHostNamedCustomer(MainContext.MainThreadNotebookProxyKernels) -export class MainThreadNotebookProxyKernels implements MainThreadNotebookProxyKernelsShape { - - private readonly _disposables = new DisposableStore(); - - private readonly _proxyKernels = new Map<number, [kernel: MainThreadProxyKernel, registraion: IDisposable]>(); - private readonly _proxyKernelProxy: ExtHostNotebookProxyKernelsShape; - - constructor( - extHostContext: IExtHostContext, - @INotebookKernelService private readonly _notebookKernelService: INotebookKernelService, - ) { - this._proxyKernelProxy = extHostContext.getProxy(ExtHostContext.ExtHostNotebookProxyKernels); - } - - dispose(): void { - this._disposables.dispose(); - - for (let [, registration] of this._proxyKernels.values()) { - registration.dispose(); - } - } - - // -- Proxy kernel - - async $addProxyKernel(handle: number, data: INotebookProxyKernelDto): Promise<void> { - const that = this; - const proxyKernel = new class extends MainThreadProxyKernel { - async resolveKernel(): Promise<string | null> { - try { - this.connectionState = ProxyKernelState.Initializing; - this._onDidChange.fire({ connectionState: true }); - const delegateKernel = await that._proxyKernelProxy.$resolveKernel(handle); - this.connectionState = ProxyKernelState.Connected; - this._onDidChange.fire({ connectionState: true }); - return delegateKernel; - } catch (err) { - onUnexpectedError(err); - this.connectionState = ProxyKernelState.Disconnected; - this._onDidChange.fire({ connectionState: true }); - return null; - } - } - }(data); - - const registration = this._notebookKernelService.registerKernel(proxyKernel); - this._proxyKernels.set(handle, [proxyKernel, registration]); - } - - $updateProxyKernel(handle: number, data: Partial<INotebookProxyKernelDto>): void { - const tuple = this._proxyKernels.get(handle); - if (tuple) { - tuple[0].update(data); - } - } - - $removeProxyKernel(handle: number): void { - const tuple = this._proxyKernels.get(handle); - if (tuple) { - tuple[1].dispose(); - this._proxyKernels.delete(handle); - } - } -} diff --git a/src/vs/workbench/api/browser/mainThreadOutputService.ts b/src/vs/workbench/api/browser/mainThreadOutputService.ts index 34859bb4235..85f8495e80d 100644 --- a/src/vs/workbench/api/browser/mainThreadOutputService.ts +++ b/src/vs/workbench/api/browser/mainThreadOutputService.ts @@ -44,7 +44,7 @@ export class MainThreadOutputService extends Disposable implements MainThreadOut public async $register(label: string, log: boolean, file: UriComponents, languageId: string, extensionId: string): Promise<string> { const idCounter = (MainThreadOutputService._extensionIdPool.get(extensionId) || 0) + 1; MainThreadOutputService._extensionIdPool.set(extensionId, idCounter); - const id = `extension-output-${extensionId}-#${idCounter}`; + const id = `extension-output-${extensionId}-#${idCounter}-${label}`; Registry.as<IOutputChannelRegistry>(Extensions.OutputChannels).registerChannel({ id, label, file: URI.revive(file), log, languageId }); this._register(toDisposable(() => this.$dispose(id))); diff --git a/src/vs/workbench/api/browser/mainThreadProgress.ts b/src/vs/workbench/api/browser/mainThreadProgress.ts index 7fa98113c30..beec4789ac9 100644 --- a/src/vs/workbench/api/browser/mainThreadProgress.ts +++ b/src/vs/workbench/api/browser/mainThreadProgress.ts @@ -57,9 +57,7 @@ export class MainThreadProgress implements MainThreadProgressShape { $progressReport(handle: number, message: IProgressStep): void { const entry = this._progress.get(handle); - if (entry) { - entry.progress.report(message); - } + entry?.progress.report(message); } $progressEnd(handle: number): void { diff --git a/src/vs/workbench/api/browser/mainThreadSCM.ts b/src/vs/workbench/api/browser/mainThreadSCM.ts index 3cb6a499c8c..5ac8c935667 100644 --- a/src/vs/workbench/api/browser/mainThreadSCM.ts +++ b/src/vs/workbench/api/browser/mainThreadSCM.ts @@ -421,6 +421,16 @@ export class MainThreadSCM implements MainThreadSCMShape { repository.input.placeholder = placeholder; } + $setInputBoxEnablement(sourceControlHandle: number, enabled: boolean): void { + const repository = this._repositories.get(sourceControlHandle); + + if (!repository) { + return; + } + + repository.input.enabled = enabled; + } + $setInputBoxVisibility(sourceControlHandle: number, visible: boolean): void { const repository = this._repositories.get(sourceControlHandle); diff --git a/src/vs/workbench/api/browser/mainThreadSearch.ts b/src/vs/workbench/api/browser/mainThreadSearch.ts index 45a9fb1d1a3..1ff5cdfb75c 100644 --- a/src/vs/workbench/api/browser/mainThreadSearch.ts +++ b/src/vs/workbench/api/browser/mainThreadSearch.ts @@ -94,9 +94,7 @@ class SearchOperation { this.matches.set(match.resource.toString(), match); } - if (this.progress) { - this.progress(match); - } + this.progress?.(match); } } diff --git a/src/vs/workbench/api/browser/mainThreadStorage.ts b/src/vs/workbench/api/browser/mainThreadStorage.ts index 690895f0fe0..caddebdfaee 100644 --- a/src/vs/workbench/api/browser/mainThreadStorage.ts +++ b/src/vs/workbench/api/browser/mainThreadStorage.ts @@ -30,7 +30,7 @@ export class MainThreadStorage implements MainThreadStorageShape { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostStorage); this._storageListener = this._storageService.onDidChangeValue(e => { - const shared = e.scope === StorageScope.GLOBAL; + const shared = e.scope === StorageScope.PROFILE; if (shared && this._sharedStorageKeysToWatch.has(e.key)) { this._proxy.$acceptValue(shared, e.key, this._extensionStorageService.getExtensionState(e.key, shared)); } diff --git a/src/vs/workbench/api/browser/mainThreadTask.ts b/src/vs/workbench/api/browser/mainThreadTask.ts index 9cd5c8235f9..69e679cc5b4 100644 --- a/src/vs/workbench/api/browser/mainThreadTask.ts +++ b/src/vs/workbench/api/browser/mainThreadTask.ts @@ -15,27 +15,27 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { IWorkspace, IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { - ContributedTask, ConfiguringTask, KeyedTaskIdentifier, TaskExecution, Task, TaskEvent, TaskEventKind, - PresentationOptions, CommandOptions, CommandConfiguration, RuntimeType, CustomTask, TaskScope, TaskSource, - TaskSourceKind, ExtensionTaskSource, RunOptions, TaskSet, TaskDefinition, TaskGroup + ContributedTask, ConfiguringTask, KeyedTaskIdentifier, ITaskExecution, Task, ITaskEvent, TaskEventKind, + IPresentationOptions, CommandOptions, ICommandConfiguration, RuntimeType, CustomTask, TaskScope, TaskSource, + TaskSourceKind, IExtensionTaskSource, IRunOptions, ITaskSet, TaskGroup, TaskDefinition, PresentationOptions, RunOptions } from 'vs/workbench/contrib/tasks/common/tasks'; -import { ResolveSet, ResolvedVariables } from 'vs/workbench/contrib/tasks/common/taskSystem'; -import { ITaskService, TaskFilter, ITaskProvider } from 'vs/workbench/contrib/tasks/common/taskService'; +import { IResolveSet, IResolvedVariables } from 'vs/workbench/contrib/tasks/common/taskSystem'; +import { ITaskService, ITaskFilter, ITaskProvider } from 'vs/workbench/contrib/tasks/common/taskService'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { ExtHostContext, MainThreadTaskShape, ExtHostTaskShape, MainContext } from 'vs/workbench/api/common/extHost.protocol'; import { - TaskDefinitionDTO, TaskExecutionDTO, ProcessExecutionOptionsDTO, TaskPresentationOptionsDTO, - ProcessExecutionDTO, ShellExecutionDTO, ShellExecutionOptionsDTO, CustomExecutionDTO, TaskDTO, TaskSourceDTO, TaskHandleDTO, TaskFilterDTO, TaskProcessStartedDTO, TaskProcessEndedDTO, TaskSystemInfoDTO, - RunOptionsDTO, TaskGroupDTO + ITaskDefinitionDTO, ITaskExecutionDTO, IProcessExecutionOptionsDTO, ITaskPresentationOptionsDTO, + IProcessExecutionDTO, IShellExecutionDTO, IShellExecutionOptionsDTO, ICustomExecutionDTO, ITaskDTO, ITaskSourceDTO, ITaskHandleDTO, ITaskFilterDTO, ITaskProcessStartedDTO, ITaskProcessEndedDTO, ITaskSystemInfoDTO, + IRunOptionsDTO, ITaskGroupDTO } from 'vs/workbench/api/common/shared/tasks'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; namespace TaskExecutionDTO { - export function from(value: TaskExecution): TaskExecutionDTO { + export function from(value: ITaskExecution): ITaskExecutionDTO { return { id: value.id, task: TaskDTO.from(value.task) @@ -44,7 +44,7 @@ namespace TaskExecutionDTO { } namespace TaskProcessStartedDTO { - export function from(value: TaskExecution, processId: number): TaskProcessStartedDTO { + export function from(value: ITaskExecution, processId: number): ITaskProcessStartedDTO { return { id: value.id, processId @@ -53,7 +53,7 @@ namespace TaskProcessStartedDTO { } namespace TaskProcessEndedDTO { - export function from(value: TaskExecution, exitCode: number | undefined): TaskProcessEndedDTO { + export function from(value: ITaskExecution, exitCode: number | undefined): ITaskProcessEndedDTO { return { id: value.id, exitCode @@ -62,12 +62,12 @@ namespace TaskProcessEndedDTO { } namespace TaskDefinitionDTO { - export function from(value: KeyedTaskIdentifier): TaskDefinitionDTO { + export function from(value: KeyedTaskIdentifier): ITaskDefinitionDTO { const result = Object.assign(Object.create(null), value); delete result._key; return result; } - export function to(value: TaskDefinitionDTO, executeOnly: boolean): KeyedTaskIdentifier | undefined { + export function to(value: ITaskDefinitionDTO, executeOnly: boolean): KeyedTaskIdentifier | undefined { let result = TaskDefinition.createTaskIdentifier(value, console); if (result === undefined && executeOnly) { result = { @@ -80,13 +80,13 @@ namespace TaskDefinitionDTO { } namespace TaskPresentationOptionsDTO { - export function from(value: PresentationOptions | undefined): TaskPresentationOptionsDTO | undefined { + export function from(value: IPresentationOptions | undefined): ITaskPresentationOptionsDTO | undefined { if (value === undefined || value === null) { return undefined; } return Object.assign(Object.create(null), value); } - export function to(value: TaskPresentationOptionsDTO | undefined): PresentationOptions { + export function to(value: ITaskPresentationOptionsDTO | undefined): IPresentationOptions { if (value === undefined || value === null) { return PresentationOptions.defaults; } @@ -95,13 +95,13 @@ namespace TaskPresentationOptionsDTO { } namespace RunOptionsDTO { - export function from(value: RunOptions): RunOptionsDTO | undefined { + export function from(value: IRunOptions): IRunOptionsDTO | undefined { if (value === undefined || value === null) { return undefined; } return Object.assign(Object.create(null), value); } - export function to(value: RunOptionsDTO | undefined): RunOptions { + export function to(value: IRunOptionsDTO | undefined): IRunOptions { if (value === undefined || value === null) { return RunOptions.defaults; } @@ -110,7 +110,7 @@ namespace RunOptionsDTO { } namespace ProcessExecutionOptionsDTO { - export function from(value: CommandOptions): ProcessExecutionOptionsDTO | undefined { + export function from(value: CommandOptions): IProcessExecutionOptionsDTO | undefined { if (value === undefined || value === null) { return undefined; } @@ -119,7 +119,7 @@ namespace ProcessExecutionOptionsDTO { env: value.env }; } - export function to(value: ProcessExecutionOptionsDTO | undefined): CommandOptions { + export function to(value: IProcessExecutionOptionsDTO | undefined): CommandOptions { if (value === undefined || value === null) { return CommandOptions.defaults; } @@ -131,14 +131,14 @@ namespace ProcessExecutionOptionsDTO { } namespace ProcessExecutionDTO { - export function is(value: ShellExecutionDTO | ProcessExecutionDTO | CustomExecutionDTO): value is ProcessExecutionDTO { - const candidate = value as ProcessExecutionDTO; + export function is(value: IShellExecutionDTO | IProcessExecutionDTO | ICustomExecutionDTO): value is IProcessExecutionDTO { + const candidate = value as IProcessExecutionDTO; return candidate && !!candidate.process; } - export function from(value: CommandConfiguration): ProcessExecutionDTO { + export function from(value: ICommandConfiguration): IProcessExecutionDTO { const process: string = Types.isString(value.name) ? value.name : value.name!.value; const args: string[] = value.args ? value.args.map(value => Types.isString(value) ? value : value.value) : []; - const result: ProcessExecutionDTO = { + const result: IProcessExecutionDTO = { process: process, args: args }; @@ -147,8 +147,8 @@ namespace ProcessExecutionDTO { } return result; } - export function to(value: ProcessExecutionDTO): CommandConfiguration { - const result: CommandConfiguration = { + export function to(value: IProcessExecutionDTO): ICommandConfiguration { + const result: ICommandConfiguration = { runtime: RuntimeType.Process, name: value.process, args: value.args, @@ -160,11 +160,11 @@ namespace ProcessExecutionDTO { } namespace ShellExecutionOptionsDTO { - export function from(value: CommandOptions): ShellExecutionOptionsDTO | undefined { + export function from(value: CommandOptions): IShellExecutionOptionsDTO | undefined { if (value === undefined || value === null) { return undefined; } - const result: ShellExecutionOptionsDTO = { + const result: IShellExecutionOptionsDTO = { cwd: value.cwd || CommandOptions.defaults.cwd, env: value.env }; @@ -175,7 +175,7 @@ namespace ShellExecutionOptionsDTO { } return result; } - export function to(value: ShellExecutionOptionsDTO): CommandOptions | undefined { + export function to(value: IShellExecutionOptionsDTO): CommandOptions | undefined { if (value === undefined || value === null) { return undefined; } @@ -199,12 +199,12 @@ namespace ShellExecutionOptionsDTO { } namespace ShellExecutionDTO { - export function is(value: ShellExecutionDTO | ProcessExecutionDTO | CustomExecutionDTO): value is ShellExecutionDTO { - const candidate = value as ShellExecutionDTO; + export function is(value: IShellExecutionDTO | IProcessExecutionDTO | ICustomExecutionDTO): value is IShellExecutionDTO { + const candidate = value as IShellExecutionDTO; return candidate && (!!candidate.commandLine || !!candidate.command); } - export function from(value: CommandConfiguration): ShellExecutionDTO { - const result: ShellExecutionDTO = {}; + export function from(value: ICommandConfiguration): IShellExecutionDTO { + const result: IShellExecutionDTO = {}; if (value.name && Types.isString(value.name) && (value.args === undefined || value.args === null || value.args.length === 0)) { result.commandLine = value.name; } else { @@ -216,8 +216,8 @@ namespace ShellExecutionDTO { } return result; } - export function to(value: ShellExecutionDTO): CommandConfiguration { - const result: CommandConfiguration = { + export function to(value: IShellExecutionDTO): ICommandConfiguration { + const result: ICommandConfiguration = { runtime: RuntimeType.Shell, name: value.commandLine ? value.commandLine : value.command, args: value.args, @@ -231,18 +231,18 @@ namespace ShellExecutionDTO { } namespace CustomExecutionDTO { - export function is(value: ShellExecutionDTO | ProcessExecutionDTO | CustomExecutionDTO): value is CustomExecutionDTO { - const candidate = value as CustomExecutionDTO; + export function is(value: IShellExecutionDTO | IProcessExecutionDTO | ICustomExecutionDTO): value is ICustomExecutionDTO { + const candidate = value as ICustomExecutionDTO; return candidate && candidate.customExecution === 'customExecution'; } - export function from(value: CommandConfiguration): CustomExecutionDTO { + export function from(value: ICommandConfiguration): ICustomExecutionDTO { return { customExecution: 'customExecution' }; } - export function to(value: CustomExecutionDTO): CommandConfiguration { + export function to(value: ICustomExecutionDTO): ICommandConfiguration { return { runtime: RuntimeType.CustomExecution, presentation: undefined @@ -251,8 +251,8 @@ namespace CustomExecutionDTO { } namespace TaskSourceDTO { - export function from(value: TaskSource): TaskSourceDTO { - const result: TaskSourceDTO = { + export function from(value: TaskSource): ITaskSourceDTO { + const result: ITaskSourceDTO = { label: value.label }; if (value.kind === TaskSourceKind.Extension) { @@ -268,7 +268,7 @@ namespace TaskSourceDTO { } return result; } - export function to(value: TaskSourceDTO, workspace: IWorkspaceContextService): ExtensionTaskSource { + export function to(value: ITaskSourceDTO, workspace: IWorkspaceContextService): IExtensionTaskSource { let scope: TaskScope; let workspaceFolder: IWorkspaceFolder | undefined; if ((value.scope === undefined) || ((typeof value.scope === 'number') && (value.scope !== TaskScope.Global))) { @@ -285,7 +285,7 @@ namespace TaskSourceDTO { scope = TaskScope.Folder; workspaceFolder = Types.withNullAsUndefined(workspace.getWorkspaceFolder(URI.revive(value.scope))); } - const result: ExtensionTaskSource = { + const result: IExtensionTaskSource = { kind: TaskSourceKind.Extension, label: value.label, extension: value.extensionId, @@ -297,18 +297,18 @@ namespace TaskSourceDTO { } namespace TaskHandleDTO { - export function is(value: any): value is TaskHandleDTO { - const candidate: TaskHandleDTO = value; + export function is(value: any): value is ITaskHandleDTO { + const candidate: ITaskHandleDTO = value; return candidate && Types.isString(candidate.id) && !!candidate.workspaceFolder; } } namespace TaskDTO { - export function from(task: Task | ConfiguringTask): TaskDTO | undefined { + export function from(task: Task | ConfiguringTask): ITaskDTO | undefined { if (task === undefined || task === null || (!CustomTask.is(task) && !ContributedTask.is(task) && !ConfiguringTask.is(task))) { return undefined; } - const result: TaskDTO = { + const result: ITaskDTO = { _id: task._id, name: task.configurationProperties.name, definition: TaskDefinitionDTO.from(task.getDefinition(true)), @@ -333,7 +333,7 @@ namespace TaskDTO { } } if (task.configurationProperties.problemMatchers) { - for (let matcher of task.configurationProperties.problemMatchers) { + for (const matcher of task.configurationProperties.problemMatchers) { if (Types.isString(matcher)) { result.problemMatchers.push(matcher); } @@ -342,12 +342,12 @@ namespace TaskDTO { return result; } - export function to(task: TaskDTO | undefined, workspace: IWorkspaceContextService, executeOnly: boolean): ContributedTask | undefined { + export function to(task: ITaskDTO | undefined, workspace: IWorkspaceContextService, executeOnly: boolean, icon?: { id?: string; color?: string }): ContributedTask | undefined { if (!task || (typeof task.name !== 'string')) { return undefined; } - let command: CommandConfiguration | undefined; + let command: ICommandConfiguration | undefined; if (task.execution) { if (ShellExecutionDTO.is(task.execution)) { command = ShellExecutionDTO.to(task.execution); @@ -382,7 +382,8 @@ namespace TaskDTO { group: task.group, isBackground: !!task.isBackground, problemMatchers: task.problemMatchers.slice(), - detail: task.detail + detail: task.detail, + icon } ); return result; @@ -390,7 +391,7 @@ namespace TaskDTO { } namespace TaskGroupDTO { - export function from(value: string | TaskGroup | undefined): TaskGroupDTO | undefined { + export function from(value: string | TaskGroup | undefined): ITaskGroupDTO | undefined { if (value === undefined) { return undefined; } @@ -402,10 +403,10 @@ namespace TaskGroupDTO { } namespace TaskFilterDTO { - export function from(value: TaskFilter): TaskFilterDTO { + export function from(value: ITaskFilter): ITaskFilterDTO { return value; } - export function to(value: TaskFilterDTO | undefined): TaskFilter | undefined { + export function to(value: ITaskFilterDTO | undefined): ITaskFilter | undefined { return value; } } @@ -425,11 +426,11 @@ export class MainThreadTask implements MainThreadTaskShape { ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTask); this._providers = new Map(); - this._taskService.onDidStateChange(async (event: TaskEvent) => { + this._taskService.onDidStateChange(async (event: ITaskEvent) => { const task = event.__task!; if (event.kind === TaskEventKind.Start) { const execution = TaskExecutionDTO.from(task.getTaskExecution()); - let resolvedDefinition: TaskDefinitionDTO = execution.task!.definition; + let resolvedDefinition: ITaskDefinitionDTO = execution.task!.definition; if (execution.task?.execution && CustomExecutionDTO.is(execution.task.execution) && event.resolvedVariables) { const dictionary: IStringDictionary<string> = {}; Array.from(event.resolvedVariables.entries()).forEach(entry => dictionary[entry[0]] = entry[1]); @@ -454,9 +455,9 @@ export class MainThreadTask implements MainThreadTaskShape { this._providers.clear(); } - $createTaskId(taskDTO: TaskDTO): Promise<string> { + $createTaskId(taskDTO: ITaskDTO): Promise<string> { return new Promise((resolve, reject) => { - let task = TaskDTO.to(taskDTO, this._workspaceContextServer, true); + const task = TaskDTO.to(taskDTO, this._workspaceContextServer, true); if (task) { resolve(task._id); } else { @@ -470,7 +471,7 @@ export class MainThreadTask implements MainThreadTaskShape { provideTasks: (validTypes: IStringDictionary<boolean>) => { return Promise.resolve(this._proxy.$provideTasks(handle, validTypes)).then((value) => { const tasks: Task[] = []; - for (let dto of value.tasks) { + for (const dto of value.tasks) { const task = TaskDTO.to(dto, this._workspaceContextServer, true); if (task) { tasks.push(task); @@ -481,7 +482,7 @@ export class MainThreadTask implements MainThreadTaskShape { return { tasks, extension: value.extension - } as TaskSet; + } as ITaskSet; }); }, resolveTask: (task: ConfiguringTask) => { @@ -491,7 +492,7 @@ export class MainThreadTask implements MainThreadTaskShape { dto.name = ((dto.name === undefined) ? '' : dto.name); // Using an empty name causes the name to default to the one given by the provider. return Promise.resolve(this._proxy.$resolveTask(handle, dto)).then(resolvedTask => { if (resolvedTask) { - return TaskDTO.to(resolvedTask, this._workspaceContextServer, true); + return TaskDTO.to(resolvedTask, this._workspaceContextServer, true, task.configurationProperties.icon); } return undefined; @@ -514,10 +515,10 @@ export class MainThreadTask implements MainThreadTaskShape { return Promise.resolve(undefined); } - public $fetchTasks(filter?: TaskFilterDTO): Promise<TaskDTO[]> { + public $fetchTasks(filter?: ITaskFilterDTO): Promise<ITaskDTO[]> { return this._taskService.tasks(TaskFilterDTO.to(filter)).then((tasks) => { - const result: TaskDTO[] = []; - for (let task of tasks) { + const result: ITaskDTO[] = []; + for (const task of tasks) { const item = TaskDTO.from(task); if (item) { result.push(item); @@ -543,7 +544,7 @@ export class MainThreadTask implements MainThreadTaskShape { return workspace; } - public async $getTaskExecution(value: TaskHandleDTO | TaskDTO): Promise<TaskExecutionDTO> { + public async $getTaskExecution(value: ITaskHandleDTO | ITaskDTO): Promise<ITaskExecutionDTO> { if (TaskHandleDTO.is(value)) { const workspace = this.getWorkspace(value.workspaceFolder); if (workspace) { @@ -569,8 +570,8 @@ export class MainThreadTask implements MainThreadTaskShape { // Passing in a TaskHandleDTO will cause the task to get re-resolved, which is important for tasks are coming from the core, // such as those gotten from a fetchTasks, since they can have missing configuration properties. - public $executeTask(value: TaskHandleDTO | TaskDTO): Promise<TaskExecutionDTO> { - return new Promise<TaskExecutionDTO>((resolve, reject) => { + public $executeTask(value: ITaskHandleDTO | ITaskDTO): Promise<ITaskExecutionDTO> { + return new Promise<ITaskExecutionDTO>((resolve, reject) => { if (TaskHandleDTO.is(value)) { const workspace = this.getWorkspace(value.workspaceFolder); if (workspace) { @@ -578,7 +579,7 @@ export class MainThreadTask implements MainThreadTaskShape { if (!task) { reject(new Error('Task not found')); } else { - const result: TaskExecutionDTO = { + const result: ITaskExecutionDTO = { id: value.id, task: TaskDTO.from(task) }; @@ -604,7 +605,7 @@ export class MainThreadTask implements MainThreadTaskShape { this._taskService.run(task).then(undefined, reason => { // eat the error, it has already been surfaced to the user and we don't care about it here }); - const result: TaskExecutionDTO = { + const result: ITaskExecutionDTO = { id: task._id, task: TaskDTO.from(task) }; @@ -617,7 +618,7 @@ export class MainThreadTask implements MainThreadTaskShape { public $customExecutionComplete(id: string, result?: number): Promise<void> { return new Promise<void>((resolve, reject) => { this._taskService.getActiveTasks().then((tasks) => { - for (let task of tasks) { + for (const task of tasks) { if (id === task._id) { this._taskService.extensionCallbackTaskComplete(task, result).then((value) => { resolve(undefined); @@ -635,7 +636,7 @@ export class MainThreadTask implements MainThreadTaskShape { public $terminateTask(id: string): Promise<void> { return new Promise<void>((resolve, reject) => { this._taskService.getActiveTasks().then((tasks) => { - for (let task of tasks) { + for (const task of tasks) { if (id === task._id) { this._taskService.terminate(task).then((value) => { resolve(undefined); @@ -650,7 +651,7 @@ export class MainThreadTask implements MainThreadTaskShape { }); } - public $registerTaskSystem(key: string, info: TaskSystemInfoDTO): void { + public $registerTaskSystem(key: string, info: ITaskSystemInfoDTO): void { let platform: Platform.Platform; switch (info.platform) { case 'Web': @@ -674,7 +675,7 @@ export class MainThreadTask implements MainThreadTaskShape { return URI.from({ scheme: info.scheme, authority: info.authority, path }); }, context: this._extHostContext, - resolveVariables: (workspaceFolder: IWorkspaceFolder, toResolve: ResolveSet, target: ConfigurationTarget): Promise<ResolvedVariables | undefined> => { + resolveVariables: (workspaceFolder: IWorkspaceFolder, toResolve: IResolveSet, target: ConfigurationTarget): Promise<IResolvedVariables | undefined> => { const vars: string[] = []; toResolve.variables.forEach(item => vars.push(item)); return Promise.resolve(this._proxy.$resolveVariables(workspaceFolder.uri, { process: toResolve.process, variables: vars })).then(values => { @@ -682,13 +683,13 @@ export class MainThreadTask implements MainThreadTaskShape { forEach(values.variables, (entry) => { partiallyResolvedVars.push(entry.value); }); - return new Promise<ResolvedVariables | undefined>((resolve, reject) => { + return new Promise<IResolvedVariables | undefined>((resolve, reject) => { this._configurationResolverService.resolveWithInteraction(workspaceFolder, partiallyResolvedVars, 'tasks', undefined, target).then(resolvedVars => { if (!resolvedVars) { resolve(undefined); } - const result: ResolvedVariables = { + const result: IResolvedVariables = { process: undefined, variables: new Map<string, string>() }; diff --git a/src/vs/workbench/api/browser/mainThreadTerminalService.ts b/src/vs/workbench/api/browser/mainThreadTerminalService.ts index db958829ce8..62b439b64ab 100644 --- a/src/vs/workbench/api/browser/mainThreadTerminalService.ts +++ b/src/vs/workbench/api/browser/mainThreadTerminalService.ts @@ -288,7 +288,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape const proxy = request.proxy; this._terminalProcessProxies.set(proxy.instanceId, proxy); - // Note that onReisze is not being listened to here as it needs to fire when max dimensions + // Note that onResize is not being listened to here as it needs to fire when max dimensions // change, excluding the dimension override const initialDimensions: ITerminalDimensionsDto | undefined = request.cols && request.rows ? { columns: request.cols, @@ -318,9 +318,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape public $sendProcessProperty(terminalId: number, property: IProcessProperty<any>): void { if (property.type === ProcessPropertyType.Title) { const instance = this._terminalService.getInstanceFromId(terminalId); - if (instance) { - instance.refreshTabLabels(property.value, TitleEventSource.Api); - } + instance?.refreshTabLabels(property.value, TitleEventSource.Api); } this._terminalProcessProxies.get(terminalId)?.emitProcessProperty(property); } diff --git a/src/vs/workbench/api/browser/mainThreadTreeViews.ts b/src/vs/workbench/api/browser/mainThreadTreeViews.ts index 6e96ff8161f..4753211a74b 100644 --- a/src/vs/workbench/api/browser/mainThreadTreeViews.ts +++ b/src/vs/workbench/api/browser/mainThreadTreeViews.ts @@ -13,11 +13,11 @@ import { isUndefinedOrNull, isNumber } from 'vs/base/common/types'; import { Registry } from 'vs/platform/registry/common/platform'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; -import { DataTransferConverter } from 'vs/workbench/api/common/shared/dataTransfer'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { IDataTransfer } from 'vs/base/common/dataTransfer'; +import { createStringDataTransferItem, VSDataTransfer } from 'vs/base/common/dataTransfer'; import { VSBuffer } from 'vs/base/common/buffer'; import { DataTransferCache } from 'vs/workbench/api/common/shared/dataTransferCache'; +import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; @extHostNamedCustomer(MainContext.MainThreadTreeViews) export class MainThreadTreeViews extends Disposable implements MainThreadTreeViewsShape { @@ -203,25 +203,30 @@ class TreeViewDragAndDropController implements ITreeViewDragAndDropController { readonly hasWillDrop: boolean, private readonly _proxy: ExtHostTreeViewsShape) { } - async handleDrop(dataTransfer: IDataTransfer, targetTreeItem: ITreeItem | undefined, token: CancellationToken, + async handleDrop(dataTransfer: VSDataTransfer, targetTreeItem: ITreeItem | undefined, token: CancellationToken, operationUuid?: string, sourceTreeId?: string, sourceTreeItemHandles?: string[]): Promise<void> { const request = this.dataTransfersCache.add(dataTransfer); try { - return await this._proxy.$handleDrop(this.treeViewId, request.id, await DataTransferConverter.toDataTransferDTO(dataTransfer), targetTreeItem?.handle, token, operationUuid, sourceTreeId, sourceTreeItemHandles); + return await this._proxy.$handleDrop(this.treeViewId, request.id, await typeConvert.DataTransfer.toDataTransferDTO(dataTransfer), targetTreeItem?.handle, token, operationUuid, sourceTreeId, sourceTreeItemHandles); } finally { request.dispose(); } } - async handleDrag(sourceTreeItemHandles: string[], operationUuid: string, token: CancellationToken): Promise<IDataTransfer | undefined> { + async handleDrag(sourceTreeItemHandles: string[], operationUuid: string, token: CancellationToken): Promise<VSDataTransfer | undefined> { if (!this.hasWillDrop) { return; } - const additionalTransferItems = await this._proxy.$handleDrag(this.treeViewId, sourceTreeItemHandles, operationUuid, token); - if (!additionalTransferItems) { + const additionalDataTransferDTO = await this._proxy.$handleDrag(this.treeViewId, sourceTreeItemHandles, operationUuid, token); + if (!additionalDataTransferDTO) { return; } - return DataTransferConverter.toDataTransfer(additionalTransferItems, () => { throw new Error('not supported'); }); + + const additionalDataTransfer = new VSDataTransfer(); + additionalDataTransferDTO.items.forEach(([type, item]) => { + additionalDataTransfer.replace(type, createStringDataTransferItem(item.asString)); + }); + return additionalDataTransfer; } public resolveDropFileData(requestId: number, dataItemIndex: number): Promise<VSBuffer> { diff --git a/src/vs/workbench/api/browser/mainThreadWorkspace.ts b/src/vs/workbench/api/browser/mainThreadWorkspace.ts index 3f0ea4ae56e..5fe29a26a0c 100644 --- a/src/vs/workbench/api/browser/mainThreadWorkspace.ts +++ b/src/vs/workbench/api/browser/mainThreadWorkspace.ts @@ -66,7 +66,7 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { dispose(): void { this._toDispose.dispose(); - for (let requestId in this._activeCancelTokens) { + for (const requestId in this._activeCancelTokens) { const tokenSource = this._activeCancelTokens[requestId]; tokenSource.cancel(); } diff --git a/src/vs/workbench/api/browser/viewsExtensionPoint.ts b/src/vs/workbench/api/browser/viewsExtensionPoint.ts index 489f34bb0fd..1932e2ce93d 100644 --- a/src/vs/workbench/api/browser/viewsExtensionPoint.ts +++ b/src/vs/workbench/api/browser/viewsExtensionPoint.ts @@ -321,7 +321,7 @@ class ViewsExtensionHandler implements IWorkbenchContribution { const viewContainersRegistry = Registry.as<IViewContainersRegistry>(ViewContainerExtensions.ViewContainersRegistry); let activityBarOrder = CUSTOM_VIEWS_START_ORDER + viewContainersRegistry.all.filter(v => !!v.extensionId && viewContainersRegistry.getViewContainerLocation(v) === ViewContainerLocation.Sidebar).length; let panelOrder = 5 + viewContainersRegistry.all.filter(v => !!v.extensionId && viewContainersRegistry.getViewContainerLocation(v) === ViewContainerLocation.Panel).length + 1; - for (let { value, collector, description } of extensionPoints) { + for (const { value, collector, description } of extensionPoints) { forEach(value, entry => { if (!this.isValidViewsContainer(entry.value, collector)) { return; @@ -359,7 +359,7 @@ class ViewsExtensionHandler implements IWorkbenchContribution { return false; } - for (let descriptor of viewsContainersDescriptors) { + for (const descriptor of viewsContainersDescriptors) { if (typeof descriptor.id !== 'string' && isFalsyOrWhitespace(descriptor.id)) { collector.error(localize('requireidstring', "property `{0}` is mandatory and must be of type `string` with non-empty value. Only alphanumeric characters, '_', and '-' are allowed.", 'id')); return false; @@ -571,7 +571,7 @@ class ViewsExtensionHandler implements IWorkbenchContribution { return false; } - for (let descriptor of viewDescriptors) { + for (const descriptor of viewDescriptors) { if (typeof descriptor.id !== 'string') { collector.error(localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'id')); return false; diff --git a/src/vs/workbench/api/common/configurationExtensionPoint.ts b/src/vs/workbench/api/common/configurationExtensionPoint.ts index 406661573d2..22ff50aca91 100644 --- a/src/vs/workbench/api/common/configurationExtensionPoint.ts +++ b/src/vs/workbench/api/common/configurationExtensionPoint.ts @@ -180,7 +180,7 @@ configurationExtPoint.setHandler((extensions, { added, removed }) => { function handleConfiguration(node: IConfigurationNode, extension: IExtensionPointUser<any>): IConfigurationNode[] { const configurations: IConfigurationNode[] = []; - let configuration = objects.deepClone(node); + const configuration = objects.deepClone(node); if (configuration.title && (typeof configuration.title !== 'string')) { extension.collector.error(nls.localize('invalid.title', "'configuration.title' must be a string")); @@ -197,14 +197,15 @@ configurationExtPoint.setHandler((extensions, { added, removed }) => { } function validateProperties(configuration: IConfigurationNode, extension: IExtensionPointUser<any>): void { - let properties = configuration.properties; + const properties = configuration.properties; if (properties) { if (typeof properties !== 'object') { extension.collector.error(nls.localize('invalid.properties', "'configuration.properties' must be an object")); configuration.properties = {}; } - for (let key in properties) { - const message = validateProperty(key); + for (const key in properties) { + const propertyConfiguration = properties[key]; + const message = validateProperty(key, propertyConfiguration); if (message) { delete properties[key]; extension.collector.warn(message); @@ -215,7 +216,6 @@ configurationExtPoint.setHandler((extensions, { added, removed }) => { extension.collector.warn(nls.localize('config.property.duplicate', "Cannot register '{0}'. This property is already registered.", key)); continue; } - const propertyConfiguration = properties[key]; if (!isObject(propertyConfiguration)) { delete properties[key]; extension.collector.error(nls.localize('invalid.property', "configuration.properties property '{0}' must be an object", key)); @@ -241,10 +241,10 @@ configurationExtPoint.setHandler((extensions, { added, removed }) => { } } } - let subNodes = configuration.allOf; + const subNodes = configuration.allOf; if (subNodes) { extension.collector.error(nls.localize('invalid.allOf', "'configuration.allOf' is deprecated and should no longer be used. Instead, pass multiple configuration sections as an array to the 'configuration' contribution point.")); - for (let node of subNodes) { + for (const node of subNodes) { validateProperties(node, extension); } } @@ -252,7 +252,7 @@ configurationExtPoint.setHandler((extensions, { added, removed }) => { if (added.length) { const addedConfigurations: IConfigurationNode[] = []; - for (let extension of added) { + for (const extension of added) { const configurations: IConfigurationNode[] = []; const value = <IConfigurationNode | IConfigurationNode[]>extension.value; if (Array.isArray(value)) { diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 313a9aadfd1..cd38535f5f6 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -51,7 +51,6 @@ import { ProxyIdentifier } from 'vs/workbench/services/extensions/common/proxyId import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; import type * as vscode from 'vscode'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { values } from 'vs/base/common/collections'; import { ExtHostEditorInsets } from 'vs/workbench/api/common/extHostCodeInsets'; import { ExtHostLabelService } from 'vs/workbench/api/common/extHostLabelService'; import { getRemoteName } from 'vs/platform/remote/common/remoteHosts'; @@ -93,7 +92,7 @@ import { ExtHostInteractive } from 'vs/workbench/api/common/extHostInteractive'; import { combinedDisposable } from 'vs/base/common/lifecycle'; import { checkProposedApiEnabled, ExtensionIdentifierSet, isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import { DebugConfigurationProviderTriggerKind } from 'vs/workbench/contrib/debug/common/debug'; -import { ExtHostNotebookProxyKernels } from 'vs/workbench/api/common/extHostNotebookProxyKernels'; +import { equalsIgnoreCase } from 'vs/base/common/strings'; export interface IExtensionRegistries { mine: ExtensionDescriptionRegistry; @@ -159,9 +158,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostDocumentSaveParticipant = rpcProtocol.set(ExtHostContext.ExtHostDocumentSaveParticipant, new ExtHostDocumentSaveParticipant(extHostLogService, extHostDocuments, rpcProtocol.getProxy(MainContext.MainThreadBulkEdits))); const extHostNotebook = rpcProtocol.set(ExtHostContext.ExtHostNotebook, new ExtHostNotebookController(rpcProtocol, extHostCommands, extHostDocumentsAndEditors, extHostDocuments, extensionStoragePaths)); const extHostNotebookDocuments = rpcProtocol.set(ExtHostContext.ExtHostNotebookDocuments, new ExtHostNotebookDocuments(extHostNotebook)); - const extHostNotebookEditors = rpcProtocol.set(ExtHostContext.ExtHostNotebookEditors, new ExtHostNotebookEditors(extHostLogService, rpcProtocol, extHostNotebook)); + const extHostNotebookEditors = rpcProtocol.set(ExtHostContext.ExtHostNotebookEditors, new ExtHostNotebookEditors(extHostLogService, extHostNotebook)); const extHostNotebookKernels = rpcProtocol.set(ExtHostContext.ExtHostNotebookKernels, new ExtHostNotebookKernels(rpcProtocol, initData, extHostNotebook, extHostCommands, extHostLogService)); - const extHostNotebookProxyKernels = rpcProtocol.set(ExtHostContext.ExtHostNotebookProxyKernels, new ExtHostNotebookProxyKernels(rpcProtocol, extHostNotebookKernels, extHostLogService)); const extHostNotebookRenderers = rpcProtocol.set(ExtHostContext.ExtHostNotebookRenderers, new ExtHostNotebookRenderers(rpcProtocol, extHostNotebook)); const extHostEditors = rpcProtocol.set(ExtHostContext.ExtHostEditors, new ExtHostEditors(rpcProtocol, extHostDocumentsAndEditors)); const extHostTreeViews = rpcProtocol.set(ExtHostContext.ExtHostTreeViews, new ExtHostTreeViews(rpcProtocol.getProxy(MainContext.MainThreadTreeViews), extHostCommands, extHostLogService)); @@ -185,10 +183,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostWebviewViews = rpcProtocol.set(ExtHostContext.ExtHostWebviewViews, new ExtHostWebviewViews(rpcProtocol, extHostWebviews)); const extHostTesting = rpcProtocol.set(ExtHostContext.ExtHostTesting, new ExtHostTesting(rpcProtocol, extHostCommands)); const extHostUriOpeners = rpcProtocol.set(ExtHostContext.ExtHostUriOpeners, new ExtHostUriOpeners(rpcProtocol)); - rpcProtocol.set(ExtHostContext.ExtHostInteractive, new ExtHostInteractive(rpcProtocol, extHostNotebook, extHostDocumentsAndEditors, extHostCommands)); + rpcProtocol.set(ExtHostContext.ExtHostInteractive, new ExtHostInteractive(rpcProtocol, extHostNotebook, extHostDocumentsAndEditors, extHostCommands, extHostLogService)); // Check that no named customers are missing - const expected: ProxyIdentifier<any>[] = values(ExtHostContext); + const expected = Object.values<ProxyIdentifier<any>>(ExtHostContext); rpcProtocol.assertRegistered(expected); // Other instances @@ -395,6 +393,11 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extensions: typeof vscode.extensions = { getExtension(extensionId: string, includeFromDifferentExtensionHosts?: boolean): vscode.Extension<any> | undefined { + if (equalsIgnoreCase(extensionId, 'ms-vscode.references-view')) { + extHostApiDeprecation.report(`The extension 'ms-vscode.references-view' has been renamed.`, extension, `Use 'vscode.references-view' instead.`); + extensionId = 'vscode.references-view'; + } + if (!isProposedApiEnabled(extension, 'extensionsAny')) { includeFromDifferentExtensionHosts = false; } @@ -459,6 +462,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I registerCodeActionsProvider(selector: vscode.DocumentSelector, provider: vscode.CodeActionProvider, metadata?: vscode.CodeActionProviderMetadata): vscode.Disposable { return extHostLanguageFeatures.registerCodeActionProvider(extension, checkSelector(selector), provider, metadata); }, + registerDocumentPasteEditProvider(selector: vscode.DocumentSelector, provider: vscode.DocumentPasteEditProvider, metadata: vscode.DocumentPasteProviderMetadata): vscode.Disposable { + checkProposedApiEnabled(extension, 'documentPaste'); + return extHostLanguageFeatures.registerDocumentPasteEditProvider(extension, checkSelector(selector), provider, metadata); + }, registerCodeLensProvider(selector: vscode.DocumentSelector, provider: vscode.CodeLensProvider): vscode.Disposable { return extHostLanguageFeatures.registerCodeLensProvider(extension, checkSelector(selector), provider); }, @@ -526,7 +533,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostLanguageFeatures.registerCompletionItemProvider(extension, checkSelector(selector), provider, triggerCharacters); }, registerInlineCompletionItemProvider(selector: vscode.DocumentSelector, provider: vscode.InlineCompletionItemProvider): vscode.Disposable { - checkProposedApiEnabled(extension, 'inlineCompletions'); if (provider.handleDidShowCompletionItem) { checkProposedApiEnabled(extension, 'inlineCompletionsAdditions'); } @@ -759,31 +765,28 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostWebviewViews.registerWebviewViewProvider(extension, viewId, provider, options?.webviewOptions); }, get activeNotebookEditor(): vscode.NotebookEditor | undefined { - checkProposedApiEnabled(extension, 'notebookEditor'); return extHostNotebook.activeNotebookEditor; }, onDidChangeActiveNotebookEditor(listener, thisArgs?, disposables?) { - checkProposedApiEnabled(extension, 'notebookEditor'); return extHostNotebook.onDidChangeActiveNotebookEditor(listener, thisArgs, disposables); }, get visibleNotebookEditors() { - checkProposedApiEnabled(extension, 'notebookEditor'); return extHostNotebook.visibleNotebookEditors; }, get onDidChangeVisibleNotebookEditors() { - checkProposedApiEnabled(extension, 'notebookEditor'); return extHostNotebook.onDidChangeVisibleNotebookEditors; }, onDidChangeNotebookEditorSelection(listener, thisArgs?, disposables?) { - checkProposedApiEnabled(extension, 'notebookEditor'); return extHostNotebookEditors.onDidChangeNotebookEditorSelection(listener, thisArgs, disposables); }, onDidChangeNotebookEditorVisibleRanges(listener, thisArgs?, disposables?) { - checkProposedApiEnabled(extension, 'notebookEditor'); return extHostNotebookEditors.onDidChangeNotebookEditorVisibleRanges(listener, thisArgs, disposables); }, showNotebookDocument(uriOrDocument, options?) { - checkProposedApiEnabled(extension, 'notebookEditor'); + if (URI.isUri(uriOrDocument)) { + extHostApiDeprecation.report('window.showNotebookDocument(uri)', extension, + `Please use 'window.openNotebookDocument' and 'window.showTextDocument'`); + } return extHostNotebook.showNotebookDocument(uriOrDocument, options); }, registerExternalUriOpener(id: string, opener: vscode.ExternalUriOpener, metadata: vscode.ExternalUriOpenerMetadata) { @@ -858,7 +861,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostWorkspace.saveAll(includeUntitled); }, applyEdit(edit: vscode.WorkspaceEdit): Thenable<boolean> { - return extHostBulkEdits.applyWorkspaceEdit(edit); + return extHostBulkEdits.applyWorkspaceEdit(edit, extension); }, createFileSystemWatcher: (pattern, ignoreCreate, ignoreChange, ignoreDelete): vscode.FileSystemWatcher => { return extHostFileSystemEvent.createFileSystemWatcher(extHostWorkspace, extension, pattern, ignoreCreate, ignoreChange, ignoreDelete); @@ -936,6 +939,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I }, registerNotebookContentProvider: (viewType: string, provider: vscode.NotebookContentProvider, options?: vscode.NotebookDocumentContentOptions, registration?: vscode.NotebookRegistrationData) => { checkProposedApiEnabled(extension, 'notebookContentProvider'); + + extHostApiDeprecation.report('workspace.registerNotebookContentProvider', extension, + `The notebookContentProvider API is not on track for finalization and will be removed.`); + return extHostNotebook.registerNotebookContentProvider(extension, viewType, provider, options, isProposedApiEnabled(extension, 'notebookLiveShare') ? registration : undefined); }, onDidChangeConfiguration: (listener: (_: any) => any, thisArgs?: any, disposables?: extHostTypes.Disposable[]) => { @@ -1144,20 +1151,12 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I registerNotebookCellStatusBarItemProvider: (notebookType: string, provider: vscode.NotebookCellStatusBarItemProvider) => { return extHostNotebook.registerNotebookCellStatusBarItemProvider(extension, notebookType, provider); }, - createNotebookEditorDecorationType(options: vscode.NotebookDecorationRenderOptions): vscode.NotebookEditorDecorationType { - checkProposedApiEnabled(extension, 'notebookEditorDecorationType'); - return extHostNotebookEditors.createNotebookEditorDecorationType(options); - }, createRendererMessaging(rendererId) { return extHostNotebookRenderers.createRendererMessaging(extension, rendererId); }, onDidChangeNotebookCellExecutionState(listener, thisArgs?, disposables?) { checkProposedApiEnabled(extension, 'notebookCellExecutionState'); return extHostNotebookKernels.onDidChangeNotebookCellExecutionState(listener, thisArgs, disposables); - }, - createNotebookProxyController(id: string, notebookType: string, label: string, handler: () => vscode.NotebookController | string | Thenable<vscode.NotebookController | string>) { - checkProposedApiEnabled(extension, 'notebookProxyController'); - return extHostNotebookProxyKernels.createNotebookProxyController(extension, id, notebookType, label, handler); } }; @@ -1269,7 +1268,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I SignatureHelpTriggerKind: extHostTypes.SignatureHelpTriggerKind, SignatureInformation: extHostTypes.SignatureInformation, SnippetString: extHostTypes.SnippetString, - SnippetTextEdit: extHostTypes.SnippetTextEdit, SourceBreakpoint: extHostTypes.SourceBreakpoint, StandardTokenType: extHostTypes.StandardTokenType, StatusBarAlignment: extHostTypes.StatusBarAlignment, @@ -1301,6 +1299,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ViewColumn: extHostTypes.ViewColumn, WorkspaceEdit: extHostTypes.WorkspaceEdit, // proposed api types + DocumentDropEdit: extHostTypes.DocumentDropEdit, + DocumentPasteEdit: extHostTypes.DocumentPasteEdit, InlayHint: extHostTypes.InlayHint, InlayHintLabelPart: extHostTypes.InlayHintLabelPart, InlayHintKind: extHostTypes.InlayHintKind, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 6d936351c35..a7b203695df 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -22,6 +22,7 @@ import { ISelection, Selection } from 'vs/editor/common/core/selection'; import { ILineChange } from 'vs/editor/common/diff/diffComputer'; import * as editorCommon from 'vs/editor/common/editorCommon'; import * as languages from 'vs/editor/common/languages'; +import { StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; import { CharacterPair, CommentRule, EnterAction } from 'vs/editor/common/languages/languageConfiguration'; import { EndOfLineSequence } from 'vs/editor/common/model'; import { IModelChangedEvent } from 'vs/editor/common/model/mirrorTextModel'; @@ -44,7 +45,6 @@ import { ThemeColor, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { ProvidedPortAttributes, TunnelCreationOptions, TunnelOptions, TunnelPrivacyId, TunnelProviderFeatures } from 'vs/platform/tunnel/common/tunnel'; import { WorkspaceTrustRequestOptions } from 'vs/platform/workspace/common/workspaceTrust'; import * as tasks from 'vs/workbench/api/common/shared/tasks'; -import { DataTransferDTO } from 'vs/workbench/api/common/shared/dataTransfer'; import { SaveReason } from 'vs/workbench/common/editor'; import { IRevealOptions, ITreeItem, IViewBadge } from 'vs/workbench/common/views'; import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; @@ -92,6 +92,7 @@ export interface MainThreadClipboardShape extends IDisposable { export interface MainThreadCommandsShape extends IDisposable { $registerCommand(id: string): void; $unregisterCommand(id: string): void; + $fireCommandActivationEvent(id: string): void; $executeCommand(id: string, args: any[] | SerializableObjectWithBuffers<any[]>, retry: boolean): Promise<unknown | undefined>; $getCommands(): Promise<string[]>; } @@ -372,6 +373,7 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $registerLinkedEditingRangeProvider(handle: number, selector: IDocumentFilterDto[]): void; $registerReferenceSupport(handle: number, selector: IDocumentFilterDto[]): void; $registerQuickFixSupport(handle: number, selector: IDocumentFilterDto[], metadata: ICodeActionProviderMetadataDto, displayName: string, supportsResolve: boolean): void; + $registerPasteEditProvider(handle: number, selector: IDocumentFilterDto[], supportsCopy: boolean, pasteMimeTypes: readonly string[]): void; $registerDocumentFormattingSupport(handle: number, selector: IDocumentFilterDto[], extensionId: ExtensionIdentifier, displayName: string): void; $registerRangeFormattingSupport(handle: number, selector: IDocumentFilterDto[], extensionId: ExtensionIdentifier, displayName: string): void; $registerOnTypeFormattingSupport(handle: number, selector: IDocumentFilterDto[], autoFormatTriggerCharacters: string[], extensionId: ExtensionIdentifier): void; @@ -399,7 +401,7 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { export interface MainThreadLanguagesShape extends IDisposable { $changeLanguage(resource: UriComponents, languageId: string): Promise<void>; - $tokensAtPosition(resource: UriComponents, position: IPosition): Promise<undefined | { type: languages.StandardTokenType; range: IRange }>; + $tokensAtPosition(resource: UriComponents, position: IPosition): Promise<undefined | { type: StandardTokenType; range: IRange }>; $setLanguageStatus(handle: number, status: ILanguageStatus): void; $removeLanguageStatus(handle: number): void; } @@ -957,10 +959,7 @@ export interface MainThreadNotebookShape extends IDisposable { export interface MainThreadNotebookEditorsShape extends IDisposable { $tryShowNotebookDocument(uriComponents: UriComponents, viewType: string, options: INotebookDocumentShowOptions): Promise<string>; $tryRevealRange(id: string, range: ICellRange, revealType: NotebookEditorRevealType): Promise<void>; - $registerNotebookEditorDecorationType(key: string, options: notebookCommon.INotebookDecorationRenderOptions): void; - $removeNotebookEditorDecorationType(key: string): void; $trySetSelections(id: string, range: ICellRange[]): void; - $trySetDecorations(id: string, range: ICellRange, decorationKey: string): void; $tryApplyEdits(editorId: string, modelVersionId: number, cellEdits: ICellEditOperationDto[]): Promise<boolean>; } @@ -998,6 +997,7 @@ export interface INotebookProxyKernelDto { export interface ICellExecuteOutputEditDto { editType: CellExecutionUpdateType.Output; + cellHandle: number; append?: boolean; outputs: NotebookOutputDto[]; } @@ -1029,12 +1029,6 @@ export interface MainThreadNotebookKernelsShape extends IDisposable { $completeExecution(handle: number, data: SerializableObjectWithBuffers<ICellExecutionCompleteDto>): void; } -export interface MainThreadNotebookProxyKernelsShape extends IDisposable { - $addProxyKernel(handle: number, data: INotebookProxyKernelDto): Promise<void>; - $updateProxyKernel(handle: number, data: Partial<INotebookProxyKernelDto>): void; - $removeProxyKernel(handle: number): void; -} - export interface MainThreadNotebookRenderersShape extends IDisposable { $postMessage(editorId: string | undefined, rendererId: string, message: unknown): Promise<boolean>; } @@ -1116,14 +1110,14 @@ export interface MainThreadSearchShape extends IDisposable { } export interface MainThreadTaskShape extends IDisposable { - $createTaskId(task: tasks.TaskDTO): Promise<string>; + $createTaskId(task: tasks.ITaskDTO): Promise<string>; $registerTaskProvider(handle: number, type: string): Promise<void>; $unregisterTaskProvider(handle: number): Promise<void>; - $fetchTasks(filter?: tasks.TaskFilterDTO): Promise<tasks.TaskDTO[]>; - $getTaskExecution(value: tasks.TaskHandleDTO | tasks.TaskDTO): Promise<tasks.TaskExecutionDTO>; - $executeTask(task: tasks.TaskHandleDTO | tasks.TaskDTO): Promise<tasks.TaskExecutionDTO>; + $fetchTasks(filter?: tasks.ITaskFilterDTO): Promise<tasks.ITaskDTO[]>; + $getTaskExecution(value: tasks.ITaskHandleDTO | tasks.ITaskDTO): Promise<tasks.ITaskExecutionDTO>; + $executeTask(task: tasks.ITaskHandleDTO | tasks.ITaskDTO): Promise<tasks.ITaskExecutionDTO>; $terminateTask(id: string): Promise<void>; - $registerTaskSystem(scheme: string, info: tasks.TaskSystemInfoDTO): void; + $registerTaskSystem(scheme: string, info: tasks.ITaskSystemInfoDTO): void; $customExecutionComplete(id: string, result?: number): Promise<void>; $registerSupportedExecutions(custom?: boolean, shell?: boolean, process?: boolean): Promise<void>; } @@ -1150,7 +1144,9 @@ export interface SCMProviderFeatures { export interface SCMActionButtonDto { command: ICommandDto; + secondaryCommands?: ICommandDto[][]; description?: string; + enabled: boolean; } export interface SCMGroupFeatures { @@ -1193,6 +1189,7 @@ export interface MainThreadSCMShape extends IDisposable { $setInputBoxValue(sourceControlHandle: number, value: string): void; $setInputBoxPlaceholder(sourceControlHandle: number, placeholder: string): void; + $setInputBoxEnablement(sourceControlHandle: number, enabled: boolean): void; $setInputBoxVisibility(sourceControlHandle: number, visible: boolean): void; $showValidationMessage(sourceControlHandle: number, message: string | IMarkdownString, type: InputValidationType): void; $setValidationProviderIsEnabled(sourceControlHandle: number, enabled: boolean): void; @@ -1369,6 +1366,20 @@ export interface ExtHostDocumentsAndEditorsShape { $acceptDocumentsAndEditorsDelta(delta: IDocumentsAndEditorsDelta): void; } +export interface IDataTransferFileDTO { + readonly name: string; + readonly uri?: UriComponents; +} + +export interface DataTransferItemDTO { + readonly asString: string; + readonly fileData: IDataTransferFileDTO | undefined; +} + +export interface DataTransferDTO { + readonly items: Array<[/* type */string, DataTransferItemDTO]>; +} + export interface ExtHostTreeViewsShape { $getChildren(treeViewId: string, treeItemHandle?: string): Promise<ITreeItem[] | undefined>; $handleDrop(destinationViewId: string, requestId: number, treeDataTransfer: DataTransferDTO, targetHandle: string | undefined, token: CancellationToken, operationUuid?: string, sourceViewId?: string, sourceTreeItemHandles?: string[]): Promise<void>; @@ -1439,7 +1450,6 @@ export interface ExtHostExtensionServiceShape { $getCanonicalURI(remoteAuthority: string, uri: UriComponents): Promise<UriComponents | null>; $startExtensionHost(extensionsDelta: IExtensionDescriptionDelta): Promise<void>; $extensionTestsExecute(): Promise<number>; - $extensionTestsExit(code: number): Promise<void>; $activateByEvent(activationEvent: string, activationKind: ActivationKind): Promise<void>; $activate(extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise<boolean>; $setRemoteEnvironment(env: { [key: string]: string | null }): Promise<void>; @@ -1594,7 +1604,7 @@ export interface IWorkspaceFileEditDto { export interface IWorkspaceTextEditDto { _type: WorkspaceEditType.Text; resource: UriComponents; - edit: languages.TextEdit; + edit: languages.TextEdit & { insertAsSnippet?: boolean }; modelVersionId?: number; metadata?: IWorkspaceEditEntryMetadataDto; } @@ -1703,6 +1713,16 @@ export interface IInlineValueContextDto { export type ITypeHierarchyItemDto = Dto<TypeHierarchyItem>; +export interface IPasteEditDto { + insertText: string | { snippet: string }; + additionalEdit?: IWorkspaceEditDto; +} + +export interface IDocumentOnDropEditDto { + insertText: string | { snippet: string }; + additionalEdit?: IWorkspaceEditDto; +} + export interface ExtHostLanguageFeaturesShape { $provideDocumentSymbols(handle: number, resource: UriComponents, token: CancellationToken): Promise<languages.DocumentSymbol[] | undefined>; $provideCodeLenses(handle: number, resource: UriComponents, token: CancellationToken): Promise<ICodeLensListDto | undefined>; @@ -1721,6 +1741,8 @@ export interface ExtHostLanguageFeaturesShape { $provideCodeActions(handle: number, resource: UriComponents, rangeOrSelection: IRange | ISelection, context: languages.CodeActionContext, token: CancellationToken): Promise<ICodeActionListDto | undefined>; $resolveCodeAction(handle: number, id: ChainedCacheId, token: CancellationToken): Promise<IWorkspaceEditDto | undefined>; $releaseCodeActions(handle: number, cacheId: number): void; + $prepareDocumentPaste(handle: number, uri: UriComponents, ranges: IRange[], dataTransfer: DataTransferDTO, token: CancellationToken): Promise<DataTransferDTO | undefined>; + $providePasteEdits(handle: number, uri: UriComponents, ranges: IRange[], dataTransfer: DataTransferDTO, token: CancellationToken): Promise<IPasteEditDto | undefined>; $provideDocumentFormattingEdits(handle: number, resource: UriComponents, options: languages.FormattingOptions, token: CancellationToken): Promise<ISingleEditOperation[] | undefined>; $provideDocumentRangeFormattingEdits(handle: number, resource: UriComponents, range: IRange, options: languages.FormattingOptions, token: CancellationToken): Promise<ISingleEditOperation[] | undefined>; $provideOnTypeFormattingEdits(handle: number, resource: UriComponents, position: IPosition, ch: string, options: languages.FormattingOptions, token: CancellationToken): Promise<ISingleEditOperation[] | undefined>; @@ -1759,7 +1781,7 @@ export interface ExtHostLanguageFeaturesShape { $provideTypeHierarchySupertypes(handle: number, sessionId: string, itemId: string, token: CancellationToken): Promise<ITypeHierarchyItemDto[] | undefined>; $provideTypeHierarchySubtypes(handle: number, sessionId: string, itemId: string, token: CancellationToken): Promise<ITypeHierarchyItemDto[] | undefined>; $releaseTypeHierarchy(handle: number, sessionId: string): void; - $provideDocumentOnDropEdits(handle: number, requestId: number, resource: UriComponents, position: IPosition, dataTransferDto: DataTransferDTO, token: CancellationToken): Promise<Dto<languages.SnippetTextEdit> | undefined>; + $provideDocumentOnDropEdits(handle: number, requestId: number, resource: UriComponents, position: IPosition, dataTransferDto: DataTransferDTO, token: CancellationToken): Promise<IDocumentOnDropEditDto | undefined>; } export interface ExtHostQuickOpenShape { @@ -1829,12 +1851,12 @@ export interface ExtHostSCMShape { } export interface ExtHostTaskShape { - $provideTasks(handle: number, validTypes: { [key: string]: boolean }): Promise<tasks.TaskSetDTO>; - $resolveTask(handle: number, taskDTO: tasks.TaskDTO): Promise<tasks.TaskDTO | undefined>; - $onDidStartTask(execution: tasks.TaskExecutionDTO, terminalId: number, resolvedDefinition: tasks.TaskDefinitionDTO): void; - $onDidStartTaskProcess(value: tasks.TaskProcessStartedDTO): void; - $onDidEndTaskProcess(value: tasks.TaskProcessEndedDTO): void; - $OnDidEndTask(execution: tasks.TaskExecutionDTO): void; + $provideTasks(handle: number, validTypes: { [key: string]: boolean }): Promise<tasks.ITaskSetDTO>; + $resolveTask(handle: number, taskDTO: tasks.ITaskDTO): Promise<tasks.ITaskDTO | undefined>; + $onDidStartTask(execution: tasks.ITaskExecutionDTO, terminalId: number, resolvedDefinition: tasks.ITaskDefinitionDTO): void; + $onDidStartTaskProcess(value: tasks.ITaskProcessStartedDTO): void; + $onDidEndTaskProcess(value: tasks.ITaskProcessEndedDTO): void; + $OnDidEndTask(execution: tasks.ITaskExecutionDTO): void; $resolveVariables(workspaceFolder: UriComponents, toResolve: { process?: { name: string; cwd?: string }; variables: string[] }): Promise<{ process?: string; variables: { [key: string]: string } }>; $jsonTasksSupported(): Promise<boolean>; $findExecutable(command: string, cwd?: string, paths?: string[]): Promise<string | undefined>; @@ -2123,10 +2145,6 @@ export interface ExtHostNotebookKernelsShape { $cellExecutionChanged(uri: UriComponents, cellHandle: number, state: notebookCommon.NotebookCellExecutionState | undefined): void; } -export interface ExtHostNotebookProxyKernelsShape { - $resolveKernel(handle: number): Promise<string | null>; -} - export interface ExtHostInteractiveShape { $willAddInteractiveDocument(uri: UriComponents, eol: string, languageId: string, notebookUri: UriComponents): void; $willRemoveInteractiveDocument(uri: UriComponents, notebookUri: UriComponents): void; @@ -2303,7 +2321,6 @@ export const MainContext = { MainThreadNotebookDocuments: createProxyIdentifier<MainThreadNotebookDocumentsShape>('MainThreadNotebookDocumentsShape'), MainThreadNotebookEditors: createProxyIdentifier<MainThreadNotebookEditorsShape>('MainThreadNotebookEditorsShape'), MainThreadNotebookKernels: createProxyIdentifier<MainThreadNotebookKernelsShape>('MainThreadNotebookKernels'), - MainThreadNotebookProxyKernels: createProxyIdentifier<MainThreadNotebookProxyKernelsShape>('MainThreadNotebookProxyKernels'), MainThreadNotebookRenderers: createProxyIdentifier<MainThreadNotebookRenderersShape>('MainThreadNotebookRenderers'), MainThreadInteractive: createProxyIdentifier<MainThreadInteractiveShape>('MainThreadInteractive'), MainThreadTheming: createProxyIdentifier<MainThreadThemingShape>('MainThreadTheming'), @@ -2356,7 +2373,6 @@ export const ExtHostContext = { ExtHostNotebookDocuments: createProxyIdentifier<ExtHostNotebookDocumentsShape>('ExtHostNotebookDocuments'), ExtHostNotebookEditors: createProxyIdentifier<ExtHostNotebookEditorsShape>('ExtHostNotebookEditors'), ExtHostNotebookKernels: createProxyIdentifier<ExtHostNotebookKernelsShape>('ExtHostNotebookKernels'), - ExtHostNotebookProxyKernels: createProxyIdentifier<ExtHostNotebookProxyKernelsShape>('ExtHostNotebookProxyKernels'), ExtHostNotebookRenderers: createProxyIdentifier<ExtHostNotebookRenderersShape>('ExtHostNotebookRenderers'), ExtHostInteractive: createProxyIdentifier<ExtHostInteractiveShape>('ExtHostInteractive'), ExtHostTheming: createProxyIdentifier<ExtHostThemingShape>('ExtHostTheming'), diff --git a/src/vs/workbench/api/common/extHostApiCommands.ts b/src/vs/workbench/api/common/extHostApiCommands.ts index 4b866a477f3..8f3c18a769f 100644 --- a/src/vs/workbench/api/common/extHostApiCommands.ts +++ b/src/vs/workbench/api/common/extHostApiCommands.ts @@ -436,6 +436,12 @@ const newCommands: ApiCommand[] = [ 'vscode.revealTestInExplorer', '_revealTestInExplorer', 'Reveals a test instance in the explorer', [ApiCommandArgument.TestItem], ApiCommandResult.Void + ), + // --- continue edit session + new ApiCommand( + 'vscode.experimental.editSession.continue', '_workbench.experimental.editSessions.actions.continueEditSession', 'Continue the current edit session in a different workspace', + [ApiCommandArgument.Uri.with('workspaceUri', 'The target workspace to continue the current edit session in')], + ApiCommandResult.Void ) ]; diff --git a/src/vs/workbench/api/common/extHostAuthentication.ts b/src/vs/workbench/api/common/extHostAuthentication.ts index afc9355c25a..ad9749b474e 100644 --- a/src/vs/workbench/api/common/extHostAuthentication.ts +++ b/src/vs/workbench/api/common/extHostAuthentication.ts @@ -11,6 +11,7 @@ import { IExtensionDescription, ExtensionIdentifier } from 'vs/platform/extensio interface GetSessionsRequest { scopes: string; + providerId: string; result: Promise<vscode.AuthenticationSession | undefined>; } @@ -48,13 +49,14 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { const extensionId = ExtensionIdentifier.toKey(requestingExtension.identifier); const inFlightRequests = this._inFlightRequests.get(extensionId) || []; const sortedScopes = [...scopes].sort().join(' '); - let inFlightRequest: GetSessionsRequest | undefined = inFlightRequests.find(request => request.scopes === sortedScopes); + let inFlightRequest: GetSessionsRequest | undefined = inFlightRequests.find(request => request.providerId === providerId && request.scopes === sortedScopes); if (inFlightRequest) { return inFlightRequest.result; } else { const session = this._getSession(requestingExtension, extensionId, providerId, scopes, options); inFlightRequest = { + providerId, scopes: sortedScopes, result: session }; @@ -65,7 +67,7 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { try { await session; } finally { - const requestIndex = inFlightRequests.findIndex(request => request.scopes === sortedScopes); + const requestIndex = inFlightRequests.findIndex(request => request.providerId === providerId && request.scopes === sortedScopes); if (requestIndex > -1) { inFlightRequests.splice(requestIndex); this._inFlightRequests.set(extensionId, inFlightRequests); diff --git a/src/vs/workbench/api/common/extHostBulkEdits.ts b/src/vs/workbench/api/common/extHostBulkEdits.ts index 6547c861438..2126d41724e 100644 --- a/src/vs/workbench/api/common/extHostBulkEdits.ts +++ b/src/vs/workbench/api/common/extHostBulkEdits.ts @@ -3,10 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { MainContext, MainThreadBulkEditsShape } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { WorkspaceEdit } from 'vs/workbench/api/common/extHostTypeConverters'; +import { isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import type * as vscode from 'vscode'; export class ExtHostBulkEdits { @@ -26,8 +28,9 @@ export class ExtHostBulkEdits { }; } - applyWorkspaceEdit(edit: vscode.WorkspaceEdit): Promise<boolean> { - const dto = WorkspaceEdit.from(edit, this._versionInformationProvider); + applyWorkspaceEdit(edit: vscode.WorkspaceEdit, extension: IExtensionDescription): Promise<boolean> { + const allowSnippetTextEdit = isProposedApiEnabled(extension, 'snippetWorkspaceEdit'); + const dto = WorkspaceEdit.from(edit, this._versionInformationProvider, allowSnippetTextEdit); return this._proxy.$tryApplyWorkspaceEdit(dto); } } diff --git a/src/vs/workbench/api/common/extHostCodeInsets.ts b/src/vs/workbench/api/common/extHostCodeInsets.ts index e91a8eae63c..5caf786ffa0 100644 --- a/src/vs/workbench/api/common/extHostCodeInsets.ts +++ b/src/vs/workbench/api/common/extHostCodeInsets.ts @@ -134,8 +134,6 @@ export class ExtHostEditorInsets implements ExtHostEditorInsetsShape { $onDidReceiveMessage(handle: number, message: any): void { const value = this._insets.get(handle); - if (value) { - value.onDidReceiveMessage.fire(message); - } + value?.onDidReceiveMessage.fire(message); } } diff --git a/src/vs/workbench/api/common/extHostCommands.ts b/src/vs/workbench/api/common/extHostCommands.ts index d0ae8f53ff4..62c9a3c5c9b 100644 --- a/src/vs/workbench/api/common/extHostCommands.ts +++ b/src/vs/workbench/api/common/extHostCommands.ts @@ -169,8 +169,11 @@ export class ExtHostCommands implements ExtHostCommandsShape { private async _doExecuteCommand<T>(id: string, args: any[], retry: boolean): Promise<T> { if (this._commands.has(id)) { - // we stay inside the extension host and support - // to pass any kind of parameters around + // - We stay inside the extension host and support + // to pass any kind of parameters around. + // - We still emit the corresponding activation event + // BUT we don't await that event + this.#proxy.$fireCommandActivationEvent(id); return this._executeContributedCommand<T>(id, args, false); } else { @@ -206,7 +209,7 @@ export class ExtHostCommands implements ExtHostCommandsShape { } catch (e) { // Rerun the command when it wasn't known, had arguments, and when retry // is enabled. We do this because the command might be registered inside - // the extension host now and can therfore accept the arguments as-is. + // the extension host now and can therefore accept the arguments as-is. if (e instanceof Error && e.message === '$executeCommand:retry') { return this._doExecuteCommand(id, args, false); } else { @@ -222,7 +225,7 @@ export class ExtHostCommands implements ExtHostCommandsShape { throw new Error('Unknown command'); } this._reportTelemetry(command, id); - let { callback, thisArg, description } = command; + const { callback, thisArg, description } = command; if (description) { for (let i = 0; i < description.args.length; i++) { try { @@ -304,8 +307,8 @@ export class ExtHostCommands implements ExtHostCommandsShape { $getContributedCommandHandlerDescriptions(): Promise<{ [id: string]: string | ICommandHandlerDescriptionDto }> { const result: { [id: string]: string | ICommandHandlerDescription } = Object.create(null); - for (let [id, command] of this._commands) { - let { description } = command; + for (const [id, command] of this._commands) { + const { description } = command; if (description) { result[id] = description; } diff --git a/src/vs/workbench/api/common/extHostComments.ts b/src/vs/workbench/api/common/extHostComments.ts index 0a5314e916f..2259201d139 100644 --- a/src/vs/workbench/api/common/extHostComments.ts +++ b/src/vs/workbench/api/common/extHostComments.ts @@ -96,9 +96,9 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo return arg; } - let commentUniqueId = arg.commentUniqueId; + const commentUniqueId = arg.commentUniqueId; - let comment = commentThread.getCommentByUniqueId(commentUniqueId); + const comment = commentThread.getCommentByUniqueId(commentUniqueId); if (!comment) { return arg; @@ -119,10 +119,10 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo return arg; } - let body = arg.text; - let commentUniqueId = arg.commentUniqueId; + const body = arg.text; + const commentUniqueId = arg.commentUniqueId; - let comment = commentThread.getCommentByUniqueId(commentUniqueId); + const comment = commentThread.getCommentByUniqueId(commentUniqueId); if (!comment) { return arg; @@ -172,9 +172,7 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo $deleteCommentThread(commentControllerHandle: number, commentThreadHandle: number) { const commentController = this._commentControllers.get(commentControllerHandle); - if (commentController) { - commentController.$deleteCommentThread(commentThreadHandle); - } + commentController?.$deleteCommentThread(commentThreadHandle); } $provideCommentingRanges(commentControllerHandle: number, uriComponents: UriComponents, token: CancellationToken): Promise<IRange[] | undefined> { @@ -487,9 +485,9 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo } getCommentByUniqueId(uniqueId: number): vscode.Comment | undefined { - for (let key of this._commentsMap) { - let comment = key[0]; - let id = key[1]; + for (const key of this._commentsMap) { + const comment = key[0]; + const id = key[1]; if (uniqueId === id) { return comment; } @@ -613,14 +611,14 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo } $updateCommentThreadTemplate(threadHandle: number, range: IRange): void { - let thread = this._threads.get(threadHandle); + const thread = this._threads.get(threadHandle); if (thread) { thread.range = extHostTypeConverter.Range.to(range); } } $deleteCommentThread(threadHandle: number): void { - let thread = this._threads.get(threadHandle); + const thread = this._threads.get(threadHandle); if (thread) { thread.dispose(); diff --git a/src/vs/workbench/api/common/extHostConfiguration.ts b/src/vs/workbench/api/common/extHostConfiguration.ts index 3a45d62f28f..2e9c5b7ada8 100644 --- a/src/vs/workbench/api/common/extHostConfiguration.ts +++ b/src/vs/workbench/api/common/extHostConfiguration.ts @@ -256,13 +256,13 @@ export class ExtHostConfigProvider { return { key, - defaultValue: config.default?.value, - globalValue: config.user?.value, + defaultValue: config.policy?.value ?? config.default?.value, + globalValue: config.user?.value ?? config.application?.value, workspaceValue: config.workspace?.value, workspaceFolderValue: config.workspaceFolder?.value, defaultLanguageValue: config.default?.override, - globalLanguageValue: config.user?.override, + globalLanguageValue: config.user?.override ?? config.application?.override, workspaceLanguageValue: config.workspace?.override, workspaceFolderLanguageValue: config.workspaceFolder?.override, diff --git a/src/vs/workbench/api/common/extHostConsoleForwarder.ts b/src/vs/workbench/api/common/extHostConsoleForwarder.ts new file mode 100644 index 00000000000..c444649a9cb --- /dev/null +++ b/src/vs/workbench/api/common/extHostConsoleForwarder.ts @@ -0,0 +1,124 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IStackArgument } from 'vs/base/common/console'; +import { safeStringify } from 'vs/base/common/objects'; +import { MainContext, MainThreadConsoleShape } from 'vs/workbench/api/common/extHost.protocol'; +import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; +import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; + +export abstract class AbstractExtHostConsoleForwarder { + + private readonly _mainThreadConsole: MainThreadConsoleShape; + private readonly _includeStack: boolean; + private readonly _logNative: boolean; + + constructor( + @IExtHostRpcService extHostRpc: IExtHostRpcService, + @IExtHostInitDataService initData: IExtHostInitDataService, + ) { + this._mainThreadConsole = extHostRpc.getProxy(MainContext.MainThreadConsole); + this._includeStack = initData.consoleForward.includeStack; + this._logNative = initData.consoleForward.logNative; + + // Pass console logging to the outside so that we have it in the main side if told so + this._wrapConsoleMethod('info', 'log'); + this._wrapConsoleMethod('log', 'log'); + this._wrapConsoleMethod('warn', 'warn'); + this._wrapConsoleMethod('error', 'error'); + } + + /** + * Wraps a console message so that it is transmitted to the renderer. If + * native logging is turned on, the original console message will be written + * as well. This is needed since the console methods are "magic" in V8 and + * are the only methods that allow later introspection of logged variables. + * + * The wrapped property is not defined with `writable: false` to avoid + * throwing errors, but rather a no-op setting. See https://github.com/microsoft/vscode-extension-telemetry/issues/88 + */ + private _wrapConsoleMethod(method: 'log' | 'info' | 'warn' | 'error', severity: 'log' | 'warn' | 'error') { + const that = this; + const original = console[method]; + + Object.defineProperty(console, method, { + set: () => { }, + get: () => function () { + that._handleConsoleCall(method, severity, original, arguments); + }, + }); + } + + private _handleConsoleCall(method: 'log' | 'info' | 'warn' | 'error', severity: 'log' | 'warn' | 'error', original: (...args: any[]) => void, args: IArguments): void { + this._mainThreadConsole.$logExtensionHostMessage({ + type: '__$console', + severity, + arguments: safeStringifyArgumentsToArray(args, this._includeStack) + }); + if (this._logNative) { + this._nativeConsoleLogMessage(method, original, args); + } + } + + protected abstract _nativeConsoleLogMessage(method: 'log' | 'info' | 'warn' | 'error', original: (...args: any[]) => void, args: IArguments): void; + +} + +const MAX_LENGTH = 100000; + +/** + * Prevent circular stringify and convert arguments to real array + */ +function safeStringifyArgumentsToArray(args: IArguments, includeStack: boolean): string { + const argsArray = []; + + // Massage some arguments with special treatment + if (args.length) { + for (let i = 0; i < args.length; i++) { + let arg = args[i]; + + // Any argument of type 'undefined' needs to be specially treated because + // JSON.stringify will simply ignore those. We replace them with the string + // 'undefined' which is not 100% right, but good enough to be logged to console + if (typeof arg === 'undefined') { + arg = 'undefined'; + } + + // Any argument that is an Error will be changed to be just the error stack/message + // itself because currently cannot serialize the error over entirely. + else if (arg instanceof Error) { + const errorObj = arg; + if (errorObj.stack) { + arg = errorObj.stack; + } else { + arg = errorObj.toString(); + } + } + + argsArray.push(arg); + } + } + + // Add the stack trace as payload if we are told so. We remove the message and the 2 top frames + // to start the stacktrace where the console message was being written + if (includeStack) { + const stack = new Error().stack; + if (stack) { + argsArray.push({ __$stack: stack.split('\n').slice(3).join('\n') } as IStackArgument); + } + } + + try { + const res = safeStringify(argsArray); + + if (res.length > MAX_LENGTH) { + return 'Output omitted for a large object that exceeds the limits'; + } + + return res; + } catch (error) { + return `Output omitted for an object that cannot be inspected ('${error.toString()}')`; + } +} diff --git a/src/vs/workbench/api/common/extHostDebugService.ts b/src/vs/workbench/api/common/extHostDebugService.ts index 178ab59c5ef..9e0e03f1379 100644 --- a/src/vs/workbench/api/common/extHostDebugService.ts +++ b/src/vs/workbench/api/common/extHostDebugService.ts @@ -495,9 +495,7 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E } const da = this._debugAdapters.get(debugAdapterHandle); - if (da) { - da.sendMessage(message); - } + da?.sendMessage(message); } public $stopDASession(debugAdapterHandle: number): Promise<void> { @@ -663,9 +661,7 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E public async $acceptDebugSessionNameChanged(sessionDto: IDebugSessionDto, name: string): Promise<void> { const session = await this.getSession(sessionDto); - if (session) { - session._acceptNameChanged(name); - } + session?._acceptNameChanged(name); } public async $acceptDebugSessionCustomEvent(sessionDto: IDebugSessionDto, event: any): Promise<void> { diff --git a/src/vs/workbench/api/common/extHostDecorations.ts b/src/vs/workbench/api/common/extHostDecorations.ts index ea4858374ac..8641121c9b1 100644 --- a/src/vs/workbench/api/common/extHostDecorations.ts +++ b/src/vs/workbench/api/common/extHostDecorations.ts @@ -47,7 +47,7 @@ export class ExtHostDecorations implements ExtHostDecorationsShape { this._proxy.$onDidChange(handle, null); return; } - let array = asArray(e); + const array = asArray(e); if (array.length <= ExtHostDecorations._maxEventSize) { this._proxy.$onDidChange(handle, array); return; @@ -58,11 +58,11 @@ export class ExtHostDecorations implements ExtHostDecorationsShape { this._logService.warn('[Decorations] CAPPING events from decorations provider', extensionId.value, array.length); const mapped = array.map(uri => ({ uri, rank: count(uri.path, '/') })); const groups = groupBy(mapped, (a, b) => a.rank - b.rank || compare(a.uri.path, b.uri.path)); - let picked: URI[] = []; - outer: for (let uris of groups) { + const picked: URI[] = []; + outer: for (const uris of groups) { let lastDirname: string | undefined; - for (let obj of uris) { - let myDirname = dirname(obj.uri.path); + for (const obj of uris) { + const myDirname = dirname(obj.uri.path); if (lastDirname !== myDirname) { lastDirname = myDirname; if (picked.push(obj.uri) >= ExtHostDecorations._maxEventSize) { diff --git a/src/vs/workbench/api/common/extHostDiagnostics.ts b/src/vs/workbench/api/common/extHostDiagnostics.ts index a81ce5faf65..81cf1f2b756 100644 --- a/src/vs/workbench/api/common/extHostDiagnostics.ts +++ b/src/vs/workbench/api/common/extHostDiagnostics.ts @@ -41,9 +41,7 @@ export class DiagnosticCollection implements vscode.DiagnosticCollection { dispose(): void { if (!this._isDisposed) { this.#onDidChangeDiagnostics.fire([...this.#data.keys()]); - if (this.#proxy) { - this.#proxy.$clear(this._owner); - } + this.#proxy?.$clear(this._owner); this.#data.clear(); this._isDisposed = true; } @@ -108,9 +106,7 @@ export class DiagnosticCollection implements vscode.DiagnosticCollection { } } else { const currentDiagnostics = this.#data.get(uri); - if (currentDiagnostics) { - currentDiagnostics.push(...diagnostics); - } + currentDiagnostics?.push(...diagnostics); } } } @@ -123,7 +119,7 @@ export class DiagnosticCollection implements vscode.DiagnosticCollection { return; } const entries: [URI, IMarkerData[]][] = []; - for (let uri of toSync) { + for (const uri of toSync) { let marker: IMarkerData[] = []; const diagnostics = this.#data.get(uri); if (diagnostics) { @@ -133,7 +129,7 @@ export class DiagnosticCollection implements vscode.DiagnosticCollection { marker = []; const order = [DiagnosticSeverity.Error, DiagnosticSeverity.Warning, DiagnosticSeverity.Information, DiagnosticSeverity.Hint]; orderLoop: for (let i = 0; i < 4; i++) { - for (let diagnostic of diagnostics) { + for (const diagnostic of diagnostics) { if (diagnostic.severity === order[i]) { const len = marker.push(converter.Diagnostic.from(diagnostic)); if (len === this._maxDiagnosticsPerFile) { @@ -166,24 +162,27 @@ export class DiagnosticCollection implements vscode.DiagnosticCollection { this._checkDisposed(); this.#onDidChangeDiagnostics.fire([uri]); this.#data.delete(uri); - if (this.#proxy) { - this.#proxy.$changeMany(this._owner, [[uri, undefined]]); - } + this.#proxy?.$changeMany(this._owner, [[uri, undefined]]); } clear(): void { this._checkDisposed(); this.#onDidChangeDiagnostics.fire([...this.#data.keys()]); this.#data.clear(); - if (this.#proxy) { - this.#proxy.$clear(this._owner); - } + this.#proxy?.$clear(this._owner); } forEach(callback: (uri: URI, diagnostics: ReadonlyArray<vscode.Diagnostic>, collection: DiagnosticCollection) => any, thisArg?: any): void { this._checkDisposed(); - for (let uri of this.#data.keys()) { - callback.apply(thisArg, [uri, this.get(uri), this]); + for (const [uri, values] of this) { + callback.call(thisArg, uri, values, this); + } + } + + *[Symbol.iterator](): IterableIterator<[uri: vscode.Uri, diagnostics: readonly vscode.Diagnostic[]]> { + this._checkDisposed(); + for (const uri of this.#data.keys()) { + yield [uri, this.get(uri)]; } } @@ -317,7 +316,7 @@ export class ExtHostDiagnostics implements ExtHostDiagnosticsShape { private _getDiagnostics(resource: vscode.Uri): ReadonlyArray<vscode.Diagnostic> { let res: vscode.Diagnostic[] = []; - for (let collection of this._collections.values()) { + for (const collection of this._collections.values()) { if (collection.has(resource)) { res = res.concat(collection.get(resource)); } diff --git a/src/vs/workbench/api/common/extHostDocumentSaveParticipant.ts b/src/vs/workbench/api/common/extHostDocumentSaveParticipant.ts index c3d05a440e8..05f7cb963d6 100644 --- a/src/vs/workbench/api/common/extHostDocumentSaveParticipant.ts +++ b/src/vs/workbench/api/common/extHostDocumentSaveParticipant.ts @@ -55,7 +55,7 @@ export class ExtHostDocumentSaveParticipant implements ExtHostDocumentSavePartic const results: boolean[] = []; try { - for (let listener of [...this._callbacks]) { // copy to prevent concurrent modifications + for (const listener of [...this._callbacks]) { // copy to prevent concurrent modifications if (didTimeout) { // timeout - no more listeners break; diff --git a/src/vs/workbench/api/common/extHostEditorTabs.ts b/src/vs/workbench/api/common/extHostEditorTabs.ts index 1fbc2d487de..21a42adb1e4 100644 --- a/src/vs/workbench/api/common/extHostEditorTabs.ts +++ b/src/vs/workbench/api/common/extHostEditorTabs.ts @@ -195,7 +195,7 @@ class ExtHostEditorTabGroup { this._activeTabId = operation.tabDto.id; } else if (this._activeTabId === operation.tabDto.id && !operation.tabDto.isActive) { // Events aren't guaranteed to be in order so if we receive a dto that matches the active tab id - // but isn't active we mark the active tab id as empty. This prevent onDidActiveTabChange frorm + // but isn't active we mark the active tab id as empty. This prevent onDidActiveTabChange from // firing incorrectly this._activeTabId = ''; } @@ -256,12 +256,12 @@ export class ExtHostEditorTabs implements IExtHostEditorTabs { return this._closeTabs(tabsOrTabGroups as vscode.Tab[], preserveFocus); } }, - // move: async (tab: vscode.Tab, viewColumn: ViewColumn, index: number, preservceFocus?: boolean) => { + // move: async (tab: vscode.Tab, viewColumn: ViewColumn, index: number, preserveFocus?: boolean) => { // const extHostTab = this._findExtHostTabFromApi(tab); // if (!extHostTab) { // throw new Error('Invalid tab'); // } - // this._proxy.$moveTab(extHostTab.tabId, index, typeConverters.ViewColumn.from(viewColumn), preservceFocus); + // this._proxy.$moveTab(extHostTab.tabId, index, typeConverters.ViewColumn.from(viewColumn), preserveFocus); // return; // } }; diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts index 49eaa65d462..337929b481d 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts @@ -424,6 +424,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme private _logExtensionActivationTimes(extensionDescription: IExtensionDescription, reason: ExtensionActivationReason, outcome: string, activationTimes?: ExtensionActivationTimes) { const event = getTelemetryActivationEvent(extensionDescription, reason); type ExtensionActivationTimesClassification = { + owner: 'jrieken'; outcome: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; } & TelemetryActivationEventFragment & ExtensionActivationTimesFragment; @@ -447,7 +448,9 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme private _doActivateExtension(extensionDescription: IExtensionDescription, reason: ExtensionActivationReason): Promise<ActivatedExtension> { const event = getTelemetryActivationEvent(extensionDescription, reason); - type ActivatePluginClassification = {} & TelemetryActivationEventFragment; + type ActivatePluginClassification = { + owner: 'jrieken'; + } & TelemetryActivationEventFragment; this._mainThreadTelemetryProxy.$publicLog2<TelemetryActivationEvent, ActivatePluginClassification>('activatePlugin', event); const entryPoint = this._getEntryPoint(extensionDescription); if (!entryPoint) { @@ -709,10 +712,6 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme }); } - public async $extensionTestsExit(code: number): Promise<void> { - this.terminate(`test runner requested exit with code ${code}`, code); - } - private _startExtensionHost(): Promise<void> { if (this._started) { throw new Error(`Extension host is already started!`); @@ -907,8 +906,8 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme } public async $test_down(size: number): Promise<VSBuffer> { - let buff = VSBuffer.alloc(size); - let value = Math.random() % 256; + const buff = VSBuffer.alloc(size); + const value = Math.random() % 256; for (let i = 0; i < size; i++) { buff.writeUInt8(value, i); } diff --git a/src/vs/workbench/api/common/extHostFileSystem.ts b/src/vs/workbench/api/common/extHostFileSystem.ts index 208bc41aabd..1b47763c98e 100644 --- a/src/vs/workbench/api/common/extHostFileSystem.ts +++ b/src/vs/workbench/api/common/extHostFileSystem.ts @@ -170,7 +170,7 @@ export class ExtHostFileSystem implements ExtHostFileSystemShape { const subscription = provider.onDidChangeFile(event => { const mapped: IFileChangeDto[] = []; for (const e of event) { - let { uri: resource, type } = e; + const { uri: resource, type } = e; if (resource.scheme !== scheme) { // dropping events for wrong scheme continue; diff --git a/src/vs/workbench/api/common/extHostFileSystemEventService.ts b/src/vs/workbench/api/common/extHostFileSystemEventService.ts index 3a8829425de..d4f86a58255 100644 --- a/src/vs/workbench/api/common/extHostFileSystemEventService.ts +++ b/src/vs/workbench/api/common/extHostFileSystemEventService.ts @@ -63,7 +63,7 @@ class FileSystemWatcher implements vscode.FileSystemWatcher { const subscription = dispatcher(events => { if (!ignoreCreateEvents) { - for (let created of events.created) { + for (const created of events.created) { const uri = URI.revive(created); if (parsedPattern(uri.fsPath) && (!excludeOutOfWorkspaceEvents || workspace.getWorkspaceFolder(uri))) { this._onDidCreate.fire(uri); @@ -71,7 +71,7 @@ class FileSystemWatcher implements vscode.FileSystemWatcher { } } if (!ignoreChangeEvents) { - for (let changed of events.changed) { + for (const changed of events.changed) { const uri = URI.revive(changed); if (parsedPattern(uri.fsPath) && (!excludeOutOfWorkspaceEvents || workspace.getWorkspaceFolder(uri))) { this._onDidChange.fire(uri); @@ -79,7 +79,7 @@ class FileSystemWatcher implements vscode.FileSystemWatcher { } } if (!ignoreDeleteEvents) { - for (let deleted of events.deleted) { + for (const deleted of events.deleted) { const uri = URI.revive(deleted); if (parsedPattern(uri.fsPath) && (!excludeOutOfWorkspaceEvents || workspace.getWorkspaceFolder(uri))) { this._onDidDelete.fire(uri); @@ -92,7 +92,7 @@ class FileSystemWatcher implements vscode.FileSystemWatcher { } private ensureWatching(mainContext: IMainContext, extension: IExtensionDescription, globPattern: string | IRelativePatternDto): Disposable { - let disposable = Disposable.from(); + const disposable = Disposable.from(); if (typeof globPattern === 'string') { return disposable; // a pattern alone does not carry sufficient information to start watching anything @@ -249,8 +249,8 @@ export class ExtHostFileSystemEventService implements ExtHostFileSystemEventServ // concat all WorkspaceEdits collected via waitUntil-call and send them over to the renderer const dto: IWorkspaceEditDto = { edits: [] }; - for (let edit of edits) { - let { edits } = typeConverter.WorkspaceEdit.from(edit, { + for (const edit of edits) { + const { edits } = typeConverter.WorkspaceEdit.from(edit, { getTextDocumentVersion: uri => this._extHostDocumentsAndEditors.getDocument(uri)?.version, getNotebookDocumentVersion: () => undefined, }); diff --git a/src/vs/workbench/api/common/extHostInteractive.ts b/src/vs/workbench/api/common/extHostInteractive.ts index b492b0c9ed4..b53d846c2e1 100644 --- a/src/vs/workbench/api/common/extHostInteractive.ts +++ b/src/vs/workbench/api/common/extHostInteractive.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { URI, UriComponents } from 'vs/base/common/uri'; +import { ILogService } from 'vs/platform/log/common/log'; import { ExtHostInteractiveShape, IMainContext } from 'vs/workbench/api/common/extHost.protocol'; import { ApiCommand, ApiCommandArgument, ApiCommandResult, ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; @@ -15,7 +16,8 @@ export class ExtHostInteractive implements ExtHostInteractiveShape { mainContext: IMainContext, private _extHostNotebooks: ExtHostNotebookController, private _textDocumentsAndEditors: ExtHostDocumentsAndEditors, - private _commands: ExtHostCommands + private _commands: ExtHostCommands, + _logService: ILogService ) { const openApiCommand = new ApiCommand( 'interactive.open', @@ -28,10 +30,13 @@ export class ExtHostInteractive implements ExtHostInteractiveShape { new ApiCommandArgument('title', 'Interactive editor title', v => true, v => v) ], new ApiCommandResult<{ notebookUri: UriComponents; inputUri: UriComponents; notebookEditorId?: string }, { notebookUri: URI; inputUri: URI; notebookEditor?: NotebookEditor }>('Notebook and input URI', (v: { notebookUri: UriComponents; inputUri: UriComponents; notebookEditorId?: string }) => { + _logService.debug('[ExtHostInteractive] open iw with notebook editor id', v.notebookEditorId); if (v.notebookEditorId !== undefined) { const editor = this._extHostNotebooks.getEditorById(v.notebookEditorId); + _logService.debug('[ExtHostInteractive] notebook editor found', editor.id); return { notebookUri: URI.revive(v.notebookUri), inputUri: URI.revive(v.inputUri), notebookEditor: editor.apiEditor }; } + _logService.debug('[ExtHostInteractive] notebook editor not found, uris for the interactive document', v.notebookUri, v.inputUri); return { notebookUri: URI.revive(v.notebookUri), inputUri: URI.revive(v.inputUri) }; }) ); diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 3b6b789641a..afdb6fbb015 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -31,11 +31,9 @@ import { IdGenerator } from 'vs/base/common/idGenerator'; import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService'; import { Cache } from './cache'; import { StopWatch } from 'vs/base/common/stopwatch'; -import { isCancellationError } from 'vs/base/common/errors'; +import { isCancellationError, NotImplementedError } from 'vs/base/common/errors'; import { raceCancellationError } from 'vs/base/common/async'; import { isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; -import { DataTransferConverter, DataTransferDTO } from 'vs/workbench/api/common/shared/dataTransfer'; -import { Dto } from 'vs/workbench/services/extensions/common/proxyIdentifier'; // --- adapter @@ -90,9 +88,7 @@ class DocumentSymbolAdapter { } const parent = parentStack[parentStack.length - 1]; if (EditorRange.containsRange(parent.range, element.range) && !EditorRange.equalsRange(parent.range, element.range)) { - if (parent.children) { - parent.children.push(element); - } + parent.children?.push(element); parentStack.push(element); break; } @@ -486,6 +482,51 @@ class CodeActionAdapter { } } +class DocumentPasteEditProvider { + + constructor( + private readonly _proxy: extHostProtocol.MainThreadLanguageFeaturesShape, + private readonly _documents: ExtHostDocuments, + private readonly _provider: vscode.DocumentPasteEditProvider, + private readonly _handle: number, + ) { } + + async prepareDocumentPaste(resource: URI, ranges: IRange[], dataTransferDto: extHostProtocol.DataTransferDTO, token: CancellationToken): Promise<extHostProtocol.DataTransferDTO | undefined> { + if (!this._provider.prepareDocumentPaste) { + return undefined; + } + + const doc = this._documents.getDocument(resource); + const vscodeRanges = ranges.map(range => typeConvert.Range.to(range)); + + const dataTransfer = typeConvert.DataTransfer.toDataTransfer(dataTransferDto, () => { + throw new NotImplementedError(); + }); + await this._provider.prepareDocumentPaste(doc, vscodeRanges, dataTransfer, token); + + return typeConvert.DataTransfer.toDataTransferDTO(dataTransfer); + } + + async providePasteEdits(requestId: number, resource: URI, ranges: IRange[], dataTransferDto: extHostProtocol.DataTransferDTO, token: CancellationToken): Promise<undefined | extHostProtocol.IPasteEditDto> { + const doc = this._documents.getDocument(resource); + const vscodeRanges = ranges.map(range => typeConvert.Range.to(range)); + + const dataTransfer = typeConvert.DataTransfer.toDataTransfer(dataTransferDto, async (index) => { + return (await this._proxy.$resolveDocumentOnDropFileData(this._handle, requestId, index)).buffer; + }); + + const edit = await this._provider.provideDocumentPasteEdits(doc, vscodeRanges, dataTransfer, token); + if (!edit) { + return; + } + + return { + insertText: typeof edit.insertText === 'string' ? edit.insertText : { snippet: edit.insertText.value }, + additionalEdit: edit.additionalEdit ? typeConvert.WorkspaceEdit.from(edit.additionalEdit) : undefined, + }; + } +} + class DocumentFormattingAdapter { constructor( @@ -1409,7 +1450,7 @@ class InlayHintsAdapter { result.label = hint.label; } else { result.label = hint.label.map(part => { - let result: languages.InlayHintLabelPart = { label: part.value }; + const result: languages.InlayHintLabelPart = { label: part.value }; result.tooltip = typeConvert.MarkdownString.fromStrict(part.tooltip); if (Location.isLocation(part.location)) { result.location = typeConvert.location.from(part.location); @@ -1754,10 +1795,10 @@ class DocumentOnDropEditAdapter { private readonly _handle: number, ) { } - async provideDocumentOnDropEdits(requestId: number, uri: URI, position: IPosition, dataTransferDto: DataTransferDTO, token: CancellationToken): Promise<Dto<languages.SnippetTextEdit> | undefined> { + async provideDocumentOnDropEdits(requestId: number, uri: URI, position: IPosition, dataTransferDto: extHostProtocol.DataTransferDTO, token: CancellationToken): Promise<extHostProtocol.IDocumentOnDropEditDto | undefined> { const doc = this._documents.getDocument(uri); const pos = typeConvert.Position.to(position); - const dataTransfer = DataTransferConverter.toDataTransfer(dataTransferDto, async (index) => { + const dataTransfer = typeConvert.DataTransfer.toDataTransfer(dataTransferDto, async (index) => { return (await this._proxy.$resolveDocumentOnDropFileData(this._handle, requestId, index)).buffer; }); @@ -1765,12 +1806,15 @@ class DocumentOnDropEditAdapter { if (!edit) { return undefined; } - return typeConvert.SnippetTextEdit.from(edit); + return { + insertText: typeof edit.insertText === 'string' ? edit.insertText : { snippet: edit.insertText.value }, + additionalEdit: edit.additionalEdit ? typeConvert.WorkspaceEdit.from(edit.additionalEdit) : undefined, + }; } } type Adapter = DocumentSymbolAdapter | CodeLensAdapter | DefinitionAdapter | HoverAdapter - | DocumentHighlightAdapter | ReferenceAdapter | CodeActionAdapter | DocumentFormattingAdapter + | DocumentHighlightAdapter | ReferenceAdapter | CodeActionAdapter | DocumentPasteEditProvider | DocumentFormattingAdapter | RangeFormattingAdapter | OnTypeFormattingAdapter | NavigateTypeAdapter | RenameAdapter | CompletionsAdapter | SignatureHelpAdapter | LinkProviderAdapter | ImplementationAdapter | TypeDefinitionAdapter | ColorProviderAdapter | FoldingProviderAdapter | DeclarationAdapter @@ -2409,11 +2453,28 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF return this._createDisposable(handle); } - $provideDocumentOnDropEdits(handle: number, requestId: number, resource: UriComponents, position: IPosition, dataTransferDto: DataTransferDTO, token: CancellationToken): Promise<Dto<languages.SnippetTextEdit> | undefined> { + $provideDocumentOnDropEdits(handle: number, requestId: number, resource: UriComponents, position: IPosition, dataTransferDto: extHostProtocol.DataTransferDTO, token: CancellationToken): Promise<extHostProtocol.IDocumentOnDropEditDto | undefined> { return this._withAdapter(handle, DocumentOnDropEditAdapter, adapter => Promise.resolve(adapter.provideDocumentOnDropEdits(requestId, URI.revive(resource), position, dataTransferDto, token)), undefined, undefined); } + // --- copy/paste actions + + registerDocumentPasteEditProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.DocumentPasteEditProvider, metadata: vscode.DocumentPasteProviderMetadata): vscode.Disposable { + const handle = this._nextHandle(); + this._adapter.set(handle, new AdapterData(new DocumentPasteEditProvider(this._proxy, this._documents, provider, handle), extension)); + this._proxy.$registerPasteEditProvider(handle, this._transformDocumentSelector(selector), !!provider.prepareDocumentPaste, metadata.pasteMimeTypes); + return this._createDisposable(handle); + } + + $prepareDocumentPaste(handle: number, resource: UriComponents, ranges: IRange[], dataTransfer: extHostProtocol.DataTransferDTO, token: CancellationToken): Promise<extHostProtocol.DataTransferDTO | undefined> { + return this._withAdapter(handle, DocumentPasteEditProvider, adapter => adapter.prepareDocumentPaste(URI.revive(resource), ranges, dataTransfer, token), undefined, token); + } + + $providePasteEdits(handle: number, resource: UriComponents, ranges: IRange[], dataTransferDto: extHostProtocol.DataTransferDTO, token: CancellationToken): Promise<extHostProtocol.IPasteEditDto | undefined> { + return this._withAdapter(handle, DocumentPasteEditProvider, adapter => adapter.providePasteEdits(0, URI.revive(resource), ranges, dataTransferDto, token), undefined, token); + } + // --- configuration private static _serializeRegExp(regExp: RegExp): extHostProtocol.IRegExpDto { @@ -2446,7 +2507,7 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF } setLanguageConfiguration(extension: IExtensionDescription, languageId: string, configuration: vscode.LanguageConfiguration): vscode.Disposable { - let { wordPattern } = configuration; + const { wordPattern } = configuration; // check for a valid word pattern if (wordPattern && regExpLeadsToEndlessLoop(wordPattern)) { diff --git a/src/vs/workbench/api/common/extHostLanguages.ts b/src/vs/workbench/api/common/extHostLanguages.ts index 908b194f58d..a0d1bbb5a77 100644 --- a/src/vs/workbench/api/common/extHostLanguages.ts +++ b/src/vs/workbench/api/common/extHostLanguages.ts @@ -102,7 +102,7 @@ export class ExtHostLanguages implements ExtHostLanguagesShape { }; let soonHandle: IDisposable | undefined; - let commandDisposables = new DisposableStore(); + const commandDisposables = new DisposableStore(); const updateAsync = () => { soonHandle?.dispose(); soonHandle = disposableTimeout(() => { diff --git a/src/vs/workbench/api/common/extHostMemento.ts b/src/vs/workbench/api/common/extHostMemento.ts index b18c4fce9ac..c309007bc30 100644 --- a/src/vs/workbench/api/common/extHostMemento.ts +++ b/src/vs/workbench/api/common/extHostMemento.ts @@ -78,7 +78,7 @@ export class ExtensionMemento implements vscode.Memento { update(key: string, value: any): Promise<void> { this._value![key] = value; - let record = this._deferredPromises.get(key); + const record = this._deferredPromises.get(key); if (record !== undefined) { return record.p; } diff --git a/src/vs/workbench/api/common/extHostMessageService.ts b/src/vs/workbench/api/common/extHostMessageService.ts index 93f71893ee7..88aa7983aa0 100644 --- a/src/vs/workbench/api/common/extHostMessageService.ts +++ b/src/vs/workbench/api/common/extHostMessageService.ts @@ -56,7 +56,7 @@ export class ExtHostMessageService { if (typeof command === 'string') { commands.push({ title: command, handle, isCloseAffordance: false }); } else if (typeof command === 'object') { - let { title, isCloseAffordance } = command; + const { title, isCloseAffordance } = command; commands.push({ title, isCloseAffordance: !!isCloseAffordance, handle }); } else { this._logService.warn('Invalid message item:', command); diff --git a/src/vs/workbench/api/common/extHostNotebookDocument.ts b/src/vs/workbench/api/common/extHostNotebookDocument.ts index b77133347e3..240ae6eede7 100644 --- a/src/vs/workbench/api/common/extHostNotebookDocument.ts +++ b/src/vs/workbench/api/common/extHostNotebookDocument.ts @@ -366,7 +366,7 @@ export class ExtHostNotebookDocument { }); if (bucket) { - for (let changeEvent of contentChangeEvents) { + for (const changeEvent of contentChangeEvents) { bucket.push(changeEvent.asApiEvent()); } } diff --git a/src/vs/workbench/api/common/extHostNotebookEditor.ts b/src/vs/workbench/api/common/extHostNotebookEditor.ts index b18e816f181..ca7037eca64 100644 --- a/src/vs/workbench/api/common/extHostNotebookEditor.ts +++ b/src/vs/workbench/api/common/extHostNotebookEditor.ts @@ -81,7 +81,6 @@ export class ExtHostNotebookEditor { private _viewColumn?: vscode.ViewColumn; private _visible: boolean = false; - private readonly _hasDecorationsForKey = new Set<string>(); private _editor?: vscode.NotebookEditor; @@ -142,9 +141,6 @@ export class ExtHostNotebookEditor { callback(edit); return that._applyEdit(edit.finalize()); }, - setDecorations(decorationType, range) { - return that.setDecorations(decorationType, range); - } }; ExtHostNotebookEditor.apiEditorsToExtHost.set(this._editor, this); @@ -211,22 +207,4 @@ export class ExtHostNotebookEditor { return this._proxy.$tryApplyEdits(this.id, editData.documentVersionId, compressedEdits); } - - setDecorations(decorationType: vscode.NotebookEditorDecorationType, range: vscode.NotebookRange): void { - if (range.isEmpty && !this._hasDecorationsForKey.has(decorationType.key)) { - // avoid no-op call to the renderer - return; - } - if (range.isEmpty) { - this._hasDecorationsForKey.delete(decorationType.key); - } else { - this._hasDecorationsForKey.add(decorationType.key); - } - - return this._proxy.$trySetDecorations( - this.id, - extHostConverter.NotebookRange.from(range), - decorationType.key - ); - } } diff --git a/src/vs/workbench/api/common/extHostNotebookEditors.ts b/src/vs/workbench/api/common/extHostNotebookEditors.ts index 6e70ab1c882..5073d92c5e0 100644 --- a/src/vs/workbench/api/common/extHostNotebookEditors.ts +++ b/src/vs/workbench/api/common/extHostNotebookEditors.ts @@ -4,33 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter } from 'vs/base/common/event'; -import { IdGenerator } from 'vs/base/common/idGenerator'; import { ILogService } from 'vs/platform/log/common/log'; -import { ExtHostNotebookEditorsShape, INotebookEditorPropertiesChangeData, INotebookEditorViewColumnInfo, MainContext, MainThreadNotebookEditorsShape } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostNotebookEditorsShape, INotebookEditorPropertiesChangeData, INotebookEditorViewColumnInfo } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook'; -import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import type * as vscode from 'vscode'; -class NotebookEditorDecorationType { - - private static readonly _Keys = new IdGenerator('NotebookEditorDecorationType'); - - readonly value: vscode.NotebookEditorDecorationType; - - constructor(proxy: MainThreadNotebookEditorsShape, options: vscode.NotebookDecorationRenderOptions) { - const key = NotebookEditorDecorationType._Keys.nextId(); - proxy.$registerNotebookEditorDecorationType(key, typeConverters.NotebookDecorationRenderOptions.from(options)); - - this.value = { - key, - dispose() { - proxy.$removeNotebookEditorDecorationType(key); - } - }; - } -} - export class ExtHostNotebookEditors implements ExtHostNotebookEditorsShape { @@ -42,16 +21,8 @@ export class ExtHostNotebookEditors implements ExtHostNotebookEditorsShape { constructor( @ILogService private readonly _logService: ILogService, - @IExtHostRpcService private readonly _extHostRpc: IExtHostRpcService, private readonly _notebooksAndEditors: ExtHostNotebookController, - ) { - - } - - - createNotebookEditorDecorationType(options: vscode.NotebookDecorationRenderOptions): vscode.NotebookEditorDecorationType { - return new NotebookEditorDecorationType(this._extHostRpc.getProxy(MainContext.MainThreadNotebookEditors), options).value; - } + ) { } $acceptEditorPropertiesChanged(id: string, data: INotebookEditorPropertiesChangeData): void { this._logService.debug('ExtHostNotebook#$acceptEditorPropertiesChanged', id, data); diff --git a/src/vs/workbench/api/common/extHostNotebookKernels.ts b/src/vs/workbench/api/common/extHostNotebookKernels.ts index f99ead59ac7..6cc843c39dc 100644 --- a/src/vs/workbench/api/common/extHostNotebookKernels.ts +++ b/src/vs/workbench/api/common/extHostNotebookKernels.ts @@ -59,7 +59,7 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { ) { this._proxy = mainContext.getProxy(MainContext.MainThreadNotebookKernels); - // todo@rebornix @joyceerhl: move to APICommands once stablized. + // todo@rebornix @joyceerhl: move to APICommands once stabilized. const selectKernelApiCommand = new ApiCommand( 'notebook.selectKernel', '_notebook.selectKernel', @@ -441,6 +441,17 @@ class NotebookCellExecutionTask extends Disposable { } } + private cellIndexToHandle(cellOrCellIndex: vscode.NotebookCell | undefined): number { + let cell: ExtHostCell | undefined = this._cell; + if (cellOrCellIndex) { + cell = this._cell.notebook.getCellFromApiCell(cellOrCellIndex); + } + if (!cell) { + throw new Error('INVALID cell'); + } + return cell.handle; + } + private validateAndConvertOutputs(items: vscode.NotebookCellOutput[]): NotebookOutputDto[] { return items.map(output => { const newOutput = NotebookCellOutput.ensureUniqueMimeTypes(output.items, true); @@ -456,10 +467,12 @@ class NotebookCellExecutionTask extends Disposable { } private async updateOutputs(outputs: vscode.NotebookCellOutput | vscode.NotebookCellOutput[], cell: vscode.NotebookCell | undefined, append: boolean): Promise<void> { + const handle = this.cellIndexToHandle(cell); const outputDtos = this.validateAndConvertOutputs(asArray(outputs)); return this.updateSoon( { editType: CellExecutionUpdateType.Output, + cellHandle: handle, append, outputs: outputDtos }); diff --git a/src/vs/workbench/api/common/extHostNotebookProxyKernels.ts b/src/vs/workbench/api/common/extHostNotebookProxyKernels.ts deleted file mode 100644 index 786101bf360..00000000000 --- a/src/vs/workbench/api/common/extHostNotebookProxyKernels.ts +++ /dev/null @@ -1,157 +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 { Emitter } from 'vs/base/common/event'; -import { DisposableStore } from 'vs/base/common/lifecycle'; -import { ResourceMap } from 'vs/base/common/map'; -import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { ILogService } from 'vs/platform/log/common/log'; -import { ExtHostNotebookProxyKernelsShape, IMainContext, INotebookProxyKernelDto, MainContext, MainThreadNotebookProxyKernelsShape } from 'vs/workbench/api/common/extHost.protocol'; -import { createKernelId, ExtHostNotebookKernels } from 'vs/workbench/api/common/extHostNotebookKernels'; -import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; -import * as vscode from 'vscode'; - -interface IProxyKernelData { - extensionId: ExtensionIdentifier; - controller: vscode.NotebookProxyController; - onDidChangeSelection: Emitter<{ selected: boolean; notebook: vscode.NotebookDocument }>; - associatedNotebooks: ResourceMap<boolean>; -} - -export type SelectKernelReturnArgs = ControllerInfo | { notebookEditorId: string } | ControllerInfo & { notebookEditorId: string } | undefined; -type ControllerInfo = { id: string; extension: string }; - - -export class ExtHostNotebookProxyKernels implements ExtHostNotebookProxyKernelsShape { - - private readonly _proxy: MainThreadNotebookProxyKernelsShape; - - private readonly _proxyKernelData: Map<number, IProxyKernelData> = new Map<number, IProxyKernelData>(); - private _handlePool: number = 0; - - private readonly _onDidChangeCellExecutionState = new Emitter<vscode.NotebookCellExecutionStateChangeEvent>(); - readonly onDidChangeNotebookCellExecutionState = this._onDidChangeCellExecutionState.event; - - constructor( - mainContext: IMainContext, - private readonly extHostNotebook: ExtHostNotebookKernels, - @ILogService private readonly _logService: ILogService - ) { - this._proxy = mainContext.getProxy(MainContext.MainThreadNotebookProxyKernels); - } - - createNotebookProxyController(extension: IExtensionDescription, id: string, viewType: string, label: string, handler: () => vscode.NotebookController | string | Thenable<vscode.NotebookController | string>): vscode.NotebookProxyController { - const handle = this._handlePool++; - - let isDisposed = false; - const commandDisposables = new DisposableStore(); - const onDidChangeSelection = new Emitter<{ selected: boolean; notebook: vscode.NotebookDocument }>(); - - const data: INotebookProxyKernelDto = { - id: createKernelId(extension.identifier, id), - notebookType: viewType, - extensionId: extension.identifier, - extensionLocation: extension.extensionLocation, - label: label || extension.identifier.value, - }; - - let _resolveHandler = handler; - - this._proxy.$addProxyKernel(handle, data).catch(err => { - // this can happen when a kernel with that ID is already registered - console.log(err); - isDisposed = true; - }); - - let tokenPool = 0; - const _update = () => { - if (isDisposed) { - return; - } - const myToken = ++tokenPool; - Promise.resolve().then(() => { - if (myToken === tokenPool) { - this._proxy.$updateProxyKernel(handle, data); - } - }); - }; - - // notebook documents that are associated to this controller - const associatedNotebooks = new ResourceMap<boolean>(); - - const controller: vscode.NotebookProxyController = { - get id() { return id; }, - get notebookType() { return data.notebookType; }, - onDidChangeSelectedNotebooks: onDidChangeSelection.event, - get label() { - return data.label; - }, - set label(value) { - data.label = value ?? extension.displayName ?? extension.name; - _update(); - }, - get detail() { - return data.detail ?? ''; - }, - set detail(value) { - data.detail = value; - _update(); - }, - get description() { - return data.description ?? ''; - }, - set description(value) { - data.description = value; - _update(); - }, - get kind() { - checkProposedApiEnabled(extension, 'notebookControllerKind'); - return data.kind ?? ''; - }, - set kind(value) { - checkProposedApiEnabled(extension, 'notebookControllerKind'); - data.kind = value; - _update(); - }, - get resolveHandler() { - return _resolveHandler; - }, - dispose: () => { - if (!isDisposed) { - this._logService.trace(`NotebookProxyController[${handle}], DISPOSED`); - isDisposed = true; - this._proxyKernelData.delete(handle); - commandDisposables.dispose(); - onDidChangeSelection.dispose(); - this._proxy.$removeProxyKernel(handle); - } - } - }; - - this._proxyKernelData.set(handle, { - extensionId: extension.identifier, - controller, - onDidChangeSelection, - associatedNotebooks - }); - return controller; - } - - async $resolveKernel(handle: number): Promise<string | null> { - const obj = this._proxyKernelData.get(handle); - if (!obj) { - // extension can dispose kernels in the meantime - return null; - } - - const controller = await obj.controller.resolveHandler(); - if (typeof controller === 'string') { - return controller; - } else { - return this.extHostNotebook.getIdByController(controller); - } - } -} - diff --git a/src/vs/workbench/api/common/extHostNotebookRenderers.ts b/src/vs/workbench/api/common/extHostNotebookRenderers.ts index 1b8925f8757..d3a3e0dd18e 100644 --- a/src/vs/workbench/api/common/extHostNotebookRenderers.ts +++ b/src/vs/workbench/api/common/extHostNotebookRenderers.ts @@ -8,7 +8,6 @@ import { IExtensionDescription } from 'vs/platform/extensions/common/extensions' import { ExtHostNotebookRenderersShape, IMainContext, MainContext, MainThreadNotebookRenderersShape } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook'; import { ExtHostNotebookEditor } from 'vs/workbench/api/common/extHostNotebookEditor'; -import { isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import * as vscode from 'vscode'; @@ -30,29 +29,16 @@ export class ExtHostNotebookRenderers implements ExtHostNotebookRenderersShape { throw new Error(`Extensions may only call createRendererMessaging() for renderers they contribute (got ${rendererId})`); } - // In the stable API, the editor is given as an empty object, and this map - // is used to maintain references. This can be removed after editor finalization. - const notebookEditorVisible = isProposedApiEnabled(manifest, 'notebookEditor'); - const notebookEditorAliases = new WeakMap<{}, vscode.NotebookEditor>(); - const messaging: vscode.NotebookRendererMessaging = { onDidReceiveMessage: (listener, thisArg, disposables) => { - const wrappedListener = notebookEditorVisible ? listener : (evt: { editor: vscode.NotebookEditor; message: any }) => { - const obj = {}; - notebookEditorAliases.set(obj, evt.editor); - listener({ editor: obj as vscode.NotebookEditor, message: evt.message }); - }; - - return this.getOrCreateEmitterFor(rendererId).event(wrappedListener, thisArg, disposables); + return this.getOrCreateEmitterFor(rendererId).event(listener, thisArg, disposables); }, postMessage: (message, editorOrAlias) => { if (ExtHostNotebookEditor.apiEditorsToExtHost.has(message)) { // back compat for swapped args [message, editorOrAlias] = [editorOrAlias, message]; } - - const editor = notebookEditorVisible ? editorOrAlias : notebookEditorAliases.get(editorOrAlias!); - const extHostEditor = editor && ExtHostNotebookEditor.apiEditorsToExtHost.get(editor); + const extHostEditor = editorOrAlias && ExtHostNotebookEditor.apiEditorsToExtHost.get(editorOrAlias); return this.proxy.$postMessage(extHostEditor?.id, rendererId, message); }, }; diff --git a/src/vs/workbench/api/common/extHostQuickOpen.ts b/src/vs/workbench/api/common/extHostQuickOpen.ts index 1a2dda0396f..1bc50dd99e2 100644 --- a/src/vs/workbench/api/common/extHostQuickOpen.ts +++ b/src/vs/workbench/api/common/extHostQuickOpen.ts @@ -136,9 +136,7 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx } $onItemSelected(handle: number): void { - if (this._onDidSelectItem) { - this._onDidSelectItem(handle); - } + this._onDidSelectItem?.(handle); } // ---- input @@ -220,9 +218,7 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx $onDidChangeValue(sessionId: number, value: string): void { const session = this._sessions.get(sessionId); - if (session) { - session._fireDidChangeValue(value); - } + session?._fireDidChangeValue(value); } $onDidAccept(sessionId: number): void { @@ -248,9 +244,7 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx $onDidTriggerButton(sessionId: number, handle: number): void { const session = this._sessions.get(sessionId); - if (session) { - session._fireDidTriggerButton(handle); - } + session?._fireDidTriggerButton(handle); } $onDidTriggerItemButton(sessionId: number, itemHandle: number, buttonHandle: number): void { diff --git a/src/vs/workbench/api/common/extHostRequireInterceptor.ts b/src/vs/workbench/api/common/extHostRequireInterceptor.ts index 4a072952392..46f3e072488 100644 --- a/src/vs/workbench/api/common/extHostRequireInterceptor.ts +++ b/src/vs/workbench/api/common/extHostRequireInterceptor.ts @@ -73,7 +73,7 @@ export abstract class RequireInterceptor { public register(interceptor: INodeModuleFactory | IAlternativeModuleProvider): void { if ('nodeModuleName' in interceptor) { if (Array.isArray(interceptor.nodeModuleName)) { - for (let moduleName of interceptor.nodeModuleName) { + for (const moduleName of interceptor.nodeModuleName) { this._factories.set(moduleName, interceptor); } } else { @@ -249,6 +249,7 @@ class KeytarNodeModuleFactory implements INodeModuleFactory { public load(_request: string, parent: URI): any { const ext = this._extensionPaths.findSubstr(parent); type ShimmingKeytarClassification = { + owner: 'jrieken'; extension: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; }; this._mainThreadTelemetry.$publicLog2<{ extension: string }, ShimmingKeytarClassification>('shimming.keytar', { extension: ext?.identifier.value ?? 'unknown_extension' }); @@ -346,6 +347,7 @@ class OpenNodeModuleFactory implements INodeModuleFactory { return; } type ShimmingOpenClassification = { + owner: 'jrieken'; extension: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; }; this._mainThreadTelemetry.$publicLog2<{ extension: string }, ShimmingOpenClassification>('shimming.open', { extension: this._extensionId }); @@ -356,6 +358,7 @@ class OpenNodeModuleFactory implements INodeModuleFactory { return; } type ShimmingOpenCallNoForwardClassification = { + owner: 'jrieken'; extension: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; }; this._mainThreadTelemetry.$publicLog2<{ extension: string }, ShimmingOpenCallNoForwardClassification>('shimming.open.call.noForward', { extension: this._extensionId }); diff --git a/src/vs/workbench/api/common/extHostSCM.ts b/src/vs/workbench/api/common/extHostSCM.ts index 49a54bc71fe..ee9ec8daf69 100644 --- a/src/vs/workbench/api/common/extHostSCM.ts +++ b/src/vs/workbench/api/common/extHostSCM.ts @@ -249,6 +249,23 @@ export class ExtHostSCMInputBox implements vscode.SourceControlInputBox { this.#proxy.$setValidationProviderIsEnabled(this._sourceControlHandle, !!fn); } + private _enabled: boolean = true; + + get enabled(): boolean { + return this._enabled; + } + + set enabled(enabled: boolean) { + enabled = !!enabled; + + if (this._enabled === enabled) { + return; + } + + this._enabled = enabled; + this.#proxy.$setInputBoxEnablement(this._sourceControlHandle, enabled); + } + private _visible: boolean = true; get visible(): boolean { @@ -520,7 +537,11 @@ class ExtHostSourceControl implements vscode.SourceControl { const internal = actionButton !== undefined ? { command: this._commands.converter.toInternal(actionButton.command, this._actionButtonDisposables.value), - description: actionButton.description + secondaryCommands: actionButton.secondaryCommands?.map(commandGroup => { + return commandGroup.map(command => this._commands.converter.toInternal(command, this._actionButtonDisposables.value!)); + }), + description: actionButton.description, + enabled: actionButton.enabled } : undefined; this.#proxy.$updateSourceControl(this.handle, { actionButton: internal ?? null }); } @@ -722,7 +743,10 @@ export class ExtHostSCM implements ExtHostSCMShape { this.logService.trace('ExtHostSCM#createSourceControl', extension.identifier.value, id, label, rootUri); type TEvent = { extensionId: string }; - type TMeta = { extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' } }; + type TMeta = { + owner: 'joaomoreno'; + extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; + }; this._telemetry.$publicLog2<TEvent, TMeta>('api/scm/createSourceControl', { extensionId: extension.identifier.value, }); diff --git a/src/vs/workbench/api/common/extHostTask.ts b/src/vs/workbench/api/common/extHostTask.ts index 00d03d0b6ee..bf122d3ab2c 100644 --- a/src/vs/workbench/api/common/extHostTask.ts +++ b/src/vs/workbench/api/common/extHostTask.ts @@ -24,7 +24,7 @@ import { Schemas } from 'vs/base/common/network'; import * as Platform from 'vs/base/common/platform'; import { ILogService } from 'vs/platform/log/common/log'; import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService'; -import { USER_TASKS_GROUP_KEY } from 'vs/workbench/contrib/tasks/common/taskService'; +import { USER_TASKS_GROUP_KEY } from 'vs/workbench/contrib/tasks/common/tasks'; import { NotSupportedError } from 'vs/base/common/errors'; export interface IExtHostTask extends ExtHostTaskShape { @@ -38,20 +38,20 @@ export interface IExtHostTask extends ExtHostTaskShape { onDidEndTaskProcess: Event<vscode.TaskProcessEndEvent>; registerTaskProvider(extension: IExtensionDescription, type: string, provider: vscode.TaskProvider): vscode.Disposable; - registerTaskSystem(scheme: string, info: tasks.TaskSystemInfoDTO): void; + registerTaskSystem(scheme: string, info: tasks.ITaskSystemInfoDTO): void; fetchTasks(filter?: vscode.TaskFilter): Promise<vscode.Task[]>; executeTask(extension: IExtensionDescription, task: vscode.Task): Promise<vscode.TaskExecution>; terminateTask(execution: vscode.TaskExecution): Promise<void>; } export namespace TaskDefinitionDTO { - export function from(value: vscode.TaskDefinition): tasks.TaskDefinitionDTO | undefined { + export function from(value: vscode.TaskDefinition): tasks.ITaskDefinitionDTO | undefined { if (value === undefined || value === null) { return undefined; } return value; } - export function to(value: tasks.TaskDefinitionDTO): vscode.TaskDefinition | undefined { + export function to(value: tasks.ITaskDefinitionDTO): vscode.TaskDefinition | undefined { if (value === undefined || value === null) { return undefined; } @@ -60,13 +60,13 @@ export namespace TaskDefinitionDTO { } export namespace TaskPresentationOptionsDTO { - export function from(value: vscode.TaskPresentationOptions): tasks.TaskPresentationOptionsDTO | undefined { + export function from(value: vscode.TaskPresentationOptions): tasks.ITaskPresentationOptionsDTO | undefined { if (value === undefined || value === null) { return undefined; } return value; } - export function to(value: tasks.TaskPresentationOptionsDTO): vscode.TaskPresentationOptions | undefined { + export function to(value: tasks.ITaskPresentationOptionsDTO): vscode.TaskPresentationOptions | undefined { if (value === undefined || value === null) { return undefined; } @@ -75,13 +75,13 @@ export namespace TaskPresentationOptionsDTO { } export namespace ProcessExecutionOptionsDTO { - export function from(value: vscode.ProcessExecutionOptions): tasks.ProcessExecutionOptionsDTO | undefined { + export function from(value: vscode.ProcessExecutionOptions): tasks.IProcessExecutionOptionsDTO | undefined { if (value === undefined || value === null) { return undefined; } return value; } - export function to(value: tasks.ProcessExecutionOptionsDTO): vscode.ProcessExecutionOptions | undefined { + export function to(value: tasks.IProcessExecutionOptionsDTO): vscode.ProcessExecutionOptions | undefined { if (value === undefined || value === null) { return undefined; } @@ -90,19 +90,19 @@ export namespace ProcessExecutionOptionsDTO { } export namespace ProcessExecutionDTO { - export function is(value: tasks.ShellExecutionDTO | tasks.ProcessExecutionDTO | tasks.CustomExecutionDTO | undefined): value is tasks.ProcessExecutionDTO { + export function is(value: tasks.IShellExecutionDTO | tasks.IProcessExecutionDTO | tasks.ICustomExecutionDTO | undefined): value is tasks.IProcessExecutionDTO { if (value) { - const candidate = value as tasks.ProcessExecutionDTO; + const candidate = value as tasks.IProcessExecutionDTO; return candidate && !!candidate.process; } else { return false; } } - export function from(value: vscode.ProcessExecution): tasks.ProcessExecutionDTO | undefined { + export function from(value: vscode.ProcessExecution): tasks.IProcessExecutionDTO | undefined { if (value === undefined || value === null) { return undefined; } - const result: tasks.ProcessExecutionDTO = { + const result: tasks.IProcessExecutionDTO = { process: value.process, args: value.args }; @@ -111,7 +111,7 @@ export namespace ProcessExecutionDTO { } return result; } - export function to(value: tasks.ProcessExecutionDTO): types.ProcessExecution | undefined { + export function to(value: tasks.IProcessExecutionDTO): types.ProcessExecution | undefined { if (value === undefined || value === null) { return undefined; } @@ -120,13 +120,13 @@ export namespace ProcessExecutionDTO { } export namespace ShellExecutionOptionsDTO { - export function from(value: vscode.ShellExecutionOptions): tasks.ShellExecutionOptionsDTO | undefined { + export function from(value: vscode.ShellExecutionOptions): tasks.IShellExecutionOptionsDTO | undefined { if (value === undefined || value === null) { return undefined; } return value; } - export function to(value: tasks.ShellExecutionOptionsDTO): vscode.ShellExecutionOptions | undefined { + export function to(value: tasks.IShellExecutionOptionsDTO): vscode.ShellExecutionOptions | undefined { if (value === undefined || value === null) { return undefined; } @@ -135,19 +135,19 @@ export namespace ShellExecutionOptionsDTO { } export namespace ShellExecutionDTO { - export function is(value: tasks.ShellExecutionDTO | tasks.ProcessExecutionDTO | tasks.CustomExecutionDTO | undefined): value is tasks.ShellExecutionDTO { + export function is(value: tasks.IShellExecutionDTO | tasks.IProcessExecutionDTO | tasks.ICustomExecutionDTO | undefined): value is tasks.IShellExecutionDTO { if (value) { - const candidate = value as tasks.ShellExecutionDTO; + const candidate = value as tasks.IShellExecutionDTO; return candidate && (!!candidate.commandLine || !!candidate.command); } else { return false; } } - export function from(value: vscode.ShellExecution): tasks.ShellExecutionDTO | undefined { + export function from(value: vscode.ShellExecution): tasks.IShellExecutionDTO | undefined { if (value === undefined || value === null) { return undefined; } - const result: tasks.ShellExecutionDTO = { + const result: tasks.IShellExecutionDTO = { }; if (value.commandLine !== undefined) { result.commandLine = value.commandLine; @@ -160,7 +160,7 @@ export namespace ShellExecutionDTO { } return result; } - export function to(value: tasks.ShellExecutionDTO): types.ShellExecution | undefined { + export function to(value: tasks.IShellExecutionDTO): types.ShellExecution | undefined { if (value === undefined || value === null || (value.command === undefined && value.commandLine === undefined)) { return undefined; } @@ -173,16 +173,16 @@ export namespace ShellExecutionDTO { } export namespace CustomExecutionDTO { - export function is(value: tasks.ShellExecutionDTO | tasks.ProcessExecutionDTO | tasks.CustomExecutionDTO | undefined): value is tasks.CustomExecutionDTO { + export function is(value: tasks.IShellExecutionDTO | tasks.IProcessExecutionDTO | tasks.ICustomExecutionDTO | undefined): value is tasks.ICustomExecutionDTO { if (value) { - let candidate = value as tasks.CustomExecutionDTO; + const candidate = value as tasks.ICustomExecutionDTO; return candidate && candidate.customExecution === 'customExecution'; } else { return false; } } - export function from(value: vscode.CustomExecution): tasks.CustomExecutionDTO { + export function from(value: vscode.CustomExecution): tasks.ICustomExecutionDTO { return { customExecution: 'customExecution' }; @@ -195,7 +195,7 @@ export namespace CustomExecutionDTO { export namespace TaskHandleDTO { - export function from(value: types.Task, workspaceService?: IExtHostWorkspace): tasks.TaskHandleDTO { + export function from(value: types.Task, workspaceService?: IExtHostWorkspace): tasks.ITaskHandleDTO { let folder: UriComponents | string; if (value.scope !== undefined && typeof value.scope !== 'number') { folder = value.scope.uri; @@ -213,7 +213,7 @@ export namespace TaskHandleDTO { } } export namespace TaskGroupDTO { - export function from(value: vscode.TaskGroup): tasks.TaskGroupDTO | undefined { + export function from(value: vscode.TaskGroup): tasks.ITaskGroupDTO | undefined { if (value === undefined || value === null) { return undefined; } @@ -222,12 +222,12 @@ export namespace TaskGroupDTO { } export namespace TaskDTO { - export function fromMany(tasks: vscode.Task[], extension: IExtensionDescription): tasks.TaskDTO[] { + export function fromMany(tasks: vscode.Task[], extension: IExtensionDescription): tasks.ITaskDTO[] { if (tasks === undefined || tasks === null) { return []; } - const result: tasks.TaskDTO[] = []; - for (let task of tasks) { + const result: tasks.ITaskDTO[] = []; + for (const task of tasks) { const converted = from(task, extension); if (converted) { result.push(converted); @@ -236,11 +236,11 @@ export namespace TaskDTO { return result; } - export function from(value: vscode.Task, extension: IExtensionDescription): tasks.TaskDTO | undefined { + export function from(value: vscode.Task, extension: IExtensionDescription): tasks.ITaskDTO | undefined { if (value === undefined || value === null) { return undefined; } - let execution: tasks.ShellExecutionDTO | tasks.ProcessExecutionDTO | tasks.CustomExecutionDTO | undefined; + let execution: tasks.IShellExecutionDTO | tasks.IProcessExecutionDTO | tasks.ICustomExecutionDTO | undefined; if (value.execution instanceof types.ProcessExecution) { execution = ProcessExecutionDTO.from(value.execution); } else if (value.execution instanceof types.ShellExecution) { @@ -249,7 +249,7 @@ export namespace TaskDTO { execution = CustomExecutionDTO.from(<types.CustomExecution>value.execution); } - const definition: tasks.TaskDefinitionDTO | undefined = TaskDefinitionDTO.from(value.definition); + const definition: tasks.ITaskDefinitionDTO | undefined = TaskDefinitionDTO.from(value.definition); let scope: number | UriComponents; if (value.scope) { if (typeof value.scope === 'number') { @@ -264,7 +264,7 @@ export namespace TaskDTO { if (!definition || !scope) { return undefined; } - const result: tasks.TaskDTO = { + const result: tasks.ITaskDTO = { _id: (value as types.Task)._id!, definition, name: value.name, @@ -284,7 +284,7 @@ export namespace TaskDTO { }; return result; } - export async function to(value: tasks.TaskDTO | undefined, workspace: IExtHostWorkspaceProvider, providedCustomExeutions: Map<string, types.CustomExecution>): Promise<types.Task | undefined> { + export async function to(value: tasks.ITaskDTO | undefined, workspace: IExtHostWorkspaceProvider, providedCustomExeutions: Map<string, types.CustomExecution>): Promise<types.Task | undefined> { if (value === undefined || value === null) { return undefined; } @@ -339,11 +339,11 @@ export namespace TaskDTO { } export namespace TaskFilterDTO { - export function from(value: vscode.TaskFilter | undefined): tasks.TaskFilterDTO | undefined { + export function from(value: vscode.TaskFilter | undefined): tasks.ITaskFilterDTO | undefined { return value; } - export function to(value: tasks.TaskFilterDTO): vscode.TaskFilter | undefined { + export function to(value: tasks.ITaskFilterDTO): vscode.TaskFilter | undefined { if (!value) { return undefined; } @@ -367,15 +367,15 @@ class TaskExecutionImpl implements vscode.TaskExecution { this.#tasks.terminateTask(this); } - public fireDidStartProcess(value: tasks.TaskProcessStartedDTO): void { + public fireDidStartProcess(value: tasks.ITaskProcessStartedDTO): void { } - public fireDidEndProcess(value: tasks.TaskProcessEndedDTO): void { + public fireDidEndProcess(value: tasks.ITaskProcessEndedDTO): void { } } export namespace TaskExecutionDTO { - export function from(value: vscode.TaskExecution): tasks.TaskExecutionDTO { + export function from(value: vscode.TaskExecution): tasks.ITaskExecutionDTO { return { id: (value as TaskExecutionImpl)._id, task: undefined @@ -453,14 +453,14 @@ export abstract class ExtHostTaskBase implements ExtHostTaskShape, IExtHostTask }); } - public registerTaskSystem(scheme: string, info: tasks.TaskSystemInfoDTO): void { + public registerTaskSystem(scheme: string, info: tasks.ITaskSystemInfoDTO): void { this._proxy.$registerTaskSystem(scheme, info); } public fetchTasks(filter?: vscode.TaskFilter): Promise<vscode.Task[]> { return this._proxy.$fetchTasks(TaskFilterDTO.from(filter)).then(async (values) => { const result: vscode.Task[] = []; - for (let value of values) { + for (const value of values) { const task = await TaskDTO.to(value, this._workspaceProvider, this._providedCustomExecutions2); if (task) { result.push(task); @@ -489,7 +489,7 @@ export abstract class ExtHostTaskBase implements ExtHostTaskShape, IExtHostTask return this._onDidExecuteTask.event; } - public async $onDidStartTask(execution: tasks.TaskExecutionDTO, terminalId: number, resolvedDefinition: tasks.TaskDefinitionDTO): Promise<void> { + public async $onDidStartTask(execution: tasks.ITaskExecutionDTO, terminalId: number, resolvedDefinition: tasks.ITaskDefinitionDTO): Promise<void> { const customExecution: types.CustomExecution | undefined = this._providedCustomExecutions2.get(execution.id); if (customExecution) { // Clone the custom execution to keep the original untouched. This is important for multiple runs of the same task. @@ -507,7 +507,7 @@ export abstract class ExtHostTaskBase implements ExtHostTaskShape, IExtHostTask return this._onDidTerminateTask.event; } - public async $OnDidEndTask(execution: tasks.TaskExecutionDTO): Promise<void> { + public async $OnDidEndTask(execution: tasks.ITaskExecutionDTO): Promise<void> { const _execution = await this.getTaskExecution(execution); this._taskExecutionPromises.delete(execution.id); this._taskExecutions.delete(execution.id); @@ -521,7 +521,7 @@ export abstract class ExtHostTaskBase implements ExtHostTaskShape, IExtHostTask return this._onDidTaskProcessStarted.event; } - public async $onDidStartTaskProcess(value: tasks.TaskProcessStartedDTO): Promise<void> { + public async $onDidStartTaskProcess(value: tasks.ITaskProcessStartedDTO): Promise<void> { const execution = await this.getTaskExecution(value.id); this._onDidTaskProcessStarted.fire({ execution: execution, @@ -533,7 +533,7 @@ export abstract class ExtHostTaskBase implements ExtHostTaskShape, IExtHostTask return this._onDidTaskProcessEnded.event; } - public async $onDidEndTaskProcess(value: tasks.TaskProcessEndedDTO): Promise<void> { + public async $onDidEndTaskProcess(value: tasks.ITaskProcessEndedDTO): Promise<void> { const execution = await this.getTaskExecution(value.id); this._onDidTaskProcessEnded.fire({ execution: execution, @@ -541,9 +541,9 @@ export abstract class ExtHostTaskBase implements ExtHostTaskShape, IExtHostTask }); } - protected abstract provideTasksInternal(validTypes: { [key: string]: boolean }, taskIdPromises: Promise<void>[], handler: HandlerData, value: vscode.Task[] | null | undefined): { tasks: tasks.TaskDTO[]; extension: IExtensionDescription }; + protected abstract provideTasksInternal(validTypes: { [key: string]: boolean }, taskIdPromises: Promise<void>[], handler: HandlerData, value: vscode.Task[] | null | undefined): { tasks: tasks.ITaskDTO[]; extension: IExtensionDescription }; - public $provideTasks(handle: number, validTypes: { [key: string]: boolean }): Promise<tasks.TaskSetDTO> { + public $provideTasks(handle: number, validTypes: { [key: string]: boolean }): Promise<tasks.ITaskSetDTO> { const handler = this._handlers.get(handle); if (!handler) { return Promise.reject(new Error('no handler found')); @@ -571,9 +571,9 @@ export abstract class ExtHostTaskBase implements ExtHostTaskShape, IExtHostTask }); } - protected abstract resolveTaskInternal(resolvedTaskDTO: tasks.TaskDTO): Promise<tasks.TaskDTO | undefined>; + protected abstract resolveTaskInternal(resolvedTaskDTO: tasks.ITaskDTO): Promise<tasks.ITaskDTO | undefined>; - public async $resolveTask(handle: number, taskDTO: tasks.TaskDTO): Promise<tasks.TaskDTO | undefined> { + public async $resolveTask(handle: number, taskDTO: tasks.ITaskDTO): Promise<tasks.ITaskDTO | undefined> { const handler = this._handlers.get(handle); if (!handler) { return Promise.reject(new Error('no handler found')); @@ -595,7 +595,7 @@ export abstract class ExtHostTaskBase implements ExtHostTaskShape, IExtHostTask this.checkDeprecation(resolvedTask, handler); - const resolvedTaskDTO: tasks.TaskDTO | undefined = TaskDTO.from(resolvedTask, handler.extension); + const resolvedTaskDTO: tasks.ITaskDTO | undefined = TaskDTO.from(resolvedTask, handler.extension); if (!resolvedTaskDTO) { throw new Error('Unexpected: Task cannot be resolved.'); } @@ -617,7 +617,7 @@ export abstract class ExtHostTaskBase implements ExtHostTaskShape, IExtHostTask return this._handleCounter++; } - protected async addCustomExecution(taskDTO: tasks.TaskDTO, task: vscode.Task, isProvided: boolean): Promise<void> { + protected async addCustomExecution(taskDTO: tasks.ITaskDTO, task: vscode.Task, isProvided: boolean): Promise<void> { const taskId = await this._proxy.$createTaskId(taskDTO); if (!isProvided && !this._providedCustomExecutions2.has(taskId)) { this._notProvidedCustomExecutions.add(taskId); @@ -627,7 +627,7 @@ export abstract class ExtHostTaskBase implements ExtHostTaskShape, IExtHostTask this._providedCustomExecutions2.set(taskId, <types.CustomExecution>task.execution); } - protected async getTaskExecution(execution: tasks.TaskExecutionDTO | string, task?: vscode.Task): Promise<TaskExecutionImpl> { + protected async getTaskExecution(execution: tasks.ITaskExecutionDTO | string, task?: vscode.Task): Promise<TaskExecutionImpl> { if (typeof execution === 'string') { const taskExecution = this._taskExecutionPromises.get(execution); if (!taskExecution) { @@ -636,12 +636,12 @@ export abstract class ExtHostTaskBase implements ExtHostTaskShape, IExtHostTask return taskExecution; } - let result: Promise<TaskExecutionImpl> | undefined = this._taskExecutionPromises.get(execution.id); + const result: Promise<TaskExecutionImpl> | undefined = this._taskExecutionPromises.get(execution.id); if (result) { return result; } const createdResult: Promise<TaskExecutionImpl> = new Promise((resolve, reject) => { - function resolvePromiseWithCreatedTask(that: ExtHostTaskBase, execution: tasks.TaskExecutionDTO, taskToCreate: vscode.Task | types.Task | undefined) { + function resolvePromiseWithCreatedTask(that: ExtHostTaskBase, execution: tasks.ITaskExecutionDTO, taskToCreate: vscode.Task | types.Task | undefined) { if (!taskToCreate) { reject('Unexpected: Task does not exist.'); } else { @@ -673,7 +673,7 @@ export abstract class ExtHostTaskBase implements ExtHostTaskShape, IExtHostTask } } - private customExecutionComplete(execution: tasks.TaskExecutionDTO): void { + private customExecutionComplete(execution: tasks.ITaskExecutionDTO): void { const extensionCallback2: vscode.CustomExecution | undefined = this._activeCustomExecutions2.get(execution.id); if (extensionCallback2) { this._activeCustomExecutions2.delete(execution.id); @@ -687,7 +687,7 @@ export abstract class ExtHostTaskBase implements ExtHostTaskShape, IExtHostTask this._providedCustomExecutions2.delete(execution.id); this._notProvidedCustomExecutions.delete(execution.id); } - let iterator = this._notProvidedCustomExecutions.values(); + const iterator = this._notProvidedCustomExecutions.values(); let iteratorResult = iterator.next(); while (!iteratorResult.done) { if (!this._activeCustomExecutions2.has(iteratorResult.value) && (this._lastStartedTask !== iteratorResult.value)) { @@ -747,16 +747,16 @@ export class WorkerExtHostTask extends ExtHostTaskBase { return execution; } - protected provideTasksInternal(validTypes: { [key: string]: boolean }, taskIdPromises: Promise<void>[], handler: HandlerData, value: vscode.Task[] | null | undefined): { tasks: tasks.TaskDTO[]; extension: IExtensionDescription } { - const taskDTOs: tasks.TaskDTO[] = []; + protected provideTasksInternal(validTypes: { [key: string]: boolean }, taskIdPromises: Promise<void>[], handler: HandlerData, value: vscode.Task[] | null | undefined): { tasks: tasks.ITaskDTO[]; extension: IExtensionDescription } { + const taskDTOs: tasks.ITaskDTO[] = []; if (value) { - for (let task of value) { + for (const task of value) { this.checkDeprecation(task, handler); if (!task.definition || !validTypes[task.definition.type]) { this._logService.warn(`The task [${task.source}, ${task.name}] uses an undefined task type. The task will be ignored in the future.`); } - const taskDTO: tasks.TaskDTO | undefined = TaskDTO.from(task, handler.extension); + const taskDTO: tasks.ITaskDTO | undefined = TaskDTO.from(task, handler.extension); if (taskDTO && CustomExecutionDTO.is(taskDTO.execution)) { taskDTOs.push(taskDTO); // The ID is calculated on the main thread task side, so, let's call into it here. @@ -774,7 +774,7 @@ export class WorkerExtHostTask extends ExtHostTaskBase { }; } - protected async resolveTaskInternal(resolvedTaskDTO: tasks.TaskDTO): Promise<tasks.TaskDTO | undefined> { + protected async resolveTaskInternal(resolvedTaskDTO: tasks.ITaskDTO): Promise<tasks.ITaskDTO | undefined> { if (CustomExecutionDTO.is(resolvedTaskDTO.execution)) { return resolvedTaskDTO; } else { diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index 1aa1e678e00..f27f16dca52 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -538,9 +538,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I public async $acceptTerminalProcessId(id: number, processId: number): Promise<void> { const terminal = this._getTerminalById(id); - if (terminal) { - terminal._setProcessId(processId); - } + terminal?._setProcessId(processId); } public async $startExtensionTerminal(id: number, initialDimensions: ITerminalDimensionsDto | undefined): Promise<ITerminalLaunchError | undefined> { @@ -693,9 +691,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I this._terminalLinkCache.delete(terminalId); const oldToken = this._terminalLinkCancellationSource.get(terminalId); - if (oldToken) { - oldToken.dispose(true); - } + oldToken?.dispose(true); const cancellationSource = new CancellationTokenSource(); this._terminalLinkCancellationSource.set(terminalId, cancellationSource); @@ -880,6 +876,10 @@ export class EnvironmentVariableCollection implements vscode.EnvironmentVariable this.map.forEach((value, key) => callback.call(thisArg, key, value, this)); } + [Symbol.iterator](): IterableIterator<[variable: string, mutator: vscode.EnvironmentVariableMutator]> { + return this.map.entries(); + } + delete(variable: string): void { this.map.delete(variable); this._onDidChangeCollection.fire(); diff --git a/src/vs/workbench/api/common/extHostTesting.ts b/src/vs/workbench/api/common/extHostTesting.ts index a7c81eb67d2..559d09d0aa0 100644 --- a/src/vs/workbench/api/common/extHostTesting.ts +++ b/src/vs/workbench/api/common/extHostTesting.ts @@ -774,7 +774,7 @@ export class MirroredTestCollection extends AbstractIncrementalTestCollection<Mi private changeEmitter = new Emitter<vscode.TestsChangeEvent>(); /** - * Change emitter that fires with the same sematics as `TestObserver.onDidChangeTests`. + * Change emitter that fires with the same semantics as `TestObserver.onDidChangeTests`. */ public readonly onDidChangeTests = this.changeEmitter.event; diff --git a/src/vs/workbench/api/common/extHostTreeViews.ts b/src/vs/workbench/api/common/extHostTreeViews.ts index b984ae6569e..fdbb535e422 100644 --- a/src/vs/workbench/api/common/extHostTreeViews.ts +++ b/src/vs/workbench/api/common/extHostTreeViews.ts @@ -5,11 +5,12 @@ import { localize } from 'vs/nls'; import type * as vscode from 'vscode'; +import * as types from './extHostTypes'; import { basename } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; -import { ExtHostTreeViewsShape, MainThreadTreeViewsShape } from './extHost.protocol'; +import { DataTransferDTO, ExtHostTreeViewsShape, MainThreadTreeViewsShape } from './extHost.protocol'; import { ITreeItem, TreeViewItemHandleArg, ITreeItemLabel, IRevealOptions } from 'vs/workbench/common/views'; import { ExtHostCommands, CommandsConverter } from 'vs/workbench/api/common/extHostCommands'; import { asPromise } from 'vs/base/common/async'; @@ -18,14 +19,12 @@ import { isUndefinedOrNull, isString } from 'vs/base/common/types'; import { equals, coalesce } from 'vs/base/common/arrays'; import { ILogService } from 'vs/platform/log/common/log'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { MarkdownString, ViewBadge } from 'vs/workbench/api/common/extHostTypeConverters'; +import { MarkdownString, ViewBadge, DataTransfer } from 'vs/workbench/api/common/extHostTypeConverters'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { Command } from 'vs/editor/common/languages'; -import { DataTransferConverter, DataTransferDTO } from 'vs/workbench/api/common/shared/dataTransfer'; import { ITreeViewsService, TreeviewsService } from 'vs/workbench/services/views/common/treeViewsService'; import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; -import { IDataTransfer } from 'vs/base/common/dataTransfer'; type TreeItemHandle = string; @@ -151,7 +150,7 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape { return Promise.reject(new Error(localize('treeView.notRegistered', 'No tree view with id \'{0}\' registered.', destinationViewId))); } - const treeDataTransfer = DataTransferConverter.toDataTransfer(treeDataTransferDTO, async dataItemIndex => { + const treeDataTransfer = DataTransfer.toDataTransfer(treeDataTransferDTO, async dataItemIndex => { return (await this._proxy.$resolveDropFileData(destinationViewId, requestId, dataItemIndex)).buffer; }); if ((sourceViewId === destinationViewId) && sourceTreeItemHandles) { @@ -160,27 +159,13 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape { return treeView.onDrop(treeDataTransfer, targetItemHandle, token); } - private async addAdditionalTransferItems(treeDataTransfer: IDataTransfer, treeView: ExtHostTreeView<any>, - sourceTreeItemHandles: string[], token: CancellationToken, operationUuid?: string): Promise<IDataTransfer | undefined> { + private async addAdditionalTransferItems(treeDataTransfer: vscode.DataTransfer, treeView: ExtHostTreeView<any>, + sourceTreeItemHandles: string[], token: CancellationToken, operationUuid?: string): Promise<vscode.DataTransfer | undefined> { const existingTransferOperation = this.treeDragAndDropService.removeDragOperationTransfer(operationUuid); if (existingTransferOperation) { (await existingTransferOperation)?.forEach((value, key) => { if (value) { - const file = value.asFile(); - treeDataTransfer.set(key, { - value: value.value, - asString: value.asString, - asFile() { - if (!file) { - return undefined; - } - return { - name: file.name, - uri: file.uri, - data: async () => await file.data() - }; - }, - }); + treeDataTransfer.set(key, value); } }); } else if (operationUuid && treeView.handleDrag) { @@ -197,12 +182,12 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape { return Promise.reject(new Error(localize('treeView.notRegistered', 'No tree view with id \'{0}\' registered.', sourceViewId))); } - const treeDataTransfer = await this.addAdditionalTransferItems(new Map(), treeView, sourceTreeItemHandles, token, operationUuid); + const treeDataTransfer = await this.addAdditionalTransferItems(new types.DataTransfer(), treeView, sourceTreeItemHandles, token, operationUuid); if (!treeDataTransfer) { return; } - return DataTransferConverter.toDataTransferDTO(treeDataTransfer); + return DataTransfer.toDataTransferDTO(treeDataTransfer); } async $hasResolve(treeViewId: string): Promise<boolean> { @@ -472,7 +457,7 @@ class ExtHostTreeView<T> extends Disposable { } } - async handleDrag(sourceTreeItemHandles: TreeItemHandle[], treeDataTransfer: IDataTransfer, token: CancellationToken): Promise<vscode.DataTransfer | undefined> { + async handleDrag(sourceTreeItemHandles: TreeItemHandle[], treeDataTransfer: vscode.DataTransfer, token: CancellationToken): Promise<vscode.DataTransfer | undefined> { const extensionTreeItems: T[] = []; for (const sourceHandle of sourceTreeItemHandles) { const extensionItem = this.getExtensionElement(sourceHandle); diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 979eebec86c..9b733dc8951 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -20,6 +20,7 @@ import * as editorRange from 'vs/editor/common/core/range'; import { ISelection } from 'vs/editor/common/core/selection'; import { IContentDecorationRenderOptions, IDecorationOptions, IDecorationRenderOptions, IThemeDecorationRenderOptions } from 'vs/editor/common/editorCommon'; import * as languages from 'vs/editor/common/languages'; +import * as encodedTokenAttributes from 'vs/editor/common/encodedTokenAttributes'; import * as languageSelector from 'vs/editor/common/languageSelector'; import { EndOfLineSequence, TrackedRangeStickiness } from 'vs/editor/common/model'; import { EditorResolution, ITextEditorOptions } from 'vs/platform/editor/common/editor'; @@ -38,6 +39,8 @@ import { EditorGroupColumn } from 'vs/workbench/services/editor/common/editorGro import { ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import type * as vscode from 'vscode'; import * as types from './extHostTypes'; +import { once } from 'vs/base/common/functional'; +import { VSDataTransfer } from 'vs/base/common/dataTransfer'; export namespace Command { @@ -111,12 +114,12 @@ export namespace Range { } export namespace TokenType { - export function to(type: languages.StandardTokenType): types.StandardTokenType { + export function to(type: encodedTokenAttributes.StandardTokenType): types.StandardTokenType { switch (type) { - case languages.StandardTokenType.Comment: return types.StandardTokenType.Comment; - case languages.StandardTokenType.Other: return types.StandardTokenType.Other; - case languages.StandardTokenType.RegEx: return types.StandardTokenType.RegEx; - case languages.StandardTokenType.String: return types.StandardTokenType.String; + case encodedTokenAttributes.StandardTokenType.Comment: return types.StandardTokenType.Comment; + case encodedTokenAttributes.StandardTokenType.Other: return types.StandardTokenType.Other; + case encodedTokenAttributes.StandardTokenType.RegEx: return types.StandardTokenType.RegEx; + case encodedTokenAttributes.StandardTokenType.String: return types.StandardTokenType.String; } } } @@ -559,15 +562,6 @@ export namespace TextEdit { } } -export namespace SnippetTextEdit { - export function from(edit: vscode.SnippetTextEdit): languages.SnippetTextEdit { - return { - range: Range.from(edit.range), - snippet: edit.snippet.value - }; - } -} - export namespace WorkspaceEdit { export interface IVersionInformationProvider { @@ -575,7 +569,7 @@ export namespace WorkspaceEdit { getNotebookDocumentVersion(uri: URI): number | undefined; } - export function from(value: vscode.WorkspaceEdit, versionInfo?: IVersionInformationProvider): extHostProtocol.IWorkspaceEditDto { + export function from(value: vscode.WorkspaceEdit, versionInfo?: IVersionInformationProvider, allowSnippetTextEdit?: boolean): extHostProtocol.IWorkspaceEditDto { const result: extHostProtocol.IWorkspaceEditDto = { edits: [] }; @@ -604,14 +598,21 @@ export namespace WorkspaceEdit { }); } else if (entry._type === types.FileEditType.Text) { + // text edits - result.edits.push(<extHostProtocol.IWorkspaceTextEditDto>{ + const dto = <extHostProtocol.IWorkspaceTextEditDto>{ _type: extHostProtocol.WorkspaceEditType.Text, resource: entry.uri, edit: TextEdit.from(entry.edit), modelVersionId: !toCreate.has(entry.uri) ? versionInfo?.getTextDocumentVersion(entry.uri) : undefined, metadata: entry.metadata - }); + }; + if (allowSnippetTextEdit && entry.edit.newText2 instanceof types.SnippetString) { + dto.edit.insertAsSnippet = true; + dto.edit.text = entry.edit.newText2.value; + } + result.edits.push(dto); + } else if (entry._type === types.FileEditType.Cell) { result.edits.push(<extHostProtocol.IWorkspaceCellEditDto>{ _type: extHostProtocol.WorkspaceEditType.Cell, @@ -1571,7 +1572,7 @@ export namespace NotebookData { metadata: data.metadata ?? Object.create(null), cells: [], }; - for (let cell of data.cells) { + for (const cell of data.cells) { types.NotebookCellData.validate(cell); res.cells.push(NotebookCellData.from(cell)); } @@ -1681,16 +1682,6 @@ export namespace NotebookExclusiveDocumentPattern { } } -export namespace NotebookDecorationRenderOptions { - export function from(options: vscode.NotebookDecorationRenderOptions): notebooks.INotebookDecorationRenderOptions { - return { - backgroundColor: <string | types.ThemeColor>options.backgroundColor, - borderColor: <string | types.ThemeColor>options.borderColor, - top: options.top ? ThemableDecorationAttachmentRenderOptions.from(options.top) : undefined - }; - } -} - export namespace NotebookStatusBarItem { export function from(item: vscode.NotebookCellStatusBarItem, commandsConverter: Command.ICommandsConverter, disposables: DisposableStore): notebooks.INotebookCellStatusBarItem { const command = typeof item.command === 'string' ? { title: '', command: item.command } : item.command; @@ -1786,6 +1777,7 @@ export namespace TestItem { add: () => { }, delete: () => { }, forEach: () => { }, + *[Symbol.iterator]() { }, get: () => undefined, replace: () => { }, size: 0, @@ -1956,3 +1948,51 @@ export namespace ViewBadge { }; } } + +export namespace DataTransferItem { + export function toDataTransferItem(item: extHostProtocol.DataTransferItemDTO, resolveFileData: () => Promise<Uint8Array>): types.DataTransferItem { + const file = item.fileData; + if (file) { + return new class extends types.DataTransferItem { + override asFile(): vscode.DataTransferFile { + return { + name: file.name, + uri: URI.revive(file.uri), + data: once(() => resolveFileData()), + }; + } + }(''); + } else { + return new types.DataTransferItem(item.asString); + } + } +} + +export namespace DataTransfer { + export function toDataTransfer(value: extHostProtocol.DataTransferDTO, resolveFileData: (dataItemIndex: number) => Promise<Uint8Array>): types.DataTransfer { + const init = value.items.map(([type, item], index) => { + return [type, DataTransferItem.toDataTransferItem(item, () => resolveFileData(index))] as const; + }); + return new types.DataTransfer(init); + } + + export async function toDataTransferDTO(value: vscode.DataTransfer | VSDataTransfer): Promise<extHostProtocol.DataTransferDTO> { + const newDTO: extHostProtocol.DataTransferDTO = { items: [] }; + + const promises: Promise<any>[] = []; + value.forEach((value, key) => { + promises.push((async () => { + const stringValue = await value.asString(); + const fileValue = value.asFile(); + newDTO.items.push([key, { + asString: stringValue, + fileData: fileValue ? { name: fileValue.name, uri: fileValue.uri } : undefined, + }]); + })()); + }); + + await Promise.all(promises); + + return newDTO; + } +} diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index dc08ede3d98..ce30f68d26e 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -97,7 +97,7 @@ export class Position { if (other instanceof Position) { return true; } - let { line, character } = <Position>other; + const { line, character } = <Position>other; if (typeof line === 'number' && typeof character === 'number') { return true; } @@ -549,6 +549,7 @@ export class TextEdit { protected _range: Range; protected _newText: string | null; + newText2?: string | SnippetString; protected _newEol?: EndOfLine; get range(): Range { @@ -647,17 +648,6 @@ export class NotebookEdit implements vscode.NotebookEdit { } } -export class SnippetTextEdit implements vscode.SnippetTextEdit { - - range: vscode.Range; - snippet: vscode.SnippetString; - - constructor(range: Range, snippet: SnippetString) { - this.range = range; - this.snippet = snippet; - } -} - export interface IFileOperationOptions { overwrite?: boolean; ignoreIfExists?: boolean; @@ -807,6 +797,8 @@ export class WorkspaceEdit implements vscode.WorkspaceEdit { if (NotebookEdit.isNotebookCellEdit(edit)) { if (edit.newCellMetadata) { this.replaceNotebookCellMetadata(uri, edit.range.start, edit.newCellMetadata); + } else if (edit.newNotebookMetadata) { + this.replaceNotebookMetadata(uri, edit.newNotebookMetadata); } else { this.replaceNotebookCells(uri, edit.range, edit.newCells); } @@ -820,7 +812,7 @@ export class WorkspaceEdit implements vscode.WorkspaceEdit { get(uri: URI): TextEdit[] { const res: TextEdit[] = []; - for (let candidate of this._edits) { + for (const candidate of this._edits) { if (candidate._type === FileEditType.Text && candidate.uri.toString() === uri.toString()) { res.push(candidate.edit); } @@ -830,7 +822,7 @@ export class WorkspaceEdit implements vscode.WorkspaceEdit { entries(): [URI, TextEdit[]][] { const textEdits = new ResourceMap<[URI, TextEdit[]]>(); - for (let candidate of this._edits) { + for (const candidate of this._edits) { if (candidate._type === FileEditType.Text) { let textEdit = textEdits.get(candidate.uri); if (!textEdit) { @@ -1218,9 +1210,7 @@ export class DocumentSymbol { if (!candidate.range.contains(candidate.selectionRange)) { throw new Error('selectionRange must be contained in fullRange'); } - if (candidate.children) { - candidate.children.forEach(DocumentSymbol.validate); - } + candidate.children?.forEach(DocumentSymbol.validate); } name: string; @@ -2028,7 +2018,7 @@ export class ProcessExecution implements vscode.ProcessExecution { props.push(this._process); } if (this._args && this._args.length > 0) { - for (let arg of this._args) { + for (const arg of this._args) { props.push(arg); } } @@ -2114,7 +2104,7 @@ export class ShellExecution implements vscode.ShellExecution { props.push(typeof this._command === 'string' ? this._command : this._command.value); } if (this._args && this._args.length > 0) { - for (let arg of this._args) { + for (const arg of this._args) { props.push(typeof arg === 'string' ? arg : arg.value); } } @@ -2438,11 +2428,12 @@ export enum TreeItemCollapsibleState { @es5ClassCompat export class DataTransferItem { + async asString(): Promise<string> { return typeof this.value === 'string' ? this.value : JSON.stringify(this.value); } - asFile(): undefined { + asFile(): undefined | vscode.DataTransferFile { return undefined; } @@ -2450,19 +2441,68 @@ export class DataTransferItem { } @es5ClassCompat -export class DataTransfer { - private readonly _items: Map<string, DataTransferItem> = new Map(); +export class DataTransfer implements vscode.DataTransfer { + #items = new Map<string, DataTransferItem[]>(); + + constructor(init?: Iterable<readonly [string, DataTransferItem]>) { + for (const [mime, item] of init ?? []) { + const existing = this.#items.get(mime); + if (existing) { + existing.push(item); + } else { + this.#items.set(mime, [item]); + } + } + } + get(mimeType: string): DataTransferItem | undefined { - return this._items.get(mimeType); + return this.#items.get(mimeType)?.[0]; } + set(mimeType: string, value: DataTransferItem): void { - this._items.set(mimeType, value); + // This intentionally overwrites all entries for a given mimetype. + // This is similar to how the DOM DataTransfer type works + this.#items.set(mimeType, [value]); } - forEach(callbackfn: (value: DataTransferItem, key: string) => void): void { - this._items.forEach(callbackfn); + + forEach(callbackfn: (value: DataTransferItem, key: string, dataTransfer: DataTransfer) => void, thisArg?: unknown): void { + for (const [mime, items] of this.#items) { + for (const item of items) { + callbackfn.call(thisArg, item, mime, this); + } + } + } + + *[Symbol.iterator](): IterableIterator<[mimeType: string, item: vscode.DataTransferItem]> { + for (const [mime, items] of this.#items) { + for (const item of items) { + yield [mime, item]; + } + } } } +@es5ClassCompat +export class DocumentDropEdit { + insertText: string | SnippetString; + + additionalEdit?: WorkspaceEdit; + + constructor(insertText: string | SnippetString) { + this.insertText = insertText; + } +} + +@es5ClassCompat +export class DocumentPasteEdit { + insertText: string | SnippetString; + + additionalEdit?: WorkspaceEdit; + + constructor(insertText: string | SnippetString) { + this.insertText = insertText; + } +} @es5ClassCompat export class ThemeIcon { @@ -2978,7 +3018,7 @@ export class SemanticTokensBuilder { } private static _sortAndDeltaEncode(data: number[]): Uint32Array { - let pos: number[] = []; + const pos: number[] = []; const tokenCount = (data.length / 5) | 0; for (let i = 0; i < tokenCount; i++) { pos[i] = i; diff --git a/src/vs/workbench/api/common/extHostWorkspace.ts b/src/vs/workbench/api/common/extHostWorkspace.ts index dda77ef065e..9127c0db90e 100644 --- a/src/vs/workbench/api/common/extHostWorkspace.ts +++ b/src/vs/workbench/api/common/extHostWorkspace.ts @@ -228,7 +228,7 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac if (this._actualWorkspace) { if (this._actualWorkspace.configuration) { if (this._actualWorkspace.isUntitled) { - return URI.from({ scheme: Schemas.untitled, path: basename(dirname(this._actualWorkspace.configuration)) }); // Untitled Worspace: return untitled URI + return URI.from({ scheme: Schemas.untitled, path: basename(dirname(this._actualWorkspace.configuration)) }); // Untitled Workspace: return untitled URI } return this._actualWorkspace.configuration; // Workspace: return the configuration location @@ -546,9 +546,7 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac } $handleTextSearchResult(result: IRawFileMatch2, requestId: number): void { - if (this._activeSearchCallbacks[requestId]) { - this._activeSearchCallbacks[requestId](result); - } + this._activeSearchCallbacks[requestId]?.(result); } saveAll(includeUntitled?: boolean): Promise<boolean> { diff --git a/src/vs/workbench/api/common/jsonValidationExtensionPoint.ts b/src/vs/workbench/api/common/jsonValidationExtensionPoint.ts index 895d36a310f..f95aa03c0d4 100644 --- a/src/vs/workbench/api/common/jsonValidationExtensionPoint.ts +++ b/src/vs/workbench/api/common/jsonValidationExtensionPoint.ts @@ -58,7 +58,7 @@ export class JSONValidationExtensionPoint { collector.error(nls.localize('invalid.fileMatch', "'configuration.jsonValidation.fileMatch' must be defined as a string or an array of strings.")); return; } - let uri = extension.url; + const uri = extension.url; if (!isString(uri)) { collector.error(nls.localize('invalid.url', "'configuration.jsonValidation.url' must be a URL or relative path")); return; diff --git a/src/vs/workbench/api/common/shared/dataTransfer.ts b/src/vs/workbench/api/common/shared/dataTransfer.ts deleted file mode 100644 index a16dfe65f96..00000000000 --- a/src/vs/workbench/api/common/shared/dataTransfer.ts +++ /dev/null @@ -1,66 +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 { once } from 'vs/base/common/functional'; -import { URI, UriComponents } from 'vs/base/common/uri'; -import { IDataTransfer, IDataTransferItem } from 'vs/base/common/dataTransfer'; - -export interface IDataTransferFileDTO { - readonly name: string; - readonly uri?: UriComponents; -} - -interface DataTransferItemDTO { - readonly asString: string; - readonly fileData: IDataTransferFileDTO | undefined; -} - -export interface DataTransferDTO { - readonly types: string[]; - readonly items: DataTransferItemDTO[]; -} - -export namespace DataTransferConverter { - export function toDataTransfer(value: DataTransferDTO, resolveFileData: (dataItemIndex: number) => Promise<Uint8Array>): IDataTransfer { - const newDataTransfer: IDataTransfer = new Map<string, IDataTransferItem>(); - value.types.forEach((type, index) => { - newDataTransfer.set(type, { - asString: async () => value.items[index].asString, - asFile: () => { - const file = value.items[index].fileData; - if (!file) { - return undefined; - } - return { - name: file.name, - uri: URI.revive(file.uri), - data: once(() => resolveFileData(index)), - }; - }, - value: undefined - }); - }); - return newDataTransfer; - } - - export async function toDataTransferDTO(value: IDataTransfer): Promise<DataTransferDTO> { - const newDTO: DataTransferDTO = { - types: [], - items: [] - }; - - const entries = Array.from(value.entries()); - for (const entry of entries) { - newDTO.types.push(entry[0]); - const stringValue = await entry[1].asString(); - const fileValue = entry[1].asFile(); - newDTO.items.push({ - asString: stringValue, - fileData: fileValue ? { name: fileValue.name, uri: fileValue.uri } : undefined, - }); - } - return newDTO; - } -} diff --git a/src/vs/workbench/api/common/shared/dataTransferCache.ts b/src/vs/workbench/api/common/shared/dataTransferCache.ts index 02184d68d18..6cc8a36b6e6 100644 --- a/src/vs/workbench/api/common/shared/dataTransferCache.ts +++ b/src/vs/workbench/api/common/shared/dataTransferCache.ts @@ -4,14 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import { VSBuffer } from 'vs/base/common/buffer'; -import { IDataTransfer, IDataTransferItem } from 'vs/base/common/dataTransfer'; +import { VSDataTransfer, IDataTransferItem } from 'vs/base/common/dataTransfer'; export class DataTransferCache { private requestIdPool = 0; private readonly dataTransfers = new Map</* requestId */ number, ReadonlyArray<IDataTransferItem>>(); - public add(dataTransfer: IDataTransfer): { id: number; dispose: () => void } { + public add(dataTransfer: VSDataTransfer): { id: number; dispose: () => void } { const requestId = this.requestIdPool++; this.dataTransfers.set(requestId, [...dataTransfer.values()]); return { diff --git a/src/vs/workbench/api/common/shared/tasks.ts b/src/vs/workbench/api/common/shared/tasks.ts index 61c0d091a59..f7f206f0649 100644 --- a/src/vs/workbench/api/common/shared/tasks.ts +++ b/src/vs/workbench/api/common/shared/tasks.ts @@ -7,12 +7,12 @@ import { UriComponents } from 'vs/base/common/uri'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import type { Dto } from 'vs/workbench/services/extensions/common/proxyIdentifier'; -export interface TaskDefinitionDTO { +export interface ITaskDefinitionDTO { type: string; [name: string]: any; } -export interface TaskPresentationOptionsDTO { +export interface ITaskPresentationOptionsDTO { reveal?: number; echo?: boolean; focus?: boolean; @@ -23,25 +23,25 @@ export interface TaskPresentationOptionsDTO { close?: boolean; } -export interface RunOptionsDTO { +export interface IRunOptionsDTO { reevaluateOnRerun?: boolean; } -export interface ExecutionOptionsDTO { +export interface IExecutionOptionsDTO { cwd?: string; env?: { [key: string]: string }; } -export interface ProcessExecutionOptionsDTO extends ExecutionOptionsDTO { +export interface IProcessExecutionOptionsDTO extends IExecutionOptionsDTO { } -export interface ProcessExecutionDTO { +export interface IProcessExecutionDTO { process: string; args: string[]; - options?: ProcessExecutionOptionsDTO; + options?: IProcessExecutionOptionsDTO; } -export interface ShellQuotingOptionsDTO { +export interface IShellQuotingOptionsDTO { escape?: string | { escapeChar: string; charsToEscape: string; @@ -50,86 +50,88 @@ export interface ShellQuotingOptionsDTO { weak?: string; } -export interface ShellExecutionOptionsDTO extends ExecutionOptionsDTO { +export interface IShellExecutionOptionsDTO extends IExecutionOptionsDTO { executable?: string; shellArgs?: string[]; - shellQuoting?: ShellQuotingOptionsDTO; + shellQuoting?: IShellQuotingOptionsDTO; } -export interface ShellQuotedStringDTO { +export interface IShellQuotedStringDTO { value: string; quoting: number; } -export interface ShellExecutionDTO { +export interface IShellExecutionDTO { commandLine?: string; - command?: string | ShellQuotedStringDTO; - args?: Array<string | ShellQuotedStringDTO>; - options?: ShellExecutionOptionsDTO; + command?: string | IShellQuotedStringDTO; + args?: Array<string | IShellQuotedStringDTO>; + options?: IShellExecutionOptionsDTO; } -export interface CustomExecutionDTO { +export interface ICustomExecutionDTO { customExecution: 'customExecution'; } -export interface TaskSourceDTO { +export interface ITaskSourceDTO { label: string; extensionId?: string; scope?: number | UriComponents; + color?: string; + icon?: string; } -export interface TaskHandleDTO { +export interface ITaskHandleDTO { id: string; workspaceFolder: UriComponents | string; } -export interface TaskGroupDTO { +export interface ITaskGroupDTO { isDefault?: boolean; _id: string; } -export interface TaskDTO { +export interface ITaskDTO { _id: string; name?: string; - execution: ProcessExecutionDTO | ShellExecutionDTO | CustomExecutionDTO | undefined; - definition: TaskDefinitionDTO; + execution: IProcessExecutionDTO | IShellExecutionDTO | ICustomExecutionDTO | undefined; + definition: ITaskDefinitionDTO; isBackground?: boolean; - source: TaskSourceDTO; - group?: TaskGroupDTO; + source: ITaskSourceDTO; + group?: ITaskGroupDTO; detail?: string; - presentationOptions?: TaskPresentationOptionsDTO; + presentationOptions?: ITaskPresentationOptionsDTO; problemMatchers: string[]; hasDefinedMatchers: boolean; - runOptions?: RunOptionsDTO; + runOptions?: IRunOptionsDTO; } -export interface TaskSetDTO { - tasks: TaskDTO[]; +export interface ITaskSetDTO { + tasks: ITaskDTO[]; extension: Dto<IExtensionDescription>; } -export interface TaskExecutionDTO { +export interface ITaskExecutionDTO { id: string; - task: TaskDTO | undefined; + task: ITaskDTO | undefined; } -export interface TaskProcessStartedDTO { +export interface ITaskProcessStartedDTO { id: string; processId: number; } -export interface TaskProcessEndedDTO { +export interface ITaskProcessEndedDTO { id: string; exitCode: number | undefined; } -export interface TaskFilterDTO { +export interface ITaskFilterDTO { version?: string; type?: string; } -export interface TaskSystemInfoDTO { +export interface ITaskSystemInfoDTO { scheme: string; authority: string; platform: string; diff --git a/src/vs/workbench/api/node/extHostConsoleForwarder.ts b/src/vs/workbench/api/node/extHostConsoleForwarder.ts new file mode 100644 index 00000000000..da357ece43d --- /dev/null +++ b/src/vs/workbench/api/node/extHostConsoleForwarder.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AbstractExtHostConsoleForwarder } from 'vs/workbench/api/common/extHostConsoleForwarder'; +import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; +import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; +import { NativeLogMarkers } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; + +const MAX_STREAM_BUFFER_LENGTH = 1024 * 1024; + +export class ExtHostConsoleForwarder extends AbstractExtHostConsoleForwarder { + + private _isMakingConsoleCall: boolean = false; + + constructor( + @IExtHostRpcService extHostRpc: IExtHostRpcService, + @IExtHostInitDataService initData: IExtHostInitDataService, + ) { + super(extHostRpc, initData); + + this._wrapStream('stderr', 'error'); + this._wrapStream('stdout', 'log'); + } + + protected override _nativeConsoleLogMessage(method: 'log' | 'info' | 'warn' | 'error', original: (...args: any[]) => void, args: IArguments) { + const stream = method === 'error' || method === 'warn' ? process.stderr : process.stdout; + this._isMakingConsoleCall = true; + stream.write(`\n${NativeLogMarkers.Start}\n`); + original.apply(console, args as any); + stream.write(`\n${NativeLogMarkers.End}\n`); + this._isMakingConsoleCall = false; + } + + /** + * Wraps process.stderr/stdout.write() so that it is transmitted to the + * renderer or CLI. It both calls through to the original method as well + * as to console.log with complete lines so that they're made available + * to the debugger/CLI. + */ + private _wrapStream(streamName: 'stdout' | 'stderr', severity: 'log' | 'warn' | 'error') { + const stream = process[streamName]; + const original = stream.write; + + let buf = ''; + + Object.defineProperty(stream, 'write', { + set: () => { }, + get: () => (chunk: Uint8Array | string, encoding?: BufferEncoding, callback?: (err?: Error) => void) => { + if (!this._isMakingConsoleCall) { + buf += (chunk as any).toString(encoding); + const eol = buf.length > MAX_STREAM_BUFFER_LENGTH ? buf.length : buf.lastIndexOf('\n'); + if (eol !== -1) { + console[severity](buf.slice(0, eol)); + buf = buf.slice(eol + 1); + } + } + + original.call(stream, chunk, encoding, callback); + }, + }); + } +} diff --git a/src/vs/workbench/api/node/extHostExtensionService.ts b/src/vs/workbench/api/node/extHostExtensionService.ts index 62e0d6b4998..352ce260dd8 100644 --- a/src/vs/workbench/api/node/extHostExtensionService.ts +++ b/src/vs/workbench/api/node/extHostExtensionService.ts @@ -6,7 +6,6 @@ import * as performance from 'vs/base/common/performance'; import { createApiFactoryAndRegisterActors } from 'vs/workbench/api/common/extHost.api.impl'; import { RequireInterceptor } from 'vs/workbench/api/common/extHostRequireInterceptor'; -import { MainContext } from 'vs/workbench/api/common/extHost.protocol'; import { ExtensionActivationTimesBuilder } from 'vs/workbench/api/common/extHostExtensionActivator'; import { connectProxyResolver } from 'vs/workbench/api/node/proxyResolver'; import { AbstractExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; @@ -17,6 +16,7 @@ import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensio import { ExtensionRuntime } from 'vs/workbench/api/common/extHostTypes'; import { CLIServer } from 'vs/workbench/api/node/extHostCLIServer'; import { realpathSync } from 'vs/base/node/extpath'; +import { ExtHostConsoleForwarder } from 'vs/workbench/api/node/extHostConsoleForwarder'; class NodeModuleRequireInterceptor extends RequireInterceptor { @@ -42,8 +42,8 @@ class NodeModuleRequireInterceptor extends RequireInterceptor { }; const applyAlternatives = (request: string) => { - for (let alternativeModuleName of that._alternatives) { - let alternative = alternativeModuleName(request); + for (const alternativeModuleName of that._alternatives) { + const alternative = alternativeModuleName(request); if (alternative) { request = alternative; break; @@ -59,6 +59,9 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { readonly extensionRuntime = ExtensionRuntime.Node; protected async _beforeAlmostReadyToRunExtensions(): Promise<void> { + // make sure console.log calls make it to the render + this._instaService.createInstance(ExtHostConsoleForwarder); + // initialize API and register actors const extensionApiFactory = this._instaService.invokeFunction(createApiFactoryAndRegisterActors); @@ -80,18 +83,6 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { const configProvider = await this._extHostConfiguration.getConfigProvider(); await connectProxyResolver(this._extHostWorkspace, configProvider, this, this._logService, this._mainThreadTelemetryProxy, this._initData); performance.mark('code/extHost/didInitProxyResolver'); - - // Use IPC messages to forward console-calls, note that the console is - // already patched to use`process.send()` - const nativeProcessSend = process.send!; - const mainThreadConsole = this._extHostContext.getProxy(MainContext.MainThreadConsole); - process.send = (...args) => { - if ((args as unknown[]).length === 0 || !args[0] || args[0].type !== '__$console') { - return nativeProcessSend.apply(process, args); - } - mainThreadConsole.$logExtensionHostMessage(args[0]); - return false; - }; } protected _getEntryPoint(extensionDescription: IExtensionDescription): string | undefined { diff --git a/src/vs/workbench/api/node/extHostSearch.ts b/src/vs/workbench/api/node/extHostSearch.ts index 9867e2a5e6b..8e1dab9ef1b 100644 --- a/src/vs/workbench/api/node/extHostSearch.ts +++ b/src/vs/workbench/api/node/extHostSearch.ts @@ -103,9 +103,7 @@ export class NativeExtHostSearch extends ExtHostSearch { } override $clearCache(cacheKey: string): Promise<void> { - if (this._internalFileSearchProvider) { - this._internalFileSearchProvider.clearCache(cacheKey); - } + this._internalFileSearchProvider?.clearCache(cacheKey); return super.$clearCache(cacheKey); } diff --git a/src/vs/workbench/api/node/extHostStoragePaths.ts b/src/vs/workbench/api/node/extHostStoragePaths.ts index 06cfede9d13..8348d9490b2 100644 --- a/src/vs/workbench/api/node/extHostStoragePaths.ts +++ b/src/vs/workbench/api/node/extHostStoragePaths.ts @@ -63,9 +63,7 @@ export class ExtensionStoragePaths extends CommonExtensionStoragePaths { override onWillDeactivateAll(): void { // the lock will be released soon - if (this._workspaceStorageLock) { - this._workspaceStorageLock.setWillRelease(6000); - } + this._workspaceStorageLock?.setWillRelease(6000); } } diff --git a/src/vs/workbench/api/node/extHostTask.ts b/src/vs/workbench/api/node/extHostTask.ts index 7afdfc40ea0..b5fac17cde7 100644 --- a/src/vs/workbench/api/node/extHostTask.ts +++ b/src/vs/workbench/api/node/extHostTask.ts @@ -92,17 +92,17 @@ export class ExtHostTask extends ExtHostTaskBase { } } - protected provideTasksInternal(validTypes: { [key: string]: boolean }, taskIdPromises: Promise<void>[], handler: HandlerData, value: vscode.Task[] | null | undefined): { tasks: tasks.TaskDTO[]; extension: IExtensionDescription } { - const taskDTOs: tasks.TaskDTO[] = []; + protected provideTasksInternal(validTypes: { [key: string]: boolean }, taskIdPromises: Promise<void>[], handler: HandlerData, value: vscode.Task[] | null | undefined): { tasks: tasks.ITaskDTO[]; extension: IExtensionDescription } { + const taskDTOs: tasks.ITaskDTO[] = []; if (value) { - for (let task of value) { + for (const task of value) { this.checkDeprecation(task, handler); if (!task.definition || !validTypes[task.definition.type]) { this._logService.warn(`The task [${task.source}, ${task.name}] uses an undefined task type. The task will be ignored in the future.`); } - const taskDTO: tasks.TaskDTO | undefined = TaskDTO.from(task, handler.extension); + const taskDTO: tasks.ITaskDTO | undefined = TaskDTO.from(task, handler.extension); if (taskDTO) { taskDTOs.push(taskDTO); @@ -121,7 +121,7 @@ export class ExtHostTask extends ExtHostTaskBase { }; } - protected async resolveTaskInternal(resolvedTaskDTO: tasks.TaskDTO): Promise<tasks.TaskDTO | undefined> { + protected async resolveTaskInternal(resolvedTaskDTO: tasks.ITaskDTO): Promise<tasks.ITaskDTO | undefined> { return resolvedTaskDTO; } @@ -160,7 +160,7 @@ export class ExtHostTask extends ExtHostTaskBase { } } : await this.getAFolder(workspaceFolders); - for (let variable of toResolve.variables) { + for (const variable of toResolve.variables) { result.variables[variable] = await resolver.resolveAsync(ws, variable); } if (toResolve.process !== undefined) { diff --git a/src/vs/workbench/api/node/extHostTunnelService.ts b/src/vs/workbench/api/node/extHostTunnelService.ts index a66f16262b2..d7abe11a16d 100644 --- a/src/vs/workbench/api/node/extHostTunnelService.ts +++ b/src/vs/workbench/api/node/extHostTunnelService.ts @@ -16,27 +16,14 @@ import * as pfs from 'vs/base/node/pfs'; import * as types from 'vs/workbench/api/common/extHostTypes'; import { isLinux } from 'vs/base/common/platform'; import { IExtHostTunnelService, TunnelDtoConverter } from 'vs/workbench/api/common/extHostTunnelService'; -import { Event, Emitter } from 'vs/base/common/event'; -import { TunnelOptions, TunnelCreationOptions, ProvidedPortAttributes, ProvidedOnAutoForward, isLocalhost, isAllInterfaces } from 'vs/platform/tunnel/common/tunnel'; +import { Emitter } from 'vs/base/common/event'; +import { TunnelOptions, TunnelCreationOptions, ProvidedPortAttributes, ProvidedOnAutoForward, isLocalhost, isAllInterfaces, DisposableTunnel } from 'vs/platform/tunnel/common/tunnel'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { MovingAverage } from 'vs/base/common/numbers'; import { CandidatePort } from 'vs/workbench/services/remote/common/remoteExplorerService'; import { ILogService } from 'vs/platform/log/common/log'; -class ExtensionTunnel implements vscode.Tunnel { - private _onDispose: Emitter<void> = new Emitter(); - onDidDispose: Event<void> = this._onDispose.event; - - constructor( - public readonly remoteAddress: { port: number; host: string }, - public readonly localAddress: { port: number; host: string } | string, - private readonly _dispose: () => Promise<void>) { } - - dispose(): Promise<void> { - this._onDispose.fire(); - return this._dispose(); - } -} +class ExtensionTunnel extends DisposableTunnel implements vscode.Tunnel { } export function getSockets(stdout: string): Record<string, { pid: number; socket: number }> { const lines = stdout.trim().split('\n'); @@ -267,7 +254,7 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe } this._candidateFindingEnabled = enable; // Regularly scan to see if the candidate ports have changed. - let movingAverage = new MovingAverage(); + const movingAverage = new MovingAverage(); let oldPorts: { host: string; port: number; detail?: string }[] | undefined = undefined; while (this._candidateFindingEnabled) { const startTime = new Date().getTime(); @@ -402,7 +389,7 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe const processes: { pid: number; cwd: string; cmd: string; }[] = []; - for (let childName of procChildren) { + for (const childName of procChildren) { try { const pid: number = Number(childName); const childUri = resources.joinPath(URI.file('/proc'), childName); diff --git a/src/vs/workbench/api/node/extensionHostProcess.ts b/src/vs/workbench/api/node/extensionHostProcess.ts index 2707ecf73d3..bad55575c87 100644 --- a/src/vs/workbench/api/node/extensionHostProcess.ts +++ b/src/vs/workbench/api/node/extensionHostProcess.ts @@ -23,6 +23,8 @@ import { IHostUtils } from 'vs/workbench/api/common/extHostExtensionService'; import { ProcessTimeRunOnceScheduler } from 'vs/base/common/async'; import { boolean } from 'vs/editor/common/config/editorOptions'; import { createURITransformer } from 'vs/workbench/api/node/uriTransformer'; +import { MessagePortMain } from 'electron'; +import { ExtHostConnectionType, readExtHostConnection } from 'vs/workbench/services/extensions/common/extensionHostEnv'; import 'vs/workbench/api/common/extHost.common.services'; import 'vs/workbench/api/node/extHost.node.services'; @@ -89,6 +91,12 @@ function patchProcess(allowExit: boolean) { const err = new Error('An extension called process.crash() and this was prevented.'); console.warn(err.stack); }; + + // Set ELECTRON_RUN_AS_NODE environment variable for extensions that use + // child_process.spawn with process.execPath and expect to run as node process + // on the desktop. + // Refs https://github.com/microsoft/vscode/issues/151012#issuecomment-1156593228 + process.env['ELECTRON_RUN_AS_NODE'] = '1'; } interface IRendererConnection { @@ -102,14 +110,45 @@ let onTerminate = function (reason: string) { nativeExit(); }; -function _createExtHostProtocol(): Promise<PersistentProtocol> { - if (process.env.VSCODE_EXTHOST_WILL_SEND_SOCKET) { +function _createExtHostProtocol(): Promise<IMessagePassingProtocol> { + const extHostConnection = readExtHostConnection(process.env); + + if (extHostConnection.type === ExtHostConnectionType.MessagePort) { + + return new Promise<IMessagePassingProtocol>((resolve, reject) => { + + const withPorts = (ports: MessagePortMain[]) => { + const port = ports[0]; + const onMessage = new BufferedEmitter<VSBuffer>(); + port.on('message', (e) => onMessage.fire(VSBuffer.wrap(e.data))); + port.on('close', () => { + onTerminate('renderer closed the MessagePort'); + }); + port.start(); + + resolve({ + onMessage: onMessage.event, + send: message => port.postMessage(message.buffer) + }); + }; + + if ((<any>global).vscodePorts) { + const ports = (<any>global).vscodePorts; + delete (<any>global).vscodePorts; + withPorts(ports); + } else { + (<any>global).vscodePortsCallback = withPorts; + } + + }); + + } else if (extHostConnection.type === ExtHostConnectionType.Socket) { return new Promise<PersistentProtocol>((resolve, reject) => { let protocol: PersistentProtocol | null = null; - let timer = setTimeout(() => { + const timer = setTimeout(() => { onTerminate('VSCODE_EXTHOST_IPC_SOCKET timeout'); }, 60000); @@ -167,20 +206,20 @@ function _createExtHostProtocol(): Promise<PersistentProtocol> { // Now that we have managed to install a message listener, ask the other side to send us the socket const req: IExtHostReadyMessage = { type: 'VSCODE_EXTHOST_IPC_READY' }; - if (process.send) { - process.send(req); - } + process.send?.(req); }); } else { - const pipeName = process.env.VSCODE_IPC_HOOK_EXTHOST!; + const pipeName = extHostConnection.pipeName; return new Promise<PersistentProtocol>((resolve, reject) => { const socket = net.createConnection(pipeName, () => { socket.removeListener('error', reject); - resolve(new PersistentProtocol(new NodeSocket(socket, 'extHost-renderer'))); + const protocol = new PersistentProtocol(new NodeSocket(socket, 'extHost-renderer')); + protocol.sendResume(); + resolve(protocol); }); socket.once('error', reject); @@ -220,8 +259,10 @@ async function createExtHostProtocol(): Promise<IMessagePassingProtocol> { } } - drain(): Promise<void> { - return protocol.drain(); + async drain(): Promise<void> { + if (protocol.drain) { + return protocol.drain(); + } } }; } diff --git a/src/vs/workbench/api/node/proxyResolver.ts b/src/vs/workbench/api/node/proxyResolver.ts index d8295df7625..ba751992465 100644 --- a/src/vs/workbench/api/node/proxyResolver.ts +++ b/src/vs/workbench/api/node/proxyResolver.ts @@ -115,7 +115,7 @@ function configureModuleLoading(extensionService: ExtHostExtensionService, looku modulesCache.set(ext, cache = {}); } if (!cache[request]) { - let mod = modules.default; + const mod = modules.default; cache[request] = <any>{ ...mod }; // Copy to work around #93167. } return cache[request]; diff --git a/src/vs/workbench/api/test/browser/extHostApiCommands.test.ts b/src/vs/workbench/api/test/browser/extHostApiCommands.test.ts index c22033dd8a5..b3fd49e72b2 100644 --- a/src/vs/workbench/api/test/browser/extHostApiCommands.test.ts +++ b/src/vs/workbench/api/test/browser/extHostApiCommands.test.ts @@ -24,7 +24,6 @@ import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocum import { MainContext, ExtHostContext } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostDiagnostics } from 'vs/workbench/api/common/extHostDiagnostics'; import type * as vscode from 'vscode'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import 'vs/workbench/contrib/search/browser/search.contribution'; import { ILogService, NullLogService } from 'vs/platform/log/common/log'; import { ITextModel } from 'vs/editor/common/model'; @@ -94,7 +93,6 @@ suite('ExtHostLanguageFeatureCommands', function () { setUnexpectedErrorHandler(() => { }); // Use IInstantiationService to get typechecking when instantiating - let insta: IInstantiationService; rpcProtocol = new TestRPCProtocol(); const services = new ServiceCollection(); services.set(ILanguageFeaturesService, new SyncDescriptor(LanguageFeaturesService)); @@ -137,7 +135,7 @@ suite('ExtHostLanguageFeatureCommands', function () { services.set(ILanguageFeatureDebounceService, new SyncDescriptor(LanguageFeatureDebounceService)); services.set(IOutlineModelService, new SyncDescriptor(OutlineModelService)); - insta = new InstantiationService(services); + const insta = new InstantiationService(services); const extHostDocumentsAndEditors = new ExtHostDocumentsAndEditors(rpcProtocol, new NullLogService()); extHostDocumentsAndEditors.$acceptDocumentsAndEditorsDelta({ @@ -183,7 +181,7 @@ suite('ExtHostLanguageFeatureCommands', function () { // --- workspace symbols test('WorkspaceSymbols, invalid arguments', function () { - let promises = [ + const promises = [ assertRejects(() => commands.executeCommand('vscode.executeWorkspaceSymbolProvider')), assertRejects(() => commands.executeCommand('vscode.executeWorkspaceSymbolProvider', null)), assertRejects(() => commands.executeCommand('vscode.executeWorkspaceSymbolProvider', undefined)), @@ -215,7 +213,7 @@ suite('ExtHostLanguageFeatureCommands', function () { return commands.executeCommand<vscode.SymbolInformation[]>('vscode.executeWorkspaceSymbolProvider', 'testing').then(value => { assert.strictEqual(value.length, 2); // de-duped - for (let info of value) { + for (const info of value) { assert.strictEqual(info instanceof types.SymbolInformation, true); assert.strictEqual(info.name, 'testing'); assert.strictEqual(info.kind, types.SymbolKind.Array); @@ -251,7 +249,7 @@ suite('ExtHostLanguageFeatureCommands', function () { })); await rpcProtocol.sync(); - let edits = await commands.executeCommand<vscode.SymbolInformation[]>('vscode.executeFormatDocumentProvider', model.uri); + const edits = await commands.executeCommand<vscode.SymbolInformation[]>('vscode.executeFormatDocumentProvider', model.uri); assert.strictEqual(edits.length, 1); }); @@ -310,7 +308,7 @@ suite('ExtHostLanguageFeatureCommands', function () { // --- definition test('Definition, invalid arguments', function () { - let promises = [ + const promises = [ assertRejects(() => commands.executeCommand('vscode.executeDefinitionProvider')), assertRejects(() => commands.executeCommand('vscode.executeDefinitionProvider', null)), assertRejects(() => commands.executeCommand('vscode.executeDefinitionProvider', undefined)), @@ -346,7 +344,7 @@ suite('ExtHostLanguageFeatureCommands', function () { return rpcProtocol.sync().then(() => { return commands.executeCommand<vscode.Location[]>('vscode.executeDefinitionProvider', model.uri, new types.Position(0, 0)).then(values => { assert.strictEqual(values.length, 4); - for (let v of values) { + for (const v of values) { assert.ok(v.uri instanceof URI); assert.ok(v.range instanceof types.Range); } @@ -403,7 +401,7 @@ suite('ExtHostLanguageFeatureCommands', function () { return rpcProtocol.sync().then(() => { return commands.executeCommand<(vscode.Location | vscode.LocationLink)[]>('vscode.executeDefinitionProvider', model.uri, new types.Position(0, 0)).then(values => { assert.strictEqual(values.length, 2); - for (let v of values) { + for (const v of values) { if (isLocation(v)) { assert.ok(v.uri instanceof URI); assert.ok(v.range instanceof types.Range); @@ -446,7 +444,7 @@ suite('ExtHostLanguageFeatureCommands', function () { return rpcProtocol.sync().then(() => { return commands.executeCommand<vscode.Location[]>('vscode.executeDeclarationProvider', model.uri, new types.Position(0, 0)).then(values => { assert.strictEqual(values.length, 4); - for (let v of values) { + for (const v of values) { assert.ok(v.uri instanceof URI); assert.ok(v.range instanceof types.Range); } @@ -467,7 +465,7 @@ suite('ExtHostLanguageFeatureCommands', function () { return rpcProtocol.sync().then(() => { return commands.executeCommand<(vscode.Location | vscode.LocationLink)[]>('vscode.executeDeclarationProvider', model.uri, new types.Position(0, 0)).then(values => { assert.strictEqual(values.length, 2); - for (let v of values) { + for (const v of values) { if (isLocation(v)) { assert.ok(v.uri instanceof URI); assert.ok(v.range instanceof types.Range); @@ -542,7 +540,7 @@ suite('ExtHostLanguageFeatureCommands', function () { return rpcProtocol.sync().then(() => { return commands.executeCommand<(vscode.Location | vscode.LocationLink)[]>('vscode.executeTypeDefinitionProvider', model.uri, new types.Position(0, 0)).then(values => { assert.strictEqual(values.length, 2); - for (let v of values) { + for (const v of values) { if (isLocation(v)) { assert.ok(v.uri instanceof URI); assert.ok(v.range instanceof types.Range); @@ -617,7 +615,7 @@ suite('ExtHostLanguageFeatureCommands', function () { return rpcProtocol.sync().then(() => { return commands.executeCommand<(vscode.Location | vscode.LocationLink)[]>('vscode.executeImplementationProvider', model.uri, new types.Position(0, 0)).then(values => { assert.strictEqual(values.length, 2); - for (let v of values) { + for (const v of values) { if (isLocation(v)) { assert.ok(v.uri instanceof URI); assert.ok(v.range instanceof types.Range); @@ -646,7 +644,7 @@ suite('ExtHostLanguageFeatureCommands', function () { return commands.executeCommand<vscode.Location[]>('vscode.executeReferenceProvider', model.uri, new types.Position(0, 0)).then(values => { assert.strictEqual(values.length, 1); - let [first] = values; + const [first] = values; assert.strictEqual(first.uri.toString(), 'some:uri/path'); assert.strictEqual(first.range.start.line, 0); assert.strictEqual(first.range.start.character, 1); @@ -670,7 +668,7 @@ suite('ExtHostLanguageFeatureCommands', function () { return rpcProtocol.sync().then(() => { return commands.executeCommand<vscode.SymbolInformation[]>('vscode.executeDocumentSymbolProvider', model.uri).then(values => { assert.strictEqual(values.length, 2); - let [first, second] = values; + const [first, second] = values; assert.strictEqual(first instanceof types.SymbolInformation, true); assert.strictEqual(second instanceof types.SymbolInformation, true); assert.strictEqual(first.name, 'testing2'); @@ -689,7 +687,7 @@ suite('ExtHostLanguageFeatureCommands', function () { })); disposables.push(extHost.registerDocumentSymbolProvider(nullExtensionDescription, defaultSelector, <vscode.DocumentSymbolProvider>{ provideDocumentSymbols(): any { - let root = new types.DocumentSymbol('DocumentSymbol', 'DocumentSymbol#detail', types.SymbolKind.Enum, new types.Range(1, 0, 1, 0), new types.Range(1, 0, 1, 0)); + const root = new types.DocumentSymbol('DocumentSymbol', 'DocumentSymbol#detail', types.SymbolKind.Enum, new types.Range(1, 0, 1, 0), new types.Range(1, 0, 1, 0)); root.children = [new types.DocumentSymbol('DocumentSymbol#child', 'DocumentSymbol#detail#child', types.SymbolKind.Enum, new types.Range(1, 0, 1, 0), new types.Range(1, 0, 1, 0))]; return [root]; } @@ -698,7 +696,7 @@ suite('ExtHostLanguageFeatureCommands', function () { return rpcProtocol.sync().then(() => { return commands.executeCommand<(vscode.SymbolInformation & vscode.DocumentSymbol)[]>('vscode.executeDocumentSymbolProvider', model.uri).then(values => { assert.strictEqual(values.length, 2); - let [first, second] = values; + const [first, second] = values; assert.strictEqual(first instanceof types.SymbolInformation, true); assert.strictEqual(first instanceof types.DocumentSymbol, false); assert.strictEqual(second instanceof types.SymbolInformation, true); @@ -714,14 +712,15 @@ suite('ExtHostLanguageFeatureCommands', function () { test('Suggest, back and forth', function () { disposables.push(extHost.registerCompletionItemProvider(nullExtensionDescription, defaultSelector, <vscode.CompletionItemProvider>{ provideCompletionItems(): any { - let a = new types.CompletionItem('item1'); - let b = new types.CompletionItem('item2'); + const a = new types.CompletionItem('item1'); + a.documentation = new types.MarkdownString('hello_md_string'); + const b = new types.CompletionItem('item2'); b.textEdit = types.TextEdit.replace(new types.Range(0, 4, 0, 8), 'foo'); // overwite after - let c = new types.CompletionItem('item3'); + const c = new types.CompletionItem('item3'); c.textEdit = types.TextEdit.replace(new types.Range(0, 1, 0, 6), 'foobar'); // overwite before & after // snippet string! - let d = new types.CompletionItem('item4'); + const d = new types.CompletionItem('item4'); d.range = new types.Range(0, 1, 0, 4);// overwite before d.insertText = new types.SnippetString('foo$0bar'); return [a, b, c, d]; @@ -732,13 +731,14 @@ suite('ExtHostLanguageFeatureCommands', function () { return commands.executeCommand<vscode.CompletionList>('vscode.executeCompletionItemProvider', model.uri, new types.Position(0, 4)).then(list => { assert.ok(list instanceof types.CompletionList); - let values = list.items; + const values = list.items; assert.ok(Array.isArray(values)); assert.strictEqual(values.length, 4); - let [first, second, third, fourth] = values; + const [first, second, third, fourth] = values; assert.strictEqual(first.label, 'item1'); assert.strictEqual(first.textEdit, undefined);// no text edit, default ranges assert.ok(!types.Range.isRange(first.range)); + assert.strictEqual((<types.MarkdownString>first.documentation).value, 'hello_md_string'); assert.strictEqual(second.label, 'item2'); assert.strictEqual(second.textEdit!.newText, 'foo'); @@ -772,8 +772,8 @@ suite('ExtHostLanguageFeatureCommands', function () { test('Suggest, return CompletionList !array', function () { disposables.push(extHost.registerCompletionItemProvider(nullExtensionDescription, defaultSelector, <vscode.CompletionItemProvider>{ provideCompletionItems(): any { - let a = new types.CompletionItem('item1'); - let b = new types.CompletionItem('item2'); + const a = new types.CompletionItem('item1'); + const b = new types.CompletionItem('item2'); return new types.CompletionList(<any>[a, b], true); } }, [])); @@ -792,10 +792,10 @@ suite('ExtHostLanguageFeatureCommands', function () { disposables.push(extHost.registerCompletionItemProvider(nullExtensionDescription, defaultSelector, <vscode.CompletionItemProvider>{ provideCompletionItems(): any { - let a = new types.CompletionItem('item1'); - let b = new types.CompletionItem('item2'); - let c = new types.CompletionItem('item3'); - let d = new types.CompletionItem('item4'); + const a = new types.CompletionItem('item1'); + const b = new types.CompletionItem('item2'); + const c = new types.CompletionItem('item3'); + const d = new types.CompletionItem('item4'); return new types.CompletionList([a, b, c, d], false); }, resolveCompletionItem(item) { @@ -806,7 +806,7 @@ suite('ExtHostLanguageFeatureCommands', function () { await rpcProtocol.sync(); - let list = await commands.executeCommand<vscode.CompletionList>( + const list = await commands.executeCommand<vscode.CompletionList>( 'vscode.executeCompletionItemProvider', model.uri, new types.Position(0, 4), @@ -822,19 +822,19 @@ suite('ExtHostLanguageFeatureCommands', function () { test('"vscode.executeCompletionItemProvider" doesnot return a preselect field #53749', async function () { disposables.push(extHost.registerCompletionItemProvider(nullExtensionDescription, defaultSelector, <vscode.CompletionItemProvider>{ provideCompletionItems(): any { - let a = new types.CompletionItem('item1'); + const a = new types.CompletionItem('item1'); a.preselect = true; - let b = new types.CompletionItem('item2'); - let c = new types.CompletionItem('item3'); + const b = new types.CompletionItem('item2'); + const c = new types.CompletionItem('item3'); c.preselect = true; - let d = new types.CompletionItem('item4'); + const d = new types.CompletionItem('item4'); return new types.CompletionList([a, b, c, d], false); } }, [])); await rpcProtocol.sync(); - let list = await commands.executeCommand<vscode.CompletionList>( + const list = await commands.executeCommand<vscode.CompletionList>( 'vscode.executeCompletionItemProvider', model.uri, new types.Position(0, 4), @@ -844,7 +844,7 @@ suite('ExtHostLanguageFeatureCommands', function () { assert.ok(list instanceof types.CompletionList); assert.strictEqual(list.items.length, 4); - let [a, b, c, d] = list.items; + const [a, b, c, d] = list.items; assert.strictEqual(a.preselect, true); assert.strictEqual(b.preselect, undefined); assert.strictEqual(c.preselect, true); @@ -854,16 +854,16 @@ suite('ExtHostLanguageFeatureCommands', function () { test('executeCompletionItemProvider doesn\'t capture commitCharacters #58228', async function () { disposables.push(extHost.registerCompletionItemProvider(nullExtensionDescription, defaultSelector, <vscode.CompletionItemProvider>{ provideCompletionItems(): any { - let a = new types.CompletionItem('item1'); + const a = new types.CompletionItem('item1'); a.commitCharacters = ['a', 'b']; - let b = new types.CompletionItem('item2'); + const b = new types.CompletionItem('item2'); return new types.CompletionList([a, b], false); } }, [])); await rpcProtocol.sync(); - let list = await commands.executeCommand<vscode.CompletionList>( + const list = await commands.executeCommand<vscode.CompletionList>( 'vscode.executeCompletionItemProvider', model.uri, new types.Position(0, 4), @@ -873,7 +873,7 @@ suite('ExtHostLanguageFeatureCommands', function () { assert.ok(list instanceof types.CompletionList); assert.strictEqual(list.items.length, 2); - let [a, b] = list.items; + const [a, b] = list.items; assert.deepStrictEqual(a.commitCharacters, ['a', 'b']); assert.strictEqual(b.commitCharacters, undefined); }); @@ -890,7 +890,7 @@ suite('ExtHostLanguageFeatureCommands', function () { await rpcProtocol.sync(); - let list = await commands.executeCommand<vscode.CompletionList>( + const list = await commands.executeCommand<vscode.CompletionList>( 'vscode.executeCompletionItemProvider', model.uri, new types.Position(0, 4), @@ -946,7 +946,7 @@ suite('ExtHostLanguageFeatureCommands', function () { return rpcProtocol.sync().then(() => { return commands.executeCommand<vscode.Command[]>('vscode.executeCodeActionProvider', model.uri, new types.Range(0, 0, 1, 1)).then(value => { assert.strictEqual(value.length, 1); - let [first] = value; + const [first] = value; assert.strictEqual(first.title, 'Title'); assert.strictEqual(first.command, 'testing'); assert.deepStrictEqual(first.arguments, [1, 2, true]); @@ -1145,7 +1145,7 @@ suite('ExtHostLanguageFeatureCommands', function () { return rpcProtocol.sync().then(() => { return commands.executeCommand<vscode.DocumentLink[]>('vscode.executeLinkProvider', model.uri).then(value => { assert.strictEqual(value.length, 1); - let [first] = value; + const [first] = value; assert.strictEqual(first.target + '', 'foo:bar'); assert.strictEqual(first.range.start.line, 0); @@ -1197,7 +1197,7 @@ suite('ExtHostLanguageFeatureCommands', function () { return rpcProtocol.sync().then(() => { return commands.executeCommand<vscode.ColorInformation[]>('vscode.executeDocumentColorProvider', model.uri).then(value => { assert.strictEqual(value.length, 1); - let [first] = value; + const [first] = value; assert.strictEqual(first.color.red, 0.1); assert.strictEqual(first.color.green, 0.2); @@ -1213,7 +1213,7 @@ suite('ExtHostLanguageFeatureCommands', function () { const range = new types.Range(0, 0, 0, 20); return commands.executeCommand<vscode.ColorPresentation[]>('vscode.executeColorPresentationProvider', color, { uri: model.uri, range }).then(value => { assert.strictEqual(value.length, 1); - let [first] = value; + const [first] = value; assert.strictEqual(first.label, '#ABC'); assert.strictEqual(first.textEdit!.newText, '#ABC'); @@ -1350,7 +1350,7 @@ suite('ExtHostLanguageFeatureCommands', function () { })); await rpcProtocol.sync(); - let value = await commands.executeCommand<vscode.SelectionRange[]>('vscode.executeSelectionRangeProvider', model.uri, [new types.Position(0, 10)]); + const value = await commands.executeCommand<vscode.SelectionRange[]>('vscode.executeSelectionRangeProvider', model.uri, [new types.Position(0, 10)]); assert.strictEqual(value.length, 1); assert.ok(value[0].parent); }); @@ -1466,7 +1466,7 @@ suite('ExtHostLanguageFeatureCommands', function () { })); await rpcProtocol.sync(); - let value = await commands.executeCommand<vscode.SelectionRange[]>('vscode.executeSelectionRangeProvider', model.uri, [new types.Position(0, 10)]); + const value = await commands.executeCommand<vscode.SelectionRange[]>('vscode.executeSelectionRangeProvider', model.uri, [new types.Position(0, 10)]); assert.strictEqual(value.length, 1); assert.strictEqual(value[0].range.start.line, 0); assert.strictEqual(value[0].range.start.character, 10); @@ -1487,7 +1487,7 @@ suite('ExtHostLanguageFeatureCommands', function () { })); await rpcProtocol.sync(); - let value = await commands.executeCommand<vscode.SelectionRange[]>( + const value = await commands.executeCommand<vscode.SelectionRange[]>( 'vscode.executeSelectionRangeProvider', model.uri, [new types.Position(0, 0), new types.Position(0, 10)] diff --git a/src/vs/workbench/api/test/browser/extHostAuthentication.test.ts b/src/vs/workbench/api/test/browser/extHostAuthentication.test.ts index dd62b92214a..7d4a9f808fb 100644 --- a/src/vs/workbench/api/test/browser/extHostAuthentication.test.ts +++ b/src/vs/workbench/api/test/browser/extHostAuthentication.test.ts @@ -59,6 +59,7 @@ class TestAuthProvider implements AuthenticationProvider { private id = 1; private sessions = new Map<string, AuthenticationSession>(); onDidChangeSessions = () => { return { dispose() { } }; }; + constructor(private readonly authProviderName: string) { } async getSessions(scopes?: readonly string[]): Promise<AuthenticationSession[]> { if (!scopes) { return [...this.sessions.values()]; @@ -76,7 +77,7 @@ class TestAuthProvider implements AuthenticationProvider { scopes, id: `${this.id}`, account: { - label: `${this.id}`, + label: this.authProviderName, id: `${this.id}`, }, accessToken: Math.random() + '', @@ -118,11 +119,11 @@ suite('ExtHostAuthentication', () => { setup(async () => { disposables = new DisposableStore(); - disposables.add(extHostAuthentication.registerAuthenticationProvider('test', 'test provider', new TestAuthProvider())); + disposables.add(extHostAuthentication.registerAuthenticationProvider('test', 'test provider', new TestAuthProvider('test'))); disposables.add(extHostAuthentication.registerAuthenticationProvider( 'test-multiple', 'test multiple provider', - new TestAuthProvider(), + new TestAuthProvider('test-multiple'), { supportsMultipleAccounts: true })); }); @@ -441,5 +442,63 @@ suite('ExtHostAuthentication', () => { assert.strictEqual(session?.scopes[0], 'foo'); }); + test('Can get multiple sessions (from different providers) in one extension', async () => { + let session: AuthenticationSession | undefined = await extHostAuthentication.getSession( + extensionDescription, + 'test-multiple', + ['foo'], + { + createIfNone: true + }); + session = await extHostAuthentication.getSession( + extensionDescription, + 'test', + ['foo'], + { + createIfNone: true + }); + assert.strictEqual(session?.id, '1'); + assert.strictEqual(session?.scopes[0], 'foo'); + assert.strictEqual(session?.account.label, 'test'); + + const session2 = await extHostAuthentication.getSession( + extensionDescription, + 'test-multiple', + ['foo'], + { + createIfNone: false + }); + assert.strictEqual(session2?.id, '1'); + assert.strictEqual(session2?.scopes[0], 'foo'); + assert.strictEqual(session2?.account.label, 'test-multiple'); + }); + + test('Can get multiple sessions (from different providers) in one extension at the same time', async () => { + const sessionP: Promise<AuthenticationSession | undefined> = extHostAuthentication.getSession( + extensionDescription, + 'test', + ['foo'], + { + createIfNone: true + }); + const session2P: Promise<AuthenticationSession | undefined> = extHostAuthentication.getSession( + extensionDescription, + 'test-multiple', + ['foo'], + { + createIfNone: true + }); + const session = await sessionP; + assert.strictEqual(session?.id, '1'); + assert.strictEqual(session?.scopes[0], 'foo'); + assert.strictEqual(session?.account.label, 'test'); + + const session2 = await session2P; + assert.strictEqual(session2?.id, '1'); + assert.strictEqual(session2?.scopes[0], 'foo'); + assert.strictEqual(session2?.account.label, 'test-multiple'); + }); + + //#endregion }); diff --git a/src/vs/workbench/api/test/browser/extHostBulkEdits.test.ts b/src/vs/workbench/api/test/browser/extHostBulkEdits.test.ts index e634cad36c4..3b393b35347 100644 --- a/src/vs/workbench/api/test/browser/extHostBulkEdits.test.ts +++ b/src/vs/workbench/api/test/browser/extHostBulkEdits.test.ts @@ -12,6 +12,7 @@ import { SingleProxyRPCProtocol, TestRPCProtocol } from 'vs/workbench/api/test/c import { NullLogService } from 'vs/platform/log/common/log'; import { assertType } from 'vs/base/common/types'; import { ExtHostBulkEdits } from 'vs/workbench/api/common/extHostBulkEdits'; +import { nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; suite('ExtHostBulkEdits.applyWorkspaceEdit', () => { @@ -22,7 +23,7 @@ suite('ExtHostBulkEdits.applyWorkspaceEdit', () => { setup(() => { workspaceResourceEdits = null!; - let rpcProtocol = new TestRPCProtocol(); + const rpcProtocol = new TestRPCProtocol(); rpcProtocol.set(MainContext.MainThreadBulkEdits, new class extends mock<MainThreadBulkEditsShape>() { override $tryApplyWorkspaceEdit(_workspaceResourceEdits: IWorkspaceEditDto): Promise<boolean> { workspaceResourceEdits = _workspaceResourceEdits; @@ -44,9 +45,9 @@ suite('ExtHostBulkEdits.applyWorkspaceEdit', () => { }); test('uses version id if document available', async () => { - let edit = new extHostTypes.WorkspaceEdit(); + const edit = new extHostTypes.WorkspaceEdit(); edit.replace(resource, new extHostTypes.Range(0, 0, 0, 0), 'hello'); - await bulkEdits.applyWorkspaceEdit(edit); + await bulkEdits.applyWorkspaceEdit(edit, nullExtensionDescription); assert.strictEqual(workspaceResourceEdits.edits.length, 1); const [first] = workspaceResourceEdits.edits; assertType(first._type === WorkspaceEditType.Text); @@ -54,9 +55,9 @@ suite('ExtHostBulkEdits.applyWorkspaceEdit', () => { }); test('does not use version id if document is not available', async () => { - let edit = new extHostTypes.WorkspaceEdit(); + const edit = new extHostTypes.WorkspaceEdit(); edit.replace(URI.parse('foo:bar2'), new extHostTypes.Range(0, 0, 0, 0), 'hello'); - await bulkEdits.applyWorkspaceEdit(edit); + await bulkEdits.applyWorkspaceEdit(edit, nullExtensionDescription); assert.strictEqual(workspaceResourceEdits.edits.length, 1); const [first] = workspaceResourceEdits.edits; assertType(first._type === WorkspaceEditType.Text); diff --git a/src/vs/workbench/api/test/browser/extHostCommands.test.ts b/src/vs/workbench/api/test/browser/extHostCommands.test.ts index da9b47ccda9..e38f919bee8 100644 --- a/src/vs/workbench/api/test/browser/extHostCommands.test.ts +++ b/src/vs/workbench/api/test/browser/extHostCommands.test.ts @@ -90,4 +90,28 @@ suite('ExtHostCommands', function () { assert.strictEqual(result, 17); assert.strictEqual(count, 2); }); + + test('onCommand:abc activates extensions when executed from command palette, but not when executed programmatically with vscode.commands.executeCommand #150293', async function () { + + const activationEvents: string[] = []; + + const shape = new class extends mock<MainThreadCommandsShape>() { + override $registerCommand(id: string): void { + // + } + override $fireCommandActivationEvent(id: string): void { + activationEvents.push(id); + } + }; + const commands = new ExtHostCommands( + SingleProxyRPCProtocol(shape), + new NullLogService() + ); + + commands.registerCommand(true, 'extCmd', (args: any): any => args); + + const result = await commands.executeCommand('extCmd', this); + assert.strictEqual(result, this); + assert.deepStrictEqual(activationEvents, ['extCmd']); + }); }); diff --git a/src/vs/workbench/api/test/browser/extHostConfiguration.test.ts b/src/vs/workbench/api/test/browser/extHostConfiguration.test.ts index 764c1c294b1..b0886289593 100644 --- a/src/vs/workbench/api/test/browser/extHostConfiguration.test.ts +++ b/src/vs/workbench/api/test/browser/extHostConfiguration.test.ts @@ -43,6 +43,8 @@ suite('ExtHostConfiguration', function () { function createConfigurationData(contents: any): IConfigurationInitData { return { defaults: new ConfigurationModel(contents), + policy: new ConfigurationModel(), + application: new ConfigurationModel(), user: new ConfigurationModel(contents), workspace: new ConfigurationModel(), folders: [], @@ -248,7 +250,7 @@ suite('ExtHostConfiguration', function () { } }); - let testObject: any = all.getConfiguration(); + const testObject: any = all.getConfiguration(); try { testObject['get'] = null; @@ -279,6 +281,8 @@ suite('ExtHostConfiguration', function () { 'wordWrap': 'off' } }, ['editor.wordWrap']), + policy: new ConfigurationModel(), + application: new ConfigurationModel(), user: new ConfigurationModel({ 'editor': { 'wordWrap': 'on' @@ -328,6 +332,8 @@ suite('ExtHostConfiguration', function () { 'wordWrap': 'off' } }, ['editor.wordWrap']), + policy: new ConfigurationModel(), + application: new ConfigurationModel(), user: new ConfigurationModel({ 'editor': { 'wordWrap': 'on' @@ -405,6 +411,8 @@ suite('ExtHostConfiguration', function () { 'lineNumbers': 'on' } }, ['editor.wordWrap']), + policy: new ConfigurationModel(), + application: new ConfigurationModel(), user: new ConfigurationModel({ 'editor': { 'wordWrap': 'on' @@ -508,6 +516,8 @@ suite('ExtHostConfiguration', function () { 'editor.wordWrap': 'bounded', } }), + policy: new ConfigurationModel(), + application: new ConfigurationModel(), user: toConfigurationModel({ 'editor.wordWrap': 'bounded', '[typescript]': { @@ -549,6 +559,59 @@ suite('ExtHostConfiguration', function () { assert.deepStrictEqual(actual.languageIds, ['markdown', 'typescript']); }); + test('application is not set in inspect', () => { + + const testObject = new ExtHostConfigProvider( + new class extends mock<MainThreadConfigurationShape>() { }, + createExtHostWorkspace(), + { + defaults: new ConfigurationModel({ + 'editor': { + 'wordWrap': 'off', + 'lineNumbers': 'on', + 'fontSize': '12px' + } + }, ['editor.wordWrap']), + policy: new ConfigurationModel(), + application: new ConfigurationModel({ + 'editor': { + 'wordWrap': 'on' + } + }, ['editor.wordWrap']), + user: new ConfigurationModel({ + 'editor': { + 'wordWrap': 'auto', + 'lineNumbers': 'off' + } + }, ['editor.wordWrap']), + workspace: new ConfigurationModel({}, []), + folders: [], + configurationScopes: [] + }, + new NullLogService() + ); + + let actual = testObject.getConfiguration().inspect('editor.wordWrap')!; + assert.strictEqual(actual.defaultValue, 'off'); + assert.strictEqual(actual.globalValue, 'auto'); + assert.strictEqual(actual.workspaceValue, undefined); + assert.strictEqual(actual.workspaceFolderValue, undefined); + assert.strictEqual(testObject.getConfiguration().get('editor.wordWrap'), 'auto'); + + actual = testObject.getConfiguration().inspect('editor.lineNumbers')!; + assert.strictEqual(actual.defaultValue, 'on'); + assert.strictEqual(actual.globalValue, 'off'); + assert.strictEqual(actual.workspaceValue, undefined); + assert.strictEqual(actual.workspaceFolderValue, undefined); + assert.strictEqual(testObject.getConfiguration().get('editor.lineNumbers'), 'off'); + + actual = testObject.getConfiguration().inspect('editor.fontSize')!; + assert.strictEqual(actual.defaultValue, '12px'); + assert.strictEqual(actual.globalValue, undefined); + assert.strictEqual(actual.workspaceValue, undefined); + assert.strictEqual(actual.workspaceFolderValue, undefined); + assert.strictEqual(testObject.getConfiguration().get('editor.fontSize'), '12px'); + }); test('getConfiguration vs get', function () { @@ -609,7 +672,7 @@ suite('ExtHostConfiguration', function () { } }, shape); - let config = allConfig.getConfiguration('foo'); + const config = allConfig.getConfiguration('foo'); config.update('bar', 42); assert.strictEqual(shape.lastArgs[0], null); diff --git a/src/vs/workbench/api/test/browser/extHostDecorations.test.ts b/src/vs/workbench/api/test/browser/extHostDecorations.test.ts index 4dc01fc51d3..ee0b37ae01e 100644 --- a/src/vs/workbench/api/test/browser/extHostDecorations.test.ts +++ b/src/vs/workbench/api/test/browser/extHostDecorations.test.ts @@ -18,7 +18,7 @@ suite('ExtHostDecorations', function () { let mainThreadShape: MainThreadDecorationsShape; let extHostDecorations: ExtHostDecorations; - let providers = new Set<number>(); + const providers = new Set<number>(); setup(function () { diff --git a/src/vs/workbench/api/test/browser/extHostDiagnostics.test.ts b/src/vs/workbench/api/test/browser/extHostDiagnostics.test.ts index 32ccfd141ff..021725f35f0 100644 --- a/src/vs/workbench/api/test/browser/extHostDiagnostics.test.ts +++ b/src/vs/workbench/api/test/browser/extHostDiagnostics.test.ts @@ -93,7 +93,7 @@ suite('ExtHostDiagnostics', () => { }); test('diagnostic collection, immutable read', function () { - let collection = new DiagnosticCollection('test', 'test', 100, extUri, new DiagnosticsShape(), new Emitter()); + const collection = new DiagnosticCollection('test', 'test', 100, extUri, new DiagnosticsShape(), new Emitter()); collection.set(URI.parse('foo:bar'), [ new Diagnostic(new Range(0, 0, 1, 1), 'message-1'), new Diagnostic(new Range(0, 0, 1, 1), 'message-2') @@ -118,8 +118,8 @@ suite('ExtHostDiagnostics', () => { test('diagnostics collection, set with dupliclated tuples', function () { - let collection = new DiagnosticCollection('test', 'test', 100, extUri, new DiagnosticsShape(), new Emitter()); - let uri = URI.parse('sc:hightower'); + const collection = new DiagnosticCollection('test', 'test', 100, extUri, new DiagnosticsShape(), new Emitter()); + const uri = URI.parse('sc:hightower'); collection.set([ [uri, [new Diagnostic(new Range(0, 0, 0, 1), 'message-1')]], [URI.parse('some:thing'), [new Diagnostic(new Range(0, 0, 1, 1), 'something')]], @@ -169,19 +169,19 @@ suite('ExtHostDiagnostics', () => { test('diagnostics collection, set tuple overrides, #11547', function () { let lastEntries!: [UriComponents, IMarkerData[]][]; - let collection = new DiagnosticCollection('test', 'test', 100, extUri, new class extends DiagnosticsShape { + const collection = new DiagnosticCollection('test', 'test', 100, extUri, new class extends DiagnosticsShape { override $changeMany(owner: string, entries: [UriComponents, IMarkerData[]][]): void { lastEntries = entries; return super.$changeMany(owner, entries); } }, new Emitter()); - let uri = URI.parse('sc:hightower'); + const uri = URI.parse('sc:hightower'); collection.set([[uri, [new Diagnostic(new Range(0, 0, 1, 1), 'error')]]]); assert.strictEqual(collection.get(uri).length, 1); assert.strictEqual(collection.get(uri)[0].message, 'error'); assert.strictEqual(lastEntries.length, 1); - let [[, data1]] = lastEntries; + const [[, data1]] = lastEntries; assert.strictEqual(data1.length, 1); assert.strictEqual(data1[0].message, 'error'); lastEntries = undefined!; @@ -190,7 +190,7 @@ suite('ExtHostDiagnostics', () => { assert.strictEqual(collection.get(uri).length, 1); assert.strictEqual(collection.get(uri)[0].message, 'warning'); assert.strictEqual(lastEntries.length, 1); - let [[, data2]] = lastEntries; + const [[, data2]] = lastEntries; assert.strictEqual(data2.length, 1); assert.strictEqual(data2[0].message, 'warning'); lastEntries = undefined!; @@ -209,8 +209,8 @@ suite('ExtHostDiagnostics', () => { } }, emitter); - let uri = URI.parse('sc:hightower'); - let diag = new Diagnostic(new Range(0, 0, 0, 1), 'ffff'); + const uri = URI.parse('sc:hightower'); + const diag = new Diagnostic(new Range(0, 0, 0, 1), 'ffff'); collection.set(uri, [diag]); assert.strictEqual(changeCount, 1); @@ -224,9 +224,9 @@ suite('ExtHostDiagnostics', () => { test('diagnostics collection, tuples and undefined (small array), #15585', function () { const collection = new DiagnosticCollection('test', 'test', 100, extUri, new DiagnosticsShape(), new Emitter()); - let uri = URI.parse('sc:hightower'); - let uri2 = URI.parse('sc:nomad'); - let diag = new Diagnostic(new Range(0, 0, 0, 1), 'ffff'); + const uri = URI.parse('sc:hightower'); + const uri2 = URI.parse('sc:nomad'); + const diag = new Diagnostic(new Range(0, 0, 0, 1), 'ffff'); collection.set([ [uri, [diag, diag, diag]], @@ -248,8 +248,8 @@ suite('ExtHostDiagnostics', () => { const tuples: [URI, Diagnostic[]][] = []; for (let i = 0; i < 500; i++) { - let uri = URI.parse('sc:hightower#' + i); - let diag = new Diagnostic(new Range(0, 0, 0, 1), i.toString()); + const uri = URI.parse('sc:hightower#' + i); + const diag = new Diagnostic(new Range(0, 0, 0, 1), i.toString()); tuples.push([uri, [diag, diag, diag]]); tuples.push([uri, undefined!]); @@ -259,7 +259,7 @@ suite('ExtHostDiagnostics', () => { collection.set(tuples); for (let i = 0; i < 500; i++) { - let uri = URI.parse('sc:hightower#' + i); + const uri = URI.parse('sc:hightower#' + i); assert.strictEqual(collection.has(uri), true); assert.strictEqual(collection.get(uri).length, 1); } @@ -268,15 +268,15 @@ suite('ExtHostDiagnostics', () => { test('diagnostic capping', function () { let lastEntries!: [UriComponents, IMarkerData[]][]; - let collection = new DiagnosticCollection('test', 'test', 250, extUri, new class extends DiagnosticsShape { + const collection = new DiagnosticCollection('test', 'test', 250, extUri, new class extends DiagnosticsShape { override $changeMany(owner: string, entries: [UriComponents, IMarkerData[]][]): void { lastEntries = entries; return super.$changeMany(owner, entries); } }, new Emitter()); - let uri = URI.parse('aa:bb'); + const uri = URI.parse('aa:bb'); - let diagnostics: Diagnostic[] = []; + const diagnostics: Diagnostic[] = []; for (let i = 0; i < 500; i++) { diagnostics.push(new Diagnostic(new Range(i, 0, i + 1, 0), `error#${i}`, i < 300 ? DiagnosticSeverity.Warning @@ -293,12 +293,12 @@ suite('ExtHostDiagnostics', () => { }); test('diagnostic eventing', async function () { - let emitter = new Emitter<Array<URI>>(); - let collection = new DiagnosticCollection('ddd', 'test', 100, extUri, new DiagnosticsShape(), emitter); + const emitter = new Emitter<Array<URI>>(); + const collection = new DiagnosticCollection('ddd', 'test', 100, extUri, new DiagnosticsShape(), emitter); - let diag1 = new Diagnostic(new Range(1, 1, 2, 3), 'diag1'); - let diag2 = new Diagnostic(new Range(1, 1, 2, 3), 'diag2'); - let diag3 = new Diagnostic(new Range(1, 1, 2, 3), 'diag3'); + const diag1 = new Diagnostic(new Range(1, 1, 2, 3), 'diag1'); + const diag2 = new Diagnostic(new Range(1, 1, 2, 3), 'diag2'); + const diag3 = new Diagnostic(new Range(1, 1, 2, 3), 'diag3'); let p = Event.toPromise(emitter.event).then(a => { assert.strictEqual(a.length, 1); @@ -331,10 +331,10 @@ suite('ExtHostDiagnostics', () => { }); test('vscode.languages.onDidChangeDiagnostics Does Not Provide Document URI #49582', async function () { - let emitter = new Emitter<Array<URI>>(); - let collection = new DiagnosticCollection('ddd', 'test', 100, extUri, new DiagnosticsShape(), emitter); + const emitter = new Emitter<Array<URI>>(); + const collection = new DiagnosticCollection('ddd', 'test', 100, extUri, new DiagnosticsShape(), emitter); - let diag1 = new Diagnostic(new Range(1, 1, 2, 3), 'diag1'); + const diag1 = new Diagnostic(new Range(1, 1, 2, 3), 'diag1'); // delete collection.set(URI.parse('aa:bb'), [diag1]); @@ -355,14 +355,14 @@ suite('ExtHostDiagnostics', () => { test('diagnostics with related information', function (done) { - let collection = new DiagnosticCollection('ddd', 'test', 100, extUri, new class extends DiagnosticsShape { + const collection = new DiagnosticCollection('ddd', 'test', 100, extUri, new class extends DiagnosticsShape { override $changeMany(owner: string, entries: [UriComponents, IMarkerData[]][]) { - let [[, data]] = entries; + const [[, data]] = entries; assert.strictEqual(entries.length, 1); assert.strictEqual(data.length, 1); - let [diag] = data; + const [diag] = data; assert.strictEqual(diag.relatedInformation!.length, 2); assert.strictEqual(diag.relatedInformation![0].message, 'more1'); assert.strictEqual(diag.relatedInformation![1].message, 'more2'); @@ -370,7 +370,7 @@ suite('ExtHostDiagnostics', () => { } }, new Emitter<any>()); - let diag = new Diagnostic(new Range(0, 0, 1, 1), 'Foo'); + const diag = new Diagnostic(new Range(0, 0, 1, 1), 'Foo'); diag.relatedInformation = [ new DiagnosticRelatedInformation(new Location(URI.parse('cc:dd'), new Range(0, 0, 0, 0)), 'more1'), new DiagnosticRelatedInformation(new Location(URI.parse('cc:ee'), new Range(0, 0, 0, 0)), 'more2') @@ -401,8 +401,8 @@ suite('ExtHostDiagnostics', () => { } }, new NullLogService(), fileSystemInfoService); - let collection1 = diags.createDiagnosticCollection(nullExtensionDescription.identifier, 'foo'); - let collection2 = diags.createDiagnosticCollection(nullExtensionDescription.identifier, 'foo'); // warns, uses a different owner + const collection1 = diags.createDiagnosticCollection(nullExtensionDescription.identifier, 'foo'); + const collection2 = diags.createDiagnosticCollection(nullExtensionDescription.identifier, 'foo'); // warns, uses a different owner collection1.clear(); collection2.clear(); @@ -414,15 +414,15 @@ suite('ExtHostDiagnostics', () => { test('Error updating diagnostics from extension #60394', function () { let callCount = 0; - let collection = new DiagnosticCollection('ddd', 'test', 100, extUri, new class extends DiagnosticsShape { + const collection = new DiagnosticCollection('ddd', 'test', 100, extUri, new class extends DiagnosticsShape { override $changeMany(owner: string, entries: [UriComponents, IMarkerData[]][]) { callCount += 1; } }, new Emitter<any>()); - let array: Diagnostic[] = []; - let diag1 = new Diagnostic(new Range(0, 0, 1, 1), 'Foo'); - let diag2 = new Diagnostic(new Range(0, 0, 1, 1), 'Bar'); + const array: Diagnostic[] = []; + const diag1 = new Diagnostic(new Range(0, 0, 1, 1), 'Foo'); + const diag2 = new Diagnostic(new Range(0, 0, 1, 1), 'Bar'); array.push(diag1, diag2); diff --git a/src/vs/workbench/api/test/browser/extHostDocumentData.test.ts b/src/vs/workbench/api/test/browser/extHostDocumentData.test.ts index a457ca659ca..638cce74296 100644 --- a/src/vs/workbench/api/test/browser/extHostDocumentData.test.ts +++ b/src/vs/workbench/api/test/browser/extHostDocumentData.test.ts @@ -19,14 +19,14 @@ suite('ExtHostDocumentData', () => { let data: ExtHostDocumentData; function assertPositionAt(offset: number, line: number, character: number) { - let position = data.document.positionAt(offset); + const position = data.document.positionAt(offset); assert.strictEqual(position.line, line); assert.strictEqual(position.character, character); } function assertOffsetAt(line: number, character: number, offset: number) { - let pos = new Position(line, character); - let actual = data.document.offsetAt(pos); + const pos = new Position(line, character); + const actual = data.document.offsetAt(pos); assert.strictEqual(actual, offset); } @@ -50,7 +50,7 @@ suite('ExtHostDocumentData', () => { test('save, when disposed', function () { let saved: URI; - let data = new ExtHostDocumentData(new class extends mock<MainThreadDocumentsShape>() { + const data = new ExtHostDocumentData(new class extends mock<MainThreadDocumentsShape>() { override $trySaveDocument(uri: URI) { assert.ok(!saved); saved = uri; @@ -344,7 +344,7 @@ suite('ExtHostDocumentData', () => { line ], '\n', 1, 'text', false); - let range = data.document.getWordRangeAtPosition(new Position(0, 27), regex)!; + const range = data.document.getWordRangeAtPosition(new Position(0, 27), regex)!; assert.strictEqual(range.start.line, 0); assert.strictEqual(range.end.line, 0); assert.strictEqual(range.start.character, 4); @@ -379,7 +379,7 @@ suite('ExtHostDocumentData updates line mapping', () => { } function assertDocumentLineMapping(doc: ExtHostDocumentData, direction: AssertDocumentLineMappingDirection): void { - let allText = doc.getText(); + const allText = doc.getText(); let line = 0, character = 0, previousIsCarriageReturn = false; for (let offset = 0; offset <= allText.length; offset++) { @@ -387,12 +387,12 @@ suite('ExtHostDocumentData updates line mapping', () => { const position: Position = new Position(line, character + (previousIsCarriageReturn ? -1 : 0)); if (direction === AssertDocumentLineMappingDirection.OffsetToPosition) { - let actualPosition = doc.document.positionAt(offset); + const actualPosition = doc.document.positionAt(offset); assert.strictEqual(positionToStr(actualPosition), positionToStr(position), 'positionAt mismatch for offset ' + offset); } else { // The position coordinate system cannot express the position between \r and \n - let expectedOffset: number = offset + (previousIsCarriageReturn ? -1 : 0); - let actualOffset = doc.document.offsetAt(position); + const expectedOffset: number = offset + (previousIsCarriageReturn ? -1 : 0); + const actualOffset = doc.document.offsetAt(position); assert.strictEqual(actualOffset, expectedOffset, 'offsetAt mismatch for position ' + positionToStr(position)); } @@ -423,7 +423,7 @@ suite('ExtHostDocumentData updates line mapping', () => { } function testLineMappingDirectionAfterEvents(lines: string[], eol: string, direction: AssertDocumentLineMappingDirection, e: IModelChangedEvent): void { - let myDocument = new ExtHostDocumentData(undefined!, URI.file(''), lines.slice(0), eol, 1, 'text', false); + const myDocument = new ExtHostDocumentData(undefined!, URI.file(''), lines.slice(0), eol, 1, 'text', false); assertDocumentLineMapping(myDocument, direction); myDocument.onEvents(e); diff --git a/src/vs/workbench/api/test/browser/extHostDocumentSaveParticipant.test.ts b/src/vs/workbench/api/test/browser/extHostDocumentSaveParticipant.test.ts index 60d1caefa3d..70e338176d7 100644 --- a/src/vs/workbench/api/test/browser/extHostDocumentSaveParticipant.test.ts +++ b/src/vs/workbench/api/test/browser/extHostDocumentSaveParticipant.test.ts @@ -19,10 +19,10 @@ import { nullExtensionDescription } from 'vs/workbench/services/extensions/commo suite('ExtHostDocumentSaveParticipant', () => { - let resource = URI.parse('foo:bar'); - let mainThreadBulkEdits = new class extends mock<MainThreadBulkEditsShape>() { }; + const resource = URI.parse('foo:bar'); + const mainThreadBulkEdits = new class extends mock<MainThreadBulkEditsShape>() { }; let documents: ExtHostDocuments; - let nullLogService = new NullLogService(); + const nullLogService = new NullLogService(); setup(() => { const documentsAndEditors = new ExtHostDocumentsAndEditors(SingleProxyRPCProtocol(null), new NullLogService()); @@ -48,7 +48,7 @@ suite('ExtHostDocumentSaveParticipant', () => { const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits); let event: vscode.TextDocumentWillSaveEvent; - let sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) { + const sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) { event = e; }); @@ -65,7 +65,7 @@ suite('ExtHostDocumentSaveParticipant', () => { const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits); let event: vscode.TextDocumentWillSaveEvent; - let sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) { + const sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) { event = e; }); @@ -80,7 +80,7 @@ suite('ExtHostDocumentSaveParticipant', () => { test('event delivery, bad listener', () => { const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits); - let sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) { + const sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) { throw new Error('💀'); }); @@ -95,11 +95,11 @@ suite('ExtHostDocumentSaveParticipant', () => { test('event delivery, bad listener doesn\'t prevent more events', () => { const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits); - let sub1 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) { + const sub1 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) { throw new Error('💀'); }); let event: vscode.TextDocumentWillSaveEvent; - let sub2 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) { + const sub2 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) { event = e; }); @@ -115,11 +115,11 @@ suite('ExtHostDocumentSaveParticipant', () => { const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits); let counter = 0; - let sub1 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) { + const sub1 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) { assert.strictEqual(counter++, 0); }); - let sub2 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) { + const sub2 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) { assert.strictEqual(counter++, 1); }); @@ -133,7 +133,7 @@ suite('ExtHostDocumentSaveParticipant', () => { const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits, { timeout: 5, errors: 1 }); let callCount = 0; - let sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) { + const sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) { callCount += 1; throw new Error('boom'); }); @@ -151,17 +151,17 @@ suite('ExtHostDocumentSaveParticipant', () => { const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits, { timeout: 20, errors: 5 }); // let callCount = 0; - let calls: number[] = []; - let sub1 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) { + const calls: number[] = []; + const sub1 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) { calls.push(1); }); - let sub2 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) { + const sub2 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) { calls.push(2); event.waitUntil(timeout(100)); }); - let sub3 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) { + const sub3 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) { calls.push(3); }); @@ -176,7 +176,7 @@ suite('ExtHostDocumentSaveParticipant', () => { test('event delivery, waitUntil', () => { const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits); - let sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) { + const sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) { event.waitUntil(timeout(10)); event.waitUntil(timeout(10)); @@ -192,7 +192,7 @@ suite('ExtHostDocumentSaveParticipant', () => { test('event delivery, waitUntil must be called sync', () => { const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits); - let sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) { + const sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) { event.waitUntil(new Promise<undefined>((resolve, reject) => { setTimeout(() => { @@ -216,7 +216,7 @@ suite('ExtHostDocumentSaveParticipant', () => { const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits, { timeout: 5, errors: 3 }); - let sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) { + const sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) { event.waitUntil(timeout(100)); }); @@ -231,12 +231,12 @@ suite('ExtHostDocumentSaveParticipant', () => { test('event delivery, waitUntil failure handling', () => { const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits); - let sub1 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) { + const sub1 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) { e.waitUntil(Promise.reject(new Error('dddd'))); }); let event: vscode.TextDocumentWillSaveEvent; - let sub2 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) { + const sub2 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) { event = e; }); @@ -257,7 +257,7 @@ suite('ExtHostDocumentSaveParticipant', () => { } }); - let sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) { + const sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) { e.waitUntil(Promise.resolve([TextEdit.insert(new Position(0, 0), 'bar')])); e.waitUntil(Promise.resolve([TextEdit.setEndOfLine(EndOfLine.CRLF)])); }); @@ -281,7 +281,7 @@ suite('ExtHostDocumentSaveParticipant', () => { } }); - let sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) { + const sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) { // concurrent change from somewhere documents.$acceptModelChanged(resource, { @@ -339,7 +339,7 @@ suite('ExtHostDocumentSaveParticipant', () => { const document = documents.getDocument(resource); - let sub1 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) { + const sub1 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) { // the document state we started with assert.strictEqual(document.version, 1); assert.strictEqual(document.getText(), 'foo'); @@ -347,7 +347,7 @@ suite('ExtHostDocumentSaveParticipant', () => { e.waitUntil(Promise.resolve([TextEdit.insert(new Position(0, 0), 'bar')])); }); - let sub2 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) { + const sub2 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) { // the document state AFTER the first listener kicked in assert.strictEqual(document.version, 2); assert.strictEqual(document.getText(), 'barfoo'); @@ -368,14 +368,14 @@ suite('ExtHostDocumentSaveParticipant', () => { test('Log failing listener', function () { let didLogSomething = false; - let participant = new ExtHostDocumentSaveParticipant(new class extends NullLogService { + const participant = new ExtHostDocumentSaveParticipant(new class extends NullLogService { override error(message: string | Error, ...args: any[]): void { didLogSomething = true; } }, documents, mainThreadBulkEdits); - let sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) { + const sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) { throw new Error('boom'); }); diff --git a/src/vs/workbench/api/test/browser/extHostEditorTabs.test.ts b/src/vs/workbench/api/test/browser/extHostEditorTabs.test.ts index 1e385fc213f..4c2501f4692 100644 --- a/src/vs/workbench/api/test/browser/extHostEditorTabs.test.ts +++ b/src/vs/workbench/api/test/browser/extHostEditorTabs.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import type * as vscode from 'vscode'; -import assert = require('assert'); +import * as assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { mock } from 'vs/base/test/common/mock'; import { IEditorTabDto, IEditorTabGroupDto, MainThreadEditorTabsShape, TabInputKind, TabModelOperationKind, TextInputDto } from 'vs/workbench/api/common/extHost.protocol'; @@ -293,7 +293,7 @@ suite('ExtHostEditorTabs', function () { tabs: [tabDtoAAA, tabDtoBBB] }]); - let all = extHostEditorTabs.tabGroups.all.map(group => group.tabs).flat(); + const all = extHostEditorTabs.tabGroups.all.map(group => group.tabs).flat(); assert.strictEqual(all.length, 2); const activeTab1 = extHostEditorTabs.tabGroups.activeTabGroup?.activeTab; @@ -346,7 +346,7 @@ suite('ExtHostEditorTabs', function () { }); test('Ensure close is called with all tab ids', function () { - let closedTabIds: string[][] = []; + const closedTabIds: string[][] = []; const extHostEditorTabs = new ExtHostEditorTabs( SingleProxyRPCProtocol(new class extends mock<MainThreadEditorTabsShape>() { // override/implement $moveTab or $closeTab @@ -384,7 +384,7 @@ suite('ExtHostEditorTabs', function () { }); test('Update tab only sends tab change event', async function () { - let closedTabIds: string[][] = []; + const closedTabIds: string[][] = []; const extHostEditorTabs = new ExtHostEditorTabs( SingleProxyRPCProtocol(new class extends mock<MainThreadEditorTabsShape>() { // override/implement $moveTab or $closeTab diff --git a/src/vs/workbench/api/test/browser/extHostLanguageFeatures.test.ts b/src/vs/workbench/api/test/browser/extHostLanguageFeatures.test.ts index da08c10521e..c0246ecc81a 100644 --- a/src/vs/workbench/api/test/browser/extHostLanguageFeatures.test.ts +++ b/src/vs/workbench/api/test/browser/extHostLanguageFeatures.test.ts @@ -53,6 +53,7 @@ import { URITransformerService } from 'vs/workbench/api/common/extHostUriTransfo import { OutlineModel } from 'vs/editor/contrib/documentSymbols/browser/outlineModel'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { LanguageFeaturesService } from 'vs/editor/common/services/languageFeaturesService'; +import { CodeActionTriggerSource } from 'vs/editor/contrib/codeAction/browser/types'; suite('ExtHostLanguageFeatures', function () { @@ -84,7 +85,7 @@ suite('ExtHostLanguageFeatures', function () { // Use IInstantiationService to get typechecking when instantiating let inst: IInstantiationService; { - let instantiationService = new TestInstantiationService(); + const instantiationService = new TestInstantiationService(); instantiationService.stub(IMarkerService, MarkerService); instantiationService.set(ILanguageFeaturesService, languageFeaturesService); inst = instantiationService; @@ -135,7 +136,7 @@ suite('ExtHostLanguageFeatures', function () { test('DocumentSymbols, register/deregister', async () => { assert.strictEqual(languageFeaturesService.documentSymbolProvider.all(model).length, 0); - let d1 = extHost.registerDocumentSymbolProvider(defaultExtension, defaultSelector, new class implements vscode.DocumentSymbolProvider { + const d1 = extHost.registerDocumentSymbolProvider(defaultExtension, defaultSelector, new class implements vscode.DocumentSymbolProvider { provideDocumentSymbols() { return <vscode.SymbolInformation[]>[]; } @@ -175,7 +176,7 @@ suite('ExtHostLanguageFeatures', function () { await rpcProtocol.sync(); const value = (await OutlineModel.create(languageFeaturesService.documentSymbolProvider, model, CancellationToken.None)).asListOfDocumentSymbols(); assert.strictEqual(value.length, 1); - let entry = value[0]; + const entry = value[0]; assert.strictEqual(entry.name, 'test'); assert.deepStrictEqual(entry.range, { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }); }); @@ -263,7 +264,7 @@ suite('ExtHostLanguageFeatures', function () { await rpcProtocol.sync(); const value = await getCodeLensModel(languageFeaturesService.codeLensProvider, model, CancellationToken.None); assert.strictEqual(value.lenses.length, 1); - let [data] = value.lenses; + const [data] = value.lenses; const symbol = await Promise.resolve(data.provider.resolveCodeLens!(model, data.symbol, CancellationToken.None)); assert.strictEqual(symbol!.command!.id, 'missing'); assert.strictEqual(symbol!.command!.title, '!!MISSING: command!!'); @@ -280,9 +281,9 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - let value = await getDefinitionsAtPosition(languageFeaturesService.definitionProvider, model, new EditorPosition(1, 1), CancellationToken.None); + const value = await getDefinitionsAtPosition(languageFeaturesService.definitionProvider, model, new EditorPosition(1, 1), CancellationToken.None); assert.strictEqual(value.length, 1); - let [entry] = value; + const [entry] = value; assert.deepStrictEqual(entry.range, { startLineNumber: 2, startColumn: 3, endLineNumber: 4, endColumn: 5 }); assert.strictEqual(entry.uri.toString(), model.uri.toString()); }); @@ -356,9 +357,9 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - let value = await getDeclarationsAtPosition(languageFeaturesService.declarationProvider, model, new EditorPosition(1, 1), CancellationToken.None); + const value = await getDeclarationsAtPosition(languageFeaturesService.declarationProvider, model, new EditorPosition(1, 1), CancellationToken.None); assert.strictEqual(value.length, 1); - let [entry] = value; + const [entry] = value; assert.deepStrictEqual(entry.range, { startLineNumber: 2, startColumn: 3, endLineNumber: 4, endColumn: 5 }); assert.strictEqual(entry.uri.toString(), model.uri.toString()); }); @@ -374,9 +375,9 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - let value = await getImplementationsAtPosition(languageFeaturesService.implementationProvider, model, new EditorPosition(1, 1), CancellationToken.None); + const value = await getImplementationsAtPosition(languageFeaturesService.implementationProvider, model, new EditorPosition(1, 1), CancellationToken.None); assert.strictEqual(value.length, 1); - let [entry] = value; + const [entry] = value; assert.deepStrictEqual(entry.range, { startLineNumber: 2, startColumn: 3, endLineNumber: 4, endColumn: 5 }); assert.strictEqual(entry.uri.toString(), model.uri.toString()); }); @@ -392,9 +393,9 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - let value = await getTypeDefinitionsAtPosition(languageFeaturesService.typeDefinitionProvider, model, new EditorPosition(1, 1), CancellationToken.None); + const value = await getTypeDefinitionsAtPosition(languageFeaturesService.typeDefinitionProvider, model, new EditorPosition(1, 1), CancellationToken.None); assert.strictEqual(value.length, 1); - let [entry] = value; + const [entry] = value; assert.deepStrictEqual(entry.range, { startLineNumber: 2, startColumn: 3, endLineNumber: 4, endColumn: 5 }); assert.strictEqual(entry.uri.toString(), model.uri.toString()); }); @@ -412,7 +413,7 @@ suite('ExtHostLanguageFeatures', function () { await rpcProtocol.sync(); getHoverPromise(languageFeaturesService.hoverProvider, model, new EditorPosition(1, 1), CancellationToken.None).then(value => { assert.strictEqual(value.length, 1); - let [entry] = value; + const [entry] = value; assert.deepStrictEqual(entry.range, { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 5 }); }); }); @@ -429,7 +430,7 @@ suite('ExtHostLanguageFeatures', function () { await rpcProtocol.sync(); getHoverPromise(languageFeaturesService.hoverProvider, model, new EditorPosition(1, 1), CancellationToken.None).then(value => { assert.strictEqual(value.length, 1); - let [entry] = value; + const [entry] = value; assert.deepStrictEqual(entry.range, { startLineNumber: 4, startColumn: 1, endLineNumber: 9, endColumn: 8 }); }); }); @@ -452,7 +453,7 @@ suite('ExtHostLanguageFeatures', function () { await rpcProtocol.sync(); const value = await getHoverPromise(languageFeaturesService.hoverProvider, model, new EditorPosition(1, 1), CancellationToken.None); assert.strictEqual(value.length, 2); - let [first, second] = (value as languages.Hover[]); + const [first, second] = (value as languages.Hover[]); assert.strictEqual(first.contents[0].value, 'registered second'); assert.strictEqual(second.contents[0].value, 'registered first'); }); @@ -573,9 +574,9 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - let value = await getReferencesAtPosition(languageFeaturesService.referenceProvider, model, new EditorPosition(1, 2), false, CancellationToken.None); + const value = await getReferencesAtPosition(languageFeaturesService.referenceProvider, model, new EditorPosition(1, 2), false, CancellationToken.None); assert.strictEqual(value.length, 2); - let [first, second] = value; + const [first, second] = value; assert.strictEqual(first.uri.path, '/second'); assert.strictEqual(second.uri.path, '/first'); }); @@ -589,9 +590,9 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - let value = await getReferencesAtPosition(languageFeaturesService.referenceProvider, model, new EditorPosition(1, 2), false, CancellationToken.None); + const value = await getReferencesAtPosition(languageFeaturesService.referenceProvider, model, new EditorPosition(1, 2), false, CancellationToken.None); assert.strictEqual(value.length, 1); - let [item] = value; + const [item] = value; assert.deepStrictEqual(item.range, { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }); assert.strictEqual(item.uri.toString(), model.uri.toString()); }); @@ -628,7 +629,7 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - const { validActions: actions } = await getCodeActions(languageFeaturesService.codeActionProvider, model, model.getFullModelRange(), { type: languages.CodeActionTriggerType.Invoke }, Progress.None, CancellationToken.None); + const { validActions: actions } = await getCodeActions(languageFeaturesService.codeActionProvider, model, model.getFullModelRange(), { type: languages.CodeActionTriggerType.Invoke, triggerAction: CodeActionTriggerSource.QuickFix }, Progress.None, CancellationToken.None); assert.strictEqual(actions.length, 2); const [first, second] = actions; assert.strictEqual(first.action.title, 'Testing1'); @@ -652,7 +653,7 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - const { validActions: actions } = await getCodeActions(languageFeaturesService.codeActionProvider, model, model.getFullModelRange(), { type: languages.CodeActionTriggerType.Invoke }, Progress.None, CancellationToken.None); + const { validActions: actions } = await getCodeActions(languageFeaturesService.codeActionProvider, model, model.getFullModelRange(), { type: languages.CodeActionTriggerType.Invoke, triggerAction: CodeActionTriggerSource.Default }, Progress.None, CancellationToken.None); assert.strictEqual(actions.length, 1); const [first] = actions; assert.strictEqual(first.action.title, 'Testing1'); @@ -675,7 +676,7 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - const { validActions: actions } = await getCodeActions(languageFeaturesService.codeActionProvider, model, model.getFullModelRange(), { type: languages.CodeActionTriggerType.Invoke }, Progress.None, CancellationToken.None); + const { validActions: actions } = await getCodeActions(languageFeaturesService.codeActionProvider, model, model.getFullModelRange(), { type: languages.CodeActionTriggerType.Invoke, triggerAction: CodeActionTriggerSource.Default }, Progress.None, CancellationToken.None); assert.strictEqual(actions.length, 1); }); @@ -693,7 +694,7 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - const { validActions: actions } = await getCodeActions(languageFeaturesService.codeActionProvider, model, model.getFullModelRange(), { type: languages.CodeActionTriggerType.Invoke }, Progress.None, CancellationToken.None); + const { validActions: actions } = await getCodeActions(languageFeaturesService.codeActionProvider, model, model.getFullModelRange(), { type: languages.CodeActionTriggerType.Invoke, triggerAction: CodeActionTriggerSource.QuickFix }, Progress.None, CancellationToken.None); assert.strictEqual(actions.length, 1); }); @@ -714,7 +715,7 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - let value = await getWorkspaceSymbols(''); + const value = await getWorkspaceSymbols(''); assert.strictEqual(value.length, 1); const [first] = value; assert.strictEqual(first.symbol.name, 'testing'); @@ -750,7 +751,7 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - let value = await getWorkspaceSymbols(''); + const value = await getWorkspaceSymbols(''); assert.strictEqual(value.length, 3); }); @@ -797,7 +798,7 @@ suite('ExtHostLanguageFeatures', function () { disposables.push(extHost.registerRenameProvider(defaultExtension, defaultSelector, new class implements vscode.RenameProvider { provideRenameEdits(): any { - let edit = new types.WorkspaceEdit(); + const edit = new types.WorkspaceEdit(); edit.replace(model.uri, new types.Range(0, 0, 0, 0), 'testing'); return edit; } @@ -812,7 +813,7 @@ suite('ExtHostLanguageFeatures', function () { disposables.push(extHost.registerRenameProvider(defaultExtension, '*', new class implements vscode.RenameProvider { provideRenameEdits(): any { - let edit = new types.WorkspaceEdit(); + const edit = new types.WorkspaceEdit(); edit.replace(model.uri, new types.Range(0, 0, 0, 0), 'testing'); edit.replace(model.uri, new types.Range(1, 0, 1, 0), 'testing'); return edit; @@ -833,12 +834,12 @@ suite('ExtHostLanguageFeatures', function () { test('Multiple RenameProviders don\'t respect all possible PrepareRename handlers, #98352', async function () { - let called = [false, false, false, false]; + const called = [false, false, false, false]; disposables.push(extHost.registerRenameProvider(defaultExtension, defaultSelector, new class implements vscode.RenameProvider { prepareRename(document: vscode.TextDocument, position: vscode.Position,): vscode.ProviderResult<vscode.Range> { called[0] = true; - let range = document.getWordRangeAtPosition(position); + const range = document.getWordRangeAtPosition(position); return range; } @@ -867,12 +868,12 @@ suite('ExtHostLanguageFeatures', function () { test('Multiple RenameProviders don\'t respect all possible PrepareRename handlers, #98352', async function () { - let called = [false, false, false]; + const called = [false, false, false]; disposables.push(extHost.registerRenameProvider(defaultExtension, defaultSelector, new class implements vscode.RenameProvider { prepareRename(document: vscode.TextDocument, position: vscode.Position,): vscode.ProviderResult<vscode.Range> { called[0] = true; - let range = document.getWordRangeAtPosition(position); + const range = document.getWordRangeAtPosition(position); return range; } @@ -1048,9 +1049,9 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - let value = (await getDocumentFormattingEditsUntilResult(NullWorkerService, languageFeaturesService, model, { insertSpaces: true, tabSize: 4 }, CancellationToken.None))!; + const value = (await getDocumentFormattingEditsUntilResult(NullWorkerService, languageFeaturesService, model, { insertSpaces: true, tabSize: 4 }, CancellationToken.None))!; assert.strictEqual(value.length, 2); - let [first, second] = value; + const [first, second] = value; assert.strictEqual(first.text, 'testing'); assert.deepStrictEqual(first.range, { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }); assert.strictEqual(second.eol, EndOfLineSequence.LF); @@ -1090,9 +1091,9 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - let value = (await getDocumentFormattingEditsUntilResult(NullWorkerService, languageFeaturesService, model, { insertSpaces: true, tabSize: 4 }, CancellationToken.None))!; + const value = (await getDocumentFormattingEditsUntilResult(NullWorkerService, languageFeaturesService, model, { insertSpaces: true, tabSize: 4 }, CancellationToken.None))!; assert.strictEqual(value.length, 1); - let [first] = value; + const [first] = value; assert.strictEqual(first.text, 'testing'); assert.deepStrictEqual(first.range, { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }); }); @@ -1177,9 +1178,9 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - let { links } = await getLinks(languageFeaturesService.linkProvider, model, CancellationToken.None); + const { links } = await getLinks(languageFeaturesService.linkProvider, model, CancellationToken.None); assert.strictEqual(links.length, 1); - let [first] = links; + const [first] = links; assert.strictEqual(first.url?.toString(), 'foo:bar#3'); assert.deepStrictEqual(first.range, { startLineNumber: 1, startColumn: 1, endLineNumber: 2, endColumn: 2 }); assert.strictEqual(first.tooltip, 'tooltip'); @@ -1200,9 +1201,9 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - let { links } = await getLinks(languageFeaturesService.linkProvider, model, CancellationToken.None); + const { links } = await getLinks(languageFeaturesService.linkProvider, model, CancellationToken.None); assert.strictEqual(links.length, 1); - let [first] = links; + const [first] = links; assert.strictEqual(first.url?.toString(), 'foo:bar#3'); assert.deepStrictEqual(first.range, { startLineNumber: 1, startColumn: 1, endLineNumber: 2, endColumn: 2 }); }); @@ -1219,9 +1220,9 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - let value = await getColors(languageFeaturesService.colorProvider, model, CancellationToken.None); + const value = await getColors(languageFeaturesService.colorProvider, model, CancellationToken.None); assert.strictEqual(value.length, 1); - let [first] = value; + const [first] = value; assert.deepStrictEqual(first.colorInfo.color, { red: 0.1, green: 0.2, blue: 0.3, alpha: 0.4 }); assert.deepStrictEqual(first.colorInfo.range, { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 21 }); }); @@ -1248,7 +1249,7 @@ suite('ExtHostLanguageFeatures', function () { test('Selection Ranges, bad data', async () => { try { - let _a = new types.SelectionRange(new types.Range(0, 10, 0, 18), + const _a = new types.SelectionRange(new types.Range(0, 10, 0, 18), new types.SelectionRange(new types.Range(0, 11, 0, 18)) ); assert.ok(false, String(_a)); diff --git a/src/vs/workbench/api/test/browser/extHostMessagerService.test.ts b/src/vs/workbench/api/test/browser/extHostMessagerService.test.ts index dcbd0b4edc6..e158253f4dc 100644 --- a/src/vs/workbench/api/test/browser/extHostMessagerService.test.ts +++ b/src/vs/workbench/api/test/browser/extHostMessagerService.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import { MainThreadMessageService } from 'vs/workbench/api/browser/mainThreadMessageService'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { INotificationService, INotification, NoOpNotification, INotificationHandle, Severity, IPromptChoice, IPromptOptions, IStatusMessageOptions, NotificationsFilter } from 'vs/platform/notification/common/notification'; +import { INotificationService, INotification, NoOpNotification, INotificationHandle, Severity, IPromptChoice, IPromptOptions, IStatusMessageOptions } from 'vs/platform/notification/common/notification'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { mock } from 'vs/base/test/common/mock'; import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; @@ -24,8 +24,10 @@ const emptyCommandService: ICommandService = { const emptyNotificationService = new class implements INotificationService { declare readonly _serviceBrand: undefined; + doNotDisturbMode: boolean = false; onDidAddNotification: Event<INotification> = Event.None; onDidRemoveNotification: Event<INotification> = Event.None; + onDidChangeDoNotDisturbMode: Event<void> = Event.None; notify(...args: any[]): never { throw new Error('not implemented'); } @@ -44,19 +46,17 @@ const emptyNotificationService = new class implements INotificationService { status(message: string | Error, options?: IStatusMessageOptions): IDisposable { return Disposable.None; } - setFilter(filter: NotificationsFilter): void { - throw new Error('not implemented.'); - } }; class EmptyNotificationService implements INotificationService { declare readonly _serviceBrand: undefined; - + doNotDisturbMode: boolean = false; constructor(private withNotify: (notification: INotification) => void) { } onDidAddNotification: Event<INotification> = Event.None; onDidRemoveNotification: Event<INotification> = Event.None; + onDidChangeDoNotDisturbMode: Event<void> = Event.None; notify(notification: INotification): INotificationHandle { this.withNotify(notification); @@ -77,16 +77,13 @@ class EmptyNotificationService implements INotificationService { status(message: string, options?: IStatusMessageOptions): IDisposable { return Disposable.None; } - setFilter(filter: NotificationsFilter): void { - throw new Error('Method not implemented.'); - } } suite('ExtHostMessageService', function () { test('propagte handle on select', async function () { - let service = new MainThreadMessageService(null!, new EmptyNotificationService(notification => { + const service = new MainThreadMessageService(null!, new EmptyNotificationService(notification => { assert.strictEqual(notification.actions!.primary!.length, 1); queueMicrotask(() => notification.actions!.primary![0].run()); }), emptyCommandService, new TestDialogService()); diff --git a/src/vs/workbench/api/test/browser/extHostNotebook.test.ts b/src/vs/workbench/api/test/browser/extHostNotebook.test.ts index 8eaf147fcfe..c61334ba316 100644 --- a/src/vs/workbench/api/test/browser/extHostNotebook.test.ts +++ b/src/vs/workbench/api/test/browser/extHostNotebook.test.ts @@ -62,7 +62,7 @@ suite('NotebookCell#Document', function () { extHostNotebooks = new ExtHostNotebookController(rpcProtocol, new ExtHostCommands(rpcProtocol, new NullLogService()), extHostDocumentsAndEditors, extHostDocuments, extHostStoragePaths); extHostNotebookDocuments = new ExtHostNotebookDocuments(extHostNotebooks); - let reg = extHostNotebooks.registerNotebookContentProvider(nullExtensionDescription, 'test', new class extends mock<vscode.NotebookContentProvider>() { + const reg = extHostNotebooks.registerNotebookContentProvider(nullExtensionDescription, 'test', new class extends mock<vscode.NotebookContentProvider>() { // async openNotebook() { } }); extHostNotebooks.$acceptDocumentAndEditorsDelta(new SerializableObjectWithBuffers({ @@ -124,7 +124,7 @@ suite('NotebookCell#Document', function () { test('cell document goes when notebook closes', async function () { const cellUris: string[] = []; - for (let cell of notebook.apiNotebook.getCells()) { + for (const cell of notebook.apiNotebook.getCells()) { assert.ok(extHostDocuments.getDocument(cell.document.uri)); cellUris.push(cell.document.uri.toString()); } @@ -203,7 +203,7 @@ suite('NotebookCell#Document', function () { const docs: vscode.TextDocument[] = []; const addData: IModelAddedData[] = []; - for (let cell of notebook.apiNotebook.getCells()) { + for (const cell of notebook.apiNotebook.getCells()) { const doc = extHostDocuments.getDocument(cell.document.uri); assert.ok(doc); assert.strictEqual(extHostDocuments.getDocument(cell.document.uri).isClosed, false); @@ -225,17 +225,17 @@ suite('NotebookCell#Document', function () { extHostDocumentsAndEditors.$acceptDocumentsAndEditorsDelta({ removedDocuments: docs.map(d => d.uri) }); // notebook is still open -> cell documents stay open - for (let cell of notebook.apiNotebook.getCells()) { + for (const cell of notebook.apiNotebook.getCells()) { assert.ok(extHostDocuments.getDocument(cell.document.uri)); assert.strictEqual(extHostDocuments.getDocument(cell.document.uri).isClosed, false); } // close notebook -> docs are closed extHostNotebooks.$acceptDocumentAndEditorsDelta(new SerializableObjectWithBuffers({ removedDocuments: [notebook.uri] })); - for (let cell of notebook.apiNotebook.getCells()) { + for (const cell of notebook.apiNotebook.getCells()) { assert.throws(() => extHostDocuments.getDocument(cell.document.uri)); } - for (let doc of docs) { + for (const doc of docs) { assert.strictEqual(doc.isClosed, true); } }); diff --git a/src/vs/workbench/api/test/browser/extHostNotebookKernel.test.ts b/src/vs/workbench/api/test/browser/extHostNotebookKernel.test.ts index 90edc6979d2..54eb5519c6f 100644 --- a/src/vs/workbench/api/test/browser/extHostNotebookKernel.test.ts +++ b/src/vs/workbench/api/test/browser/extHostNotebookKernel.test.ts @@ -162,7 +162,7 @@ suite('NotebookKernel', function () { await rpcProtocol.sync(); assert.strictEqual(kernelData.size, 1); - let [first] = kernelData.values(); + const [first] = kernelData.values(); assert.strictEqual(first.id, 'nullExtensionDescription/foo'); assert.strictEqual(ExtensionIdentifier.equals(first.extensionId, nullExtensionDescription.identifier), true); assert.strictEqual(first.label, 'Foo'); @@ -283,7 +283,7 @@ suite('NotebookKernel', function () { assert.strictEqual(cellExecuteUpdates.length > 0, true); let found = false; - for (let edit of cellExecuteUpdates) { + for (const edit of cellExecuteUpdates) { if (edit.editType === CellExecutionUpdateType.Output) { assert.strictEqual(edit.append, false); assert.strictEqual(edit.outputs.length, 1); @@ -317,7 +317,7 @@ suite('NotebookKernel', function () { assert.strictEqual(cellExecuteUpdates.length > 0, true); let found = false; - for (let edit of cellExecuteUpdates) { + for (const edit of cellExecuteUpdates) { if (edit.editType === CellExecutionUpdateType.Output) { assert.strictEqual(edit.append, false); assert.strictEqual(edit.outputs.length, 1); diff --git a/src/vs/workbench/api/test/browser/extHostTesting.test.ts b/src/vs/workbench/api/test/browser/extHostTesting.test.ts index c4ce107acf1..2ab14e477a1 100644 --- a/src/vs/workbench/api/test/browser/extHostTesting.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTesting.test.ts @@ -36,8 +36,8 @@ const assertTreesEqual = (a: TestItemImpl | undefined, b: TestItemImpl | undefin assert.deepStrictEqual(simplify(a), simplify(b)); - const aChildren = [...a.children].map(c => c.id).sort(); - const bChildren = [...b.children].map(c => c.id).sort(); + const aChildren = [...a.children].map(([_, c]) => c.id).sort(); + const bChildren = [...b.children].map(([_, c]) => c.id).sort(); assert.strictEqual(aChildren.length, bChildren.length, `expected ${a.label}.children.length == ${b.label}.children.length`); aChildren.forEach(key => assertTreesEqual(a.children.get(key) as TestItemImpl, b.children.get(key) as TestItemImpl)); }; @@ -242,7 +242,7 @@ suite('ExtHost Testing', () => { const oldA = single.root.children.get('id-a') as TestItemImpl; const newA = new TestItemImpl('ctrlId', 'id-a', 'Hello world', undefined); - newA.children.replace([...oldA.children]); + newA.children.replace([...oldA.children].map(([_, item]) => item)); single.root.children.replace([ newA, new TestItemImpl('ctrlId', 'id-b', single.root.children.get('id-b')!.label, undefined), @@ -334,7 +334,7 @@ suite('ExtHost Testing', () => { }, ]); - assert.deepStrictEqual([...single.root.children], [single.root.children.get('id-a')]); + assert.deepStrictEqual([...single.root.children].map(([_, item]) => item), [single.root.children.get('id-a')]); assert.deepStrictEqual(b.parent, a); }); }); diff --git a/src/vs/workbench/api/test/browser/extHostTextEditor.test.ts b/src/vs/workbench/api/test/browser/extHostTextEditor.test.ts index 9d69ae57537..accded46c5d 100644 --- a/src/vs/workbench/api/test/browser/extHostTextEditor.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTextEditor.test.ts @@ -16,7 +16,7 @@ import { Lazy } from 'vs/base/common/lazy'; suite('ExtHostTextEditor', () => { let editor: ExtHostTextEditor; - let doc = new ExtHostDocumentData(undefined!, URI.file(''), [ + const doc = new ExtHostDocumentData(undefined!, URI.file(''), [ 'aaaa bbbb+cccc abc' ], '\n', 1, 'text', false); @@ -42,7 +42,7 @@ suite('ExtHostTextEditor', () => { test('API [bug]: registerTextEditorCommand clears redo stack even if no edits are made #55163', async function () { let applyCount = 0; - let editor = new ExtHostTextEditor('edt1', + const editor = new ExtHostTextEditor('edt1', new class extends mock<MainThreadTextEditorsShape>() { override $tryApplyEdits(): Promise<boolean> { applyCount += 1; @@ -68,7 +68,7 @@ suite('ExtHostTextEditorOptions', () => { setup(() => { calls = []; - let mockProxy: MainThreadTextEditorsShape = { + const mockProxy: MainThreadTextEditorsShape = { dispose: undefined!, $trySetOptions: (id: string, options: ITextEditorConfigurationUpdate) => { assert.strictEqual(id, '1'); @@ -102,7 +102,7 @@ suite('ExtHostTextEditorOptions', () => { }); function assertState(opts: ExtHostTextEditorOptions, expected: IResolvedTextEditorConfiguration): void { - let actual = { + const actual = { tabSize: opts.value.tabSize, insertSpaces: opts.value.insertSpaces, cursorStyle: opts.value.cursorStyle, diff --git a/src/vs/workbench/api/test/browser/extHostTreeViews.test.ts b/src/vs/workbench/api/test/browser/extHostTreeViews.test.ts index 48e90584747..c77bc003cc5 100644 --- a/src/vs/workbench/api/test/browser/extHostTreeViews.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTreeViews.test.ts @@ -65,11 +65,11 @@ suite('ExtHostTreeView', function () { labels = {}; nodes = {}; - let rpcProtocol = new TestRPCProtocol(); + const rpcProtocol = new TestRPCProtocol(); // Use IInstantiationService to get typechecking when instantiating let inst: IInstantiationService; { - let instantiationService = new TestInstantiationService(); + const instantiationService = new TestInstantiationService(); inst = instantiationService; } @@ -738,7 +738,7 @@ suite('ExtHostTreeView', function () { if (!key) { return Object.keys(tree); } - let treeElement = getTreeElement(key); + const treeElement = getTreeElement(key); if (treeElement) { return Object.keys(treeElement); } diff --git a/src/vs/workbench/api/test/browser/extHostTypeConverter.test.ts b/src/vs/workbench/api/test/browser/extHostTypeConverter.test.ts index fb2b02bc890..a7a0b914279 100644 --- a/src/vs/workbench/api/test/browser/extHostTypeConverter.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTypeConverter.test.ts @@ -15,7 +15,7 @@ import { URI } from 'vs/base/common/uri'; suite('ExtHostTypeConverter', function () { function size<T>(from: Record<any, any>): number { let count = 0; - for (let key in from) { + for (const key in from) { if (Object.prototype.hasOwnProperty.call(from, key)) { count += 1; } @@ -71,7 +71,7 @@ suite('ExtHostTypeConverter', function () { test('NPM script explorer running a script from the hover does not work #65561', function () { - let data = MarkdownString.from('*hello* [click](command:npm.runScriptFromHover?%7B%22documentUri%22%3A%7B%22%24mid%22%3A1%2C%22external%22%3A%22file%3A%2F%2F%2Fc%253A%2Ffoo%2Fbaz.ex%22%2C%22path%22%3A%22%2Fc%3A%2Ffoo%2Fbaz.ex%22%2C%22scheme%22%3A%22file%22%7D%2C%22script%22%3A%22dev%22%7D)'); + const data = MarkdownString.from('*hello* [click](command:npm.runScriptFromHover?%7B%22documentUri%22%3A%7B%22%24mid%22%3A1%2C%22external%22%3A%22file%3A%2F%2F%2Fc%253A%2Ffoo%2Fbaz.ex%22%2C%22path%22%3A%22%2Fc%3A%2Ffoo%2Fbaz.ex%22%2C%22scheme%22%3A%22file%22%7D%2C%22script%22%3A%22dev%22%7D)'); // assert that both uri get extracted but that the latter is only decoded once... assert.strictEqual(size(data.uris!), 2); forEach(data.uris!, entry => { diff --git a/src/vs/workbench/api/test/browser/extHostTypes.test.ts b/src/vs/workbench/api/test/browser/extHostTypes.test.ts index 4fa83071049..5bf6668b649 100644 --- a/src/vs/workbench/api/test/browser/extHostTypes.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTypes.test.ts @@ -22,7 +22,7 @@ suite('ExtHostTypes', function () { test('URI, toJSON', function () { - let uri = URI.parse('file:///path/test.file'); + const uri = URI.parse('file:///path/test.file'); assert.deepStrictEqual(uri.toJSON(), { $mid: MarshalledId.Uri, scheme: 'file', @@ -52,7 +52,7 @@ suite('ExtHostTypes', function () { test('Disposable', () => { let count = 0; - let d = new types.Disposable(() => { + const d = new types.Disposable(() => { count += 1; return 12; }); @@ -80,25 +80,25 @@ suite('ExtHostTypes', function () { assert.throws(() => new types.Position(-1, 0)); assert.throws(() => new types.Position(0, -1)); - let pos = new types.Position(0, 0); + const pos = new types.Position(0, 0); assert.throws(() => (pos as any).line = -1); assert.throws(() => (pos as any).character = -1); assert.throws(() => (pos as any).line = 12); - let { line, character } = pos.toJSON(); + const { line, character } = pos.toJSON(); assert.strictEqual(line, 0); assert.strictEqual(character, 0); }); test('Position, toJSON', function () { - let pos = new types.Position(4, 2); + const pos = new types.Position(4, 2); assertToJSON(pos, { line: 4, character: 2 }); }); test('Position, isBefore(OrEqual)?', function () { - let p1 = new types.Position(1, 3); - let p2 = new types.Position(1, 2); - let p3 = new types.Position(0, 4); + const p1 = new types.Position(1, 3); + const p2 = new types.Position(1, 2); + const p3 = new types.Position(0, 4); assert.ok(p1.isBeforeOrEqual(p1)); assert.ok(!p1.isBefore(p1)); @@ -107,9 +107,9 @@ suite('ExtHostTypes', function () { }); test('Position, isAfter(OrEqual)?', function () { - let p1 = new types.Position(1, 3); - let p2 = new types.Position(1, 2); - let p3 = new types.Position(0, 4); + const p1 = new types.Position(1, 3); + const p2 = new types.Position(1, 2); + const p3 = new types.Position(0, 4); assert.ok(p1.isAfterOrEqual(p1)); assert.ok(!p1.isAfter(p1)); @@ -119,9 +119,9 @@ suite('ExtHostTypes', function () { }); test('Position, compareTo', function () { - let p1 = new types.Position(1, 3); - let p2 = new types.Position(1, 2); - let p3 = new types.Position(0, 4); + const p1 = new types.Position(1, 3); + const p2 = new types.Position(1, 2); + const p3 = new types.Position(0, 4); assert.strictEqual(p1.compareTo(p1), 0); assert.strictEqual(p2.compareTo(p1), -1); @@ -131,7 +131,7 @@ suite('ExtHostTypes', function () { }); test('Position, translate', function () { - let p1 = new types.Position(1, 3); + const p1 = new types.Position(1, 3); assert.ok(p1.translate() === p1); assert.ok(p1.translate({}) === p1); @@ -169,7 +169,7 @@ suite('ExtHostTypes', function () { }); test('Position, with', function () { - let p1 = new types.Position(1, 3); + const p1 = new types.Position(1, 3); assert.ok(p1.with() === p1); assert.ok(p1.with(1) === p1); @@ -180,7 +180,7 @@ suite('ExtHostTypes', function () { assert.ok(p1.with({ character: 3 }) === p1); assert.ok(p1.with({ line: 1, character: 3 }) === p1); - let p2 = p1.with({ line: 0, character: 11 }); + const p2 = p1.with({ line: 0, character: 11 }); assert.strictEqual(p2.line, 0); assert.strictEqual(p2.character, 11); @@ -199,14 +199,14 @@ suite('ExtHostTypes', function () { assert.throws(() => new types.Range(undefined!, new types.Position(0, 0))); assert.throws(() => new types.Range(null!, new types.Position(0, 0))); - let range = new types.Range(1, 0, 0, 0); + const range = new types.Range(1, 0, 0, 0); assert.throws(() => { (range as any).start = null; }); assert.throws(() => { (range as any).start = new types.Position(0, 3); }); }); test('Range, toJSON', function () { - let range = new types.Range(1, 2, 3, 4); + const range = new types.Range(1, 2, 3, 4); assertToJSON(range, [{ line: 1, character: 2 }, { line: 3, character: 4 }]); }); @@ -240,7 +240,7 @@ suite('ExtHostTypes', function () { }); test('Range, contains', function () { - let range = new types.Range(1, 1, 2, 11); + const range = new types.Range(1, 1, 2, 11); assert.ok(range.contains(range.start)); assert.ok(range.contains(range.end)); @@ -253,11 +253,11 @@ suite('ExtHostTypes', function () { }); test('Range, contains (no instanceof)', function () { - let range = new types.Range(1, 1, 2, 11); + const range = new types.Range(1, 1, 2, 11); - let startLike = { line: range.start.line, character: range.start.character }; - let endLike = { line: range.end.line, character: range.end.character }; - let rangeLike = { start: startLike, end: endLike }; + const startLike = { line: range.start.line, character: range.start.character }; + const endLike = { line: range.end.line, character: range.end.character }; + const rangeLike = { start: startLike, end: endLike }; assert.ok(range.contains((<types.Position>startLike))); assert.ok(range.contains((<types.Position>endLike))); @@ -265,7 +265,7 @@ suite('ExtHostTypes', function () { }); test('Range, intersection', function () { - let range = new types.Range(1, 1, 2, 11); + const range = new types.Range(1, 1, 2, 11); let res: types.Range; res = range.intersection(range)!; @@ -312,7 +312,7 @@ suite('ExtHostTypes', function () { }); test('Range, with', function () { - let range = new types.Range(1, 1, 2, 11); + const range = new types.Range(1, 1, 2, 11); assert.ok(range.with(range.start) === range); assert.ok(range.with(undefined, range.end) === range); @@ -349,7 +349,7 @@ suite('ExtHostTypes', function () { test('TextEdit', () => { - let range = new types.Range(1, 1, 2, 11); + const range = new types.Range(1, 1, 2, 11); let edit = new types.TextEdit(range, undefined!); assert.strictEqual(edit.newText, ''); assertToJSON(edit, { range: [{ line: 1, character: 1 }, { line: 2, character: 11 }], newText: '' }); @@ -363,10 +363,10 @@ suite('ExtHostTypes', function () { test('WorkspaceEdit', () => { - let a = URI.file('a.ts'); - let b = URI.file('b.ts'); + const a = URI.file('a.ts'); + const b = URI.file('b.ts'); - let edit = new types.WorkspaceEdit(); + const edit = new types.WorkspaceEdit(); assert.ok(!edit.has(a)); edit.set(a, [types.TextEdit.insert(new types.Position(0, 0), 'fff')]); @@ -418,13 +418,13 @@ suite('ExtHostTypes', function () { }); test('WorkspaceEdit - two edits for one resource', function () { - let edit = new types.WorkspaceEdit(); - let uri = URI.parse('foo:bar'); + const edit = new types.WorkspaceEdit(); + const uri = URI.parse('foo:bar'); edit.insert(uri, new types.Position(0, 0), 'Hello'); edit.insert(uri, new types.Position(0, 0), 'Foo'); assert.strictEqual(edit._allEntries().length, 2); - let [first, second] = edit._allEntries(); + const [first, second] = edit._allEntries(); assertType(first._type === types.FileEditType.Text); assertType(second._type === types.FileEditType.Text); @@ -444,7 +444,7 @@ suite('ExtHostTypes', function () { assertToJSON(new types.Location(URI.file('u.ts'), new types.Position(3, 4)), { uri: URI.parse('file:///u.ts').toJSON(), range: [{ line: 3, character: 4 }, { line: 3, character: 4 }] }); assertToJSON(new types.Location(URI.file('u.ts'), new types.Range(1, 2, 3, 4)), { uri: URI.parse('file:///u.ts').toJSON(), range: [{ line: 1, character: 2 }, { line: 3, character: 4 }] }); - let diag = new types.Diagnostic(new types.Range(0, 1, 2, 3), 'hello'); + const diag = new types.Diagnostic(new types.Range(0, 1, 2, 3), 'hello'); assertToJSON(diag, { severity: 'Error', message: 'hello', range: [{ line: 0, character: 1 }, { line: 2, character: 3 }] }); diag.source = 'me'; assertToJSON(diag, { severity: 'Error', message: 'hello', range: [{ line: 0, character: 1 }, { line: 2, character: 3 }], source: 'me' }); @@ -468,7 +468,7 @@ suite('ExtHostTypes', function () { assertToJSON(new types.CompletionItem('complete'), { label: 'complete' }); - let item = new types.CompletionItem('complete'); + const item = new types.CompletionItem('complete'); item.kind = types.CompletionItemKind.Interface; assertToJSON(item, { label: 'complete', kind: 'Interface' }); @@ -476,7 +476,7 @@ suite('ExtHostTypes', function () { test('SymbolInformation, old ctor', function () { - let info = new types.SymbolInformation('foo', types.SymbolKind.Array, new types.Range(1, 1, 2, 3)); + const info = new types.SymbolInformation('foo', types.SymbolKind.Array, new types.Range(1, 1, 2, 3)); assert.ok(info.location instanceof types.Location); assert.strictEqual(info.location.uri, undefined); }); diff --git a/src/vs/workbench/api/test/browser/extHostWorkspace.test.ts b/src/vs/workbench/api/test/browser/extHostWorkspace.test.ts index c5fa028c83c..92f0b4f7246 100644 --- a/src/vs/workbench/api/test/browser/extHostWorkspace.test.ts +++ b/src/vs/workbench/api/test/browser/extHostWorkspace.test.ts @@ -179,7 +179,7 @@ suite('ExtHostWorkspace', function () { }); test('Multiroot change event should have a delta, #29641', function (done) { - let ws = createExtHostWorkspace(new TestRPCProtocol(), { id: 'foo', name: 'Test', folders: [] }, new NullLogService()); + const ws = createExtHostWorkspace(new TestRPCProtocol(), { id: 'foo', name: 'Test', folders: [] }, new NullLogService()); let finished = false; const finish = (error?: any) => { @@ -242,9 +242,9 @@ suite('ExtHostWorkspace', function () { }); test('Multiroot change keeps existing workspaces live', function () { - let ws = createExtHostWorkspace(new TestRPCProtocol(), { id: 'foo', name: 'Test', folders: [aWorkspaceFolderData(URI.parse('foo:bar'), 0)] }, new NullLogService()); + const ws = createExtHostWorkspace(new TestRPCProtocol(), { id: 'foo', name: 'Test', folders: [aWorkspaceFolderData(URI.parse('foo:bar'), 0)] }, new NullLogService()); - let firstFolder = ws.getWorkspaceFolders()![0]; + const firstFolder = ws.getWorkspaceFolders()![0]; ws.$acceptWorkspaceData({ id: 'foo', name: 'Test', folders: [aWorkspaceFolderData(URI.parse('foo:bar2'), 0), aWorkspaceFolderData(URI.parse('foo:bar'), 1, 'renamed')] }); assert.strictEqual(ws.getWorkspaceFolders()![1], firstFolder); @@ -528,8 +528,8 @@ suite('ExtHostWorkspace', function () { } }; - let ws = createExtHostWorkspace(new TestRPCProtocol(), { id: 'foo', name: 'Test', folders: [] }, new NullLogService()); - let sub = ws.onDidChangeWorkspace(e => { + const ws = createExtHostWorkspace(new TestRPCProtocol(), { id: 'foo', name: 'Test', folders: [] }, new NullLogService()); + const sub = ws.onDidChangeWorkspace(e => { try { assert.throws(() => { (<any>e).added = []; @@ -549,7 +549,7 @@ suite('ExtHostWorkspace', function () { test('`vscode.workspace.getWorkspaceFolder(file)` don\'t return workspace folder when file open from command line. #36221', function () { if (isWindows) { - let ws = createExtHostWorkspace(new TestRPCProtocol(), { + const ws = createExtHostWorkspace(new TestRPCProtocol(), { id: 'foo', name: 'Test', folders: [ aWorkspaceFolderData(URI.file('c:/Users/marek/Desktop/vsc_test/'), 0) ] diff --git a/src/vs/workbench/api/test/browser/mainThreadConfiguration.test.ts b/src/vs/workbench/api/test/browser/mainThreadConfiguration.test.ts index c367473c822..46462e2f5b0 100644 --- a/src/vs/workbench/api/test/browser/mainThreadConfiguration.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadConfiguration.test.ts @@ -18,7 +18,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' suite('MainThreadConfiguration', function () { - let proxy = { + const proxy = { $initializeConfiguration: () => { } }; let instantiationService: TestInstantiationService; diff --git a/src/vs/workbench/api/test/browser/mainThreadDiagnostics.test.ts b/src/vs/workbench/api/test/browser/mainThreadDiagnostics.test.ts index de58c41de23..4e531a7aeb0 100644 --- a/src/vs/workbench/api/test/browser/mainThreadDiagnostics.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadDiagnostics.test.ts @@ -26,7 +26,7 @@ suite('MainThreadDiagnostics', function () { test('clear markers on dispose', function () { - let diag = new MainThreadDiagnostics( + const diag = new MainThreadDiagnostics( new class implements IExtHostContext { remoteAuthority = ''; extensionHostKind = ExtensionHostKind.LocalProcess; @@ -68,7 +68,7 @@ suite('MainThreadDiagnostics', function () { const changedData: [UriComponents, IMarkerData[]][][] = []; - let diag = new MainThreadDiagnostics( + const diag = new MainThreadDiagnostics( new class implements IExtHostContext { remoteAuthority = ''; extensionHostKind = ExtensionHostKind.LocalProcess; @@ -132,7 +132,7 @@ suite('MainThreadDiagnostics', function () { const changedData: [UriComponents, IMarkerData[]][][] = []; - let diag = new MainThreadDiagnostics( + const diag = new MainThreadDiagnostics( new class implements IExtHostContext { remoteAuthority = ''; extensionHostKind = ExtensionHostKind.LocalProcess; diff --git a/src/vs/workbench/api/test/browser/mainThreadDocumentContentProviders.test.ts b/src/vs/workbench/api/test/browser/mainThreadDocumentContentProviders.test.ts index bf3ddf7797a..a1619c52ec4 100644 --- a/src/vs/workbench/api/test/browser/mainThreadDocumentContentProviders.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadDocumentContentProviders.test.ts @@ -17,10 +17,10 @@ suite('MainThreadDocumentContentProviders', function () { test('events are processed properly', function () { - let uri = URI.parse('test:uri'); - let model = createTextModel('1', undefined, undefined, uri); + const uri = URI.parse('test:uri'); + const model = createTextModel('1', undefined, undefined, uri); - let providers = new MainThreadDocumentContentProviders(new TestRPCProtocol(), null!, null!, + const providers = new MainThreadDocumentContentProviders(new TestRPCProtocol(), null!, null!, new class extends mock<IModelService>() { override getModel(_uri: URI) { assert.strictEqual(uri.toString(), _uri.toString()); diff --git a/src/vs/workbench/api/test/browser/mainThreadDocuments.test.ts b/src/vs/workbench/api/test/browser/mainThreadDocuments.test.ts index 6eb53b71970..69554639bd2 100644 --- a/src/vs/workbench/api/test/browser/mainThreadDocuments.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadDocuments.test.ts @@ -40,7 +40,7 @@ suite('BoundModelReferenceCollection', function () { test('max size', function () { - let disposed: number[] = []; + const disposed: number[] = []; col.add( URI.parse('test://farboo'), @@ -76,7 +76,7 @@ suite('BoundModelReferenceCollection', function () { col.dispose(); col = new BoundModelReferenceCollection(extUri, 10000, 10000, 2); - let disposed: number[] = []; + const disposed: number[] = []; col.add( URI.parse('test://xxxxxxx'), diff --git a/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts b/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts index 3ecd7f8a546..8e0aa30b356 100644 --- a/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts @@ -43,7 +43,7 @@ suite('MainThreadDocumentsAndEditors', () => { let modelService: ModelService; let codeEditorService: TestCodeEditorService; let textFileService: ITextFileService; - let deltas: IDocumentsAndEditorsDelta[] = []; + const deltas: IDocumentsAndEditorsDelta[] = []; function myCreateTestCodeEditor(model: ITextModel | undefined): ITestCodeEditor { return createTestCodeEditor(model, { diff --git a/src/vs/workbench/api/test/browser/mainThreadEditors.test.ts b/src/vs/workbench/api/test/browser/mainThreadEditors.test.ts index 6e509185ca3..dac02ac748e 100644 --- a/src/vs/workbench/api/test/browser/mainThreadEditors.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadEditors.test.ts @@ -194,9 +194,9 @@ suite('MainThreadEditors', () => { test(`applyWorkspaceEdit returns false if model is changed by user`, () => { - let model = modelService.createModel('something', null, resource); + const model = modelService.createModel('something', null, resource); - let workspaceResourceEdit: IWorkspaceTextEditDto = { + const workspaceResourceEdit: IWorkspaceTextEditDto = { _type: WorkspaceEditType.Text, resource: resource, modelVersionId: model.getVersionId(), @@ -216,9 +216,9 @@ suite('MainThreadEditors', () => { test(`issue #54773: applyWorkspaceEdit checks model version in race situation`, () => { - let model = modelService.createModel('something', null, resource); + const model = modelService.createModel('something', null, resource); - let workspaceResourceEdit1: IWorkspaceTextEditDto = { + const workspaceResourceEdit1: IWorkspaceTextEditDto = { _type: WorkspaceEditType.Text, resource: resource, modelVersionId: model.getVersionId(), @@ -227,7 +227,7 @@ suite('MainThreadEditors', () => { range: new Range(1, 1, 1, 1) } }; - let workspaceResourceEdit2: IWorkspaceTextEditDto = { + const workspaceResourceEdit2: IWorkspaceTextEditDto = { _type: WorkspaceEditType.Text, resource: resource, modelVersionId: model.getVersionId(), @@ -237,11 +237,11 @@ suite('MainThreadEditors', () => { } }; - let p1 = bulkEdits.$tryApplyWorkspaceEdit({ edits: [workspaceResourceEdit1] }).then((result) => { + const p1 = bulkEdits.$tryApplyWorkspaceEdit({ edits: [workspaceResourceEdit1] }).then((result) => { // first edit request succeeds assert.strictEqual(result, true); }); - let p2 = bulkEdits.$tryApplyWorkspaceEdit({ edits: [workspaceResourceEdit2] }).then((result) => { + const p2 = bulkEdits.$tryApplyWorkspaceEdit({ edits: [workspaceResourceEdit2] }).then((result) => { // second edit request fails assert.strictEqual(result, false); }); diff --git a/src/vs/workbench/api/test/common/testRPCProtocol.ts b/src/vs/workbench/api/test/common/testRPCProtocol.ts index 2947d378288..4ba0e9f51e2 100644 --- a/src/vs/workbench/api/test/common/testRPCProtocol.ts +++ b/src/vs/workbench/api/test/common/testRPCProtocol.ts @@ -57,9 +57,7 @@ export class TestRPCProtocol implements IExtHostContext, IExtHostRpcService { private set _callCount(value: number) { this._callCountValue = value; if (this._callCountValue === 0) { - if (this._completeIdle) { - this._completeIdle(); - } + this._completeIdle?.(); this._idle = undefined; } } @@ -88,7 +86,7 @@ export class TestRPCProtocol implements IExtHostContext, IExtHostRpcService { } private _createProxy<T>(proxyId: string): T { - let handler = { + const handler = { get: (target: any, name: PropertyKey) => { if (typeof name === 'string' && !target[name] && name.charCodeAt(0) === CharCode.DollarSign) { target[name] = (...myArgs: any[]) => { @@ -118,7 +116,7 @@ export class TestRPCProtocol implements IExtHostContext, IExtHostRpcService { const wireArgs = simulateWireTransfer(args); let p: Promise<any>; try { - let result = (<Function>instance[path]).apply(instance, wireArgs); + const result = (<Function>instance[path]).apply(instance, wireArgs); p = isThenable(result) ? result : Promise.resolve(result); } catch (err) { p = Promise.reject(err); diff --git a/src/vs/workbench/api/test/node/extHostSearch.test.ts b/src/vs/workbench/api/test/node/extHostSearch.test.ts index 02bfbb101ee..4a692128824 100644 --- a/src/vs/workbench/api/test/node/extHostSearch.test.ts +++ b/src/vs/workbench/api/test/node/extHostSearch.test.ts @@ -711,9 +711,9 @@ suite('ExtHostSearch', () => { function assertResults(actual: IFileMatch[], expected: vscode.TextSearchResult[]) { const actualTextSearchResults: vscode.TextSearchResult[] = []; - for (let fileMatch of actual) { + for (const fileMatch of actual) { // Make relative - for (let lineResult of fileMatch.results!) { + for (const lineResult of fileMatch.results!) { if (resultIsMatch(lineResult)) { actualTextSearchResults.push({ preview: { diff --git a/src/vs/workbench/api/worker/extHostConsoleForwarder.ts b/src/vs/workbench/api/worker/extHostConsoleForwarder.ts new file mode 100644 index 00000000000..5241c7d90ef --- /dev/null +++ b/src/vs/workbench/api/worker/extHostConsoleForwarder.ts @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AbstractExtHostConsoleForwarder } from 'vs/workbench/api/common/extHostConsoleForwarder'; +import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; +import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; + +export class ExtHostConsoleForwarder extends AbstractExtHostConsoleForwarder { + + constructor( + @IExtHostRpcService extHostRpc: IExtHostRpcService, + @IExtHostInitDataService initData: IExtHostInitDataService, + ) { + super(extHostRpc, initData); + } + + protected override _nativeConsoleLogMessage(method: 'log' | 'info' | 'warn' | 'error', original: (...args: any[]) => void, args: IArguments) { + original.apply(console, args as any); + } +} diff --git a/src/vs/workbench/api/worker/extHostExtensionService.ts b/src/vs/workbench/api/worker/extHostExtensionService.ts index a74fcfbc69b..cc5f3b090fc 100644 --- a/src/vs/workbench/api/worker/extHostExtensionService.ts +++ b/src/vs/workbench/api/worker/extHostExtensionService.ts @@ -11,15 +11,15 @@ import { RequireInterceptor } from 'vs/workbench/api/common/extHostRequireInterc import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ExtensionRuntime } from 'vs/workbench/api/common/extHostTypes'; import { timeout } from 'vs/base/common/async'; -import { MainContext, MainThreadConsoleShape } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostConsoleForwarder } from 'vs/workbench/api/worker/extHostConsoleForwarder'; class WorkerRequireInterceptor extends RequireInterceptor { _installInterceptor() { } getModule(request: string, parent: URI): undefined | any { - for (let alternativeModuleName of this._alternatives) { - let alternative = alternativeModuleName(request); + for (const alternativeModuleName of this._alternatives) { + const alternative = alternativeModuleName(request); if (alternative) { request = alternative; break; @@ -39,8 +39,8 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { private _fakeModules?: WorkerRequireInterceptor; protected async _beforeAlmostReadyToRunExtensions(): Promise<void> { - const mainThreadConsole = this._extHostContext.getProxy(MainContext.MainThreadConsole); - wrapConsoleMethods(mainThreadConsole, this._initData.environment.isExtensionDevelopmentDebug); + // make sure console.log calls make it to the render + this._instaService.createInstance(ExtHostConsoleForwarder); // initialize API and register actors const apiFactory = this._instaService.invokeFunction(createApiFactoryAndRegisterActors); @@ -140,78 +140,3 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { function ensureSuffix(path: string, suffix: string): string { return path.endsWith(suffix) ? path : path + suffix; } - -// copied from bootstrap-fork.js -function wrapConsoleMethods(service: MainThreadConsoleShape, callToNative: boolean) { - wrap('info', 'log'); - wrap('log', 'log'); - wrap('warn', 'warn'); - wrap('error', 'error'); - - function wrap(method: 'error' | 'warn' | 'info' | 'log', severity: 'error' | 'warn' | 'log') { - const original = console[method]; - console[method] = function () { - service.$logExtensionHostMessage({ type: '__$console', severity, arguments: safeToArray(arguments) }); - if (callToNative) { - original.apply(console, arguments as any); - } - }; - } - - const MAX_LENGTH = 100000; - - function safeToArray(args: IArguments) { - const seen: any[] = []; - const argsArray = []; - - // Massage some arguments with special treatment - if (args.length) { - for (let i = 0; i < args.length; i++) { - - // Any argument of type 'undefined' needs to be specially treated because - // JSON.stringify will simply ignore those. We replace them with the string - // 'undefined' which is not 100% right, but good enough to be logged to console - if (typeof args[i] === 'undefined') { - args[i] = 'undefined'; - } - - // Any argument that is an Error will be changed to be just the error stack/message - // itself because currently cannot serialize the error over entirely. - else if (args[i] instanceof Error) { - const errorObj = args[i]; - if (errorObj.stack) { - args[i] = errorObj.stack; - } else { - args[i] = errorObj.toString(); - } - } - - argsArray.push(args[i]); - } - } - - try { - const res = JSON.stringify(argsArray, function (key, value) { - - // Objects get special treatment to prevent circles - if (value && typeof value === 'object') { - if (seen.indexOf(value) !== -1) { - return '[Circular]'; - } - - seen.push(value); - } - - return value; - }); - - if (res.length > MAX_LENGTH) { - return 'Output omitted for a large object that exceeds the limits'; - } - - return res; - } catch (error) { - return `Output omitted for an object that cannot be inspected ('${error.toString()}')`; - } - } -} diff --git a/src/vs/workbench/browser/actions/developerActions.ts b/src/vs/workbench/browser/actions/developerActions.ts index c5a04846dc0..5a0d08a8623 100644 --- a/src/vs/workbench/browser/actions/developerActions.ts +++ b/src/vs/workbench/browser/actions/developerActions.ts @@ -48,9 +48,7 @@ class InspectContextKeysAction extends Action2 { const stylesheet = createStyleSheet(); disposables.add(toDisposable(() => { - if (stylesheet.parentNode) { - stylesheet.parentNode.removeChild(stylesheet); - } + stylesheet.parentNode?.removeChild(stylesheet); })); createCSSRule('*', 'cursor: crosshair !important;', stylesheet); @@ -275,7 +273,7 @@ class LogStorageAction extends Action2 { } run(accessor: ServicesAccessor): void { - accessor.get(IStorageService).logStorage(); + accessor.get(IStorageService).log(); } } diff --git a/src/vs/workbench/browser/actions/layoutActions.ts b/src/vs/workbench/browser/actions/layoutActions.ts index 8ace4020a72..4b4cc8b38f0 100644 --- a/src/vs/workbench/browser/actions/layoutActions.ts +++ b/src/vs/workbench/browser/actions/layoutActions.ts @@ -967,7 +967,7 @@ registerAction2(class extends Action2 { abstract class BaseResizeViewAction extends Action2 { - protected static readonly RESIZE_INCREMENT = 6.5; // This is a media-size percentage + protected static readonly RESIZE_INCREMENT = 60; // This is a css pixel size protected resizePart(widthChange: number, heightChange: number, layoutService: IWorkbenchLayoutService, partToResize?: Parts): void { diff --git a/src/vs/workbench/browser/actions/quickAccessActions.ts b/src/vs/workbench/browser/actions/quickAccessActions.ts index 8c08c8e8057..6cda33c142c 100644 --- a/src/vs/workbench/browser/actions/quickAccessActions.ts +++ b/src/vs/workbench/browser/actions/quickAccessActions.ts @@ -141,7 +141,7 @@ registerAction2(class QuickAccessAction extends Action2 { }, f1: true, menu: { - id: MenuId.TitleMenu, + id: MenuId.CommandCenter, order: 100 } }); diff --git a/src/vs/workbench/browser/actions/windowActions.ts b/src/vs/workbench/browser/actions/windowActions.ts index a0d14adf991..8858f514ec5 100644 --- a/src/vs/workbench/browser/actions/windowActions.ts +++ b/src/vs/workbench/browser/actions/windowActions.ts @@ -141,7 +141,7 @@ abstract class BaseOpenRecentAction extends Action2 { const pick = await quickInputService.pick(picks, { contextKey: inRecentFilesPickerContextKey, activeItem: [...workspacePicks, ...filePicks][autoFocusSecondEntry ? 1 : 0], - placeHolder: isMacintosh ? localize('openRecentPlaceholderMac', "Select to open (hold Cmd-key to force new window or Alt-key for same window)") : localize('openRecentPlaceholder', "Select to open (hold Ctrl-key to force new window or Alt-key for same window)"), + placeHolder: isMacintosh ? localize('openRecentPlaceholderMac', "Select to open (hold Cmd-key to force new window or Option-key for same window)") : localize('openRecentPlaceholder', "Select to open (hold Ctrl-key to force new window or Alt-key for same window)"), matchOnDescription: true, onKeyMods: mods => keyMods = mods, quickNavigate: this.isQuickNavigate() ? { keybindings: keybindingService.lookupKeybindings(this.desc.id) } : undefined, @@ -270,7 +270,7 @@ class QuickPickRecentAction extends BaseOpenRecentAction { id: 'workbench.action.quickOpenRecent', title: { value: localize('quickOpenRecent', "Quick Open Recent..."), original: 'Quick Open Recent...' }, category: fileCategory, - f1: true + f1: false // hide quick pickers from command palette to not confuse with the other entry that shows a input field }); } diff --git a/src/vs/workbench/browser/codeeditor.ts b/src/vs/workbench/browser/codeeditor.ts index 0b23958dfed..0acd7e04b7b 100644 --- a/src/vs/workbench/browser/codeeditor.ts +++ b/src/vs/workbench/browser/codeeditor.ts @@ -152,7 +152,7 @@ export class FloatingClickWidget extends Widget implements IOverlayWidget { super(); this._domNode = $('.floating-click-widget'); - this._domNode.style.padding = '10px'; + this._domNode.style.padding = '6px 11px'; this._domNode.style.cursor = 'pointer'; if (keyBindingAction) { diff --git a/src/vs/workbench/browser/composite.ts b/src/vs/workbench/browser/composite.ts index d5e691faaf9..a619fe25a1f 100644 --- a/src/vs/workbench/browser/composite.ts +++ b/src/vs/workbench/browser/composite.ts @@ -134,7 +134,7 @@ export abstract class Composite extends Component implements IComposite { * The composite will be on-DOM if visible is set to true and off-DOM otherwise. * * Typically this operation should be fast though because setVisible might be called many times during a session. - * If there is a long running opertaion it is fine to have it running in the background asyncly and return before. + * If there is a long running operation it is fine to have it running in the background asyncly and return before. */ setVisible(visible: boolean): void { if (this.visible !== !!visible) { @@ -238,7 +238,7 @@ export abstract class Composite extends Component implements IComposite { } /** - * A composite descriptor is a leightweight descriptor of a composite in the workbench. + * A composite descriptor is a lightweight descriptor of a composite in the workbench. */ export abstract class CompositeDescriptor<T extends Composite> { diff --git a/src/vs/workbench/browser/contextkeys.ts b/src/vs/workbench/browser/contextkeys.ts index df9b2c329d5..3d287e57781 100644 --- a/src/vs/workbench/browser/contextkeys.ts +++ b/src/vs/workbench/browser/contextkeys.ts @@ -6,7 +6,7 @@ import { Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { InputFocusedContext, IsMacContext, IsLinuxContext, IsWindowsContext, IsWebContext, IsMacNativeContext, IsDevelopmentContext, IsIOSContext } from 'vs/platform/contextkey/common/contextkeys'; +import { InputFocusedContext, IsMacContext, IsLinuxContext, IsWindowsContext, IsWebContext, IsMacNativeContext, IsDevelopmentContext, IsIOSContext, ProductQualityContext } from 'vs/platform/contextkey/common/contextkeys'; import { SplitEditorsVertically, InEditorZenModeContext, ActiveEditorCanRevertContext, ActiveEditorGroupLockedContext, ActiveEditorCanSplitInGroupContext, SideBySideEditorActiveContext, AuxiliaryBarVisibleContext, SideBarVisibleContext, PanelAlignmentContext, PanelMaximizedContext, PanelVisibleContext, ActiveEditorContext, EditorsVisibleContext, TextCompareEditorVisibleContext, TextCompareEditorActiveContext, ActiveEditorGroupEmptyContext, MultipleEditorGroupsContext, EditorTabsVisibleContext, IsCenteredLayoutContext, ActiveEditorGroupIndexContext, ActiveEditorGroupLastContext, ActiveEditorReadonlyContext, EditorAreaVisibleContext, ActiveEditorAvailableEditorIdsContext, DirtyWorkingCopiesContext, EmptyWorkspaceSupportContext, EnterMultiRootWorkspaceSupportContext, HasWebFileSystemAccess, IsFullscreenContext, OpenFolderWorkspaceSupportContext, RemoteNameContext, VirtualWorkspaceContext, WorkbenchStateContext, WorkspaceFolderCountContext, PanelPositionContext } from 'vs/workbench/common/contextkeys'; import { TEXT_DIFF_EDITOR_ID, EditorInputCapabilities, SIDE_BY_SIDE_EDITOR_ID, DEFAULT_EDITOR_ASSOCIATION } from 'vs/workbench/common/editor'; import { trackFocus, addDisposableListener, EventType } from 'vs/base/browser/dom'; @@ -24,6 +24,7 @@ import { IEditorResolverService } from 'vs/workbench/services/editor/common/edit import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; import { Schemas } from 'vs/base/common/network'; import { WebFileSystemAccess } from 'vs/platform/files/browser/webFileSystemAccess'; +import { IProductService } from 'vs/platform/product/common/productService'; export class WorkbenchContextKeysHandler extends Disposable { private inputFocusedContext: IContextKey<boolean>; @@ -76,6 +77,7 @@ export class WorkbenchContextKeysHandler extends Disposable { @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IConfigurationService private readonly configurationService: IConfigurationService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IProductService private readonly productService: IProductService, @IEditorService private readonly editorService: IEditorService, @IEditorResolverService private readonly editorResolverService: IEditorResolverService, @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, @@ -105,6 +107,9 @@ export class WorkbenchContextKeysHandler extends Disposable { // Development IsDevelopmentContext.bindTo(this.contextKeyService).set(!this.environmentService.isBuilt || this.environmentService.isExtensionDevelopment); + // Product Quality + ProductQualityContext.bindTo(this.contextKeyService).set(this.productService.quality || ''); + // Editors this.activeEditorContext = ActiveEditorContext.bindTo(this.contextKeyService); this.activeEditorIsReadonly = ActiveEditorReadonlyContext.bindTo(this.contextKeyService); diff --git a/src/vs/workbench/browser/dnd.ts b/src/vs/workbench/browser/dnd.ts index 63cfe74d6de..8bc290a1655 100644 --- a/src/vs/workbench/browser/dnd.ts +++ b/src/vs/workbench/browser/dnd.ts @@ -10,7 +10,7 @@ import { IListDragAndDrop } from 'vs/base/browser/ui/list/list'; import { ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView'; import { ITreeDragOverReaction } from 'vs/base/browser/ui/tree/tree'; import { coalesce } from 'vs/base/common/arrays'; -import { IDataTransfer } from 'vs/base/common/dataTransfer'; +import { VSDataTransfer } from 'vs/base/common/dataTransfer'; import { Emitter } from 'vs/base/common/event'; import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { stringify } from 'vs/base/common/marshalling'; @@ -19,7 +19,8 @@ import { FileAccess, Schemas } from 'vs/base/common/network'; import { isWindows } from 'vs/base/common/platform'; import { basename, isEqual } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; -import { CodeDataTransfers, createDraggedEditorInputFromRawResourcesData, Extensions, extractEditorsDropData, IDragAndDropContributionRegistry, IDraggedResourceEditorInput, IResourceStat } from 'vs/platform/dnd/browser/dnd'; +import { UriList } from 'vs/editor/browser/dnd'; +import { CodeDataTransfers, createDraggedEditorInputFromRawResourcesData, Extensions, extractEditorsAndFilesDropData, IDragAndDropContributionRegistry, IDraggedResourceEditorInput, IResourceStat } from 'vs/platform/dnd/browser/dnd'; import { IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ILabelService } from 'vs/platform/label/common/label'; @@ -52,7 +53,7 @@ export class DraggedTreeItemsIdentifier { constructor(readonly identifier: string) { } } -export async function extractTreeDropData(dataTransfer: IDataTransfer): Promise<Array<IDraggedResourceEditorInput>> { +export async function extractTreeDropData(dataTransfer: VSDataTransfer): Promise<Array<IDraggedResourceEditorInput>> { const editors: IDraggedResourceEditorInput[] = []; const resourcesKey = Mimes.uriList.toLowerCase(); @@ -60,7 +61,7 @@ export async function extractTreeDropData(dataTransfer: IDataTransfer): Promise< if (dataTransfer.has(resourcesKey)) { try { const asString = await dataTransfer.get(resourcesKey)?.asString(); - const rawResourcesData = JSON.stringify(asString?.split('\\n').filter(value => !value.startsWith('#'))); + const rawResourcesData = JSON.stringify(UriList.parse(asString ?? '')); editors.push(...createDraggedEditorInputFromRawResourcesData(rawResourcesData)); } catch (error) { // Invalid transfer @@ -70,11 +71,6 @@ export async function extractTreeDropData(dataTransfer: IDataTransfer): Promise< return editors; } -export function convertResourceUrlsToUriList(resourceUrls: string): string { - const asJson: URI[] = JSON.parse(resourceUrls); - return asJson.map(uri => uri.toString()).join('\n'); -} - export interface IResourcesDropHandlerOptions { /** @@ -105,7 +101,7 @@ export class ResourcesDropHandler { } async handleDrop(event: DragEvent, resolveTargetGroup: () => IEditorGroup | undefined, afterDrop: (targetGroup: IEditorGroup | undefined) => void, targetIndex?: number): Promise<void> { - const editors = await this.instantiationService.invokeFunction(accessor => extractEditorsDropData(accessor, event)); + const editors = await this.instantiationService.invokeFunction(accessor => extractEditorsAndFilesDropData(accessor, event)); if (!editors.length) { return; } @@ -193,7 +189,7 @@ export class ResourcesDropHandler { await this.workspaceEditingService.addFolders(folderURIs); } - // Finaly, enter untitled workspace when dropping >1 folders + // Finally, enter untitled workspace when dropping >1 folders else { await this.workspaceEditingService.createAndEnterWorkspace(folderURIs); } @@ -609,9 +605,7 @@ export class CompositeDragAndDropObserver extends Disposable { return; } - if (callbacks.onDragLeave) { - callbacks.onDragLeave({ eventData: e, dragAndDropData: data! }); - } + callbacks.onDragLeave?.({ eventData: e, dragAndDropData: data! }); }, onDrop: e => { if (callbacks.onDrop) { diff --git a/src/vs/workbench/browser/labels.ts b/src/vs/workbench/browser/labels.ts index 4cfe9f32bb5..3e47d3df06a 100644 --- a/src/vs/workbench/browser/labels.ts +++ b/src/vs/workbench/browser/labels.ts @@ -421,7 +421,7 @@ class ResourceLabelWidget extends IconLabel { } if (typeof label.description === 'string') { - let untitledDescription = untitledModel.resource.path; + const untitledDescription = untitledModel.resource.path; if (label.name !== untitledDescription) { label.description = untitledDescription; } else { @@ -429,7 +429,7 @@ class ResourceLabelWidget extends IconLabel { } } - let untitledTitle = untitledModel.resource.path; + const untitledTitle = untitledModel.resource.path; if (untitledModel.name !== untitledTitle) { options.title = `${untitledModel.name} • ${untitledTitle}`; } else { diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index acb2fc49198..240b52cc2b9 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -5,7 +5,7 @@ import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; -import { EventType, addDisposableListener, getClientArea, Dimension, position, size, IDimension, isAncestorUsingFlowTo } from 'vs/base/browser/dom'; +import { EventType, addDisposableListener, getClientArea, Dimension, position, size, IDimension, isAncestorUsingFlowTo, computeScreenAwareSize } from 'vs/base/browser/dom'; import { onDidChangeFullscreen, isFullscreen } from 'vs/base/browser/browser'; import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { isWindows, isLinux, isMacintosh, isWeb, isNative, isIOS } from 'vs/base/common/platform'; @@ -33,7 +33,7 @@ import { IFileService } from 'vs/platform/files/common/files'; import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { coalesce } from 'vs/base/common/arrays'; import { assertIsDefined, isNumber } from 'vs/base/common/types'; -import { INotificationService, NotificationsFilter } from 'vs/platform/notification/common/notification'; +import { INotificationService } from 'vs/platform/notification/common/notification'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { WINDOW_ACTIVE_BORDER, WINDOW_INACTIVE_BORDER } from 'vs/workbench/common/theme'; import { LineNumbersType } from 'vs/editor/common/config/editorOptions'; @@ -148,7 +148,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi let quickPickTop = 0; if (this.isVisible(Parts.TITLEBAR_PART)) { top = this.getPart(Parts.TITLEBAR_PART).maximumHeight; - quickPickTop = this.titleService.titleMenuVisible ? 0 : top; + quickPickTop = this.titleService.isCommandCenterVisible ? 0 : top; } return { top, quickPickTop }; } @@ -269,7 +269,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } // Title Menu changes - this._register(this.titleService.onDidChangeTitleMenuVisibility(() => this._onDidLayout.fire(this._dimension))); + this._register(this.titleService.onDidChangeCommandCenterVisibility(() => this.layout())); // Theme changes this._register(this.themeService.onDidColorThemeChange(() => this.updateStyles())); @@ -504,7 +504,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // Panel View Container To Restore if (this.isVisible(Parts.PANEL_PART)) { - let viewContainerToRestore = this.storageService.get(PanelPart.activePanelSettingsKey, StorageScope.WORKSPACE, this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.Panel)?.id); + const viewContainerToRestore = this.storageService.get(PanelPart.activePanelSettingsKey, StorageScope.WORKSPACE, this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.Panel)?.id); if (viewContainerToRestore) { this.windowState.initialization.views.containerToRestore.panel = viewContainerToRestore; @@ -515,7 +515,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // Auxiliary Panel to restore if (this.isVisible(Parts.AUXILIARYBAR_PART)) { - let viewContainerToRestore = this.storageService.get(AuxiliaryBarPart.activePanelSettingsKey, StorageScope.WORKSPACE, this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.AuxiliaryBar)?.id); + const viewContainerToRestore = this.storageService.get(AuxiliaryBarPart.activePanelSettingsKey, StorageScope.WORKSPACE, this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.AuxiliaryBar)?.id); if (viewContainerToRestore) { this.windowState.initialization.views.containerToRestore.auxiliaryBar = viewContainerToRestore; @@ -993,6 +993,11 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi return true; } + // with the command center enabled, we should always show + if (this.configurationService.getValue<boolean>('window.commandCenter')) { + return true; + } + // remaining behavior is based on menubar visibility switch (getMenuBarVisibility(this.configurationService)) { case 'classic': @@ -1082,6 +1087,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi if (!restoring) { zenModeExitInfo.transitionedToFullScreen = toggleFullScreen; zenModeExitInfo.transitionedToCenteredEditorLayout = !this.isEditorLayoutCentered() && config.centerLayout; + zenModeExitInfo.handleNotificationsDoNotDisturbMode = !this.notificationService.doNotDisturbMode; zenModeExitInfo.wasVisible.sideBar = this.isVisible(Parts.SIDEBAR_PART); zenModeExitInfo.wasVisible.panel = this.isVisible(Parts.PANEL_PART); zenModeExitInfo.wasVisible.auxiliaryBar = this.isVisible(Parts.AUXILIARYBAR_PART); @@ -1109,13 +1115,15 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.windowState.runtime.zenMode.transitionDisposables.add(this.editorGroupService.enforcePartOptions({ showTabs: false })); } - if (config.silentNotifications) { - this.notificationService.setFilter(NotificationsFilter.ERROR); + if (config.silentNotifications && zenModeExitInfo.handleNotificationsDoNotDisturbMode) { + this.notificationService.doNotDisturbMode = true; } this.windowState.runtime.zenMode.transitionDisposables.add(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(WorkbenchLayoutSettings.ZEN_MODE_SILENT_NOTIFICATIONS)) { - const filter = this.configurationService.getValue(WorkbenchLayoutSettings.ZEN_MODE_SILENT_NOTIFICATIONS) ? NotificationsFilter.ERROR : NotificationsFilter.OFF; - this.notificationService.setFilter(filter); + const zenModeSilentNotifications = !!this.configurationService.getValue(WorkbenchLayoutSettings.ZEN_MODE_SILENT_NOTIFICATIONS); + if (zenModeExitInfo.handleNotificationsDoNotDisturbMode) { + this.notificationService.doNotDisturbMode = zenModeSilentNotifications; + } } })); @@ -1150,13 +1158,14 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.centerEditorLayout(false, true); } + if (zenModeExitInfo.handleNotificationsDoNotDisturbMode) { + this.notificationService.doNotDisturbMode = false; + } + setLineNumbers(); this.focus(); - // Clear notifications filter - this.notificationService.setFilter(NotificationsFilter.OFF); - toggleFullScreen = zenModeExitInfo.transitionedToFullScreen && this.windowState.runtime.fullscreen; } @@ -1241,6 +1250,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.setEditorHidden(!visible, true); } this._onDidChangePartVisibility.fire(); + this._onDidLayout.fire(this._dimension); })); } @@ -1328,8 +1338,8 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } resizePart(part: Parts, sizeChangeWidth: number, sizeChangeHeight: number): void { - const sizeChangePxWidth = this.workbenchGrid.width * sizeChangeWidth / 100; - const sizeChangePxHeight = this.workbenchGrid.height * sizeChangeHeight / 100; + const sizeChangePxWidth = Math.sign(sizeChangeWidth) * computeScreenAwareSize(Math.abs(sizeChangeWidth)); + const sizeChangePxHeight = Math.sign(sizeChangeHeight) * computeScreenAwareSize(Math.abs(sizeChangeHeight)); let viewSize: IViewSize; diff --git a/src/vs/workbench/browser/layoutState.ts b/src/vs/workbench/browser/layoutState.ts index 6c01f1a8965..70a4e637a26 100644 --- a/src/vs/workbench/browser/layoutState.ts +++ b/src/vs/workbench/browser/layoutState.ts @@ -46,6 +46,7 @@ export const LayoutStateKeys = { ZEN_MODE_EXIT_INFO: new RuntimeStateKey('zenMode.exitInfo', StorageScope.WORKSPACE, StorageTarget.USER, { transitionedToCenteredEditorLayout: false, transitionedToFullScreen: false, + handleNotificationsDoNotDisturbMode: false, wasVisible: { auxiliaryBar: false, panel: false, @@ -54,19 +55,19 @@ export const LayoutStateKeys = { }), // Part Sizing - GRID_SIZE: new InitializationStateKey('grid.size', StorageScope.GLOBAL, StorageTarget.MACHINE, { width: 800, height: 600 }), - SIDEBAR_SIZE: new InitializationStateKey<number>('sideBar.size', StorageScope.GLOBAL, StorageTarget.MACHINE, 200), - AUXILIARYBAR_SIZE: new InitializationStateKey<number>('auxiliaryBar.size', StorageScope.GLOBAL, StorageTarget.MACHINE, 200), - PANEL_SIZE: new InitializationStateKey<number>('panel.size', StorageScope.GLOBAL, StorageTarget.MACHINE, 300), + GRID_SIZE: new InitializationStateKey('grid.size', StorageScope.PROFILE, StorageTarget.MACHINE, { width: 800, height: 600 }), + SIDEBAR_SIZE: new InitializationStateKey<number>('sideBar.size', StorageScope.PROFILE, StorageTarget.MACHINE, 200), + AUXILIARYBAR_SIZE: new InitializationStateKey<number>('auxiliaryBar.size', StorageScope.PROFILE, StorageTarget.MACHINE, 200), + PANEL_SIZE: new InitializationStateKey<number>('panel.size', StorageScope.PROFILE, StorageTarget.MACHINE, 300), - PANEL_LAST_NON_MAXIMIZED_HEIGHT: new RuntimeStateKey<number>('panel.lastNonMaximizedHeight', StorageScope.GLOBAL, StorageTarget.MACHINE, 300), - PANEL_LAST_NON_MAXIMIZED_WIDTH: new RuntimeStateKey<number>('panel.lastNonMaximizedWidth', StorageScope.GLOBAL, StorageTarget.MACHINE, 300), + PANEL_LAST_NON_MAXIMIZED_HEIGHT: new RuntimeStateKey<number>('panel.lastNonMaximizedHeight', StorageScope.PROFILE, StorageTarget.MACHINE, 300), + PANEL_LAST_NON_MAXIMIZED_WIDTH: new RuntimeStateKey<number>('panel.lastNonMaximizedWidth', StorageScope.PROFILE, StorageTarget.MACHINE, 300), PANEL_WAS_LAST_MAXIMIZED: new RuntimeStateKey<boolean>('panel.wasLastMaximized', StorageScope.WORKSPACE, StorageTarget.USER, false), // Part Positions SIDEBAR_POSITON: new RuntimeStateKey<Position>('sideBar.position', StorageScope.WORKSPACE, StorageTarget.USER, Position.LEFT), PANEL_POSITION: new RuntimeStateKey<Position>('panel.position', StorageScope.WORKSPACE, StorageTarget.USER, Position.BOTTOM), - PANEL_ALIGNMENT: new RuntimeStateKey<PanelAlignment>('panel.alignment', StorageScope.GLOBAL, StorageTarget.USER, 'center'), + PANEL_ALIGNMENT: new RuntimeStateKey<PanelAlignment>('panel.alignment', StorageScope.PROFILE, StorageTarget.USER, 'center'), // Part Visibility ACTIVITYBAR_HIDDEN: new RuntimeStateKey<boolean>('activityBar.hidden', StorageScope.WORKSPACE, StorageTarget.USER, false, true), @@ -170,7 +171,7 @@ export class LayoutStateModel extends Disposable { let key: keyof typeof LayoutStateKeys; for (key in LayoutStateKeys) { const stateKey = LayoutStateKeys[key] as WorkbenchLayoutStateKey<StorageKeyType>; - if (stateKey instanceof RuntimeStateKey && stateKey.scope === StorageScope.GLOBAL && stateKey.target === StorageTarget.USER) { + if (stateKey instanceof RuntimeStateKey && stateKey.scope === StorageScope.PROFILE && stateKey.target === StorageTarget.USER) { if (`${LayoutStateModel.STORAGE_PREFIX}${stateKey.name}` === storageChangeEvent.key) { const value = this.loadKeyFromStorage(stateKey) ?? stateKey.defaultValue; if (this.stateCache.get(stateKey.name) !== value) { @@ -191,7 +192,7 @@ export class LayoutStateModel extends Disposable { for (key in LayoutStateKeys) { const stateKey = LayoutStateKeys[key] as WorkbenchLayoutStateKey<StorageKeyType>; if ((workspace && stateKey.scope === StorageScope.WORKSPACE) || - (global && stateKey.scope === StorageScope.GLOBAL)) { + (global && stateKey.scope === StorageScope.PROFILE)) { // Don't write out specific keys while in zen mode if (isZenMode && stateKey instanceof RuntimeStateKey && stateKey.zenModeIgnore) { continue; @@ -232,7 +233,7 @@ export class LayoutStateModel extends Disposable { this.stateCache.set(key.name, value); const isZenMode = this.getRuntimeValue(LayoutStateKeys.ZEN_MODE_ACTIVE); - if (key.scope === StorageScope.GLOBAL) { + if (key.scope === StorageScope.PROFILE) { if (!isZenMode || !key.zenModeIgnore) { this.saveKeyToStorage<T>(key); this.updateLegacySettingsFromState(key, value); diff --git a/src/vs/workbench/browser/panecomposite.ts b/src/vs/workbench/browser/panecomposite.ts index 9d642b89346..21230d84325 100644 --- a/src/vs/workbench/browser/panecomposite.ts +++ b/src/vs/workbench/browser/panecomposite.ts @@ -139,7 +139,7 @@ export abstract class PaneComposite extends Composite implements IPaneComposite /** - * A Pane Composite descriptor is a leightweight descriptor of a Pane Composite in the workbench. + * A Pane Composite descriptor is a lightweight descriptor of a Pane Composite in the workbench. */ export class PaneCompositeDescriptor extends CompositeDescriptor<PaneComposite> { diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts index 36647e6dcfe..ed7324042f0 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts @@ -102,6 +102,7 @@ export class ViewContainerActivityAction extends ActivityAction { private logAction(action: string) { type ActivityBarActionClassification = { + owner: 'sbatten'; viewletId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; action: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; }; @@ -141,7 +142,7 @@ class MenuActivityActionViewItem extends ActivityActionViewItem { })); this._register(addDisposableListener(this.container, EventType.KEY_UP, (e: KeyboardEvent) => { - let event = new StandardKeyboardEvent(e); + const event = new StandardKeyboardEvent(e); if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) { EventHelper.stop(e, true); this.showContextMenu(); @@ -295,7 +296,7 @@ export class AccountsActivityActionViewItem extends MenuActivityActionViewItem { const actions = await super.resolveContextMenuActions(disposables); actions.unshift(...[ - toAction({ id: 'hideAccounts', label: localize('hideAccounts', "Hide Accounts"), run: () => this.storageService.store(AccountsActivityActionViewItem.ACCOUNTS_VISIBILITY_PREFERENCE_KEY, false, StorageScope.GLOBAL, StorageTarget.USER) }), + toAction({ id: 'hideAccounts', label: localize('hideAccounts', "Hide Accounts"), run: () => this.storageService.store(AccountsActivityActionViewItem.ACCOUNTS_VISIBILITY_PREFERENCE_KEY, false, StorageScope.PROFILE, StorageTarget.USER) }), new Separator() ]); diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index c94c38b2e47..5ee5d6e78ac 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -43,7 +43,7 @@ import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; import { GestureEvent } from 'vs/base/browser/touch'; import { IPaneCompositePart, IPaneCompositeSelectorPart } from 'vs/workbench/browser/parts/paneCompositePart'; import { Registry } from 'vs/platform/registry/common/platform'; -import { Extensions, IProfileStorageRegistry } from 'vs/workbench/services/profiles/common/profileStorageRegistry'; +import { Extensions, IProfileStorageRegistry } from 'vs/workbench/services/userDataProfile/common/userDataProfileStorageRegistry'; interface IPlaceholderViewContainer { readonly id: string; @@ -250,7 +250,7 @@ export class ActivitybarPart extends Part implements IPaneCompositeSelectorPart this.paneCompositePart.onDidPaneCompositeClose(e => this.onDidChangeViewContainerVisibility(e.getId(), false)); // Extension registration - let disposables = this._register(new DisposableStore()); + const disposables = this._register(new DisposableStore()); this._register(this.extensionService.onDidRegisterExtensions(() => { disposables.clear(); this.onDidRegisterExtensions(); @@ -495,9 +495,7 @@ export class ActivitybarPart extends Part implements IPaneCompositeSelectorPart this.keyboardNavigationDisposables.add(addDisposableListener(this.compositeBarContainer, EventType.KEY_DOWN, e => { const kbEvent = new StandardKeyboardEvent(e); if (kbEvent.equals(KeyCode.DownArrow) || kbEvent.equals(KeyCode.RightArrow)) { - if (this.globalActivityActionBar) { - this.globalActivityActionBar.focus(true); - } + this.globalActivityActionBar?.focus(true); } else if (kbEvent.equals(KeyCode.UpArrow) || kbEvent.equals(KeyCode.LeftArrow)) { if (this.menuBar) { this.menuBar.toggleFocus(); @@ -511,9 +509,7 @@ export class ActivitybarPart extends Part implements IPaneCompositeSelectorPart this.keyboardNavigationDisposables.add(addDisposableListener(this.globalActivitiesContainer, EventType.KEY_DOWN, e => { const kbEvent = new StandardKeyboardEvent(e); if (kbEvent.equals(KeyCode.UpArrow) || kbEvent.equals(KeyCode.LeftArrow)) { - if (this.compositeBar) { - this.compositeBar.focus(this.getVisiblePaneCompositeIds().length - 1); - } + this.compositeBar?.focus(this.getVisiblePaneCompositeIds().length - 1); } })); } @@ -558,6 +554,9 @@ export class ActivitybarPart extends Part implements IPaneCompositeSelectorPart } private toggleAccountsActivity() { + if (!!this.accountsActivityAction === this.accountsVisibilityPreference) { + return; + } if (this.globalActivityActionBar) { if (this.accountsActivityAction) { this.globalActivityActionBar.pull(ActivitybarPart.ACCOUNTS_ACTION_INDEX); @@ -820,7 +819,7 @@ export class ActivitybarPart extends Part implements IPaneCompositeSelectorPart } private onDidStorageValueChange(e: IStorageValueChangeEvent): void { - if (e.key === ActivitybarPart.PINNED_VIEW_CONTAINERS && e.scope === StorageScope.GLOBAL + if (e.key === ActivitybarPart.PINNED_VIEW_CONTAINERS && e.scope === StorageScope.PROFILE && this.pinnedViewContainersValue !== this.getStoredPinnedViewContainersValue() /* This checks if current window changed the value or not */) { this._pinnedViewContainersValue = undefined; this._cachedViewContainers = undefined; @@ -841,14 +840,20 @@ export class ActivitybarPart extends Part implements IPaneCompositeSelectorPart for (let index = 0; index < compositeItems.length; index++) { // Add items currently exists but does not exist in new. if (!newCompositeItems.some(({ id }) => id === compositeItems[index].id)) { - newCompositeItems.splice(index, 0, compositeItems[index]); + const viewContainer = this.viewDescriptorService.getViewContainerById(compositeItems[index].id); + newCompositeItems.splice(index, 0, { + ...compositeItems[index], + pinned: true, + visible: true, + order: viewContainer?.order, + }); } } this.compositeBar.setCompositeBarItems(newCompositeItems); } - if (e.key === AccountsActivityActionViewItem.ACCOUNTS_VISIBILITY_PREFERENCE_KEY && e.scope === StorageScope.GLOBAL) { + if (e.key === AccountsActivityActionViewItem.ACCOUNTS_VISIBILITY_PREFERENCE_KEY && e.scope === StorageScope.PROFILE) { this.toggleAccountsActivity(); } } @@ -945,11 +950,11 @@ export class ActivitybarPart extends Part implements IPaneCompositeSelectorPart } private getStoredPinnedViewContainersValue(): string { - return this.storageService.get(ActivitybarPart.PINNED_VIEW_CONTAINERS, StorageScope.GLOBAL, '[]'); + return this.storageService.get(ActivitybarPart.PINNED_VIEW_CONTAINERS, StorageScope.PROFILE, '[]'); } private setStoredPinnedViewContainersValue(value: string): void { - this.storageService.store(ActivitybarPart.PINNED_VIEW_CONTAINERS, value, StorageScope.GLOBAL, StorageTarget.USER); + this.storageService.store(ActivitybarPart.PINNED_VIEW_CONTAINERS, value, StorageScope.PROFILE, StorageTarget.USER); } private getPlaceholderViewContainers(): IPlaceholderViewContainer[] { @@ -977,19 +982,19 @@ export class ActivitybarPart extends Part implements IPaneCompositeSelectorPart } private getStoredPlaceholderViewContainersValue(): string { - return this.storageService.get(ActivitybarPart.PLACEHOLDER_VIEW_CONTAINERS, StorageScope.GLOBAL, '[]'); + return this.storageService.get(ActivitybarPart.PLACEHOLDER_VIEW_CONTAINERS, StorageScope.PROFILE, '[]'); } private setStoredPlaceholderViewContainersValue(value: string): void { - this.storageService.store(ActivitybarPart.PLACEHOLDER_VIEW_CONTAINERS, value, StorageScope.GLOBAL, StorageTarget.MACHINE); + this.storageService.store(ActivitybarPart.PLACEHOLDER_VIEW_CONTAINERS, value, StorageScope.PROFILE, StorageTarget.MACHINE); } private get accountsVisibilityPreference(): boolean { - return this.storageService.getBoolean(AccountsActivityActionViewItem.ACCOUNTS_VISIBILITY_PREFERENCE_KEY, StorageScope.GLOBAL, true); + return this.storageService.getBoolean(AccountsActivityActionViewItem.ACCOUNTS_VISIBILITY_PREFERENCE_KEY, StorageScope.PROFILE, true); } private set accountsVisibilityPreference(value: boolean) { - this.storageService.store(AccountsActivityActionViewItem.ACCOUNTS_VISIBILITY_PREFERENCE_KEY, value, StorageScope.GLOBAL, StorageTarget.USER); + this.storageService.store(AccountsActivityActionViewItem.ACCOUNTS_VISIBILITY_PREFERENCE_KEY, value, StorageScope.PROFILE, StorageTarget.USER); } toJSON(): object { diff --git a/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarActions.ts b/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarActions.ts index 65fa0d523c4..2b0b723d0e3 100644 --- a/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarActions.ts +++ b/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarActions.ts @@ -62,7 +62,7 @@ class FocusAuxiliaryBarAction extends Action { } // Focus into active composite - let composite = this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.AuxiliaryBar); + const composite = this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.AuxiliaryBar); if (composite) { composite.focus(); } diff --git a/src/vs/workbench/browser/parts/compositeBar.ts b/src/vs/workbench/browser/parts/compositeBar.ts index de6e99067ef..d15c1f36d96 100644 --- a/src/vs/workbench/browser/parts/compositeBar.ts +++ b/src/vs/workbench/browser/parts/compositeBar.ts @@ -79,9 +79,7 @@ export class CompositeDragAndDrop implements ICompositeDragAndDrop { } this.openComposite(newContainer.id, true).then(composite => { - if (composite) { - composite.openView(viewToMove.id, true); - } + composite?.openView(viewToMove.id, true); }); } } @@ -301,9 +299,7 @@ export class CompositeBar extends Widget implements ICompositeBar { } focus(index?: number): void { - if (this.compositeSwitcherBar) { - this.compositeSwitcherBar.focus(index); - } + this.compositeSwitcherBar?.focus(index); } recomputeSizes(): void { @@ -507,7 +503,7 @@ export class CompositeBar extends Widget implements ICompositeBar { // Ensure we are not showing more composites than we have height for let maxVisible = compositesToShow.length; - let totalComposites = compositesToShow.length; + const totalComposites = compositesToShow.length; let size = 0; const limit = this.options.orientation === ActionsOrientation.VERTICAL ? this.dimension.height : this.dimension.width; diff --git a/src/vs/workbench/browser/parts/compositePart.ts b/src/vs/workbench/browser/parts/compositePart.ts index b6fddb4625f..9ad13c8a659 100644 --- a/src/vs/workbench/browser/parts/compositePart.ts +++ b/src/vs/workbench/browser/parts/compositePart.ts @@ -232,9 +232,7 @@ export abstract class CompositePart<T extends Composite> extends Part { // Take Composite on-DOM and show const contentArea = this.getContentArea(); - if (contentArea) { - contentArea.appendChild(compositeContainer); - } + contentArea?.appendChild(compositeContainer); show(compositeContainer); // Setup action runner @@ -362,9 +360,7 @@ export abstract class CompositePart<T extends Composite> extends Part { } // Clear any running Progress - if (this.progressBar) { - this.progressBar.stop().hide(); - } + this.progressBar?.stop().hide(); // Empty Actions if (this.toolBar) { @@ -477,9 +473,7 @@ export abstract class CompositePart<T extends Composite> extends Part { this.contentAreaSize = Dimension.lift(super.layoutContents(width, height).contentSize); // Layout composite - if (this.activeComposite) { - this.activeComposite.layout(this.contentAreaSize); - } + this.activeComposite?.layout(this.contentAreaSize); } protected removeComposite(compositeId: string): boolean { diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbs.ts b/src/vs/workbench/browser/parts/editor/breadcrumbs.ts index 30e3c09e948..f2a44d8b1db 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbs.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbs.ts @@ -79,9 +79,9 @@ export abstract class BreadcrumbsConfig<T> { private static _stub<T>(name: string): { bindTo(service: IConfigurationService): BreadcrumbsConfig<T> } { return { bindTo(service) { - let onDidChange = new Emitter<void>(); + const onDidChange = new Emitter<void>(); - let listener = service.onDidChangeConfiguration(e => { + const listener = service.onDidChangeConfiguration(e => { if (e.affectsConfiguration(name)) { onDidChange.fire(undefined); } diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts index a969831de5b..57aa2f38d91 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts @@ -130,7 +130,7 @@ class FileItem extends BreadcrumbsItem { render(container: HTMLElement): void { // file/folder - let label = this._instantiationService.createInstance(ResourceLabel, container, {}); + const label = this._instantiationService.createInstance(ResourceLabel, container, {}); label.element.setFile(this.element.uri, { hidePath: true, hideIcon: this.element.kind === FileKind.FOLDER || !this.options.showFileIcons, @@ -389,11 +389,11 @@ export class BreadcrumbsControl { picker = this._instantiationService.createInstance(BreadcrumbsOutlinePicker, parent, event.item.model.resource); } - let selectListener = picker.onWillPickElement(() => this._contextViewService.hideContextView({ source: this, didPick: true })); - let zoomListener = PixelRatio.onDidChange(() => this._contextViewService.hideContextView({ source: this })); + const selectListener = picker.onWillPickElement(() => this._contextViewService.hideContextView({ source: this, didPick: true })); + const zoomListener = PixelRatio.onDidChange(() => this._contextViewService.hideContextView({ source: this })); - let focusTracker = dom.trackFocus(parent); - let blurListener = focusTracker.onDidBlur(() => { + const focusTracker = dom.trackFocus(parent); + const blurListener = focusTracker.onDidBlur(() => { this._breadcrumbsPickerIgnoreOnceItem = this._widget.isDOMFocused() ? event.item : undefined; this._contextViewService.hideContextView({ source: this }); }); @@ -411,15 +411,15 @@ export class BreadcrumbsControl { }, getAnchor: () => { if (!pickerAnchor) { - let maxInnerWidth = window.innerWidth - 8 /*a little less the full widget*/; + const maxInnerWidth = window.innerWidth - 8 /*a little less the full widget*/; let maxHeight = Math.min(window.innerHeight * 0.7, 300); - let pickerWidth = Math.min(maxInnerWidth, Math.max(240, maxInnerWidth / 4.17)); - let pickerArrowSize = 8; + const pickerWidth = Math.min(maxInnerWidth, Math.max(240, maxInnerWidth / 4.17)); + const pickerArrowSize = 8; let pickerArrowOffset: number; - let data = dom.getDomNodePagePosition(event.node.firstChild as HTMLElement); - let y = data.top + data.height + pickerArrowSize; + const data = dom.getDomNodePagePosition(event.node.firstChild as HTMLElement); + const y = data.top + data.height + pickerArrowSize; if (y + maxHeight >= window.innerHeight) { maxHeight = window.innerHeight - y - 30 /* room for shadow and status bar*/; } @@ -428,7 +428,7 @@ export class BreadcrumbsControl { x = maxInnerWidth - pickerWidth; } if (event.payload instanceof StandardMouseEvent) { - let maxPickerArrowOffset = pickerWidth - 2 * pickerArrowSize; + const maxPickerArrowOffset = pickerWidth - 2 * pickerArrowSize; pickerArrowOffset = event.payload.posx - x; if (pickerArrowOffset > maxPickerArrowOffset) { x = Math.min(maxInnerWidth - pickerWidth, x + pickerArrowOffset - maxPickerArrowOffset); @@ -469,8 +469,8 @@ export class BreadcrumbsControl { await this._editorService.openEditor({ resource: element.uri, options: { pinned } }, group); } else { // show next picker - let items = this._widget.getItems(); - let idx = items.indexOf(event.item); + const items = this._widget.getItems(); + const idx = items.indexOf(event.item); this._widget.setFocused(items[idx + 1]); this._widget.setSelection(items[idx + 1], BreadcrumbsControl.Payload_Pick); } @@ -514,8 +514,8 @@ registerAction2(class ToggleBreadcrumb extends Action2 { } run(accessor: ServicesAccessor): void { - let config = accessor.get(IConfigurationService); - let value = BreadcrumbsConfig.IsEnabled.bindTo(config).getValue(); + const config = accessor.get(IConfigurationService); + const value = BreadcrumbsConfig.IsEnabled.bindTo(config).getValue(); BreadcrumbsConfig.IsEnabled.bindTo(config).updateValue(!value); } diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts index 5d4c050eeea..d3d8b188c83 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts @@ -122,7 +122,7 @@ export class BreadcrumbsModel { }; } - let info: FileInfo = { + const info: FileInfo = { folder: withNullAsUndefined(this._workspaceService.getWorkspaceFolder(uri)), path: [] }; @@ -133,7 +133,7 @@ export class BreadcrumbsModel { break; } info.path.unshift(new FileElement(uriPrefix, info.path.length === 0 ? FileKind.FILE : FileKind.FOLDER)); - let prevPathLength = uriPrefix.path.length; + const prevPathLength = uriPrefix.path.length; uriPrefix = dirname(uriPrefix); if (uriPrefix.path.length === prevPathLength) { break; diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts index 81fb175ec5b..e6535720005 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts @@ -286,7 +286,7 @@ class FileFilter implements ITreeFilter<IWorkspaceFolder | IFileStat> { if (typeof excludesConfig[pattern] !== 'boolean') { continue; } - let patternAbs = pattern.indexOf('**/') !== 0 + const patternAbs = pattern.indexOf('**/') !== 0 ? posix.join(folder.uri.path, pattern) : pattern; diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index 8e0c175392e..399f58c4f6a 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -23,7 +23,7 @@ import { TextDiffEditor } from 'vs/workbench/browser/parts/editor/textDiffEditor import { BinaryResourceDiffEditor } from 'vs/workbench/browser/parts/editor/binaryDiffEditor'; import { ChangeEncodingAction, ChangeEOLAction, ChangeLanguageAction, EditorStatus } from 'vs/workbench/browser/parts/editor/editorStatus'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions, CATEGORIES } from 'vs/workbench/common/actions'; -import { SyncActionDescriptor, MenuRegistry, MenuId, IMenuItem } from 'vs/platform/actions/common/actions'; +import { SyncActionDescriptor, MenuRegistry, MenuId, IMenuItem, registerAction2 } from 'vs/platform/actions/common/actions'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes'; import { @@ -142,7 +142,7 @@ quickAccessRegistry.registerQuickAccessProvider({ prefix: ActiveGroupEditorsByMostRecentlyUsedQuickAccess.PREFIX, contextKey: editorPickerContextKey, placeholder: localize('editorQuickAccessPlaceholder', "Type the name of an editor to open it."), - helpEntries: [{ description: localize('activeGroupEditorsByMostRecentlyUsedQuickAccess', "Show Editors in Active Group by Most Recently Used"), needsEditor: false }] + helpEntries: [{ description: localize('activeGroupEditorsByMostRecentlyUsedQuickAccess', "Show Editors in Active Group by Most Recently Used"), commandId: ShowEditorsInActiveGroupByMostRecentlyUsedAction.ID }] }); quickAccessRegistry.registerQuickAccessProvider({ @@ -150,7 +150,7 @@ quickAccessRegistry.registerQuickAccessProvider({ prefix: AllEditorsByAppearanceQuickAccess.PREFIX, contextKey: editorPickerContextKey, placeholder: localize('editorQuickAccessPlaceholder', "Type the name of an editor to open it."), - helpEntries: [{ description: localize('allEditorsByAppearanceQuickAccess', "Show All Opened Editors By Appearance"), needsEditor: false }] + helpEntries: [{ description: localize('allEditorsByAppearanceQuickAccess', "Show All Opened Editors By Appearance"), commandId: ShowAllEditorsByAppearanceAction.ID }] }); quickAccessRegistry.registerQuickAccessProvider({ @@ -158,7 +158,7 @@ quickAccessRegistry.registerQuickAccessProvider({ prefix: AllEditorsByMostRecentlyUsedQuickAccess.PREFIX, contextKey: editorPickerContextKey, placeholder: localize('editorQuickAccessPlaceholder', "Type the name of an editor to open it."), - helpEntries: [{ description: localize('allEditorsByMostRecentlyUsedQuickAccess', "Show All Opened Editors By Most Recently Used"), needsEditor: false }] + helpEntries: [{ description: localize('allEditorsByMostRecentlyUsedQuickAccess', "Show All Opened Editors By Most Recently Used"), commandId: ShowAllEditorsByMostRecentlyUsedAction.ID }] }); //#endregion @@ -244,8 +244,6 @@ registry.registerWorkbenchAction(SyncActionDescriptor.from(NewEditorGroupLeftAct registry.registerWorkbenchAction(SyncActionDescriptor.from(NewEditorGroupRightAction), 'View: New Editor Group to the Right', CATEGORIES.View.value); registry.registerWorkbenchAction(SyncActionDescriptor.from(NewEditorGroupAboveAction), 'View: New Editor Group Above', CATEGORIES.View.value); registry.registerWorkbenchAction(SyncActionDescriptor.from(NewEditorGroupBelowAction), 'View: New Editor Group Below', CATEGORIES.View.value); -registry.registerWorkbenchAction(SyncActionDescriptor.from(NavigateForwardAction, { primary: 0, win: { primary: KeyMod.Alt | KeyCode.RightArrow }, mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.Minus }, linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Minus } }), 'Go Forward'); -registry.registerWorkbenchAction(SyncActionDescriptor.from(NavigateBackwardsAction, { primary: 0, win: { primary: KeyMod.Alt | KeyCode.LeftArrow }, mac: { primary: KeyMod.WinCtrl | KeyCode.Minus }, linux: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Minus } }), 'Go Back'); registry.registerWorkbenchAction(SyncActionDescriptor.from(NavigatePreviousAction), 'Go Previous'); registry.registerWorkbenchAction(SyncActionDescriptor.from(NavigateForwardInEditsAction), 'Go Forward in Edit Locations'); registry.registerWorkbenchAction(SyncActionDescriptor.from(NavigateBackwardsInEditsAction), 'Go Back in Edit Locations'); @@ -273,6 +271,9 @@ registry.registerWorkbenchAction(SyncActionDescriptor.from(QuickAccessPreviousRe registry.registerWorkbenchAction(SyncActionDescriptor.from(QuickAccessLeastRecentlyUsedEditorInGroupAction, { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Tab, mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.Tab } }, ActiveEditorGroupEmptyContext.toNegated()), 'View: Quick Open Least Recently Used Editor in Group', CATEGORIES.View.value); registry.registerWorkbenchAction(SyncActionDescriptor.from(QuickAccessPreviousEditorFromHistoryAction), 'Quick Open Previous Editor from History'); +registerAction2(NavigateForwardAction); +registerAction2(NavigateBackwardsAction); + const quickAccessNavigateNextInEditorPickerId = 'workbench.action.quickOpenNavigateNextInEditorPicker'; KeybindingsRegistry.registerCommandAndKeybindingRule({ id: quickAccessNavigateNextInEditorPickerId, @@ -314,9 +315,6 @@ if (isMacintosh) { }); } -MenuRegistry.appendMenuItem(MenuId.TitleMenu, { command: { id: NavigateBackwardsAction.ID, title: NavigateBackwardsAction.LABEL, icon: Codicon.arrowLeft } }); -MenuRegistry.appendMenuItem(MenuId.TitleMenu, { command: { id: NavigateForwardAction.ID, title: NavigateForwardAction.LABEL, icon: Codicon.arrowRight } }); - // Empty Editor Group Toolbar MenuRegistry.appendMenuItem(MenuId.EmptyEditorGroup, { command: { id: UNLOCK_GROUP_COMMAND_ID, title: localize('unlockGroupAction', "Unlock Group"), icon: Codicon.lock }, group: 'navigation', order: 10, when: ActiveEditorGroupLockedContext }); MenuRegistry.appendMenuItem(MenuId.EmptyEditorGroup, { command: { id: CLOSE_EDITOR_GROUP_COMMAND_ID, title: localize('closeGroupAction', "Close Group"), icon: Codicon.close }, group: 'navigation', order: 20 }); @@ -351,7 +349,7 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: TOGGLE_DIFF_SID MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: SHOW_EDITORS_IN_GROUP, title: localize('showOpenedEditors', "Show Opened Editors") }, group: '3_open', order: 10 }); MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: CLOSE_EDITORS_IN_GROUP_COMMAND_ID, title: localize('closeAll', "Close All") }, group: '5_close', order: 10 }); MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: CLOSE_SAVED_EDITORS_COMMAND_ID, title: localize('closeAllSaved', "Close Saved") }, group: '5_close', order: 20 }); -MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: TOGGLE_KEEP_EDITORS_COMMAND_ID, title: localize('toggleKeepEditors', "Keep Editors Open"), toggled: ContextKeyExpr.not('config.workbench.editor.enablePreview') }, group: '7_settings', order: 10 }); +MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: TOGGLE_KEEP_EDITORS_COMMAND_ID, title: localize('togglePreviewMode', "Enable Preview Editors"), toggled: ContextKeyExpr.has('config.workbench.editor.enablePreview') }, group: '7_settings', order: 10 }); MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: TOGGLE_LOCK_GROUP_COMMAND_ID, title: localize('lockGroup', "Lock Group"), toggled: ActiveEditorGroupLockedContext }, group: '8_lock', order: 10, when: MultipleEditorGroupsContext }); interface IEditorToolItem { id: string; title: string; icon?: { dark?: URI; light?: URI } | ThemeIcon } @@ -581,6 +579,13 @@ MenuRegistry.appendMenuItem(MenuId.MenubarRecentMenu, { order: 1 }); +MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + title: localize('miShare', "Share"), + submenu: MenuId.MenubarShare, + group: '45_share', + order: 1, +}); + // Layout menu MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { group: '2_appearance', @@ -775,27 +780,6 @@ MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { // Main Menu Bar Contributions: -// Forward/Back -MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { - group: '1_history_nav', - command: { - id: 'workbench.action.navigateBack', - title: localize({ key: 'miBack', comment: ['&& denotes a mnemonic'] }, "&&Back"), - precondition: ContextKeyExpr.has('canNavigateBack') - }, - order: 1 -}); - -MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { - group: '1_history_nav', - command: { - id: 'workbench.action.navigateForward', - title: localize({ key: 'miForward', comment: ['&& denotes a mnemonic'] }, "&&Forward"), - precondition: ContextKeyExpr.has('canNavigateForward') - }, - order: 2 -}); - MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { group: '1_history_nav', command: { diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index a993829e847..bf39a77a241 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -26,6 +26,11 @@ import { Codicon } from 'vs/base/common/codicons'; import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { IEditorResolverService } from 'vs/workbench/services/editor/common/editorResolverService'; import { isLinux, isNative, isWindows } from 'vs/base/common/platform'; +import { Action2, MenuId } from 'vs/platform/actions/common/actions'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; export class ExecuteCommandAction extends Action { @@ -1258,39 +1263,67 @@ export class OpenLastEditorInGroup extends AbstractNavigateEditorAction { } } -export class NavigateForwardAction extends Action { +export class NavigateForwardAction extends Action2 { static readonly ID = 'workbench.action.navigateForward'; static readonly LABEL = localize('navigateForward', "Go Forward"); - constructor( - id: string, - label: string, - @IHistoryService private readonly historyService: IHistoryService - ) { - super(id, label); + constructor() { + super({ + id: NavigateForwardAction.ID, + title: { value: localize('navigateForward', "Go Forward"), original: 'Go Forward', mnemonicTitle: localize({ key: 'miForward', comment: ['&& denotes a mnemonic'] }, "&&Forward") }, + f1: true, + icon: Codicon.arrowRight, + precondition: ContextKeyExpr.has('canNavigateForward'), + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + win: { primary: KeyMod.Alt | KeyCode.RightArrow }, + mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.Minus }, + linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Minus } + }, + menu: [ + { id: MenuId.MenubarGoMenu, group: '1_history_nav', order: 2 }, + { id: MenuId.CommandCenter, order: 2 } + ] + }); } - override async run(): Promise<void> { - await this.historyService.goForward(GoFilter.NONE); + async run(accessor: ServicesAccessor): Promise<void> { + const historyService = accessor.get(IHistoryService); + + await historyService.goForward(GoFilter.NONE); } } -export class NavigateBackwardsAction extends Action { +export class NavigateBackwardsAction extends Action2 { static readonly ID = 'workbench.action.navigateBack'; static readonly LABEL = localize('navigateBack', "Go Back"); - constructor( - id: string, - label: string, - @IHistoryService private readonly historyService: IHistoryService - ) { - super(id, label); + constructor() { + super({ + id: NavigateBackwardsAction.ID, + title: { value: localize('navigateBack', "Go Back"), original: 'Go Back', mnemonicTitle: localize({ key: 'miBack', comment: ['&& denotes a mnemonic'] }, "&&Back") }, + f1: true, + precondition: ContextKeyExpr.has('canNavigateBack'), + icon: Codicon.arrowLeft, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + win: { primary: KeyMod.Alt | KeyCode.LeftArrow }, + mac: { primary: KeyMod.WinCtrl | KeyCode.Minus }, + linux: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Minus } + }, + menu: [ + { id: MenuId.MenubarGoMenu, group: '1_history_nav', order: 1 }, + { id: MenuId.CommandCenter, order: 1 } + ] + }); } - override async run(): Promise<void> { - await this.historyService.goBack(GoFilter.NONE); + async run(accessor: ServicesAccessor): Promise<void> { + const historyService = accessor.get(IHistoryService); + + await historyService.goBack(GoFilter.NONE); } } diff --git a/src/vs/workbench/browser/parts/editor/editorCommands.ts b/src/vs/workbench/browser/parts/editor/editorCommands.ts index 3624936e41f..717d7b138c1 100644 --- a/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/editorCommands.ts @@ -968,6 +968,7 @@ function registerCloseEditorCommands() { ]); type WorkbenchEditorReopenClassification = { + owner: 'rebornix'; scheme: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; ext: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; from: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; @@ -1150,7 +1151,7 @@ function registerSplitEditorInGroupCommands(): void { constructor() { super({ id: TOGGLE_SPLIT_EDITOR_IN_GROUP_LAYOUT, - title: { value: localize('toggleSplitEditorInGroupLayout', "Toggle Split Editor in Group Layout"), original: 'Toggle Split Editor in Group Layout' }, + title: { value: localize('toggleSplitEditorInGroupLayout', "Toggle Layout of Split Editor in Group"), original: 'Toggle Layout of Split Editor in Group' }, category: CATEGORIES.View, precondition: SideBySideEditorActiveContext, f1: true @@ -1280,9 +1281,7 @@ function registerOtherEditorCommands(): void { const editorGroupService = accessor.get(IEditorGroupsService); const { group } = resolveCommandsContext(editorGroupService, getCommandsContext(resourceOrContext, context)); - if (group) { - group.lock(locked ?? !group.isLocked); - } + group?.lock(locked ?? !group.isLocked); } registerAction2(class extends Action2 { diff --git a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts index ad6a0289f12..4d29cc8f143 100644 --- a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts +++ b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts @@ -408,15 +408,6 @@ class DropOverlay extends Themable { const splitWidthThreshold = editorControlWidth / 3; // offer to split left/right at 33% const splitHeightThreshold = editorControlHeight / 3; // offer to split up/down at 33% - // Enable to debug the drop threshold square - // let child = this.overlay.children.item(0) as HTMLElement || this.overlay.appendChild(document.createElement('div')); - // child.style.backgroundColor = 'red'; - // child.style.position = 'absolute'; - // child.style.width = (groupViewWidth - (2 * edgeWidthThreshold)) + 'px'; - // child.style.height = (groupViewHeight - (2 * edgeHeightThreshold)) + 'px'; - // child.style.left = edgeWidthThreshold + 'px'; - // child.style.top = edgeHeightThreshold + 'px'; - // No split if mouse is above certain threshold in the center of the view let splitDirection: GroupDirection | undefined; if ( @@ -606,6 +597,11 @@ export class EditorDropTarget extends Themable { private registerListeners(): void { this._register(addDisposableListener(this.container, EventType.DRAG_ENTER, e => this.onDragEnter(e))); this._register(addDisposableListener(this.container, EventType.DRAG_LEAVE, () => this.onDragLeave())); + this._register(addDisposableListener(this.container, EventType.DRAG_OVER, e => { + if (!this.overlay) { + this.onDragEnter(e); + } + })); [this.container, window].forEach(node => this._register(addDisposableListener(node as HTMLElement, EventType.DRAG_END, () => this.onDragEnd()))); } diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 7ea0ca109d4..21b5d732107 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -575,6 +575,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { /* __GDPR__ "editorOpened" : { + "owner": "bpasero", "${include}": [ "${EditorTelemetryDescriptor}" ] @@ -611,6 +612,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { /* __GDPR__ "editorClosed" : { + "owner": "bpasero", "${include}": [ "${EditorTelemetryDescriptor}" ] diff --git a/src/vs/workbench/browser/parts/editor/editorPane.ts b/src/vs/workbench/browser/parts/editor/editorPane.ts index e7788200b8c..c4ce0bf3d90 100644 --- a/src/vs/workbench/browser/parts/editor/editorPane.ts +++ b/src/vs/workbench/browser/parts/editor/editorPane.ts @@ -278,7 +278,7 @@ export class EditorMemento<T> extends Disposable implements IEditorMemento<T> { const mementosForResource = cache.get(resource.toString()); if (mementosForResource) { - let mementoForResourceAndGroup = mementosForResource[group.id]; + const mementoForResourceAndGroup = mementosForResource[group.id]; // Return state for group if present if (mementoForResourceAndGroup) { diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts index 6c5f32362ac..cf162aa9dae 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -72,9 +72,7 @@ class GridWidgetView<T extends IView> implements IView { } layout(width: number, height: number, top: number, left: number): void { - if (this.gridWidget) { - this.gridWidget.layout(width, height, top, left); - } + this.gridWidget?.layout(width, height, top, left); } dispose(): void { @@ -129,7 +127,7 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro //#endregion private readonly workspaceMemento = this.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE); - private readonly globalMemento = this.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); + private readonly profileMemento = this.getMemento(StorageScope.PROFILE, StorageTarget.MACHINE); private readonly groupViews = new Map<GroupIdentifier, IEditorGroupView>(); private mostRecentActiveGroups: GroupIdentifier[] = []; @@ -595,9 +593,7 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro this.doUpdateMostRecentActive(group, true); // Mark previous one as inactive - if (previousActiveGroup) { - previousActiveGroup.setActive(false); - } + previousActiveGroup?.setActive(false); // Mark group as new active group.setActive(true); @@ -866,7 +862,7 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro this.doCreateGridControl(options); // Centered layout widget - this.centeredLayoutWidget = this._register(new CenteredViewLayout(this.container, this.gridWidgetView, this.globalMemento[EditorPart.EDITOR_PART_CENTERED_VIEW_STORAGE_KEY])); + this.centeredLayoutWidget = this._register(new CenteredViewLayout(this.container, this.gridWidgetView, this.profileMemento[EditorPart.EDITOR_PART_CENTERED_VIEW_STORAGE_KEY])); // Drag & Drop support this.setupDragAndDropSupport(parent, this.container); @@ -1166,9 +1162,9 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro if (this.centeredLayoutWidget) { const centeredLayoutState = this.centeredLayoutWidget.state; if (this.centeredLayoutWidget.isDefault(centeredLayoutState)) { - delete this.globalMemento[EditorPart.EDITOR_PART_CENTERED_VIEW_STORAGE_KEY]; + delete this.profileMemento[EditorPart.EDITOR_PART_CENTERED_VIEW_STORAGE_KEY]; } else { - this.globalMemento[EditorPart.EDITOR_PART_CENTERED_VIEW_STORAGE_KEY] = centeredLayoutState; + this.profileMemento[EditorPart.EDITOR_PART_CENTERED_VIEW_STORAGE_KEY] = centeredLayoutState; } } diff --git a/src/vs/workbench/browser/parts/editor/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts index 7571481ae97..48245d942a3 100644 --- a/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/src/vs/workbench/browser/parts/editor/editorStatus.ts @@ -733,7 +733,7 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { } private onLanguageChange(editorWidget: ICodeEditor | undefined, editorInput: EditorInput | undefined): void { - let info: StateDelta = { type: 'languageId', languageId: undefined }; + const info: StateDelta = { type: 'languageId', languageId: undefined }; // We only support text based editors if (editorWidget && editorInput && toEditorWithLanguageSupport(editorInput)) { @@ -837,7 +837,7 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { if (info.selections.length === 1) { const editorPosition = editorWidget.getPosition(); - let selectionClone = new Selection( + const selectionClone = new Selection( info.selections[0].selectionStartLineNumber, info.selections[0].selectionStartColumn, info.selections[0].positionLineNumber, diff --git a/src/vs/workbench/browser/parts/editor/media/editorplaceholder.css b/src/vs/workbench/browser/parts/editor/media/editorplaceholder.css index 692baa8c240..f3bde77fdca 100644 --- a/src/vs/workbench/browser/parts/editor/media/editorplaceholder.css +++ b/src/vs/workbench/browser/parts/editor/media/editorplaceholder.css @@ -29,7 +29,8 @@ .monaco-editor-pane-placeholder .editor-placeholder-label-container { font-size: 14px; max-width: 450px; - text-align: center + text-align: center; + word-break: break-word; } .monaco-editor-pane-placeholder .monaco-link, diff --git a/src/vs/workbench/browser/parts/editor/media/notabstitlecontrol.css b/src/vs/workbench/browser/parts/editor/media/notabstitlecontrol.css index 21848636100..61440406d69 100644 --- a/src/vs/workbench/browser/parts/editor/media/notabstitlecontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/notabstitlecontrol.css @@ -23,7 +23,7 @@ } .monaco-workbench .part.editor > .content .editor-group-container > .title > .label-container > .title-label > .monaco-icon-label-container { - flex: none; /* helps to show decorations right next to the label and not at the end */ + flex: initial; /* helps to show decorations right next to the label and not at the end while still preserving text overflow ellipsis */ } /* Breadcrumbs */ diff --git a/src/vs/workbench/browser/parts/editor/media/titlecontrol.css b/src/vs/workbench/browser/parts/editor/media/titlecontrol.css index 1237c847967..378d2bfa63f 100644 --- a/src/vs/workbench/browser/parts/editor/media/titlecontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/titlecontrol.css @@ -5,6 +5,10 @@ /* Editor Label */ +.monaco-workbench .part.editor > .content .editor-group-container > .title { + cursor: pointer; +} + .monaco-workbench .part.editor > .content .editor-group-container > .title .title-label, .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab .tab-label { white-space: nowrap; @@ -36,10 +40,6 @@ /* Drag and Drop */ -.monaco-workbench .part.editor > .content .editor-group-container > .title { - cursor: grab; -} - .monaco-editor-group-drag-image { display: inline-block; padding: 1px 7px; diff --git a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index 6d834b41807..386e13b2e0b 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -97,6 +97,8 @@ export class TabsTitleControl extends TitleControl { private static readonly TAB_HEIGHT = 35; + private static readonly DRAG_OVER_OPEN_TAB_THRESHOLD = 1500; + private static readonly MOUSE_WHEEL_EVENT_THRESHOLD = 150; private static readonly MOUSE_WHEEL_DISTANCE_THRESHOLD = 1.5; @@ -898,6 +900,15 @@ export class TabsTitleControl extends TitleControl { this.updateDropFeedback(tab, true, index); }, + onDragOver: (_, dragDuration) => { + if (dragDuration >= TabsTitleControl.DRAG_OVER_OPEN_TAB_THRESHOLD) { + const draggedOverTab = this.group.getEditorByIndex(index); + if (draggedOverTab && this.group.activeEditor !== draggedOverTab) { + this.group.openEditor(draggedOverTab, { preserveFocus: true }); + } + } + }, + onDragLeave: () => { tab.classList.remove('dragged-over'); this.updateDropFeedback(tab, false, index); diff --git a/src/vs/workbench/browser/parts/editor/textCodeEditor.ts b/src/vs/workbench/browser/parts/editor/textCodeEditor.ts new file mode 100644 index 00000000000..6bc581eee8f --- /dev/null +++ b/src/vs/workbench/browser/parts/editor/textCodeEditor.ts @@ -0,0 +1,112 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { URI } from 'vs/base/common/uri'; +import { assertIsDefined, withNullAsUndefined } from 'vs/base/common/types'; +import { ITextEditorPane } from 'vs/workbench/common/editor'; +import { applyTextEditorOptions } from 'vs/workbench/common/editor/editorOptions'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; +import { isEqual } from 'vs/base/common/resources'; +import { IEditorOptions as ICodeEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget'; +import { IEditorViewState, ScrollType } from 'vs/editor/common/editorCommon'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { AbstractTextEditor } from 'vs/workbench/browser/parts/editor/textEditor'; +import { Dimension } from 'vs/base/browser/dom'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; + +/** + * A text editor using the code editor widget. + */ +export abstract class AbstractTextCodeEditor<T extends IEditorViewState> extends AbstractTextEditor<T> implements ITextEditorPane { + + protected editorControl: ICodeEditor | undefined = undefined; + + override get scopedContextKeyService(): IContextKeyService | undefined { + return this.editorControl?.invokeWithinContext(accessor => accessor.get(IContextKeyService)); + } + + override getTitle(): string { + if (this.input) { + return this.input.getName(); + } + + return localize('textEditor', "Text Editor"); + } + + protected createEditorControl(parent: HTMLElement, initialOptions: ICodeEditorOptions): void { + this.editorControl = this._register(this.instantiationService.createInstance(CodeEditorWidget, parent, initialOptions, this.getCodeEditorWidgetOptions())); + } + + protected getCodeEditorWidgetOptions(): ICodeEditorWidgetOptions { + return Object.create(null); + } + + protected updateEditorControlOptions(options: ICodeEditorOptions): void { + this.editorControl?.updateOptions(options); + } + + protected getMainControl(): ICodeEditor | undefined { + return this.editorControl; + } + + override getControl(): ICodeEditor | undefined { + return this.editorControl; + } + + protected override computeEditorViewState(resource: URI): T | undefined { + if (!this.editorControl) { + return undefined; + } + + const model = this.editorControl.getModel(); + if (!model) { + return undefined; // view state always needs a model + } + + const modelUri = model.uri; + if (!modelUri) { + return undefined; // model URI is needed to make sure we save the view state correctly + } + + if (!isEqual(modelUri, resource)) { + return undefined; // prevent saving view state for a model that is not the expected one + } + + return withNullAsUndefined(this.editorControl.saveViewState() as unknown as T); + } + + override setOptions(options: ITextEditorOptions | undefined): void { + super.setOptions(options); + + if (options) { + applyTextEditorOptions(options, assertIsDefined(this.editorControl), ScrollType.Smooth); + } + } + + override focus(): void { + this.editorControl?.focus(); + } + + override hasFocus(): boolean { + return this.editorControl?.hasTextFocus() || super.hasFocus(); + } + + protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { + super.setEditorVisible(visible, group); + + if (visible) { + this.editorControl?.onVisible(); + } else { + this.editorControl?.onHide(); + } + } + + override layout(dimension: Dimension): void { + this.editorControl?.layout(dimension); + } +} diff --git a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts index 3078d3eb35e..2aa1285f9a1 100644 --- a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts @@ -6,9 +6,9 @@ import { localize } from 'vs/nls'; import { deepClone } from 'vs/base/common/objects'; import { isObject, isArray, assertIsDefined, withUndefinedAsNull, withNullAsUndefined } from 'vs/base/common/types'; -import { IDiffEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditor, IDiffEditor } from 'vs/editor/browser/editorBrowser'; import { IDiffEditorOptions, IEditorOptions as ICodeEditorOptions } from 'vs/editor/common/config/editorOptions'; -import { BaseTextEditor, IEditorConfiguration } from 'vs/workbench/browser/parts/editor/textEditor'; +import { AbstractTextEditor, IEditorConfiguration } from 'vs/workbench/browser/parts/editor/textEditor'; import { TEXT_DIFF_EDITOR_ID, IEditorFactoryRegistry, EditorExtensions, ITextDiffEditorPane, IEditorOpenContext, EditorInputCapabilities, isEditorInput, isTextEditorViewState } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { applyTextEditorOptions } from 'vs/workbench/common/editor/editorOptions'; @@ -23,38 +23,37 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IThemeService } from 'vs/platform/theme/common/themeService'; import { TextFileOperationError, TextFileOperationResult } from 'vs/workbench/services/textfile/common/textfiles'; import { ScrollType, IDiffEditorViewState, IDiffEditorModel } from 'vs/editor/common/editorCommon'; -import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; import { URI } from 'vs/base/common/uri'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { EditorActivation, ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { isEqual } from 'vs/base/common/resources'; -import { multibyteAwareBtoa } from 'vs/base/browser/dom'; +import { Dimension, multibyteAwareBtoa } from 'vs/base/browser/dom'; import { IFileService } from 'vs/platform/files/common/files'; /** * The text editor that leverages the diff text editor for the editing experience. */ -export class TextDiffEditor extends BaseTextEditor<IDiffEditorViewState> implements ITextDiffEditorPane { +export class TextDiffEditor extends AbstractTextEditor<IDiffEditorViewState> implements ITextDiffEditorPane { static readonly ID = TEXT_DIFF_EDITOR_ID; + private diffEditorControl: IDiffEditor | undefined = undefined; + private diffNavigator: DiffNavigator | undefined; private readonly diffNavigatorDisposables = this._register(new DisposableStore()); - private readonly inputListener = this._register(new MutableDisposable()); - override get scopedContextKeyService(): IContextKeyService | undefined { - const control = this.getControl(); - if (!control) { + if (!this.diffEditorControl) { return undefined; } - const originalEditor = control.getOriginalEditor(); - const modifiedEditor = control.getModifiedEditor(); + const originalEditor = this.diffEditorControl.getOriginalEditor(); + const modifiedEditor = this.diffEditorControl.getModifiedEditor(); return (originalEditor.hasTextFocus() ? originalEditor : modifiedEditor).invokeWithinContext(accessor => accessor.get(IContextKeyService)); } @@ -67,35 +66,9 @@ export class TextDiffEditor extends BaseTextEditor<IDiffEditorViewState> impleme @IEditorService editorService: IEditorService, @IThemeService themeService: IThemeService, @IEditorGroupsService editorGroupService: IEditorGroupsService, - @IFileService private readonly fileService: IFileService + @IFileService fileService: IFileService ) { - super(TextDiffEditor.ID, telemetryService, instantiationService, storageService, configurationService, themeService, editorService, editorGroupService); - - // 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 onDidChangeFileSystemProvider(scheme: string): void { - if (this.input instanceof DiffEditorInput && (this.input.original.resource?.scheme === scheme || this.input.modified.resource?.scheme === scheme)) { - this.updateReadonly(this.input); - } - } - - private onDidChangeInputCapabilities(input: DiffEditorInput): void { - if (this.input === input) { - this.updateReadonly(input); - } - } - - private updateReadonly(input: DiffEditorInput): void { - const control = this.getControl(); - if (control) { - control.updateOptions({ - readOnly: input.modified.hasCapability(EditorInputCapabilities.Readonly), - originalEditable: !input.original.hasCapability(EditorInputCapabilities.Readonly) - }); - } + super(TextDiffEditor.ID, telemetryService, instantiationService, storageService, configurationService, themeService, editorService, editorGroupService, fileService); } override getTitle(): string { @@ -106,14 +79,19 @@ export class TextDiffEditor extends BaseTextEditor<IDiffEditorViewState> impleme return localize('textDiffEditor', "Text Diff Editor"); } - override createEditorControl(parent: HTMLElement, configuration: ICodeEditorOptions): IDiffEditor { - return this.instantiationService.createInstance(DiffEditorWidget, parent, configuration, {}); + override createEditorControl(parent: HTMLElement, configuration: ICodeEditorOptions): void { + this.diffEditorControl = this._register(this.instantiationService.createInstance(DiffEditorWidget, parent, configuration, {})); } - override async setInput(input: DiffEditorInput, options: ITextEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise<void> { + protected updateEditorControlOptions(options: ICodeEditorOptions): void { + this.diffEditorControl?.updateOptions(options); + } + + protected getMainControl(): ICodeEditor | undefined { + return this.diffEditorControl?.getModifiedEditor(); + } - // Update our listener for input capabilities - this.inputListener.value = input.onDidChangeCapabilities(() => this.onDidChangeInputCapabilities(input)); + override async setInput(input: DiffEditorInput, options: ITextEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise<void> { // Dispose previous diff navigator this.diffNavigatorDisposables.clear(); @@ -136,24 +114,24 @@ export class TextDiffEditor extends BaseTextEditor<IDiffEditorViewState> impleme } // Set Editor Model - const diffEditor = assertIsDefined(this.getControl()); + const control = assertIsDefined(this.diffEditorControl); const resolvedDiffEditorModel = resolvedModel as TextDiffEditorModel; - diffEditor.setModel(withUndefinedAsNull(resolvedDiffEditorModel.textDiffEditorModel)); + control.setModel(withUndefinedAsNull(resolvedDiffEditorModel.textDiffEditorModel)); // Restore view state (unless provided by options) let hasPreviousViewState = false; if (!isTextEditorViewState(options?.viewState)) { - hasPreviousViewState = this.restoreTextDiffEditorViewState(input, options, context, diffEditor); + hasPreviousViewState = this.restoreTextDiffEditorViewState(input, options, context, control); } // Apply options to editor if any let optionsGotApplied = false; if (options) { - optionsGotApplied = applyTextEditorOptions(options, diffEditor, ScrollType.Immediate); + optionsGotApplied = applyTextEditorOptions(options, control, ScrollType.Immediate); } // Diff navigator - this.diffNavigator = new DiffNavigator(diffEditor, { + this.diffNavigator = new DiffNavigator(control, { alwaysRevealFirst: !optionsGotApplied && !hasPreviousViewState // only reveal first change if we had no options or viewstate }); this.diffNavigatorDisposables.add(this.diffNavigator); @@ -163,7 +141,7 @@ export class TextDiffEditor extends BaseTextEditor<IDiffEditorViewState> impleme // 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. - diffEditor.updateOptions({ + control.updateOptions({ readOnly: resolvedDiffEditorModel.modifiedModel?.isReadonly(), originalEditable: !resolvedDiffEditorModel.originalModel?.isReadonly() }); @@ -227,6 +205,14 @@ export class TextDiffEditor extends BaseTextEditor<IDiffEditorViewState> impleme }]); } + override setOptions(options: ITextEditorOptions | undefined): void { + super.setOptions(options); + + if (options) { + applyTextEditorOptions(options, assertIsDefined(this.diffEditorControl), ScrollType.Smooth); + } + } + protected override computeConfiguration(configuration: IEditorConfiguration): ICodeEditorOptions { const editorConfiguration = super.computeConfiguration(configuration); @@ -248,14 +234,28 @@ export class TextDiffEditor extends BaseTextEditor<IDiffEditorViewState> impleme return editorConfiguration; } - protected override getConfigurationOverrides(): ICodeEditorOptions { - const options: IDiffEditorOptions = super.getConfigurationOverrides(); + protected override getConfigurationOverrides(): IDiffEditorOptions { + const readOnly = this.input instanceof DiffEditorInput && this.input.modified.hasCapability(EditorInputCapabilities.Readonly); - options.readOnly = this.input instanceof DiffEditorInput && this.input.modified.hasCapability(EditorInputCapabilities.Readonly); - options.originalEditable = this.input instanceof DiffEditorInput && !this.input.original.hasCapability(EditorInputCapabilities.Readonly); - options.lineDecorationsWidth = '2ch'; + return { + ...super.getConfigurationOverrides(), + readOnly, + enableDropIntoEditor: !readOnly, + originalEditable: this.input instanceof DiffEditorInput && !this.input.original.hasCapability(EditorInputCapabilities.Readonly), + lineDecorationsWidth: '2ch' + }; + } - return options; + protected override updateReadonly(input: EditorInput): void { + if (input instanceof DiffEditorInput) { + this.diffEditorControl?.updateOptions({ + readOnly: input.hasCapability(EditorInputCapabilities.Readonly), + originalEditable: !input.original.hasCapability(EditorInputCapabilities.Readonly), + enableDropIntoEditor: !input.hasCapability(EditorInputCapabilities.Readonly) + }); + } else { + super.updateReadonly(input); + } } private isFileBinaryError(error: Error[]): boolean; @@ -273,15 +273,11 @@ export class TextDiffEditor extends BaseTextEditor<IDiffEditorViewState> impleme override clearInput(): void { super.clearInput(); - // Clear input listener - this.inputListener.clear(); - // Dispose previous diff navigator this.diffNavigatorDisposables.clear(); // Clear Model - const diffEditor = this.getControl(); - diffEditor?.setModel(null); + this.diffEditorControl?.setModel(null); } getDiffNavigator(): DiffNavigator | undefined { @@ -289,7 +285,29 @@ export class TextDiffEditor extends BaseTextEditor<IDiffEditorViewState> impleme } override getControl(): IDiffEditor | undefined { - return super.getControl() as IDiffEditor | undefined; + return this.diffEditorControl; + } + + override focus(): void { + this.diffEditorControl?.focus(); + } + + override hasFocus(): boolean { + return this.diffEditorControl?.hasTextFocus() || super.hasFocus(); + } + + protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { + super.setEditorVisible(visible, group); + + if (visible) { + this.diffEditorControl?.onVisible(); + } else { + this.diffEditorControl?.onHide(); + } + } + + override layout(dimension: Dimension): void { + this.diffEditorControl?.layout(dimension); } protected override tracksEditorViewState(input: EditorInput): boolean { @@ -297,12 +315,11 @@ export class TextDiffEditor extends BaseTextEditor<IDiffEditorViewState> impleme } protected override computeEditorViewState(resource: URI): IDiffEditorViewState | undefined { - const control = this.getControl(); - if (!isDiffEditor(control)) { + if (!this.diffEditorControl) { return undefined; } - const model = control.getModel(); + const model = this.diffEditorControl.getModel(); if (!model || !model.modified || !model.original) { return undefined; // view state always needs a model } @@ -316,7 +333,7 @@ export class TextDiffEditor extends BaseTextEditor<IDiffEditorViewState> impleme return undefined; // prevent saving view state for a model that is not the expected one } - return withNullAsUndefined(control.saveViewState()); + return withNullAsUndefined(this.diffEditorControl.saveViewState()); } protected override toEditorViewStateResource(modelOrInput: IDiffEditorModel | EditorInput): URI | undefined { diff --git a/src/vs/workbench/browser/parts/editor/textEditor.ts b/src/vs/workbench/browser/parts/editor/textEditor.ts index f614fd793d2..c803409ef74 100644 --- a/src/vs/workbench/browser/parts/editor/textEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textEditor.ts @@ -7,15 +7,14 @@ import { localize } from 'vs/nls'; import { URI } from 'vs/base/common/uri'; import { distinct, deepClone } from 'vs/base/common/objects'; import { Emitter, Event } from 'vs/base/common/event'; -import { isObject, assertIsDefined, withNullAsUndefined } from 'vs/base/common/types'; -import { Dimension } from 'vs/base/browser/dom'; -import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { isObject, assertIsDefined } from 'vs/base/common/types'; +import { MutableDisposable } from 'vs/base/common/lifecycle'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorOpenContext, EditorInputCapabilities, IEditorPaneSelection, EditorPaneSelectionCompareResult, EditorPaneSelectionChangeReason, IEditorPaneWithSelection, IEditorPaneSelectionChangeEvent } from 'vs/workbench/common/editor'; -import { applyTextEditorOptions } from 'vs/workbench/common/editor/editorOptions'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { computeEditorAriaLabel } from 'vs/workbench/browser/editor'; import { AbstractEditorWithViewState } from 'vs/workbench/browser/parts/editor/editorWithViewState'; -import { IEditorViewState, IEditor, ScrollType } from 'vs/editor/common/editorCommon'; +import { IEditorViewState } from 'vs/editor/common/editorCommon'; import { Selection } from 'vs/editor/common/core/selection'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -23,14 +22,12 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; import { IEditorOptions as ICodeEditorOptions } from 'vs/editor/common/config/editorOptions'; -import { isCodeEditor, getCodeEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorGroupsService, IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IEditorOptions, ITextEditorOptions, TextEditorSelectionRevealType, TextEditorSelectionSource } from 'vs/platform/editor/common/editor'; -import { isEqual } from 'vs/base/common/resources'; import { ICursorPositionChangedEvent } from 'vs/editor/common/cursorEvents'; +import { IFileService } from 'vs/platform/files/common/files'; export interface IEditorConfiguration { editor: object; @@ -38,24 +35,21 @@ export interface IEditorConfiguration { } /** - * The base class of editors that leverage the text editor for the editing experience. This class is only intended to - * be subclassed and not instantiated. + * The base class of editors that leverage any kind of text editor for the editing experience. */ -export abstract class BaseTextEditor<T extends IEditorViewState> extends AbstractEditorWithViewState<T> implements IEditorPaneWithSelection { +export abstract class AbstractTextEditor<T extends IEditorViewState> extends AbstractEditorWithViewState<T> implements IEditorPaneWithSelection { private static readonly VIEW_STATE_PREFERENCE_KEY = 'textEditorViewState'; protected readonly _onDidChangeSelection = this._register(new Emitter<IEditorPaneSelectionChangeEvent>()); readonly onDidChangeSelection = this._onDidChangeSelection.event; - private editorControl: IEditor | undefined; private editorContainer: HTMLElement | undefined; + private hasPendingConfigurationChange: boolean | undefined; private lastAppliedEditorOptions?: ICodeEditorOptions; - override get scopedContextKeyService(): IContextKeyService | undefined { - return isCodeEditor(this.editorControl) ? this.editorControl.invokeWithinContext(accessor => accessor.get(IContextKeyService)) : undefined; - } + private readonly inputListener = this._register(new MutableDisposable()); constructor( id: string, @@ -65,9 +59,10 @@ export abstract class BaseTextEditor<T extends IEditorViewState> extends Abstrac @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService, @IThemeService themeService: IThemeService, @IEditorService editorService: IEditorService, - @IEditorGroupsService editorGroupService: IEditorGroupsService + @IEditorGroupsService editorGroupService: IEditorGroupsService, + @IFileService protected readonly fileService: IFileService ) { - super(id, BaseTextEditor.VIEW_STATE_PREFERENCE_KEY, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService); + super(id, AbstractTextEditor.VIEW_STATE_PREFERENCE_KEY, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService); this._register(this.textResourceConfigurationService.onDidChangeConfiguration(() => { const resource = this.getActiveResource(); @@ -78,15 +73,20 @@ export abstract class BaseTextEditor<T extends IEditorViewState> extends Abstrac // ARIA: if a group is added or removed, update the editor's ARIA // label so that it appears in the label for when there are > 1 groups + this._register(Event.any(this.editorGroupService.onDidAddGroup, this.editorGroupService.onDidRemoveGroup)(() => { const ariaLabel = this.computeAriaLabel(); this.editorContainer?.setAttribute('aria-label', ariaLabel); - this.editorControl?.updateOptions({ ariaLabel }); + this.updateEditorControlOptions({ ariaLabel }); })); + + // 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))); } - protected handleConfigurationChangeEvent(configuration?: IEditorConfiguration): void { + private handleConfigurationChangeEvent(configuration?: IEditorConfiguration): void { if (this.isVisible()) { this.updateEditorConfiguration(configuration); } else { @@ -117,15 +117,41 @@ export abstract class BaseTextEditor<T extends IEditorViewState> extends Abstrac return this._input ? computeEditorAriaLabel(this._input, undefined, this.group, this.editorGroupService.count) : localize('editor', "Editor"); } + private onDidChangeFileSystemProvider(scheme: string): void { + if (!this.input) { + return; + } + + if (this.getActiveResource()?.scheme === scheme) { + this.updateReadonly(this.input); + } + } + + private onDidChangeInputCapabilities(input: EditorInput): void { + if (this.input === input) { + this.updateReadonly(input); + } + } + + protected updateReadonly(input: EditorInput): void { + const readOnly = input.hasCapability(EditorInputCapabilities.Readonly); + + this.updateEditorControlOptions({ + readOnly, + enableDropIntoEditor: !readOnly + }); + } + protected getConfigurationOverrides(): ICodeEditorOptions { + const readOnly = this.input?.hasCapability(EditorInputCapabilities.Readonly); + return { overviewRulerLanes: 3, lineNumbersMinChars: 3, fixedOverflowWidgets: true, - readOnly: this.input?.hasCapability(EditorInputCapabilities.Readonly), - // render problems even in readonly editors - // https://github.com/microsoft/vscode/issues/89057 - renderValidationDecorations: 'on' + readOnly, + enableDropIntoEditor: !readOnly, + renderValidationDecorations: 'on' // render problems even in readonly editors (https://github.com/microsoft/vscode/issues/89057) }; } @@ -133,19 +159,19 @@ export abstract class BaseTextEditor<T extends IEditorViewState> extends Abstrac // Create editor control this.editorContainer = parent; - this.editorControl = this._register(this.createEditorControl(parent, this.computeConfiguration(this.textResourceConfigurationService.getValue<IEditorConfiguration>(this.getActiveResource())))); + this.createEditorControl(parent, this.computeConfiguration(this.textResourceConfigurationService.getValue<IEditorConfiguration>(this.getActiveResource()))); // Listeners this.registerCodeEditorListeners(); } private registerCodeEditorListeners(): void { - const codeEditor = getCodeEditor(this.editorControl); - if (codeEditor) { - this._register(codeEditor.onDidChangeModelLanguage(() => this.updateEditorConfiguration())); - this._register(codeEditor.onDidChangeModel(() => this.updateEditorConfiguration())); - this._register(codeEditor.onDidChangeCursorPosition(e => this._onDidChangeSelection.fire({ reason: this.toEditorPaneSelectionChangeReason(e) }))); - this._register(codeEditor.onDidChangeModelContent(() => this._onDidChangeSelection.fire({ reason: EditorPaneSelectionChangeReason.EDIT }))); + const mainControl = this.getMainControl(); + if (mainControl) { + this._register(mainControl.onDidChangeModelLanguage(() => this.updateEditorConfiguration())); + this._register(mainControl.onDidChangeModel(() => this.updateEditorConfiguration())); + this._register(mainControl.onDidChangeCursorPosition(e => this._onDidChangeSelection.fire({ reason: this.toEditorPaneSelectionChangeReason(e) }))); + this._register(mainControl.onDidChangeModelContent(() => this._onDidChangeSelection.fire({ reason: EditorPaneSelectionChangeReason.EDIT }))); } } @@ -159,9 +185,9 @@ export abstract class BaseTextEditor<T extends IEditorViewState> extends Abstrac } getSelection(): IEditorPaneSelection | undefined { - const codeEditor = getCodeEditor(this.editorControl); - if (codeEditor) { - const selection = codeEditor.getSelection(); + const mainControl = this.getMainControl(); + if (mainControl) { + const selection = mainControl.getSelection(); if (selection) { return new TextEditorPaneSelection(selection); } @@ -171,20 +197,34 @@ export abstract class BaseTextEditor<T extends IEditorViewState> extends Abstrac } /** - * This method creates and returns the text editor control to be used. Subclasses can override to - * provide their own editor control that should be used (e.g. a DiffEditor). + * This method creates and returns the text editor control to be used. + * Subclasses must override to provide their own editor control that + * should be used (e.g. a text diff editor). * - * The passed in configuration object should be passed to the editor control when creating it. + * The passed in configuration object should be passed to the editor + * control when creating it. */ - protected createEditorControl(parent: HTMLElement, configuration: ICodeEditorOptions): IEditor { + protected abstract createEditorControl(parent: HTMLElement, initialOptions: ICodeEditorOptions): void; - // Use a getter for the instantiation service since some subclasses might use scoped instantiation services - return this.instantiationService.createInstance(CodeEditorWidget, parent, { enableDropIntoEditor: true, ...configuration }, {}); - } + /** + * The method asks to update the editor control options and is called + * whenever there is change to the options. + */ + protected abstract updateEditorControlOptions(options: ICodeEditorOptions): void; + + /** + * This method returns the main, dominant instance of `ICodeEditor` + * for the editor pane. E.g. for a diff editor, this is the right + * hand (modified) side. + */ + protected abstract getMainControl(): ICodeEditor | undefined; override async setInput(input: EditorInput, options: ITextEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise<void> { await super.setInput(input, options, context, token); + // Update our listener for input capabilities + this.inputListener.value = input.onDidChangeCapabilities(() => this.onDidChangeInputCapabilities(input)); + // Update editor options after having set the input. We do this because there can be // editor input specific options (e.g. an ARIA label depending on the input showing) this.updateEditorConfiguration(); @@ -194,82 +234,26 @@ export abstract class BaseTextEditor<T extends IEditorViewState> extends Abstrac editorContainer.setAttribute('aria-label', this.computeAriaLabel()); } - override setOptions(options: ITextEditorOptions | undefined): void { - super.setOptions(options); + override clearInput(): void { - if (options) { - applyTextEditorOptions(options, assertIsDefined(this.getControl()), ScrollType.Smooth); - } + // Clear input listener + this.inputListener.clear(); + + super.clearInput(); } protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { - - // Pass on to Editor - const editorControl = assertIsDefined(this.editorControl); if (visible) { this.consumePendingConfigurationChangeEvent(); - - editorControl.onVisible(); - } else { - editorControl.onHide(); } super.setEditorVisible(visible, group); } - override focus(): void { - - // Pass on to Editor - const editorControl = assertIsDefined(this.editorControl); - editorControl.focus(); - } - - override hasFocus(): boolean { - if (this.editorControl?.hasTextFocus()) { - return true; - } - - return super.hasFocus(); - } - - layout(dimension: Dimension): void { - - // Pass on to Editor - const editorControl = assertIsDefined(this.editorControl); - editorControl.layout(dimension); - } - - override getControl(): IEditor | undefined { - return this.editorControl; - } - protected override toEditorViewStateResource(input: EditorInput): URI | undefined { return input.resource; } - protected override computeEditorViewState(resource: URI): T | undefined { - const control = this.getControl(); - if (!isCodeEditor(control)) { - return undefined; - } - - const model = control.getModel(); - if (!model) { - return undefined; // view state always needs a model - } - - const modelUri = model.uri; - if (!modelUri) { - return undefined; // model URI is needed to make sure we save the view state correctly - } - - if (!isEqual(modelUri, resource)) { - return undefined; // prevent saving view state for a model that is not the expected one - } - - return withNullAsUndefined(control.saveViewState() as unknown as T); - } - private updateEditorConfiguration(configuration?: IEditorConfiguration): void { if (!configuration) { const resource = this.getActiveResource(); @@ -278,7 +262,7 @@ export abstract class BaseTextEditor<T extends IEditorViewState> extends Abstrac } } - if (!this.editorControl || !configuration) { + if (!configuration) { return; } @@ -295,14 +279,14 @@ export abstract class BaseTextEditor<T extends IEditorViewState> extends Abstrac if (Object.keys(editorSettingsToApply).length > 0) { this.lastAppliedEditorOptions = editorConfiguration; - this.editorControl.updateOptions(editorSettingsToApply); + this.updateEditorControlOptions(editorSettingsToApply); } } private getActiveResource(): URI | undefined { - const codeEditor = getCodeEditor(this.editorControl); - if (codeEditor) { - const model = codeEditor.getModel(); + const mainControl = this.getMainControl(); + if (mainControl) { + const model = mainControl.getModel(); if (model) { return model.uri; } diff --git a/src/vs/workbench/browser/parts/editor/textResourceEditor.ts b/src/vs/workbench/browser/parts/editor/textResourceEditor.ts index 1822c2661df..325a6d1c3f9 100644 --- a/src/vs/workbench/browser/parts/editor/textResourceEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textResourceEditor.ts @@ -3,22 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from 'vs/nls'; import { assertIsDefined, withNullAsUndefined } from 'vs/base/common/types'; -import { ICodeEditor, getCodeEditor, IPasteEvent } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditor, IPasteEvent } from 'vs/editor/browser/editorBrowser'; import { IEditorOpenContext, isTextEditorViewState } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { applyTextEditorOptions } from 'vs/workbench/common/editor/editorOptions'; import { AbstractTextResourceEditorInput, TextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput'; import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; -import { BaseTextEditor } from 'vs/workbench/browser/parts/editor/textEditor'; +import { AbstractTextCodeEditor } from 'vs/workbench/browser/parts/editor/textCodeEditor'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { ScrollType, IEditor, ICodeEditorViewState } from 'vs/editor/common/editorCommon'; +import { ScrollType, ICodeEditorViewState } from 'vs/editor/common/editorCommon'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -28,12 +27,13 @@ import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry' import { EditorOption, IEditorOptions as ICodeEditorOptions } from 'vs/editor/common/config/editorOptions'; import { ModelConstants } from 'vs/editor/common/model'; import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; +import { IFileService } from 'vs/platform/files/common/files'; /** * An editor implementation that is capable of showing the contents of resource inputs. Uses * the TextEditor widget to show the contents. */ -export class AbstractTextResourceEditor extends BaseTextEditor<ICodeEditorViewState> { +export abstract class AbstractTextResourceEditor extends AbstractTextCodeEditor<ICodeEditorViewState> { constructor( id: string, @@ -43,17 +43,10 @@ export class AbstractTextResourceEditor extends BaseTextEditor<ICodeEditorViewSt @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService, @IThemeService themeService: IThemeService, @IEditorGroupsService editorGroupService: IEditorGroupsService, - @IEditorService editorService: IEditorService + @IEditorService editorService: IEditorService, + @IFileService fileService: IFileService ) { - super(id, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService); - } - - override getTitle(): string | undefined { - if (this.input) { - return this.input.getName(); - } - - return localize('textEditor', "Text Editor"); + super(id, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService, fileService); } override async setInput(input: AbstractTextResourceEditorInput, options: ITextEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise<void> { @@ -73,9 +66,9 @@ export class AbstractTextResourceEditor extends BaseTextEditor<ICodeEditorViewSt } // Set Editor Model - const textEditor = assertIsDefined(this.getControl()); + const control = assertIsDefined(this.editorControl); const textEditorModel = resolvedModel.textEditorModel; - textEditor.setModel(textEditorModel); + control.setModel(textEditorModel); // Restore view state (unless provided by options) if (!isTextEditorViewState(options?.viewState)) { @@ -85,13 +78,13 @@ export class AbstractTextResourceEditor extends BaseTextEditor<ICodeEditorViewSt 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 @@ -99,19 +92,23 @@ export class AbstractTextResourceEditor extends BaseTextEditor<ICodeEditorViewSt // 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: resolvedModel.isReadonly() }); + control.updateOptions({ readOnly: resolvedModel.isReadonly() }); } /** * Reveals the last line of this editor if it has a model set. */ revealLastLine(): void { - const codeEditor = <ICodeEditor>this.getControl(); - const model = codeEditor.getModel(); + const control = this.editorControl; + if (!control) { + return; + } + + const model = control.getModel(); if (model) { const lastLine = model.getLineCount(); - codeEditor.revealPosition({ lineNumber: lastLine, column: model.getLineMaxColumn(lastLine) }, ScrollType.Smooth); + control.revealPosition({ lineNumber: lastLine, column: model.getLineMaxColumn(lastLine) }, ScrollType.Smooth); } } @@ -119,10 +116,7 @@ export class AbstractTextResourceEditor extends BaseTextEditor<ICodeEditorViewSt super.clearInput(); // Clear Model - const textEditor = this.getControl(); - if (textEditor) { - textEditor.setModel(null); - } + this.editorControl?.setModel(null); } protected override tracksEditorViewState(input: EditorInput): boolean { @@ -144,22 +138,21 @@ export class TextResourceEditor extends AbstractTextResourceEditor { @IEditorService editorService: IEditorService, @IEditorGroupsService editorGroupService: IEditorGroupsService, @IModelService private readonly modelService: IModelService, - @ILanguageService private readonly languageService: ILanguageService + @ILanguageService private readonly languageService: ILanguageService, + @IFileService fileService: IFileService ) { - super(TextResourceEditor.ID, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorGroupService, editorService); + super(TextResourceEditor.ID, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorGroupService, editorService, fileService); } - protected override createEditorControl(parent: HTMLElement, configuration: ICodeEditorOptions): IEditor { - const control = super.createEditorControl(parent, configuration); + protected override createEditorControl(parent: HTMLElement, configuration: ICodeEditorOptions): void { + super.createEditorControl(parent, configuration); // Install a listener for paste to update this editors // language if the paste includes a specific language - const codeEditor = getCodeEditor(control); - if (codeEditor) { - this._register(codeEditor.onDidPaste(e => this.onDidEditorPaste(e, codeEditor))); + const control = this.editorControl; + if (control) { + this._register(control.onDidPaste(e => this.onDidEditorPaste(e, control))); } - - return control; } private onDidEditorPaste(e: IPasteEvent, codeEditor: ICodeEditor): void { diff --git a/src/vs/workbench/browser/parts/notifications/notificationsActions.ts b/src/vs/workbench/browser/parts/notifications/notificationsActions.ts index 3402b0d5987..b4b01e896e8 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsActions.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsActions.ts @@ -9,7 +9,7 @@ import { localize } from 'vs/nls'; import { Action, IAction, ActionRunner, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from 'vs/base/common/actions'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { CLEAR_NOTIFICATION, EXPAND_NOTIFICATION, COLLAPSE_NOTIFICATION, CLEAR_ALL_NOTIFICATIONS, HIDE_NOTIFICATIONS_CENTER } from 'vs/workbench/browser/parts/notifications/notificationsCommands'; +import { CLEAR_NOTIFICATION, EXPAND_NOTIFICATION, COLLAPSE_NOTIFICATION, CLEAR_ALL_NOTIFICATIONS, HIDE_NOTIFICATIONS_CENTER, TOGGLE_DO_NOT_DISTURB_MODE } from 'vs/workbench/browser/parts/notifications/notificationsCommands'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { Codicon } from 'vs/base/common/codicons'; @@ -23,6 +23,7 @@ const hideIcon = registerIcon('notifications-hide', Codicon.chevronDown, localiz const expandIcon = registerIcon('notifications-expand', Codicon.chevronUp, localize('expandIcon', 'Icon for the expand action in notifications.')); const collapseIcon = registerIcon('notifications-collapse', Codicon.chevronDown, localize('collapseIcon', 'Icon for the collapse action in notifications.')); const configureIcon = registerIcon('notifications-configure', Codicon.gear, localize('configureIcon', 'Icon for the configure action in notifications.')); +const doNotDisturbIcon = registerIcon('notifications-do-not-disturb', Codicon.bellSlash, localize('doNotDisturbIcon', 'Icon for the mute all action in notifications.')); export class ClearNotificationAction extends Action { @@ -60,6 +61,24 @@ export class ClearAllNotificationsAction extends Action { } } +export class ToggleDoNotDisturbAction extends Action { + + static readonly ID = TOGGLE_DO_NOT_DISTURB_MODE; + static readonly LABEL = localize('toggleDoNotDisturbMode', "Toggle Do Not Disturb Mode"); + + constructor( + id: string, + label: string, + @ICommandService private readonly commandService: ICommandService + ) { + super(id, label, ThemeIcon.asClassName(doNotDisturbIcon)); + } + + override async run(): Promise<void> { + this.commandService.executeCommand(TOGGLE_DO_NOT_DISTURB_MODE); + } +} + export class HideNotificationsCenterAction extends Action { static readonly ID = HIDE_NOTIFICATIONS_CENTER; diff --git a/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts b/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts index 56d5ebde2f0..a230bb759e2 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts @@ -19,11 +19,12 @@ import { widgetShadow } from 'vs/platform/theme/common/colorRegistry'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { localize } from 'vs/nls'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; -import { ClearAllNotificationsAction, HideNotificationsCenterAction, NotificationActionRunner } from 'vs/workbench/browser/parts/notifications/notificationsActions'; +import { ClearAllNotificationsAction, HideNotificationsCenterAction, NotificationActionRunner, ToggleDoNotDisturbAction } from 'vs/workbench/browser/parts/notifications/notificationsActions'; import { IAction } from 'vs/base/common/actions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { assertAllDefined, assertIsDefined } from 'vs/base/common/types'; import { NotificationsCenterVisibleContext } from 'vs/workbench/common/contextkeys'; +import { INotificationService } from 'vs/platform/notification/common/notification'; export class NotificationsCenter extends Themable implements INotificationsCenterController { @@ -40,6 +41,7 @@ export class NotificationsCenter extends Themable implements INotificationsCente private workbenchDimensions: Dimension | undefined; private readonly notificationsCenterVisibleContextKey = NotificationsCenterVisibleContext.bindTo(this.contextKeyService); private clearAllAction: ClearAllNotificationsAction | undefined; + private toggleDoNotDisturbAction: ToggleDoNotDisturbAction | undefined; constructor( private readonly container: HTMLElement, @@ -49,7 +51,8 @@ export class NotificationsCenter extends Themable implements INotificationsCente @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, - @IKeybindingService private readonly keybindingService: IKeybindingService + @IKeybindingService private readonly keybindingService: IKeybindingService, + @INotificationService private readonly notificationService: INotificationService, ) { super(themeService); @@ -61,6 +64,11 @@ export class NotificationsCenter extends Themable implements INotificationsCente private registerListeners(): void { this._register(this.model.onDidChangeNotification(e => this.onDidChangeNotification(e))); this._register(this.layoutService.onDidLayout(dimension => this.layout(Dimension.lift(dimension)))); + this._register(this.notificationService.onDidChangeDoNotDisturbMode(() => this.onDidChangeDoNotDisturbMode())); + } + + private onDidChangeDoNotDisturbMode(): void { + this.hide(); // hide the notification center when do not disturb is toggled } get isVisible(): boolean { @@ -154,6 +162,9 @@ export class NotificationsCenter extends Themable implements INotificationsCente this.clearAllAction = this._register(this.instantiationService.createInstance(ClearAllNotificationsAction, ClearAllNotificationsAction.ID, ClearAllNotificationsAction.LABEL)); notificationsToolBar.push(this.clearAllAction, { icon: true, label: false, keybinding: this.getKeybindingLabel(this.clearAllAction) }); + this.toggleDoNotDisturbAction = this._register(this.instantiationService.createInstance(ToggleDoNotDisturbAction, ToggleDoNotDisturbAction.ID, ToggleDoNotDisturbAction.LABEL)); + notificationsToolBar.push(this.toggleDoNotDisturbAction, { icon: true, label: false, keybinding: this.getKeybindingLabel(this.toggleDoNotDisturbAction) }); + const hideAllAction = this._register(this.instantiationService.createInstance(HideNotificationsCenterAction, HideNotificationsCenterAction.ID, HideNotificationsCenterAction.LABEL)); notificationsToolBar.push(hideAllAction, { icon: true, label: false, keybinding: this.getKeybindingLabel(hideAllAction) }); @@ -271,8 +282,8 @@ export class NotificationsCenter extends Themable implements INotificationsCente this.workbenchDimensions = dimension; if (this._isVisible && this.notificationsCenterContainer) { - let maxWidth = NotificationsCenter.MAX_DIMENSIONS.width; - let maxHeight = NotificationsCenter.MAX_DIMENSIONS.height; + const maxWidth = NotificationsCenter.MAX_DIMENSIONS.width; + const maxHeight = NotificationsCenter.MAX_DIMENSIONS.height; let availableWidth = maxWidth; let availableHeight = maxHeight; @@ -316,6 +327,7 @@ export class NotificationsCenter extends Themable implements INotificationsCente } } + registerThemingParticipant((theme, collector) => { const notificationBorderColor = theme.getColor(NOTIFICATIONS_BORDER); if (notificationBorderColor) { diff --git a/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts b/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts index ee0cd3f780c..7649aba7b08 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts @@ -14,6 +14,7 @@ import { IListService, WorkbenchList } from 'vs/platform/list/browser/listServic import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NotificationMetrics, NotificationMetricsClassification, notificationToMetrics } from 'vs/workbench/browser/parts/notifications/notificationsTelemetry'; import { NotificationFocusedContext, NotificationsCenterVisibleContext, NotificationsToastsVisibleContext } from 'vs/workbench/common/contextkeys'; +import { INotificationService } from 'vs/platform/notification/common/notification'; // Center export const SHOW_NOTIFICATIONS_CENTER = 'notifications.showList'; @@ -34,6 +35,7 @@ export const EXPAND_NOTIFICATION = 'notification.expand'; const TOGGLE_NOTIFICATION = 'notification.toggle'; export const CLEAR_NOTIFICATION = 'notification.clear'; export const CLEAR_ALL_NOTIFICATIONS = 'notifications.clearAll'; +export const TOGGLE_DO_NOT_DISTURB_MODE = 'notifications.toggleDoNotDisturbMode'; export interface INotificationsCenterController { readonly isVisible: boolean; @@ -240,13 +242,21 @@ export function registerNotificationCommands(center: INotificationsCenterControl } }); - /// Clear All Notifications + // Clear All Notifications CommandsRegistry.registerCommand(CLEAR_ALL_NOTIFICATIONS, () => center.clearAll()); + // Toggle Do Not Disturb Mode + CommandsRegistry.registerCommand(TOGGLE_DO_NOT_DISTURB_MODE, accessor => { + const notificationService = accessor.get(INotificationService); + + notificationService.doNotDisturbMode = !notificationService.doNotDisturbMode; + }); + // Commands for Command Palette const category = { value: localize('notifications', "Notifications"), original: 'Notifications' }; MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: SHOW_NOTIFICATIONS_CENTER, title: { value: localize('showNotifications', "Show Notifications"), original: 'Show Notifications' }, category } }); MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: HIDE_NOTIFICATIONS_CENTER, title: { value: localize('hideNotifications', "Hide Notifications"), original: 'Hide Notifications' }, category }, when: NotificationsCenterVisibleContext }); MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: CLEAR_ALL_NOTIFICATIONS, title: { value: localize('clearAllNotifications', "Clear All Notifications"), original: 'Clear All Notifications' }, category } }); + MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: TOGGLE_DO_NOT_DISTURB_MODE, title: { value: localize('toggleDoNotDisturbMode', "Toggle Do Not Disturb Mode"), original: 'Toggle Do Not Disturb Mode' }, category } }); MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: FOCUS_NOTIFICATION_TOAST, title: { value: localize('focusNotificationToasts', "Focus Notification Toast"), original: 'Focus Notification Toast' }, category }, when: NotificationsToastsVisibleContext }); } diff --git a/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts b/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts index ec31db99758..d3da50394ad 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts @@ -8,6 +8,7 @@ import { IStatusbarService, StatusbarAlignment, IStatusbarEntryAccessor, IStatus import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; import { HIDE_NOTIFICATIONS_CENTER, SHOW_NOTIFICATIONS_CENTER } from 'vs/workbench/browser/parts/notifications/notificationsCommands'; import { localize } from 'vs/nls'; +import { INotificationService } from 'vs/platform/notification/common/notification'; export class NotificationsStatus extends Disposable { @@ -21,7 +22,8 @@ export class NotificationsStatus extends Disposable { constructor( private readonly model: INotificationsModel, - @IStatusbarService private readonly statusbarService: IStatusbarService + @IStatusbarService private readonly statusbarService: IStatusbarService, + @INotificationService private readonly notificationService: INotificationService ) { super(); @@ -37,6 +39,7 @@ export class NotificationsStatus extends Disposable { private registerListeners(): void { this._register(this.model.onDidChangeNotification(e => this.onDidChangeNotification(e))); this._register(this.model.onDidChangeStatusMessage(e => this.onDidChangeStatusMessage(e))); + this._register(this.notificationService.onDidChangeDoNotDisturbMode(() => this.updateNotificationsCenterStatusItem())); } private onDidChangeNotification(e: INotificationChangeEvent): void { @@ -69,8 +72,9 @@ export class NotificationsStatus extends Disposable { } } - // Show the bell with a dot if there are unread or in-progress notifications - const statusProperties: IStatusbarEntry = { + // Show the status bar entry depending on do not disturb setting + + let statusProperties: IStatusbarEntry = { name: localize('status.notifications', "Notifications"), text: `${notificationsInProgress > 0 || this.newNotificationsCount > 0 ? '$(bell-dot)' : '$(bell)'}`, ariaLabel: localize('status.notifications', "Notifications"), @@ -79,6 +83,16 @@ export class NotificationsStatus extends Disposable { showBeak: this.isNotificationsCenterVisible }; + if (this.notificationService.doNotDisturbMode) { + statusProperties = { + ...statusProperties, + name: localize('status.doNotDisturb', "Do Not Disturb"), + text: `${notificationsInProgress > 0 || this.newNotificationsCount > 0 ? '$(bell-slash-dot)' : '$(bell-slash)'}`, + ariaLabel: localize('status.doNotDisturb', "Do Not Disturb"), + tooltip: localize('status.doNotDisturbTooltip', "Do Not Disturb Mode is Enabled") + }; + } + if (!this.notificationsCenterStatusItem) { this.notificationsCenterStatusItem = this.statusbarService.addEntry( statusProperties, diff --git a/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts b/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts index 5a5517629d4..ab589ba87e0 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts @@ -367,9 +367,7 @@ export class NotificationsToasts extends Themable implements INotificationsToast } private doHide(): void { - if (this.notificationsToastsContainer) { - this.notificationsToastsContainer.classList.remove('visible'); - } + this.notificationsToastsContainer?.classList.remove('visible'); // Context Key this.notificationsToastsVisibleContextKey.set(false); @@ -525,7 +523,7 @@ export class NotificationsToasts extends Themable implements INotificationsToast } private computeMaxDimensions(): Dimension { - let maxWidth = NotificationsToasts.MAX_WIDTH; + const maxWidth = NotificationsToasts.MAX_WIDTH; let availableWidth = maxWidth; let availableHeight: number | undefined; diff --git a/src/vs/workbench/browser/parts/panel/panelActions.ts b/src/vs/workbench/browser/parts/panel/panelActions.ts index 8663a928675..e64439239c5 100644 --- a/src/vs/workbench/browser/parts/panel/panelActions.ts +++ b/src/vs/workbench/browser/parts/panel/panelActions.ts @@ -69,7 +69,7 @@ class FocusPanelAction extends Action { } // Focus into active panel - let panel = this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.Panel); + const panel = this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.Panel); if (panel) { panel.focus(); } diff --git a/src/vs/workbench/browser/parts/panel/panelPart.ts b/src/vs/workbench/browser/parts/panel/panelPart.ts index 453dd8c5f11..2263ec0769e 100644 --- a/src/vs/workbench/browser/parts/panel/panelPart.ts +++ b/src/vs/workbench/browser/parts/panel/panelPart.ts @@ -45,7 +45,7 @@ import { IPaneCompositePart, IPaneCompositeSelectorPart } from 'vs/workbench/bro import { IPartOptions } from 'vs/workbench/browser/part'; import { StringSHA1 } from 'vs/base/common/hash'; import { URI } from 'vs/base/common/uri'; -import { Extensions, IProfileStorageRegistry } from 'vs/workbench/services/profiles/common/profileStorageRegistry'; +import { Extensions, IProfileStorageRegistry } from 'vs/workbench/services/userDataProfile/common/userDataProfileStorageRegistry'; interface ICachedPanel { id: string; @@ -417,7 +417,7 @@ export abstract class BasePanelPart extends CompositePart<PaneComposite> impleme this._register(this.onDidPaneCompositeClose(this.onPanelClose, this)); // Extension registration - let disposables = this._register(new DisposableStore()); + const disposables = this._register(new DisposableStore()); this._register(this.extensionService.onDidRegisterExtensions(() => { disposables.clear(); this.onDidRegisterExtensions(); @@ -717,9 +717,7 @@ export abstract class BasePanelPart extends CompositePart<PaneComposite> impleme private emptyPanelMessageElement: HTMLElement | undefined; private layoutEmptyMessage(): void { - if (this.emptyPanelMessageElement) { - this.emptyPanelMessageElement.classList.toggle('visible', this.compositeBar.getVisibleComposites().length === 0); - } + this.emptyPanelMessageElement?.classList.toggle('visible', this.compositeBar.getVisibleComposites().length === 0); } private getViewContainer(id: string): ViewContainer | undefined { @@ -731,9 +729,7 @@ export abstract class BasePanelPart extends CompositePart<PaneComposite> impleme const primaryActions = this.globalActions.getPrimaryActions(); const secondaryActions = this.globalActions.getSecondaryActions(); - if (this.globalToolBar) { - this.globalToolBar.setActions(prepareActions(primaryActions), prepareActions(secondaryActions)); - } + this.globalToolBar?.setActions(prepareActions(primaryActions), prepareActions(secondaryActions)); } private getCompositeActions(compositeId: string): { activityAction: PanelActivityAction; pinnedAction: ToggleCompositePinnedAction } { @@ -787,7 +783,7 @@ export abstract class BasePanelPart extends CompositePart<PaneComposite> impleme } private onDidStorageValueChange(e: IStorageValueChangeEvent): void { - if (e.key === this.pinnedPanelsKey && e.scope === StorageScope.GLOBAL + if (e.key === this.pinnedPanelsKey && e.scope === StorageScope.PROFILE && this.cachedPanelsValue !== this.getStoredCachedPanelsValue() /* This checks if current window changed the value or not */) { this._cachedPanelsValue = undefined; const newCompositeItems: ICompositeBarItem[] = []; @@ -827,6 +823,8 @@ export abstract class BasePanelPart extends CompositePart<PaneComposite> impleme const viewContainerModel = this.viewDescriptorService.getViewContainerModel(viewContainer); state.push({ id: compositeItem.id, name: viewContainerModel.title, pinned: compositeItem.pinned, order: compositeItem.order, visible: compositeItem.visible }); placeholders.push({ id: compositeItem.id, name: this.getCompositeActions(compositeItem.id).activityAction.label }); + } else { + state.push({ id: compositeItem.id, name: compositeItem.name, pinned: compositeItem.pinned, order: compositeItem.order, visible: compositeItem.visible }); } } @@ -872,11 +870,11 @@ export abstract class BasePanelPart extends CompositePart<PaneComposite> impleme } private getStoredCachedPanelsValue(): string { - return this.storageService.get(this.pinnedPanelsKey, StorageScope.GLOBAL, '[]'); + return this.storageService.get(this.pinnedPanelsKey, StorageScope.PROFILE, '[]'); } private setStoredCachedViewletsValue(value: string): void { - this.storageService.store(this.pinnedPanelsKey, value, StorageScope.GLOBAL, StorageTarget.USER); + this.storageService.store(this.pinnedPanelsKey, value, StorageScope.PROFILE, StorageTarget.USER); } private getPlaceholderViewContainers(): IPlaceholderViewContainer[] { diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts b/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts index f3ea7a8e5f6..1f5f723f8f0 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts @@ -9,7 +9,7 @@ import { hide, show, isAncestor } from 'vs/base/browser/dom'; import { IStorageService, StorageScope, IStorageValueChangeEvent, StorageTarget } from 'vs/platform/storage/common/storage'; import { Emitter } from 'vs/base/common/event'; import { Registry } from 'vs/platform/registry/common/platform'; -import { Extensions, IProfileStorageRegistry } from 'vs/workbench/services/profiles/common/profileStorageRegistry'; +import { Extensions, IProfileStorageRegistry } from 'vs/workbench/services/userDataProfile/common/userDataProfileStorageRegistry'; import { localize } from 'vs/nls'; export interface IStatusbarEntryPriority { @@ -76,7 +76,7 @@ export class StatusbarViewModel extends Disposable { } private restoreState(): void { - const hiddenRaw = this.storageService.get(StatusbarViewModel.HIDDEN_ENTRIES_KEY, StorageScope.GLOBAL); + const hiddenRaw = this.storageService.get(StatusbarViewModel.HIDDEN_ENTRIES_KEY, StorageScope.PROFILE); if (hiddenRaw) { try { const hiddenArray: string[] = JSON.parse(hiddenRaw); @@ -92,7 +92,7 @@ export class StatusbarViewModel extends Disposable { } private onDidStorageValueChange(event: IStorageValueChangeEvent): void { - if (event.key === StatusbarViewModel.HIDDEN_ENTRIES_KEY && event.scope === StorageScope.GLOBAL) { + if (event.key === StatusbarViewModel.HIDDEN_ENTRIES_KEY && event.scope === StorageScope.PROFILE) { // Keep current hidden entries const currentlyHidden = new Set(this.hidden); @@ -281,9 +281,9 @@ export class StatusbarViewModel extends Disposable { private saveState(): void { if (this.hidden.size > 0) { - this.storageService.store(StatusbarViewModel.HIDDEN_ENTRIES_KEY, JSON.stringify(Array.from(this.hidden.values())), StorageScope.GLOBAL, StorageTarget.USER); + this.storageService.store(StatusbarViewModel.HIDDEN_ENTRIES_KEY, JSON.stringify(Array.from(this.hidden.values())), StorageScope.PROFILE, StorageTarget.USER); } else { - this.storageService.remove(StatusbarViewModel.HIDDEN_ENTRIES_KEY, StorageScope.GLOBAL); + this.storageService.remove(StatusbarViewModel.HIDDEN_ENTRIES_KEY, StorageScope.PROFILE); } } @@ -406,13 +406,9 @@ export class StatusbarViewModel extends Disposable { } // Mark: first visible item - if (firstVisibleItem) { - firstVisibleItem.container.classList.add('first-visible-item'); - } + firstVisibleItem?.container.classList.add('first-visible-item'); // Mark: last visible item - if (lastVisibleItem) { - lastVisibleItem.container.classList.add('last-visible-item'); - } + lastVisibleItem?.container.classList.add('last-visible-item'); } } diff --git a/src/vs/workbench/browser/parts/titlebar/titleMenuControl.ts b/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts index 77e1ebb32eb..de2f74a7b2f 100644 --- a/src/vs/workbench/browser/parts/titlebar/titleMenuControl.ts +++ b/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts @@ -16,7 +16,6 @@ import { assertType } from 'vs/base/common/types'; import { localize } from 'vs/nls'; import { createActionViewItem, createAndFillInContextMenuActions, MenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -24,10 +23,9 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import * as colors from 'vs/platform/theme/common/colorRegistry'; import { WindowTitle } from 'vs/workbench/browser/parts/titlebar/windowTitle'; -import { MENUBAR_SELECTION_BACKGROUND, MENUBAR_SELECTION_FOREGROUND, TITLE_BAR_ACTIVE_FOREGROUND } from 'vs/workbench/common/theme'; -import { IHoverService } from 'vs/workbench/services/hover/browser/hover'; +import { MENUBAR_SELECTION_BACKGROUND, MENUBAR_SELECTION_FOREGROUND, PANEL_BORDER, TITLE_BAR_ACTIVE_FOREGROUND } from 'vs/workbench/common/theme'; -export class TitleMenuControl { +export class CommandCenterControl { private readonly _disposables = new DisposableStore(); @@ -38,34 +36,15 @@ export class TitleMenuControl { constructor( windowTitle: WindowTitle, + hoverDelegate: IHoverDelegate, @IContextMenuService contextMenuService: IContextMenuService, @IContextKeyService contextKeyService: IContextKeyService, @IInstantiationService instantiationService: IInstantiationService, @IMenuService menuService: IMenuService, @IQuickInputService quickInputService: IQuickInputService, - @IHoverService hoverService: IHoverService, - @IConfigurationService configurationService: IConfigurationService, @IKeybindingService keybindingService: IKeybindingService, ) { - this.element.classList.add('title-menu'); - - const hoverDelegate = new class implements IHoverDelegate { - - private _lastHoverHideTime: number = 0; - - readonly showHover = hoverService.showHover.bind(hoverService); - readonly placement = 'element'; - - get delay(): number { - return Date.now() - this._lastHoverHideTime < 200 - ? 0 // show instantly when a hover was recently shown - : configurationService.getValue<number>('workbench.hover.delay'); - } - - onDidHideHover() { - this._lastHoverHideTime = Date.now(); - } - }; + this.element.classList.add('command-center'); const titleToolbar = new ToolBar(this.element, contextMenuService, { actionViewItemProvider: (action) => { @@ -99,6 +78,9 @@ export class TitleMenuControl { // label: just workspace name and optional decorations const { prefix, suffix } = windowTitle.getTitleDecorations(); let label = windowTitle.workspaceName; + if (!label) { + label = localize('label.dfl', "Search"); + } if (prefix) { label = localize('label1', "{0} {1}", prefix, label); } @@ -119,7 +101,7 @@ export class TitleMenuControl { const container = document.createElement('span'); container.classList.add('all-options'); parent.appendChild(container); - const action = new Action('all', localize('all', "Show Quick Pick Options..."), Codicon.chevronDown.classNames, true, () => { + const action = new Action('all', localize('all', "Show Search Modes..."), Codicon.chevronDown.classNames, true, () => { quickInputService.quickAccess.show('?'); }); const dropdown = new ActionViewItem(undefined, action, { icon: true, label: false, hoverDelegate }); @@ -134,16 +116,19 @@ export class TitleMenuControl { return createActionViewItem(instantiationService, action, { hoverDelegate }); } }); - const titleMenu = this._disposables.add(menuService.createMenu(MenuId.TitleMenu, contextKeyService)); - const titleMenuDisposables = this._disposables.add(new DisposableStore()); - const updateTitleMenu = () => { - titleMenuDisposables.clear(); + const menu = this._disposables.add(menuService.createMenu(MenuId.CommandCenter, contextKeyService)); + const menuDisposables = this._disposables.add(new DisposableStore()); + const menuUpdater = () => { + menuDisposables.clear(); const actions: IAction[] = []; - titleMenuDisposables.add(createAndFillInContextMenuActions(titleMenu, undefined, actions)); + menuDisposables.add(createAndFillInContextMenuActions(menu, undefined, actions)); titleToolbar.setActions(actions); }; - updateTitleMenu(); - this._disposables.add(titleMenu.onDidChange(updateTitleMenu)); + menuUpdater(); + this._disposables.add(menu.onDidChange(menuUpdater)); + this._disposables.add(keybindingService.onDidUpdateKeybindings(() => { + menuUpdater(); + })); this._disposables.add(quickInputService.onShow(this._setVisibility.bind(this, false))); this._disposables.add(quickInputService.onHide(this._setVisibility.bind(this, true))); } @@ -162,34 +147,33 @@ export class TitleMenuControl { // foreground (inactive and active) colors.registerColor( - 'titleMenu.foreground', + 'commandCenter.foreground', { dark: TITLE_BAR_ACTIVE_FOREGROUND, hcDark: TITLE_BAR_ACTIVE_FOREGROUND, light: TITLE_BAR_ACTIVE_FOREGROUND, hcLight: TITLE_BAR_ACTIVE_FOREGROUND }, - localize('titleMenu-foreground', "Foreground color of the title menu"), + localize('commandCenter-foreground', "Foreground color of the command center"), false ); colors.registerColor( - 'titleMenu.activeForeground', + 'commandCenter.activeForeground', { dark: MENUBAR_SELECTION_FOREGROUND, hcDark: MENUBAR_SELECTION_FOREGROUND, light: MENUBAR_SELECTION_FOREGROUND, hcLight: MENUBAR_SELECTION_FOREGROUND }, - localize('titleMenu-activeForeground', "Active foreground color of the title menu"), + localize('commandCenter-activeForeground', "Active foreground color of the command center"), false ); // background (inactive and active) colors.registerColor( - 'titleMenu.background', + 'commandCenter.background', { dark: null, hcDark: null, light: null, hcLight: null }, - localize('titleMenu-background', "Background color of the title menu"), + localize('commandCenter-background', "Background color of the command center"), false ); -const activeBackground = colors.registerColor( - 'titleMenu.activeBackground', +colors.registerColor( + 'commandCenter.activeBackground', { dark: MENUBAR_SELECTION_BACKGROUND, hcDark: MENUBAR_SELECTION_BACKGROUND, light: MENUBAR_SELECTION_BACKGROUND, hcLight: MENUBAR_SELECTION_BACKGROUND }, - localize('titleMenu-activeBackground', "Active background color of the title menu"), + localize('commandCenter-activeBackground', "Active background color of the command center"), false ); // border: defaults to active background colors.registerColor( - 'titleMenu.border', - { dark: activeBackground, hcDark: colors.inputBorder, light: activeBackground, hcLight: colors.inputBorder }, - localize('titleMenu-border', "Border color of the title menu"), + 'commandCenter.border', { dark: PANEL_BORDER, hcDark: PANEL_BORDER, light: PANEL_BORDER, hcLight: PANEL_BORDER }, + localize('commandCenter-border', "Border color of the command center"), false ); diff --git a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css index 8b516b544a7..b661db98d5c 100644 --- a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css +++ b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css @@ -41,11 +41,17 @@ .monaco-workbench.web .part.titlebar>.titlebar-container, .monaco-workbench.windows .part.titlebar>.titlebar-container, .monaco-workbench.linux .part.titlebar>.titlebar-container { - height: 30px; line-height: 22px; justify-content: left; } +.monaco-workbench.web.safari .part.titlebar, +.monaco-workbench.web.safari .part.titlebar>.titlebar-container { + /* Must be scoped to safari due to #148851 */ + /* Is required in safari due to #149476 */ + overflow: visible; +} + /* Draggable region */ .monaco-workbench .part.titlebar>.titlebar-container>.titlebar-drag-region { top: 0; @@ -80,65 +86,65 @@ } /* Window Title Menu */ -.monaco-workbench .part.titlebar>.titlebar-container>.window-title>.title-menu { - z-index: 3000; +.monaco-workbench .part.titlebar>.titlebar-container>.window-title>.command-center { + z-index: 2500; -webkit-app-region: no-drag; padding: 0 8px; } -.monaco-workbench .part.titlebar>.titlebar-container>.window-title>.title-menu.hide { +.monaco-workbench .part.titlebar>.titlebar-container>.window-title>.command-center.hide { display: none; } -.monaco-workbench .part.titlebar>.titlebar-container>.window-title>.title-menu .action-item.quickopen { +.monaco-workbench .part.titlebar>.titlebar-container>.window-title>.command-center .action-item.quickopen { display: flex; - color: var(--vscode-titleMenu-foreground); - background-color: var(--vscode-titleMenu-background); - border: 1px solid var(--vscode-titleMenu-border); + color: var(--vscode-commandCenter-foreground); + background-color: var(--vscode-commandCenter-background); + border: 1px solid var(--vscode-commandCenter-border); border-radius: 5px; height: 20px; line-height: 18px; width: 38vw; max-width: 600px; - margin: 4px 4px; + min-width: 32px; + margin: 0 4px; flex-direction: row; justify-content: center; overflow: hidden; } -.monaco-workbench .part.titlebar>.titlebar-container>.window-title>.title-menu .action-item.quickopen:HOVER { - color: var(--vscode-titleMenu-activeForeground); - background-color: var(--vscode-titleMenu-activeBackground); +.monaco-workbench .part.titlebar>.titlebar-container>.window-title>.command-center .action-item.quickopen:HOVER { + color: var(--vscode-commandCenter-activeForeground); + background-color: var(--vscode-commandCenter-activeBackground); line-height: 18px; } -.monaco-workbench .part.titlebar>.titlebar-container>.window-title>.title-menu:HOVER .quickopen .action-label { +.monaco-workbench .part.titlebar>.titlebar-container>.window-title>.command-center:HOVER .quickopen .action-label { background-color: transparent !important; outline-color: transparent !important; } -.monaco-workbench .part.titlebar>.titlebar-container>.window-title>.title-menu .action-item.quickopen>.action-label.search { +.monaco-workbench .part.titlebar>.titlebar-container>.window-title>.command-center .action-item.quickopen>.action-label.search { display: inline-flex; text-align: center; font-size: 12px; - line-height: 14px; background-color: inherit; justify-content: center; width: calc(100% - 19px); } -.monaco-workbench .part.titlebar>.titlebar-container>.window-title>.title-menu .action-item.quickopen>.action-label.search>.search-icon { +.monaco-workbench .part.titlebar>.titlebar-container>.window-title>.command-center .action-item.quickopen>.action-label.search>.search-icon { font-size: 14px; opacity: .8; - padding: 0 3px; + margin: auto 3px; } -.monaco-workbench .part.titlebar>.titlebar-container>.window-title>.title-menu .action-item.quickopen>.action-label.search>.search-label { +.monaco-workbench .part.titlebar>.titlebar-container>.window-title>.command-center .action-item.quickopen>.action-label.search>.search-label { overflow: hidden; text-overflow: ellipsis; } -.monaco-workbench .part.titlebar>.titlebar-container>.window-title>.title-menu .action-item.quickopen>.all-options>.action-label { +.monaco-workbench .part.titlebar>.titlebar-container>.window-title>.command-center .action-item.quickopen>.all-options>.action-label { text-align: center; font-size: 12px; width: 16px; @@ -147,8 +153,8 @@ padding-right: 0; } -.monaco-workbench .part.titlebar>.titlebar-container>.window-title>.title-menu:HOVER .action-item.quickopen>.all-options>.action-label { - border-color: var(--vscode-titleMenu-border); +.monaco-workbench .part.titlebar>.titlebar-container>.window-title>.command-center:HOVER .action-item.quickopen>.all-options>.action-label { + border-color: var(--vscode-commandCenter-border); } /* Menubar */ @@ -156,6 +162,11 @@ /* move menubar above drag region as negative z-index on drag region cause greyscale AA */ z-index: 2500; min-width: 36px; + flex-wrap: nowrap; +} + +.monaco-workbench .part.titlebar>.titlebar-container.counter-zoom > .menubar .menubar-menu-button > .menubar-menu-items-holder.monaco-menu-container { + zoom: var(--zoom-factor); } /* Resizer */ @@ -222,7 +233,7 @@ text-align: center; z-index: 3000; -webkit-app-region: no-drag; - height: 30px; + height: 100%; width: 138px; zoom: calc(1 / var(--zoom-factor)); } @@ -240,13 +251,19 @@ /* Window Control Icons */ .monaco-workbench .part.titlebar>.window-controls-container>.window-icon { - display: inline-block; - line-height: 30px; + display: flex; + justify-content: center; + align-items: center; height: 100%; width: 46px; font-size: 16px; } +.monaco-workbench .part.titlebar>.window-controls-container>.window-icon::before { + height: 16px; + line-height: 16px; +} + .monaco-workbench .part.titlebar>.window-controls-container>.window-icon:hover { background-color: rgba(255, 255, 255, 0.1); } @@ -271,15 +288,14 @@ flex-shrink: 0; text-align: center; position: relative; - z-index: 3000; + z-index: 2500; -webkit-app-region: no-drag; height: 100%; margin-left: auto; min-width: 28px; } -.monaco-workbench.mac:not(.web) .part.titlebar>.layout-controls-container { - position: absolute; +.monaco-workbench.mac:not(.web) .part.titlebar>.titlebar-container>.layout-controls-container { right: 8px; } diff --git a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts index 88e69dc8ceb..6c0a24014c8 100644 --- a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts +++ b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts @@ -223,7 +223,7 @@ export abstract class MenubarControl extends Disposable { } protected calculateActionLabel(action: { id: string; label: string }): string { - let label = action.label; + const label = action.label; switch (action.id) { default: break; @@ -349,7 +349,7 @@ export abstract class MenubarControl extends Disposable { return; } - const hasBeenNotified = this.storageService.getBoolean('menubar/accessibleMenubarNotified', StorageScope.GLOBAL, false); + const hasBeenNotified = this.storageService.getBoolean('menubar/accessibleMenubarNotified', StorageScope.APPLICATION, false); const usingCustomMenubar = getTitleBarStyle(this.configurationService) === 'custom'; if (hasBeenNotified || usingCustomMenubar || !this.accessibilityService.isScreenReaderOptimized()) { @@ -366,7 +366,7 @@ export abstract class MenubarControl extends Disposable { } ]); - this.storageService.store('menubar/accessibleMenubarNotified', true, StorageScope.GLOBAL, StorageTarget.USER); + this.storageService.store('menubar/accessibleMenubarNotified', true, StorageScope.APPLICATION, StorageTarget.USER); } } @@ -433,8 +433,8 @@ export class CustomMenubarControl extends MenubarControl { const activityBarInactiveFgColor = theme.getColor(ACTIVITY_BAR_INACTIVE_FOREGROUND); if (activityBarInactiveFgColor) { collector.addRule(` - .monaco-workbench .menubar.compact > .menubar-menu-button, - .monaco-workbench .menubar.compact .toolbar-toggle-more { + .monaco-workbench .activitybar .menubar.compact > .menubar-menu-button, + .monaco-workbench .activitybar .menubar.compact .toolbar-toggle-more { color: ${activityBarInactiveFgColor}; } `); @@ -443,12 +443,12 @@ export class CustomMenubarControl extends MenubarControl { const activityBarFgColor = theme.getColor(ACTIVITY_BAR_FOREGROUND); if (activityBarFgColor) { collector.addRule(` - .monaco-workbench .menubar.compact > .menubar-menu-button.open, - .monaco-workbench .menubar.compact > .menubar-menu-button:focus, - .monaco-workbench .menubar.compact:not(:focus-within) > .menubar-menu-button:hover, - .monaco-workbench .menubar.compact > .menubar-menu-button.open .toolbar-toggle-more, - .monaco-workbench .menubar.compact > .menubar-menu-button:focus .toolbar-toggle-more, - .monaco-workbench .menubar.compact:not(:focus-within) > .menubar-menu-button:hover .toolbar-toggle-more { + .monaco-workbench .activitybar .menubar.compact > .menubar-menu-button.open, + .monaco-workbench .activitybar .menubar.compact > .menubar-menu-button:focus, + .monaco-workbench .activitybar .menubar.compact:not(:focus-within) > .menubar-menu-button:hover, + .monaco-workbench .activitybar .menubar.compact > .menubar-menu-button.open .toolbar-toggle-more, + .monaco-workbench .activitybar .menubar.compact > .menubar-menu-button:focus .toolbar-toggle-more, + .monaco-workbench .activitybar .menubar.compact:not(:focus-within) > .menubar-menu-button:hover .toolbar-toggle-more { color: ${activityBarFgColor}; } `); @@ -481,9 +481,9 @@ export class CustomMenubarControl extends MenubarControl { const menubarSelectedBgColor = theme.getColor(MENUBAR_SELECTION_BACKGROUND); if (menubarSelectedBgColor) { collector.addRule(` - .monaco-workbench .menubar:not(.compact) > .menubar-menu-button.open, - .monaco-workbench .menubar:not(.compact) > .menubar-menu-button:focus, - .monaco-workbench .menubar:not(:focus-within):not(.compact) > .menubar-menu-button:hover { + .monaco-workbench .menubar:not(.compact) > .menubar-menu-button.open .menubar-menu-title, + .monaco-workbench .menubar:not(.compact) > .menubar-menu-button:focus .menubar-menu-title, + .monaco-workbench .menubar:not(:focus-within):not(.compact) > .menubar-menu-button:hover .menubar-menu-title { background-color: ${menubarSelectedBgColor}; } `); @@ -492,19 +492,20 @@ export class CustomMenubarControl extends MenubarControl { const menubarSelectedBorderColor = theme.getColor(MENUBAR_SELECTION_BORDER); if (menubarSelectedBorderColor) { collector.addRule(` - .monaco-workbench .menubar > .menubar-menu-button:hover { + .monaco-workbench .menubar > .menubar-menu-button:hover .menubar-menu-title { outline: dashed 1px; } - .monaco-workbench .menubar > .menubar-menu-button.open, - .monaco-workbench .menubar > .menubar-menu-button:focus { + .monaco-workbench .menubar > .menubar-menu-button.open .menubar-menu-title, + .monaco-workbench .menubar > .menubar-menu-button:focus .menubar-menu-title { outline: solid 1px; } - .monaco-workbench .menubar > .menubar-menu-button.open, - .monaco-workbench .menubar > .menubar-menu-button:focus, - .monaco-workbench .menubar > .menubar-menu-button:hover { + .monaco-workbench .menubar > .menubar-menu-button.open .menubar-menu-title, + .monaco-workbench .menubar > .menubar-menu-button:focus .menubar-menu-title, + .monaco-workbench .menubar > .menubar-menu-button:hover .menubar-menu-title { outline-color: ${menubarSelectedBorderColor}; + outline-offset: -1px; } `); } @@ -580,8 +581,19 @@ export class CustomMenubarControl extends MenubarControl { return getMenuBarVisibility(this.configurationService); } + private get currentCommandCenterEnabled(): boolean { + const settingValue = this.configurationService.getValue<boolean>('window.commandCenter'); + + let enableCommandCenter = false; + if (typeof settingValue === 'boolean') { + enableCommandCenter = !!settingValue; + } + + return enableCommandCenter; + } + private get currentDisableMenuBarAltFocus(): boolean { - let settingValue = this.configurationService.getValue<boolean>('window.customMenuBarAltFocus'); + const settingValue = this.configurationService.getValue<boolean>('window.customMenuBarAltFocus'); let disableMenuBarAltBehavior = false; if (typeof settingValue === 'boolean') { @@ -625,9 +637,15 @@ export class CustomMenubarControl extends MenubarControl { private get currentCompactMenuMode(): Direction | undefined { if (this.currentMenubarVisibility !== 'compact') { + // With the command center enabled, use compact menu in title bar and flow to the right + if (this.currentCommandCenterEnabled) { + return Direction.Down; + } + return undefined; } + // Menu bar lives in activity bar and should flow based on its location const currentSidebarLocation = this.configurationService.getValue<string>('workbench.sideBar.location'); return currentSidebarLocation === 'right' ? Direction.Left : Direction.Right; } @@ -680,6 +698,11 @@ export class CustomMenubarControl extends MenubarControl { })); this.reinstallDisposables.add(attachMenuStyler(this.menubar, this.themeService)); + + // Fire visibility change for the first install if menu is shown + if (this.menubar.isVisible) { + this.onDidVisibilityChange(true); + } } else { this.menubar?.update(this.getMenuBarOptions()); } @@ -687,12 +710,12 @@ export class CustomMenubarControl extends MenubarControl { // Update the menu actions const updateActions = (menu: IMenu, target: IAction[], topLevelTitle: string) => { target.splice(0); - let groups = menu.getActions(); + const groups = menu.getActions(); - for (let group of groups) { + for (const group of groups) { const [, actions] = group; - for (let action of actions) { + for (const action of actions) { this.insertActionsBefore(action, target); // use mnemonicTitle whenever possible @@ -751,9 +774,7 @@ export class CustomMenubarControl extends MenubarControl { if (!this.focusInsideMenubar) { const actions: IAction[] = []; updateActions(menu, actions, title); - if (this.menubar) { - this.menubar.updateMenu({ actions: actions, label: mnemonicMenuLabel(this.topLevelTitles[title]) }); - } + this.menubar?.updateMenu({ actions: actions, label: mnemonicMenuLabel(this.topLevelTitles[title]) }); } })); @@ -763,9 +784,7 @@ export class CustomMenubarControl extends MenubarControl { if (!this.focusInsideMenubar) { const actions: IAction[] = []; updateActions(menu, actions, title); - if (this.menubar) { - this.menubar.updateMenu({ actions: actions, label: mnemonicMenuLabel(this.topLevelTitles[title]) }); - } + this.menubar?.updateMenu({ actions: actions, label: mnemonicMenuLabel(this.topLevelTitles[title]) }); } })); } diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index e35c289cb8e..2c7855bd9cc 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -33,11 +33,13 @@ import { Codicon } from 'vs/base/common/codicons'; import { getIconRegistry } from 'vs/platform/theme/common/iconRegistry'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; import { WindowTitle } from 'vs/workbench/browser/parts/titlebar/windowTitle'; -import { TitleMenuControl } from 'vs/workbench/browser/parts/titlebar/titleMenuControl'; +import { CommandCenterControl } from 'vs/workbench/browser/parts/titlebar/commandCenterControl'; +import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; +import { IHoverService } from 'vs/workbench/services/hover/browser/hover'; export class TitlebarPart extends Part implements ITitleService { - private static readonly configTitleMenu = 'window.experimental.titleMenu'; + private static readonly configCommandCenter = 'window.commandCenter'; declare readonly _serviceBrand: undefined; @@ -45,7 +47,10 @@ export class TitlebarPart extends Part implements ITitleService { readonly minimumWidth: number = 0; readonly maximumWidth: number = Number.POSITIVE_INFINITY; - get minimumHeight(): number { return 30 / (this.currentMenubarVisibility === 'hidden' || getZoomFactor() < 1 ? getZoomFactor() : 1); } + get minimumHeight(): number { + const value = this.isCommandCenterVisible ? 35 : 30; + return value / (this.currentMenubarVisibility === 'hidden' || getZoomFactor() < 1 ? getZoomFactor() : 1); + } get maximumHeight(): number { return this.minimumHeight; } //#endregion @@ -53,8 +58,8 @@ export class TitlebarPart extends Part implements ITitleService { private _onMenubarVisibilityChange = this._register(new Emitter<boolean>()); readonly onMenubarVisibilityChange = this._onMenubarVisibilityChange.event; - private readonly _onDidChangeTitleMenuVisibility = new Emitter<void>(); - readonly onDidChangeTitleMenuVisibility: Event<void> = this._onDidChangeTitleMenuVisibility.event; + private readonly _onDidChangeCommandCenterVisibility = new Emitter<void>(); + readonly onDidChangeCommandCenterVisibility: Event<void> = this._onDidChangeCommandCenterVisibility.event; protected rootContainer!: HTMLElement; protected windowControls: HTMLElement | undefined; @@ -68,6 +73,8 @@ export class TitlebarPart extends Part implements ITitleService { private layoutToolbar: ToolBar | undefined; protected lastLayoutDimensions: Dimension | undefined; + private hoverDelegate: IHoverDelegate; + private readonly titleDisposables = this._register(new DisposableStore()); private titleBarStyle: 'native' | 'custom'; @@ -88,6 +95,7 @@ export class TitlebarPart extends Part implements ITitleService { @IMenuService private readonly menuService: IMenuService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IHostService private readonly hostService: IHostService, + @IHoverService hoverService: IHoverService, ) { super(Parts.TITLEBAR_PART, { hasTitle: false }, themeService, storageService, layoutService); this.windowTitle = this._register(instantiationService.createInstance(WindowTitle)); @@ -95,6 +103,24 @@ export class TitlebarPart extends Part implements ITitleService { this.titleBarStyle = getTitleBarStyle(this.configurationService); + this.hoverDelegate = new class implements IHoverDelegate { + + private _lastHoverHideTime: number = 0; + + readonly showHover = hoverService.showHover.bind(hoverService); + readonly placement = 'element'; + + get delay(): number { + return Date.now() - this._lastHoverHideTime < 200 + ? 0 // show instantly when a hover was recently shown + : configurationService.getValue<number>('workbench.hover.delay'); + } + + onDidHideHover() { + this._lastHoverHideTime = Date.now(); + } + }; + this.registerListeners(); } @@ -102,8 +128,8 @@ export class TitlebarPart extends Part implements ITitleService { this.windowTitle.updateProperties(properties); } - get titleMenuVisible() { - return this.configurationService.getValue<boolean>(TitlebarPart.configTitleMenu); + get isCommandCenterVisible() { + return this.configurationService.getValue<boolean>(TitlebarPart.configCommandCenter); } private registerListeners(): void { @@ -131,22 +157,32 @@ export class TitlebarPart extends Part implements ITitleService { this.installMenubar(); } } + + // Trigger a re-install of the menubar with command center change + if (event.affectsConfiguration('window.commandCenter')) { + if (this.currentMenubarVisibility !== 'compact') { + this.uninstallMenubar(); + this.installMenubar(); + } + } } if (this.titleBarStyle !== 'native' && this.layoutControls && event.affectsConfiguration('workbench.layoutControl.enabled')) { this.layoutControls.classList.toggle('show-layout-control', this.layoutControlEnabled); } - if (event.affectsConfiguration(TitlebarPart.configTitleMenu)) { + if (event.affectsConfiguration(TitlebarPart.configCommandCenter)) { this.updateTitle(); this.adjustTitleMarginToCenter(); - this._onDidChangeTitleMenuVisibility.fire(); + this._onDidChangeCommandCenterVisibility.fire(); } } protected onMenubarVisibilityChanged(visible: boolean): void { if (isWeb || isWindows || isLinux) { - this.adjustTitleMarginToCenter(); + if (this.lastLayoutDimensions) { + this.layout(this.lastLayoutDimensions.width, this.lastLayoutDimensions.height); + } this._onMenubarVisibilityChange.fire(visible); } @@ -163,6 +199,8 @@ export class TitlebarPart extends Part implements ITitleService { this.menubar.remove(); this.menubar = undefined; } + + this.onMenubarVisibilityChanged(false); } protected installMenubar(): void { @@ -176,14 +214,14 @@ export class TitlebarPart extends Part implements ITitleService { this.menubar = this.rootContainer.insertBefore($('div.menubar'), this.title); this.menubar.setAttribute('role', 'menubar'); - this.customMenubar.create(this.menubar); - this._register(this.customMenubar.onVisibilityChange(e => this.onMenubarVisibilityChanged(e))); + + this.customMenubar.create(this.menubar); } private updateTitle(): void { this.titleDisposables.clear(); - if (!this.titleMenuVisible) { + if (!this.isCommandCenterVisible) { // Text Title this.title.innerText = this.windowTitle.value; this.titleDisposables.add(this.windowTitle.onDidChange(() => { @@ -192,10 +230,10 @@ export class TitlebarPart extends Part implements ITitleService { })); } else { // Menu Title - const titleMenu = this.instantiationService.createInstance(TitleMenuControl, this.windowTitle); - reset(this.title, titleMenu.element); - this.titleDisposables.add(titleMenu); - this.titleDisposables.add(titleMenu.onDidChangeVisibility(this.adjustTitleMarginToCenter, this)); + const commandCenter = this.instantiationService.createInstance(CommandCenterControl, this.windowTitle, this.hoverDelegate); + reset(this.title, commandCenter.element); + this.titleDisposables.add(commandCenter); + this.titleDisposables.add(commandCenter.onDidChangeVisibility(this.adjustTitleMarginToCenter, this)); } } @@ -241,7 +279,7 @@ export class TitlebarPart extends Part implements ITitleService { this.layoutToolbar = new ToolBar(this.layoutControls, this.contextMenuService, { actionViewItemProvider: action => { - return createActionViewItem(this.instantiationService, action); + return createActionViewItem(this.instantiationService, action, { hoverDelegate: this.hoverDelegate }); }, allowContextMenu: true }); diff --git a/src/vs/workbench/browser/parts/titlebar/windowTitle.ts b/src/vs/workbench/browser/parts/titlebar/windowTitle.ts index 1920790f520..5f5e16ead26 100644 --- a/src/vs/workbench/browser/parts/titlebar/windowTitle.ts +++ b/src/vs/workbench/browser/parts/titlebar/windowTitle.ts @@ -109,7 +109,7 @@ export class WindowTitle extends Disposable { private getWindowTitle(): string { let title = this.doGetWindowTitle() || this.productService.nameLong; - let { prefix, suffix } = this.getTitleDecorations(); + const { prefix, suffix } = this.getTitleDecorations(); if (prefix) { title = `${prefix} ${title}`; } diff --git a/src/vs/workbench/browser/parts/views/treeView.ts b/src/vs/workbench/browser/parts/views/treeView.ts index 9d2c5d0dab0..67efd200c19 100644 --- a/src/vs/workbench/browser/parts/views/treeView.ts +++ b/src/vs/workbench/browser/parts/views/treeView.ts @@ -31,7 +31,7 @@ import { isString } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import 'vs/css!./media/views'; -import { IDataTransfer } from 'vs/base/common/dataTransfer'; +import { VSDataTransfer } from 'vs/base/common/dataTransfer'; import { Command } from 'vs/editor/common/languages'; import { localize } from 'vs/nls'; import { createActionViewItem, createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; @@ -54,7 +54,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { focusBorder, listFilterMatchHighlight, listFilterMatchHighlightBorder, textCodeBlockBackground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; import { ColorScheme } from 'vs/platform/theme/common/theme'; import { FileThemeIcon, FolderThemeIcon, IThemeService, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { convertResourceUrlsToUriList, DraggedTreeItemsIdentifier, fillEditorsDragData, LocalSelectionTransfer } from 'vs/workbench/browser/dnd'; +import { DraggedTreeItemsIdentifier, fillEditorsDragData, LocalSelectionTransfer } from 'vs/workbench/browser/dnd'; import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; import { API_OPEN_DIFF_EDITOR_COMMAND_ID, API_OPEN_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; @@ -64,9 +64,9 @@ import { Extensions, ITreeItem, ITreeItemLabel, ITreeView, ITreeViewDataProvider import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IHoverService } from 'vs/workbench/services/hover/browser/hover'; -import { ThemeSettings } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { ITreeViewsService } from 'vs/workbench/services/views/browser/treeViewsService'; import { CodeDataTransfers } from 'vs/platform/dnd/browser/dnd'; +import { addExternalEditorsDropData, toVSDataTransfer } from 'vs/editor/browser/dnd'; export class TreeViewPane extends ViewPane { @@ -722,9 +722,7 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { this._width = width; const treeHeight = height - DOM.getTotalHeight(this.messageElement); this.treeContainer.style.height = treeHeight + 'px'; - if (this.tree) { - this.tree.layout(treeHeight, width); - } + this.tree?.layout(treeHeight, width); } } @@ -780,9 +778,7 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { } setSelection(items: ITreeItem[]): void { - if (this.tree) { - this.tree.setSelection(items); - } + this.tree?.setSelection(items); } setFocus(item: ITreeItem): void { @@ -825,7 +821,10 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { const isTreeEmpty = !this.root.children || this.root.children.length === 0; // Hide tree container only when there is a message and tree is empty and not refreshing if (this._messageValue && isTreeEmpty && !this.refreshing) { - this.treeContainer.classList.add('hide'); + // If there's a dnd controller then hiding the tree prevents it from being dragged into. + if (!this.dragAndDropController) { + this.treeContainer.classList.add('hide'); + } this.domNode.setAttribute('tabindex', '0'); } else { this.treeContainer.classList.remove('hide'); @@ -1015,7 +1014,7 @@ class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyS return ({ start, end }); }) : undefined; const icon = this.themeService.getColorTheme().type === ColorScheme.LIGHT ? node.icon : node.iconDark; - const iconUrl = icon ? URI.revive(icon) : null; + const iconUrl = icon ? URI.revive(icon) : undefined; const title = this.getHover(label, resource, node); // reset @@ -1028,7 +1027,7 @@ class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyS templateData.resourceLabel.setResource({ name: label, description, resource: labelResource }, { fileKind: this.getFileKind(node), title, - hideIcon: !!iconUrl || this.shouldShowThemeIcon(!!resource, node.themeIcon), + hideIcon: this.shouldHideResourceLabelIcon(iconUrl, node.themeIcon), fileDecorations, extraClasses: ['custom-view-tree-node-item-resourceLabel'], matches: matches ? matches : createMatches(element.filterData), @@ -1049,8 +1048,6 @@ class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyS templateData.icon.style.backgroundImage = DOM.asCSSUrl(iconUrl); } else { let iconClass: string | undefined; - // If there is a resource for this tree item then we should respect the file icon theme's choice about - // whether to show a folder icon. if (this.shouldShowThemeIcon(!!resource, node.themeIcon)) { iconClass = ThemeIcon.asClassName(node.themeIcon); if (node.themeIcon.color) { @@ -1083,25 +1080,20 @@ class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyS container.parentElement!.classList.toggle('align-icon-with-twisty', this.aligner.alignIconWithTwisty(treeItem)); } + private shouldHideResourceLabelIcon(iconUrl: URI | undefined, icon: ThemeIcon | undefined): boolean { + // We always hide the resource label in favor of the iconUrl when it's provided. + // When `ThemeIcon` is provided, we hide the resource label icon in favor of it only if it's a not a file icon. + return (!!iconUrl || (!!icon && !this.isFileKindThemeIcon(icon))); + } + private shouldShowThemeIcon(hasResource: boolean, icon: ThemeIcon | undefined): icon is ThemeIcon { if (!icon) { return false; } - if (hasResource && (this.isFileKindThemeIcon(icon) || !this.shouldShowFileIcons())) { - return false; - } else if (hasResource && (this.isFolderThemeIcon(icon) || !this.shouldShowFolderIcons())) { - return false; - } - return true; - } - - private shouldShowFileIcons(): boolean { - return this.configurationService.getValue(ThemeSettings.FILE_ICON_THEME); - } - - private shouldShowFolderIcons(): boolean { - return this.themeService.getFileIconTheme().hasFolderIcons && this.shouldShowFileIcons(); + // If there's a resource and the icon is a file icon, then the icon (or lack thereof) will already be coming from the + // icon theme and should use whatever the icon theme has provided. + return !(hasResource && this.isFileKindThemeIcon(icon)); } private isFolderThemeIcon(icon: ThemeIcon | undefined): boolean { @@ -1340,8 +1332,6 @@ interface TreeDragSourceInfo { itemHandles: string[]; } -const INTERNAL_MIME_TYPES = [CodeDataTransfers.EDITORS.toLowerCase(), CodeDataTransfers.FILES.toLowerCase()]; - export class CustomTreeViewDragAndDrop implements ITreeDragAndDrop<ITreeItem> { private readonly treeMimeType: string; private readonly treeItemsTransfer = LocalSelectionTransfer.getInstance<DraggedTreeItemsIdentifier>(); @@ -1361,7 +1351,7 @@ export class CustomTreeViewDragAndDrop implements ITreeDragAndDrop<ITreeItem> { this.dndController = controller; } - private handleDragAndLog(dndController: ITreeViewDragAndDropController, itemHandles: string[], uuid: string, dragCancellationToken: CancellationToken): Promise<IDataTransfer | undefined> { + private handleDragAndLog(dndController: ITreeViewDragAndDropController, itemHandles: string[], uuid: string, dragCancellationToken: CancellationToken): Promise<VSDataTransfer | undefined> { return dndController.handleDrag(itemHandles, uuid, dragCancellationToken).then(additionalDataTransfer => { if (additionalDataTransfer) { const unlistedTypes: string[] = []; @@ -1440,14 +1430,23 @@ export class CustomTreeViewDragAndDrop implements ITreeDragAndDrop<ITreeItem> { } onDragOver(data: IDragAndDropData, targetElement: ITreeItem, targetIndex: number, originalEvent: DragEvent): boolean | ITreeDragOverReaction { - const types: Set<string> = new Set(); - originalEvent.dataTransfer?.types.forEach((value, index) => { - if (INTERNAL_MIME_TYPES.indexOf(value) < 0) { - types.add(this.convertKnownMimes(value).type); + const dataTransfer = toVSDataTransfer(originalEvent.dataTransfer!); + addExternalEditorsDropData(dataTransfer, originalEvent); + + const types = new Set<string>(Array.from(dataTransfer.entries()).map(x => x[0])); + + if (originalEvent.dataTransfer) { + // Also add uri-list if we have any files. At this stage we can't actually access the file itself though. + for (const item of originalEvent.dataTransfer.items) { + if (item.kind === 'file' || item.type === DataTransfers.RESOURCES.toLowerCase()) { + types.add(Mimes.uriList); + break; + } } - }); + } this.debugLog(types); + const dndController = this.dndController; if (!dndController || !originalEvent.dataTransfer || (dndController.dropMimeTypes.length === 0)) { return false; @@ -1483,104 +1482,42 @@ export class CustomTreeViewDragAndDrop implements ITreeDragAndDrop<ITreeItem> { return element.label ? element.label.label : (element.resourceUri ? this.labelService.getUriLabel(URI.revive(element.resourceUri)) : undefined); } - private convertKnownMimes(type: string, kind?: string, value?: string | FileSystemHandle): { type: string; value?: string } { - let convertedValue = undefined; - let convertedType = type; - if (type === DataTransfers.RESOURCES.toLowerCase()) { - convertedValue = value ? convertResourceUrlsToUriList(value as string) : undefined; - convertedType = Mimes.uriList; - } else if ((type === 'Files') || (kind === 'file')) { - convertedType = Mimes.uriList; - convertedValue = value ? (value as FileSystemHandle).name : undefined; - } - return { type: convertedType, value: convertedValue }; - } - async drop(data: IDragAndDropData, targetNode: ITreeItem | undefined, targetIndex: number | undefined, originalEvent: DragEvent): Promise<void> { const dndController = this.dndController; if (!originalEvent.dataTransfer || !dndController) { return; } - const treeDataTransfer: IDataTransfer = new Map(); - const uris: URI[] = []; - let itemsCount = Array.from(originalEvent.dataTransfer.items).reduce((previous, current) => { - if ((current.kind === 'string') || (current.kind === 'file')) { - return previous + 1; - } - return previous; - }, 0); let treeSourceInfo: TreeDragSourceInfo | undefined; let willDropUuid: string | undefined; if (this.treeItemsTransfer.hasData(DraggedTreeItemsIdentifier.prototype)) { willDropUuid = this.treeItemsTransfer.getData(DraggedTreeItemsIdentifier.prototype)![0].identifier; } - await new Promise<void>(resolve => { - function decrementStringCount() { - itemsCount--; - if (itemsCount === 0) { - // Check if there are uris to add and add them - if (uris.length) { - treeDataTransfer.set(Mimes.uriList, { - asString: () => Promise.resolve(uris.map(uri => uri.toString()).join('\n')), - asFile: () => undefined, - value: undefined - }); - } - resolve(); - } - } - if (!originalEvent.dataTransfer) { - return; - } - for (const dataItem of originalEvent.dataTransfer.items) { - const type = dataItem.type; - const kind = dataItem.kind; - const convertedType = this.convertKnownMimes(type, kind).type; - if ((INTERNAL_MIME_TYPES.indexOf(convertedType) < 0) - && (convertedType === this.treeMimeType) || (dndController.dropMimeTypes.indexOf(convertedType) >= 0)) { - if (dataItem.kind === 'string') { - dataItem.getAsString(dataValue => { - if (convertedType === this.treeMimeType) { - treeSourceInfo = JSON.parse(dataValue); - } - if (dataValue) { - const converted = this.convertKnownMimes(type, kind, dataValue); - treeDataTransfer.set(converted.type, { - asString: () => Promise.resolve(converted.value!), - asFile: () => undefined, - value: undefined - }); - } - decrementStringCount(); - }); - } else if (dataItem.kind === 'file') { - const dataValue = dataItem.getAsFile(); - if (dataValue) { - uris.push(URI.file(dataValue.path)); - } - decrementStringCount(); + const originalDataTransfer = toVSDataTransfer(originalEvent.dataTransfer); + addExternalEditorsDropData(originalDataTransfer, originalEvent, true); + + const outDataTransfer = new VSDataTransfer(); + for (const [type, item] of originalDataTransfer.entries()) { + if (type === this.treeMimeType || dndController.dropMimeTypes.includes(type) || (item.asFile() && dndController.dropMimeTypes.includes(DataTransfers.FILES.toLowerCase()))) { + outDataTransfer.append(type, item); + if (type === this.treeMimeType) { + try { + treeSourceInfo = JSON.parse(await item.asString()); + } catch { + // noop } - } else if (dataItem.kind === 'string' || dataItem.kind === 'file') { - decrementStringCount(); } } - }); - - const additionalWillDropPromise = this.treeViewsDragAndDropService.removeDragOperationTransfer(willDropUuid); - if (!additionalWillDropPromise) { - return dndController.handleDrop(treeDataTransfer, targetNode, new CancellationTokenSource().token, willDropUuid, treeSourceInfo?.id, treeSourceInfo?.itemHandles); } - return additionalWillDropPromise.then(additionalDataTransfer => { - if (additionalDataTransfer) { - for (const item of additionalDataTransfer.entries()) { - treeDataTransfer.set(item[0], item[1]); - } - } - return dndController.handleDrop(treeDataTransfer, targetNode, new CancellationTokenSource().token, willDropUuid, treeSourceInfo?.id, treeSourceInfo?.itemHandles); - }); + const additionalDataTransfer = await this.treeViewsDragAndDropService.removeDragOperationTransfer(willDropUuid); + if (additionalDataTransfer) { + for (const [type, item] of additionalDataTransfer.entries()) { + outDataTransfer.append(type, item); + } + } + return dndController.handleDrop(outDataTransfer, targetNode, CancellationToken.None, willDropUuid, treeSourceInfo?.id, treeSourceInfo?.itemHandles); } onDragEnd(originalEvent: DragEvent): void { diff --git a/src/vs/workbench/browser/parts/views/viewPane.ts b/src/vs/workbench/browser/parts/views/viewPane.ts index 80be8b96724..b8b9f5046ce 100644 --- a/src/vs/workbench/browser/parts/views/viewPane.ts +++ b/src/vs/workbench/browser/parts/views/viewPane.ts @@ -51,6 +51,7 @@ export interface IViewPaneOptions extends IPaneOptions { } type WelcomeActionClassification = { + owner: 'joaomoreno'; viewId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; uri: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; }; diff --git a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts index be75bf31cd1..1223f47b6a8 100644 --- a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts +++ b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts @@ -580,7 +580,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { event.stopPropagation(); event.preventDefault(); - let anchor: { x: number; y: number } = { x: event.posx, y: event.posy }; + const anchor: { x: number; y: number } = { x: event.posx, y: event.posy }; this.contextMenuService.showContextMenu({ getAnchor: () => anchor, getActions: () => this.menuActions?.getContextMenuActions() ?? [] @@ -741,7 +741,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { const actions: IAction[] = viewPane.menuActions.getContextMenuActions(); - let anchor: { x: number; y: number } = { x: event.posx, y: event.posy }; + const anchor: { x: number; y: number } = { x: event.posx, y: event.posy }; this.contextMenuService.showContextMenu({ getAnchor: () => anchor, getActions: () => actions @@ -820,11 +820,6 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { // Check if view is active if (this.viewContainerModel.activeViewDescriptors.some(viewDescriptor => viewDescriptor.id === viewId)) { const visible = !this.viewContainerModel.isVisible(viewId); - type ViewsToggleVisibilityClassification = { - viewId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; - visible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; - }; - this.telemetryService.publicLog2<{ viewId: String; visible: boolean }, ViewsToggleVisibilityClassification>('views.toggleVisibility', { viewId, visible }); this.viewContainerModel.setVisible(viewId, visible); } } @@ -966,7 +961,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { if (viewsToMove.length > 1) { viewsToMove.slice(1).forEach(view => { let toIndex = this.panes.findIndex(p => p.id === anchorView!.id); - let fromIndex = this.panes.findIndex(p => p.id === view.id); + const fromIndex = this.panes.findIndex(p => p.id === view.id); if (fromIndex >= 0 && toIndex >= 0) { if (fromIndex > toIndex) { toIndex++; diff --git a/src/vs/workbench/browser/parts/views/viewsViewlet.ts b/src/vs/workbench/browser/parts/views/viewsViewlet.ts index 324056503c0..77ec32db719 100644 --- a/src/vs/workbench/browser/parts/views/viewsViewlet.ts +++ b/src/vs/workbench/browser/parts/views/viewsViewlet.ts @@ -62,7 +62,7 @@ export abstract class FilterViewPaneContainer extends ViewPaneContainer { private updateAllViews(viewDescriptors: ReadonlyArray<IViewDescriptor>) { viewDescriptors.forEach(descriptor => { - let filterOnValue = this.getFilterOn(descriptor); + const filterOnValue = this.getFilterOn(descriptor); if (!filterOnValue) { return; } diff --git a/src/vs/workbench/browser/web.api.ts b/src/vs/workbench/browser/web.api.ts index f41ce51c118..637b1af74cd 100644 --- a/src/vs/workbench/browser/web.api.ts +++ b/src/vs/workbench/browser/web.api.ts @@ -110,6 +110,16 @@ export interface IWorkbench { * has been persisted. */ shutdown: () => Promise<void>; + + /** + * Forwards a port. If the current embedder implements a tunnelFactory then that will be used to make the tunnel. + * By default, openTunnel only support localhost; however, a tunnelFactory can be used to support other ips. + * + * @throws When run in an environment without a remote. + * + * @param tunnelOptions The `localPort` is a suggestion only. If that port is not available another will be chosen. + */ + openTunnel(tunnelOptions: ITunnelOptions): Thenable<ITunnel>; } export interface IWorkbenchConstructionOptions { @@ -160,6 +170,11 @@ export interface IWorkbenchConstructionOptions { readonly codeExchangeProxyEndpoints?: { [providerId: string]: string }; /** + * The identifier of an edit session associated with the current workspace. + */ + readonly editSessionId?: string; + + /** * [TEMPORARY]: This will be removed soon. * Endpoints to be used for proxying repository tarball download calls in the browser. */ @@ -236,7 +251,7 @@ export interface IWorkbenchConstructionOptions { readonly commands?: readonly ICommand[]; /** - * Optional default layout to apply on first time the workspace is opened (uness `force` is specified). + * Optional default layout to apply on first time the workspace is opened (unless `force` is specified). */ readonly defaultLayout?: IDefaultLayout; diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index b8dd198abc8..e83eb19ce63 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -31,8 +31,8 @@ import { WorkspaceService } from 'vs/workbench/services/configuration/browser/co import { ConfigurationCache } from 'vs/workbench/services/configuration/common/configurationCache'; import { ISignService } from 'vs/platform/sign/common/sign'; import { SignService } from 'vs/platform/sign/browser/signService'; -import { IWorkbenchConstructionOptions, IWorkbench } from 'vs/workbench/browser/web.api'; -import { BrowserStorageService } from 'vs/platform/storage/browser/storageService'; +import { IWorkbenchConstructionOptions, IWorkbench, ITunnel } from 'vs/workbench/browser/web.api'; +import { BrowserStorageService } from 'vs/workbench/services/storage/browser/storageService'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { BufferLogService } from 'vs/platform/log/common/bufferLog'; import { FileLogger } from 'vs/platform/log/common/fileLog'; @@ -73,6 +73,13 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IProgressService } from 'vs/platform/progress/common/progress'; import { DelayedLogChannel } from 'vs/workbench/services/output/common/delayedLogChannel'; import { dirname, joinPath } from 'vs/base/common/resources'; +import { IUserDataProfilesService, UserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { NullPolicyService } from 'vs/platform/policy/common/policy'; +import { IRemoteExplorerService, TunnelSource } from 'vs/workbench/services/remote/common/remoteExplorerService'; +import { DisposableTunnel } from 'vs/platform/tunnel/common/tunnel'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { UserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfileService'; +import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; export class BrowserMain extends Disposable { @@ -132,6 +139,8 @@ export class BrowserMain extends Disposable { const progessService = accessor.get(IProgressService); const environmentService = accessor.get(IBrowserWorkbenchEnvironmentService); const instantiationService = accessor.get(IInstantiationService); + const remoteExplorerService = accessor.get(IRemoteExplorerService); + const labelService = accessor.get(ILabelService); const embedderLogger = instantiationService.createInstance(DelayedLogChannel, 'webEmbedder', productService.embedderIdentifier || localize('vscode.dev', "vscode.dev"), joinPath(dirname(environmentService.logFile), `webEmbedder.log`)); @@ -161,7 +170,26 @@ export class BrowserMain extends Disposable { window: { withProgress: (options, task) => progessService.withProgress(options, task) }, - shutdown: () => lifecycleService.shutdown() + shutdown: () => lifecycleService.shutdown(), + openTunnel: async (tunnelOptions) => { + const tunnel = await remoteExplorerService.forward({ + remote: tunnelOptions.remoteAddress, + local: tunnelOptions.localAddressPort, + name: tunnelOptions.label, + source: { + source: TunnelSource.Extension, + description: labelService.getHostLabel(Schemas.vscodeRemote, this.configuration.remoteAuthority) + }, + elevateIfNeeded: false + }); + if (!tunnel) { + throw new Error('cannot open tunnel'); + } + + return new class extends DisposableTunnel implements ITunnel { + override localAddress!: string; + }({ port: tunnel.tunnelRemotePort, host: tunnel.tunnelRemoteHost }, tunnel.localAddress, () => tunnel.dispose()); + } }; }); } @@ -231,13 +259,20 @@ export class BrowserMain extends Disposable { serviceCollection.set(IWorkbenchFileService, fileService); await this.registerFileSystemProviders(environmentService, fileService, remoteAgentService, logService, logsPath); + // User Data Profiles + const userDataProfilesService = new UserDataProfilesService(environmentService, fileService, logService); + serviceCollection.set(IUserDataProfilesService, userDataProfilesService); + + const userDataProfileService = new UserDataProfileService(userDataProfilesService.defaultProfile); + serviceCollection.set(IUserDataProfileService, userDataProfileService); + // URI Identity const uriIdentityService = new UriIdentityService(fileService); serviceCollection.set(IUriIdentityService, uriIdentityService); // Long running services (workspace, config, storage) const [configurationService, storageService] = await Promise.all([ - this.createWorkspaceService(payload, environmentService, fileService, remoteAgentService, uriIdentityService, logService).then(service => { + this.createWorkspaceService(payload, environmentService, userDataProfileService, userDataProfilesService, fileService, remoteAgentService, uriIdentityService, logService).then(service => { // Workspace serviceCollection.set(IWorkspaceContextService, service); @@ -248,7 +283,7 @@ export class BrowserMain extends Disposable { return service; }), - this.createStorageService(payload, logService).then(service => { + this.createStorageService(payload, logService, userDataProfileService).then(service => { // Storage serviceCollection.set(IStorageService, service); @@ -302,7 +337,7 @@ export class BrowserMain extends Disposable { serviceCollection.set(ICredentialsService, credentialsService); // Userdata Initialize Service - const userDataInitializationService = new UserDataInitializationService(environmentService, credentialsService, userDataSyncStoreManagementService, fileService, storageService, productService, requestService, logService, uriIdentityService); + const userDataInitializationService = new UserDataInitializationService(environmentService, credentialsService, userDataSyncStoreManagementService, fileService, userDataProfilesService, storageService, productService, requestService, logService, uriIdentityService); serviceCollection.set(IUserDataInitializationService, userDataInitializationService); if (await userDataInitializationService.requiresInitialization()) { @@ -420,8 +455,8 @@ export class BrowserMain extends Disposable { }); } - private async createStorageService(payload: IAnyWorkspaceIdentifier, logService: ILogService): Promise<IStorageService> { - const storageService = new BrowserStorageService(payload, logService); + private async createStorageService(payload: IAnyWorkspaceIdentifier, logService: ILogService, userDataProfileService: IUserDataProfileService): Promise<IStorageService> { + const storageService = new BrowserStorageService(payload, userDataProfileService, logService); try { await storageService.initialize(); @@ -438,9 +473,9 @@ export class BrowserMain extends Disposable { } } - private async createWorkspaceService(payload: IAnyWorkspaceIdentifier, environmentService: IWorkbenchEnvironmentService, fileService: FileService, remoteAgentService: IRemoteAgentService, uriIdentityService: IUriIdentityService, logService: ILogService): Promise<WorkspaceService> { + private async createWorkspaceService(payload: IAnyWorkspaceIdentifier, environmentService: IWorkbenchEnvironmentService, userDataProfileService: IUserDataProfileService, userDataProfilesService: IUserDataProfilesService, fileService: FileService, remoteAgentService: IRemoteAgentService, uriIdentityService: IUriIdentityService, logService: ILogService): Promise<WorkspaceService> { const configurationCache = new ConfigurationCache([Schemas.file, Schemas.vscodeUserData, Schemas.tmp] /* Cache all non native resources */, environmentService, fileService); - const workspaceService = new WorkspaceService({ remoteAuthority: this.configuration.remoteAuthority, configurationCache }, environmentService, fileService, remoteAgentService, uriIdentityService, logService); + const workspaceService = new WorkspaceService({ remoteAuthority: this.configuration.remoteAuthority, configurationCache }, environmentService, userDataProfileService, userDataProfilesService, fileService, remoteAgentService, uriIdentityService, logService, new NullPolicyService()); try { await workspaceService.initialize(payload); diff --git a/src/vs/workbench/browser/window.ts b/src/vs/workbench/browser/window.ts index 7d15b8bf58f..254bdc18153 100644 --- a/src/vs/workbench/browser/window.ts +++ b/src/vs/workbench/browser/window.ts @@ -6,6 +6,7 @@ import { isSafari, setFullscreen } from 'vs/base/browser/browser'; import { addDisposableListener, addDisposableThrottledListener, detectFullscreen, EventHelper, EventType, windowOpenNoOpener, windowOpenPopup, windowOpenWithSuccess } from 'vs/base/browser/dom'; import { DomEmitter } from 'vs/base/browser/event'; +import { HidDeviceData, requestHidDevice, requestSerialPort, requestUsbDevice, SerialPortData, UsbDeviceData } from 'vs/base/browser/deviceAccess'; import { timeout } from 'vs/base/common/async'; import { Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -14,8 +15,10 @@ import { isIOS, isMacintosh } from 'vs/base/common/platform'; import Severity from 'vs/base/common/severity'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { registerWindowDriver } from 'vs/platform/driver/browser/driver'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ILabelService } from 'vs/platform/label/common/label'; import { IOpenerService, matchesScheme } from 'vs/platform/opener/common/opener'; import { IProductService } from 'vs/platform/product/common/productService'; @@ -120,6 +123,9 @@ export class BrowserWindow extends Disposable { // Label formatting this.registerLabelFormatters(); + // Commands + this.registerCommands(); + // Smoke Test Driver this.setupDriver(); } @@ -227,7 +233,7 @@ export class BrowserWindow extends Disposable { }); } - private registerLabelFormatters() { + private registerLabelFormatters(): void { this._register(this.labelService.registerFormatter({ scheme: Schemas.vscodeUserData, priority: true, @@ -237,4 +243,22 @@ export class BrowserWindow extends Disposable { } })); } + + private registerCommands(): void { + + // Allow extensions to request USB devices in Web + CommandsRegistry.registerCommand('workbench.experimental.requestUsbDevice', async (_accessor: ServicesAccessor, options?: { filters?: unknown[] }): Promise<UsbDeviceData | undefined> => { + return requestUsbDevice(options); + }); + + // Allow extensions to request Serial devices in Web + CommandsRegistry.registerCommand('workbench.experimental.requestSerialPort', async (_accessor: ServicesAccessor, options?: { filters?: unknown[] }): Promise<SerialPortData | undefined> => { + return requestSerialPort(options); + }); + + // Allow extensions to request HID devices in Web + CommandsRegistry.registerCommand('workbench.experimental.requestHidDevice', async (_accessor: ServicesAccessor, options?: { filters?: unknown[] }): Promise<HidDeviceData | undefined> => { + return requestHidDevice(options); + }); + } } diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index a0adbe40cb2..9c75b526575 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -7,15 +7,19 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { localize } from 'vs/nls'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { isMacintosh, isWindows, isLinux, isWeb, isNative } from 'vs/base/common/platform'; -import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration'; +import { ConfigurationMigrationWorkbenchContribution, workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration'; import { isStandalone } from 'vs/base/browser/browser'; -import 'vs/workbench/common/configurationMigration'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; const registry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration); // Configuration (function registerConfiguration(): void { + // Migration support + Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(ConfigurationMigrationWorkbenchContribution, LifecyclePhase.Eventually); + // Workbench registry.registerConfiguration({ ...workbenchConfigurationNodeBase, @@ -92,7 +96,7 @@ const registry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Con 'type': 'string', 'enum': ['text', 'hidden'], 'default': 'text', - 'markdownDescription': localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'untitledHint' }, "Controls if the untitled hint should be inline text in the editor or a floating button or hidden.") + 'markdownDescription': localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'untitledHint' }, "Controls if the untitled text hint should be visible in the editor.") }, 'workbench.editor.languageDetection': { type: 'boolean', @@ -538,10 +542,10 @@ const registry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Con 'default': isMacintosh ? ' \u2014 ' : ' - ', 'markdownDescription': localize("window.titleSeparator", "Separator used by `window.title`.") }, - 'window.experimental.titleMenu': { + 'window.commandCenter': { type: 'boolean', default: false, - description: localize('window.experimental.titleMenu', "Show window title as menu") + markdownDescription: localize('window.commandCenter', "Show command launcher together with the window title. This setting only has an effect when `#window.titleBarStyle#` is set to `custom`.") }, 'window.menuBarVisibility': { 'type': 'string', @@ -673,7 +677,7 @@ const registry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Con 'zenMode.silentNotifications': { 'type': 'boolean', 'default': true, - 'description': localize('zenMode.silentNotifications', "Controls whether notifications are shown while in zen mode. If true, only error notifications will pop out.") + 'description': localize('zenMode.silentNotifications', "Controls whether notifications do not disturb mode should be enabled while in zen mode. If true, only error notifications will pop out.") } } }); diff --git a/src/vs/workbench/browser/workbench.ts b/src/vs/workbench/browser/workbench.ts index 27c50770822..8094c87aca2 100644 --- a/src/vs/workbench/browser/workbench.ts +++ b/src/vs/workbench/browser/workbench.ts @@ -198,7 +198,7 @@ export class Workbench extends Layout { // All Contributed Services const contributedServices = getSingletonServiceDescriptors(); - for (let [id, descriptor] of contributedServices) { + for (const [id, descriptor] of contributedServices) { serviceCollection.set(id, descriptor); } @@ -284,7 +284,7 @@ export class Workbench extends Layout { } private restoreFontInfo(storageService: IStorageService, configurationService: IConfigurationService): void { - const storedFontInfoRaw = storageService.get('editorFontInfo', StorageScope.GLOBAL); + const storedFontInfoRaw = storageService.get('editorFontInfo', StorageScope.APPLICATION); if (storedFontInfoRaw) { try { const storedFontInfo = JSON.parse(storedFontInfoRaw); @@ -302,7 +302,7 @@ export class Workbench extends Layout { private storeFontInfo(storageService: IStorageService): void { const serializedFontInfo = FontMeasurements.serializeFontInfo(); if (serializedFontInfo) { - storageService.store('editorFontInfo', JSON.stringify(serializedFontInfo), StorageScope.GLOBAL, StorageTarget.MACHINE); + storageService.store('editorFontInfo', JSON.stringify(serializedFontInfo), StorageScope.APPLICATION, StorageTarget.MACHINE); } } @@ -408,7 +408,7 @@ export class Workbench extends Layout { } // Transition into restored phase after layout has restored - // but do not wait indefinitly on this to account for slow + // but do not wait indefinitely on this to account for slow // editors restoring. Since the workbench is fully functional // even when the visible editors have not resolved, we still // want contributions on the `Restored` phase to work before diff --git a/src/vs/workbench/common/actions.ts b/src/vs/workbench/common/actions.ts index 599d15e4a07..02bbceb32e6 100644 --- a/src/vs/workbench/common/actions.ts +++ b/src/vs/workbench/common/actions.ts @@ -63,7 +63,7 @@ Registry.add(Extensions.WorkbenchActions, new class implements IWorkbenchActionR // https://github.com/microsoft/vscode/blob/main/src/vs/workbench/contrib/search/electron-browser/search.contribution.ts#L266 if (descriptor.label) { - let idx = alias.indexOf(': '); + const idx = alias.indexOf(': '); let categoryOriginal = ''; if (idx > 0) { categoryOriginal = alias.substr(0, idx); diff --git a/src/vs/workbench/common/configuration.ts b/src/vs/workbench/common/configuration.ts index 8405ae63a5f..d25d5728cef 100644 --- a/src/vs/workbench/common/configuration.ts +++ b/src/vs/workbench/common/configuration.ts @@ -5,6 +5,12 @@ import { localize } from 'vs/nls'; import { IConfigurationNode } from 'vs/platform/configuration/common/configurationRegistry'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { ConfigurationTarget, IConfigurationOverrides, IConfigurationService, IConfigurationValue } from 'vs/platform/configuration/common/configuration'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { Emitter } from 'vs/base/common/event'; export const workbenchConfigurationNodeBase = Object.freeze<IConfigurationNode>({ 'id': 'workbench', @@ -12,3 +18,88 @@ export const workbenchConfigurationNodeBase = Object.freeze<IConfigurationNode>( 'title': localize('workbenchConfigurationTitle', "Workbench"), 'type': 'object', }); + +export const Extensions = { + ConfigurationMigration: 'base.contributions.configuration.migration' +}; + +export type ConfigurationValue = { value: any | undefined /* Remove */ }; +export type ConfigurationKeyValuePairs = [string, ConfigurationValue][]; +export type ConfigurationMigrationFn = (value: any, valueAccessor: (key: string) => any) => ConfigurationValue | ConfigurationKeyValuePairs | Promise<ConfigurationValue | ConfigurationKeyValuePairs>; +export type ConfigurationMigration = { key: string; migrateFn: ConfigurationMigrationFn }; + +export interface IConfigurationMigrationRegistry { + registerConfigurationMigrations(configurationMigrations: ConfigurationMigration[]): void; +} + +class ConfigurationMigrationRegistry implements IConfigurationMigrationRegistry { + + readonly migrations: ConfigurationMigration[] = []; + + private readonly _onDidRegisterConfigurationMigrations = new Emitter<ConfigurationMigration[]>(); + readonly onDidRegisterConfigurationMigration = this._onDidRegisterConfigurationMigrations.event; + + registerConfigurationMigrations(configurationMigrations: ConfigurationMigration[]): void { + this.migrations.push(...configurationMigrations); + } + +} + +const configurationMigrationRegistry = new ConfigurationMigrationRegistry(); +Registry.add(Extensions.ConfigurationMigration, configurationMigrationRegistry); + +export class ConfigurationMigrationWorkbenchContribution extends Disposable implements IWorkbenchContribution { + + constructor( + @IConfigurationService private readonly configurationService: IConfigurationService, + @IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService, + ) { + super(); + this._register(this.workspaceService.onDidChangeWorkspaceFolders(async (e) => { + for (const folder of e.added) { + await this.migrateConfigurationsForFolder(folder, configurationMigrationRegistry.migrations); + } + })); + this.migrateConfigurations(configurationMigrationRegistry.migrations); + this._register(configurationMigrationRegistry.onDidRegisterConfigurationMigration(migration => this.migrateConfigurations(migration))); + } + + private async migrateConfigurations(migrations: ConfigurationMigration[]): Promise<void> { + await this.migrateConfigurationsForFolder(undefined, migrations); + for (const folder of this.workspaceService.getWorkspace().folders) { + await this.migrateConfigurationsForFolder(folder, migrations); + } + } + + private async migrateConfigurationsForFolder(folder: IWorkspaceFolder | undefined, migrations: ConfigurationMigration[]): Promise<void> { + await Promise.all(migrations.map(migration => this.migrateConfigurationsForFolderAndOverride(migration, { resource: folder?.uri }))); + } + + private async migrateConfigurationsForFolderAndOverride(migration: ConfigurationMigration, overrides: IConfigurationOverrides): Promise<void> { + const data = this.configurationService.inspect(migration.key, overrides); + + await this.migrateConfigurationForFolderOverrideAndTarget(migration, overrides, data, 'userValue', ConfigurationTarget.USER); + await this.migrateConfigurationForFolderOverrideAndTarget(migration, overrides, data, 'userLocalValue', ConfigurationTarget.USER_LOCAL); + await this.migrateConfigurationForFolderOverrideAndTarget(migration, overrides, data, 'userRemoteValue', ConfigurationTarget.USER_REMOTE); + await this.migrateConfigurationForFolderOverrideAndTarget(migration, overrides, data, 'workspaceFolderValue', ConfigurationTarget.WORKSPACE_FOLDER); + await this.migrateConfigurationForFolderOverrideAndTarget(migration, overrides, data, 'workspaceValue', ConfigurationTarget.WORKSPACE); + + if (typeof overrides.overrideIdentifier === 'undefined' && typeof data.overrideIdentifiers !== 'undefined') { + for (const overrideIdentifier of data.overrideIdentifiers) { + await this.migrateConfigurationsForFolderAndOverride(migration, { resource: overrides.resource, overrideIdentifier }); + } + } + } + + private async migrateConfigurationForFolderOverrideAndTarget(migration: ConfigurationMigration, overrides: IConfigurationOverrides, data: IConfigurationValue<any>, dataKey: keyof IConfigurationValue<any>, target: ConfigurationTarget): Promise<void> { + const value = data[dataKey]; + if (typeof value === 'undefined') { + return; + } + + const valueAccessor = (key: string) => this.configurationService.inspect(key, overrides)[dataKey]; + const result = await migration.migrateFn(value, valueAccessor); + const keyValuePairs: ConfigurationKeyValuePairs = Array.isArray(result) ? result : [[migration.key, result]]; + await Promise.allSettled(keyValuePairs.map(async ([key, value]) => this.configurationService.updateValue(key, value.value, overrides, target))); + } +} diff --git a/src/vs/workbench/common/configurationMigration.ts b/src/vs/workbench/common/configurationMigration.ts deleted file mode 100644 index 44bcd77a4ad..00000000000 --- a/src/vs/workbench/common/configurationMigration.ts +++ /dev/null @@ -1,99 +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 { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; -import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { ConfigurationTarget, IConfigurationOverrides, IConfigurationService, IConfigurationValue } from 'vs/platform/configuration/common/configuration'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { Emitter } from 'vs/base/common/event'; - -export const Extensions = { - ConfigurationMigration: 'base.contributions.configuration.migration' -}; - -export type ConfigurationValue = { value: any | undefined /* Remove */ }; -export type ConfigurationKeyValuePairs = [string, ConfigurationValue][]; -export type ConfigurationMigrationFn = (value: any, valueAccessor: (key: string) => any) => ConfigurationValue | ConfigurationKeyValuePairs | Promise<ConfigurationValue | ConfigurationKeyValuePairs>; -export type ConfigurationMigration = { key: string; migrateFn: ConfigurationMigrationFn }; - -export interface IConfigurationMigrationRegistry { - registerConfigurationMigrations(configurationMigrations: ConfigurationMigration[]): void; -} - -class ConfigurationMigrationRegistry implements IConfigurationMigrationRegistry { - - readonly migrations: ConfigurationMigration[] = []; - - private readonly _onDidRegisterConfigurationMigrations = new Emitter<ConfigurationMigration[]>(); - readonly onDidRegisterConfigurationMigration = this._onDidRegisterConfigurationMigrations.event; - - registerConfigurationMigrations(configurationMigrations: ConfigurationMigration[]): void { - this.migrations.push(...configurationMigrations); - } - -} - -const configurationMigrationRegistry = new ConfigurationMigrationRegistry(); -Registry.add(Extensions.ConfigurationMigration, configurationMigrationRegistry); - -class ConfigurationMigrationWorkbenchContribution extends Disposable implements IWorkbenchContribution { - - constructor( - @IConfigurationService private readonly configurationService: IConfigurationService, - @IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService, - ) { - super(); - this._register(this.workspaceService.onDidChangeWorkspaceFolders(async (e) => { - for (const folder of e.added) { - await this.migrateConfigurationsForFolder(folder, configurationMigrationRegistry.migrations); - } - })); - this.migrateConfigurations(configurationMigrationRegistry.migrations); - this._register(configurationMigrationRegistry.onDidRegisterConfigurationMigration(migration => this.migrateConfigurations(migration))); - } - - private async migrateConfigurations(migrations: ConfigurationMigration[]): Promise<void> { - await this.migrateConfigurationsForFolder(undefined, migrations); - for (const folder of this.workspaceService.getWorkspace().folders) { - await this.migrateConfigurationsForFolder(folder, migrations); - } - } - - private async migrateConfigurationsForFolder(folder: IWorkspaceFolder | undefined, migrations: ConfigurationMigration[]): Promise<void> { - await Promise.all(migrations.map(migration => this.migrateConfigurationsForFolderAndOverride(migration, { resource: folder?.uri }))); - } - - private async migrateConfigurationsForFolderAndOverride(migration: ConfigurationMigration, overrides: IConfigurationOverrides): Promise<void> { - const data = this.configurationService.inspect(migration.key, overrides); - - await this.migrateConfigurationForFolderOverrideAndTarget(migration, overrides, data, 'userValue', ConfigurationTarget.USER); - await this.migrateConfigurationForFolderOverrideAndTarget(migration, overrides, data, 'userLocalValue', ConfigurationTarget.USER_LOCAL); - await this.migrateConfigurationForFolderOverrideAndTarget(migration, overrides, data, 'userRemoteValue', ConfigurationTarget.USER_REMOTE); - await this.migrateConfigurationForFolderOverrideAndTarget(migration, overrides, data, 'workspaceFolderValue', ConfigurationTarget.WORKSPACE_FOLDER); - await this.migrateConfigurationForFolderOverrideAndTarget(migration, overrides, data, 'workspaceValue', ConfigurationTarget.WORKSPACE); - - if (typeof overrides.overrideIdentifier === 'undefined' && typeof data.overrideIdentifiers !== 'undefined') { - for (const overrideIdentifier of data.overrideIdentifiers) { - await this.migrateConfigurationsForFolderAndOverride(migration, { resource: overrides.resource, overrideIdentifier }); - } - } - } - - private async migrateConfigurationForFolderOverrideAndTarget(migration: ConfigurationMigration, overrides: IConfigurationOverrides, data: IConfigurationValue<any>, dataKey: keyof IConfigurationValue<any>, target: ConfigurationTarget): Promise<void> { - const value = data[dataKey]; - if (typeof value === 'undefined') { - return; - } - - const valueAccessor = (key: string) => this.configurationService.inspect(key, overrides)[dataKey]; - const result = await migration.migrateFn(value, valueAccessor); - const keyValuePairs: ConfigurationKeyValuePairs = Array.isArray(result) ? result : [[migration.key, result]]; - await Promise.allSettled(keyValuePairs.map(async ([key, value]) => this.configurationService.updateValue(key, value.value, overrides, target))); - } -} - -Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(ConfigurationMigrationWorkbenchContribution, LifecyclePhase.Eventually); diff --git a/src/vs/workbench/common/contextkeys.ts b/src/vs/workbench/common/contextkeys.ts index 9682738ae6c..907cfa0dda2 100644 --- a/src/vs/workbench/common/contextkeys.ts +++ b/src/vs/workbench/common/contextkeys.ts @@ -142,7 +142,7 @@ export class ResourceContextKey { // UNDEFINED! IT IS IMPORTANT THAT DEFAULTS ARE INHERITED // FROM THE PARENT CONTEXT AND ONLY UNDEFINED DOES THIS - static readonly Scheme = new RawContextKey<string>('resourceScheme', undefined, { type: 'string', description: localize('resourceScheme', "The scheme of the rsource") }); + static readonly Scheme = new RawContextKey<string>('resourceScheme', undefined, { type: 'string', description: localize('resourceScheme', "The scheme of the resource") }); static readonly Filename = new RawContextKey<string>('resourceFilename', undefined, { type: 'string', description: localize('resourceFilename', "The file name of the resource") }); static readonly Dirname = new RawContextKey<string>('resourceDirname', undefined, { type: 'string', description: localize('resourceDirname', "The folder name the resource is contained in") }); static readonly Path = new RawContextKey<string>('resourcePath', undefined, { type: 'string', description: localize('resourcePath', "The full path of the resource") }); diff --git a/src/vs/workbench/common/contributions.ts b/src/vs/workbench/common/contributions.ts index 02a61714ec8..c122ea35982 100644 --- a/src/vs/workbench/common/contributions.ts +++ b/src/vs/workbench/common/contributions.ts @@ -98,9 +98,9 @@ class WorkbenchContributionsRegistry implements IWorkbenchContributionsRegistry // for the Eventually-phase we instantiate contributions // only when idle. this might take a few idle-busy-cycles // but will finish within the timeouts - let forcedTimeout = 3000; + const forcedTimeout = 3000; let i = 0; - let instantiateSome = (idle: IdleDeadline) => { + const instantiateSome = (idle: IdleDeadline) => { while (i < toBeInstantiated.length) { const ctor = toBeInstantiated[i++]; this.safeCreateInstance(instantiationService, ctor); // catch error so that other contributions are still considered diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index b12e6477782..03fd014d39b 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -8,7 +8,7 @@ import { Event } from 'vs/base/common/event'; import { assertIsDefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { ICodeEditorViewState, IDiffEditor, IDiffEditorViewState, IEditorViewState } from 'vs/editor/common/editorCommon'; +import { ICodeEditorViewState, IDiffEditor, IDiffEditorViewState, IEditor, IEditorViewState } from 'vs/editor/common/editorCommon'; import { IEditorOptions, IResourceEditorInput, ITextResourceEditorInput, IBaseTextResourceEditorInput, IBaseUntypedEditorInput } from 'vs/platform/editor/common/editor'; import type { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { IInstantiationService, IConstructorSignature, ServicesAccessor, BrandedService } from 'vs/platform/instantiation/common/instantiation'; @@ -326,12 +326,23 @@ export interface IVisibleEditorPane extends IEditorPane { } /** + * The text editor pane is the container for workbench text editors. + */ +export interface ITextEditorPane extends IEditorPane { + + /** + * Returns the underlying text editor widget of this editor. + */ + getControl(): IEditor | undefined; +} + +/** * The text editor pane is the container for workbench text diff editors. */ export interface ITextDiffEditorPane extends IEditorPane { /** - * Returns the underlying text editor widget of this editor. + * Returns the underlying text diff editor widget of this editor. */ getControl(): IDiffEditor | undefined; } @@ -709,7 +720,7 @@ export interface EditorInputWithPreferredResource { * URIs. But when displaying the editor label to the user, the * preferred URI should be used. * - * Not all editors have a `preferredResouce`. The `EditorResourceAccessor` + * Not all editors have a `preferredResource`. The `EditorResourceAccessor` * utility can be used to always get the right resource without having * to do instanceof checks. */ diff --git a/src/vs/workbench/common/editor/editorGroupModel.ts b/src/vs/workbench/common/editor/editorGroupModel.ts index 8aab2bcbf6a..f3382a2dd5e 100644 --- a/src/vs/workbench/common/editor/editorGroupModel.ts +++ b/src/vs/workbench/common/editor/editorGroupModel.ts @@ -971,8 +971,8 @@ export class EditorGroupModel extends Disposable { // Serialize all editor inputs so that we can store them. // Editors that cannot be serialized need to be ignored // from mru, active, preview and sticky if any. - let serializableEditors: EditorInput[] = []; - let serializedEditors: ISerializedEditorInput[] = []; + const serializableEditors: EditorInput[] = []; + const serializedEditors: ISerializedEditorInput[] = []; let serializablePreviewIndex: number | undefined; let serializableSticky = this.sticky; diff --git a/src/vs/workbench/common/editor/textResourceEditorInput.ts b/src/vs/workbench/common/editor/textResourceEditorInput.ts index 94236604209..ee14358d2b8 100644 --- a/src/vs/workbench/common/editor/textResourceEditorInput.ts +++ b/src/vs/workbench/common/editor/textResourceEditorInput.ts @@ -133,9 +133,7 @@ export class TextResourceEditorInput extends AbstractTextResourceEditorInput imp setLanguageId(languageId: string): void { this.setPreferredLanguageId(languageId); - if (this.cachedModel) { - this.cachedModel.setLanguageId(languageId); - } + this.cachedModel?.setLanguageId(languageId); } setPreferredLanguageId(languageId: string): void { diff --git a/src/vs/workbench/common/memento.ts b/src/vs/workbench/common/memento.ts index 2d52437793a..4c6cea09123 100644 --- a/src/vs/workbench/common/memento.ts +++ b/src/vs/workbench/common/memento.ts @@ -11,7 +11,8 @@ export type MementoObject = { [key: string]: any }; export class Memento { - private static readonly globalMementos = new Map<string, ScopedMemento>(); + private static readonly applicationMementos = new Map<string, ScopedMemento>(); + private static readonly profileMementos = new Map<string, ScopedMemento>(); private static readonly workspaceMementos = new Map<string, ScopedMemento>(); private static readonly COMMON_PREFIX = 'memento/'; @@ -23,53 +24,60 @@ export class Memento { } getMemento(scope: StorageScope, target: StorageTarget): MementoObject { + switch (scope) { - // Scope by Workspace - if (scope === StorageScope.WORKSPACE) { - let workspaceMemento = Memento.workspaceMementos.get(this.id); - if (!workspaceMemento) { - workspaceMemento = new ScopedMemento(this.id, scope, target, this.storageService); - Memento.workspaceMementos.set(this.id, workspaceMemento); - } + // Scope by Workspace + case StorageScope.WORKSPACE: { + let workspaceMemento = Memento.workspaceMementos.get(this.id); + if (!workspaceMemento) { + workspaceMemento = new ScopedMemento(this.id, scope, target, this.storageService); + Memento.workspaceMementos.set(this.id, workspaceMemento); + } - return workspaceMemento.getMemento(); - } + return workspaceMemento.getMemento(); + } - // Scope Global - let globalMemento = Memento.globalMementos.get(this.id); - if (!globalMemento) { - globalMemento = new ScopedMemento(this.id, scope, target, this.storageService); - Memento.globalMementos.set(this.id, globalMemento); - } + // Scope Profile + case StorageScope.PROFILE: { + let profileMemento = Memento.profileMementos.get(this.id); + if (!profileMemento) { + profileMemento = new ScopedMemento(this.id, scope, target, this.storageService); + Memento.profileMementos.set(this.id, profileMemento); + } - return globalMemento.getMemento(); - } + return profileMemento.getMemento(); + } - saveMemento(): void { + // Scope Application + case StorageScope.APPLICATION: { + let applicationMemento = Memento.applicationMementos.get(this.id); + if (!applicationMemento) { + applicationMemento = new ScopedMemento(this.id, scope, target, this.storageService); + Memento.applicationMementos.set(this.id, applicationMemento); + } - // Workspace - const workspaceMemento = Memento.workspaceMementos.get(this.id); - if (workspaceMemento) { - workspaceMemento.save(); + return applicationMemento.getMemento(); + } } + } - // Global - const globalMemento = Memento.globalMementos.get(this.id); - if (globalMemento) { - globalMemento.save(); - } + saveMemento(): void { + Memento.workspaceMementos.get(this.id)?.save(); + Memento.profileMementos.get(this.id)?.save(); + Memento.applicationMementos.get(this.id)?.save(); } static clear(scope: StorageScope): void { - - // Workspace - if (scope === StorageScope.WORKSPACE) { - Memento.workspaceMementos.clear(); - } - - // Global - if (scope === StorageScope.GLOBAL) { - Memento.globalMementos.clear(); + switch (scope) { + case StorageScope.WORKSPACE: + Memento.workspaceMementos.clear(); + break; + case StorageScope.PROFILE: + Memento.profileMementos.clear(); + break; + case StorageScope.APPLICATION: + Memento.applicationMementos.clear(); + break; } } } diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index 5c9c7d93a9d..ac9b39c0c21 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -27,7 +27,7 @@ import { mixin } from 'vs/base/common/objects'; import { Codicon } from 'vs/base/common/codicons'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { IDataTransfer } from 'vs/base/common/dataTransfer'; +import { VSDataTransfer } from 'vs/base/common/dataTransfer'; export const defaultViewIcon = registerIcon('default-view-icon', Codicon.window, localize('defaultViewIcon', 'Default view icon.')); @@ -831,8 +831,8 @@ export interface ITreeViewDataProvider { export interface ITreeViewDragAndDropController { readonly dropMimeTypes: string[]; readonly dragMimeTypes: string[]; - handleDrag(sourceTreeItemHandles: string[], operationUuid: string, token: CancellationToken): Promise<IDataTransfer | undefined>; - handleDrop(elements: IDataTransfer, target: ITreeItem | undefined, token: CancellationToken, operationUuid?: string, sourceTreeId?: string, sourceTreeItemHandles?: string[]): Promise<void>; + handleDrag(sourceTreeItemHandles: string[], operationUuid: string, token: CancellationToken): Promise<VSDataTransfer | undefined>; + handleDrop(elements: VSDataTransfer, target: ITreeItem | undefined, token: CancellationToken, operationUuid?: string, sourceTreeId?: string, sourceTreeItemHandles?: string[]): Promise<void>; } export interface IEditableData { 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 { diff --git a/src/vs/workbench/electron-sandbox/actions/windowActions.ts b/src/vs/workbench/electron-sandbox/actions/windowActions.ts index a89681fa292..438925fd576 100644 --- a/src/vs/workbench/electron-sandbox/actions/windowActions.ts +++ b/src/vs/workbench/electron-sandbox/actions/windowActions.ts @@ -266,7 +266,7 @@ export class QuickSwitchWindowAction extends BaseSwitchWindow { super({ id: 'workbench.action.quickSwitchWindow', title: { value: localize('quickSwitchWindow', "Quick Switch Window..."), original: 'Quick Switch Window...' }, - f1: true + f1: false // hide quick pickers from command palette to not confuse with the other entry that shows a input field }); } diff --git a/src/vs/workbench/electron-sandbox/desktop.contribution.ts b/src/vs/workbench/electron-sandbox/desktop.contribution.ts index 8c88a219e24..15bcde982e2 100644 --- a/src/vs/workbench/electron-sandbox/desktop.contribution.ts +++ b/src/vs/workbench/electron-sandbox/desktop.contribution.ts @@ -8,7 +8,7 @@ import { localize } from 'vs/nls'; import { MenuRegistry, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; -import { isLinux, isMacintosh } from 'vs/base/common/platform'; +import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { ConfigureRuntimeArgumentsAction, ToggleDevToolsAction, ToggleSharedProcessAction, ReloadWindowWithExtensionsDisabledAction } from 'vs/workbench/electron-sandbox/actions/developerActions'; import { ZoomResetAction, ZoomOutAction, ZoomInAction, CloseWindowAction, SwitchWindowAction, QuickSwitchWindowAction, NewWindowTabHandler, ShowPreviousWindowTabHandler, ShowNextWindowTabHandler, MoveWindowTabToNewWindowHandler, MergeWindowTabsHandlerHandler, ToggleWindowTabsBarHandler } from 'vs/workbench/electron-sandbox/actions/windowActions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; @@ -203,6 +203,13 @@ import { ModifierKeyEmitter } from 'vs/base/browser/dom'; 'scope': ConfigurationScope.APPLICATION, 'description': localize('titleBarStyle', "Adjust the appearance of the window title bar. On Linux and Windows, this setting also affects the application and context menu appearances. Changes require a full restart to apply.") }, + 'window.experimental.windowControlsOverlay.enabled': { + 'type': 'boolean', + 'default': false, + 'scope': ConfigurationScope.APPLICATION, + 'description': localize('windowControlsOverlay', "Use window controls provided by the platform instead of our HTML-based window controls. Changes require a full restart to apply."), + 'included': isWindows + }, 'window.dialogStyle': { 'type': 'string', 'enum': ['native', 'custom'], diff --git a/src/vs/workbench/electron-sandbox/desktop.main.ts b/src/vs/workbench/electron-sandbox/desktop.main.ts index 98b95174864..86706f54945 100644 --- a/src/vs/workbench/electron-sandbox/desktop.main.ts +++ b/src/vs/workbench/electron-sandbox/desktop.main.ts @@ -16,7 +16,7 @@ import { WorkspaceService } from 'vs/workbench/services/configuration/browser/co import { INativeWorkbenchEnvironmentService, NativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { ILoggerService, ILogService, LogLevel } from 'vs/platform/log/common/log'; -import { NativeStorageService } from 'vs/platform/storage/electron-sandbox/storageService'; +import { NativeWorkbenchStorageService } from 'vs/workbench/services/storage/electron-sandbox/storageService'; import { IWorkspaceContextService, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IAnyWorkspaceIdentifier, reviveIdentifier } from 'vs/platform/workspace/common/workspace'; import { IWorkbenchConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; import { IStorageService } from 'vs/platform/storage/common/storage'; @@ -50,6 +50,12 @@ import { isCI, isMacintosh } from 'vs/base/common/platform'; import { Schemas } from 'vs/base/common/network'; import { DiskFileSystemProvider } from 'vs/workbench/services/files/electron-sandbox/diskFileSystemProvider'; import { FileUserDataProvider } from 'vs/platform/userData/common/fileUserDataProvider'; +import { IUserDataProfilesService, reviveProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { UserDataProfilesNativeService } from 'vs/platform/userDataProfile/electron-sandbox/userDataProfile'; +import { PolicyChannelClient } from 'vs/platform/policy/common/policyIpc'; +import { IPolicyService, NullPolicyService } from 'vs/platform/policy/common/policy'; +import { UserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfileService'; +import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; export class DesktopMain extends Disposable { @@ -127,14 +133,14 @@ export class DesktopMain extends Disposable { return []; } - private registerListeners(workbench: Workbench, storageService: NativeStorageService): void { + private registerListeners(workbench: Workbench, storageService: NativeWorkbenchStorageService): void { // Workbench Lifecycle this._register(workbench.onWillShutdown(event => event.join(storageService.close(), { id: 'join.closeStorage', label: localize('join.closeStorage', "Saving UI state") }))); this._register(workbench.onDidShutdown(() => this.dispose())); } - private async initServices(): Promise<{ serviceCollection: ServiceCollection; logService: ILogService; storageService: NativeStorageService }> { + private async initServices(): Promise<{ serviceCollection: ServiceCollection; logService: ILogService; storageService: NativeWorkbenchStorageService }> { const serviceCollection = new ServiceCollection(); @@ -155,6 +161,10 @@ export class DesktopMain extends Disposable { const mainProcessService = this._register(new ElectronIPCMainProcessService(this.configuration.windowId)); serviceCollection.set(IMainProcessService, mainProcessService); + // Policies + const policyService = this.configuration.policiesData ? new PolicyChannelClient(this.configuration.policiesData, mainProcessService.getChannel('policy')) : new NullPolicyService(); + serviceCollection.set(IPolicyService, policyService); + // Product const productService: IProductService = { _serviceBrand: undefined, ...product }; serviceCollection.set(IProductService, productService); @@ -230,6 +240,11 @@ export class DesktopMain extends Disposable { const uriIdentityService = new UriIdentityService(fileService); serviceCollection.set(IUriIdentityService, uriIdentityService); + // User Data Profiles + const userDataProfilesService = new UserDataProfilesNativeService(this.configuration.profiles.all, mainProcessService, environmentService, fileService, logService); + serviceCollection.set(IUserDataProfilesService, userDataProfilesService); + const userDataProfileService = new UserDataProfileService(reviveProfile(this.configuration.profiles.current, userDataProfilesService.profilesHome.scheme)); + serviceCollection.set(IUserDataProfileService, userDataProfileService); // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // @@ -247,7 +262,7 @@ export class DesktopMain extends Disposable { const payload = this.resolveWorkspaceInitializationPayload(environmentService); const [configurationService, storageService] = await Promise.all([ - this.createWorkspaceService(payload, environmentService, fileService, remoteAgentService, uriIdentityService, logService).then(service => { + this.createWorkspaceService(payload, environmentService, userDataProfileService, userDataProfilesService, fileService, remoteAgentService, uriIdentityService, logService, policyService).then(service => { // Workspace serviceCollection.set(IWorkspaceContextService, service); @@ -258,7 +273,7 @@ export class DesktopMain extends Disposable { return service; }), - this.createStorageService(payload, environmentService, mainProcessService).then(service => { + this.createStorageService(payload, environmentService, userDataProfileService, userDataProfilesService, mainProcessService).then(service => { // Storage serviceCollection.set(IStorageService, service); @@ -325,9 +340,19 @@ export class DesktopMain extends Disposable { return workspaceInitializationPayload; } - private async createWorkspaceService(payload: IAnyWorkspaceIdentifier, environmentService: INativeWorkbenchEnvironmentService, fileService: FileService, remoteAgentService: IRemoteAgentService, uriIdentityService: IUriIdentityService, logService: ILogService): Promise<WorkspaceService> { + private async createWorkspaceService( + payload: IAnyWorkspaceIdentifier, + environmentService: INativeWorkbenchEnvironmentService, + userDataProfileService: IUserDataProfileService, + userDataProfilesService: IUserDataProfilesService, + fileService: FileService, + remoteAgentService: IRemoteAgentService, + uriIdentityService: IUriIdentityService, + logService: ILogService, + policyService: IPolicyService + ): Promise<WorkspaceService> { const configurationCache = new ConfigurationCache([Schemas.file, Schemas.vscodeUserData] /* Cache all non native resources */, environmentService, fileService); - const workspaceService = new WorkspaceService({ remoteAuthority: environmentService.remoteAuthority, configurationCache }, environmentService, fileService, remoteAgentService, uriIdentityService, logService); + const workspaceService = new WorkspaceService({ remoteAuthority: environmentService.remoteAuthority, configurationCache }, environmentService, userDataProfileService, userDataProfilesService, fileService, remoteAgentService, uriIdentityService, logService, policyService); try { await workspaceService.initialize(payload); @@ -340,8 +365,8 @@ export class DesktopMain extends Disposable { } } - private async createStorageService(payload: IAnyWorkspaceIdentifier, environmentService: INativeWorkbenchEnvironmentService, mainProcessService: IMainProcessService): Promise<NativeStorageService> { - const storageService = new NativeStorageService(payload, mainProcessService, environmentService); + private async createStorageService(payload: IAnyWorkspaceIdentifier, environmentService: INativeWorkbenchEnvironmentService, userDataProfileService: IUserDataProfileService, userDataProfilesService: IUserDataProfilesService, mainProcessService: IMainProcessService): Promise<NativeWorkbenchStorageService> { + const storageService = new NativeWorkbenchStorageService(payload, userDataProfileService, userDataProfilesService, mainProcessService, environmentService); try { await storageService.initialize(); diff --git a/src/vs/workbench/electron-sandbox/parts/titlebar/menubarControl.ts b/src/vs/workbench/electron-sandbox/parts/titlebar/menubarControl.ts index a2a5bb55453..813613e9b80 100644 --- a/src/vs/workbench/electron-sandbox/parts/titlebar/menubarControl.ts +++ b/src/vs/workbench/electron-sandbox/parts/titlebar/menubarControl.ts @@ -104,9 +104,9 @@ export class NativeMenubarControl extends MenubarControl { } private populateMenuItems(menu: IMenu, menuToPopulate: IMenubarMenu, keybindings: { [id: string]: IMenubarKeybinding | undefined }) { - let groups = menu.getActions(); + const groups = menu.getActions(); - for (let group of groups) { + for (const group of groups) { const [, actions] = group; actions.forEach(menuItem => { @@ -128,7 +128,7 @@ export class NativeMenubarControl extends MenubarControl { this.populateMenuItems(menuToDispose, submenu, keybindings); if (submenu.items.length > 0) { - let menubarSubmenuItem: IMenubarMenuItemSubmenu = { + const menubarSubmenuItem: IMenubarMenuItemSubmenu = { id: menuItem.id, label: title, submenu: submenu @@ -144,7 +144,7 @@ export class NativeMenubarControl extends MenubarControl { menuToPopulate.items.push(...actions); } - let menubarMenuItem: IMenubarMenuItemAction = { + const menubarMenuItem: IMenubarMenuItemAction = { id: menuItem.id, label: title }; diff --git a/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts b/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts index 75a1a5fa6bd..7e3dd0559f3 100644 --- a/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts @@ -17,16 +17,18 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; -import { getTitleBarStyle } from 'vs/platform/window/common/window'; +import { getTitleBarStyle, useWindowControlsOverlay } from 'vs/platform/window/common/window'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Codicon } from 'vs/base/common/codicons'; import { NativeMenubarControl } from 'vs/workbench/electron-sandbox/parts/titlebar/menubarControl'; +import { IHoverService } from 'vs/workbench/services/hover/browser/hover'; export class TitlebarPart extends BrowserTitleBarPart { private maxRestoreControl: HTMLElement | undefined; private dragRegion: HTMLElement | undefined; private resizer: HTMLElement | undefined; private cachedWindowControlStyles: { bgColor: string; fgColor: string } | undefined; + private cachedWindowControlHeight: number | undefined; private getMacTitlebarSize() { const osVersion = this.environmentService.os.release; @@ -37,7 +39,12 @@ export class TitlebarPart extends BrowserTitleBarPart { return 22; } - override get minimumHeight(): number { return isMacintosh ? this.getMacTitlebarSize() / getZoomFactor() : super.minimumHeight; } + override get minimumHeight(): number { + if (!isMacintosh) { + return super.minimumHeight; + } + return (this.isCommandCenterVisible ? 35 : this.getMacTitlebarSize()) / getZoomFactor(); + } override get maximumHeight(): number { return this.minimumHeight; } protected override readonly environmentService: INativeWorkbenchEnvironmentService; @@ -54,8 +61,9 @@ export class TitlebarPart extends BrowserTitleBarPart { @IContextKeyService contextKeyService: IContextKeyService, @IHostService hostService: IHostService, @INativeHostService private readonly nativeHostService: INativeHostService, + @IHoverService hoverService: IHoverService, ) { - super(contextMenuService, configurationService, environmentService, instantiationService, themeService, storageService, layoutService, menuService, contextKeyService, hostService); + super(contextMenuService, configurationService, environmentService, instantiationService, themeService, storageService, layoutService, menuService, contextKeyService, hostService, hoverService); this.environmentService = environmentService; } @@ -196,11 +204,23 @@ export class TitlebarPart extends BrowserTitleBarPart { super.updateStyles(); // WCO styles only supported on Windows currently - if (isWindows) { + if (useWindowControlsOverlay(this.configurationService, this.environmentService)) { if (!this.cachedWindowControlStyles || this.cachedWindowControlStyles.bgColor !== this.element.style.backgroundColor || this.cachedWindowControlStyles.fgColor !== this.element.style.color) { - this.nativeHostService.updateTitleBarOverlay(this.element.style.backgroundColor, this.element.style.color); + this.nativeHostService.updateTitleBarOverlay({ backgroundColor: this.element.style.backgroundColor, foregroundColor: this.element.style.color }); + } + } + } + + override layout(width: number, height: number): void { + super.layout(width, height); + + if (useWindowControlsOverlay(this.configurationService, this.environmentService)) { + const newHeight = Math.trunc(this.element.clientHeight * getZoomFactor()); + if (newHeight !== this.cachedWindowControlHeight) { + this.cachedWindowControlHeight = newHeight; + this.nativeHostService.updateTitleBarOverlay({ height: newHeight }); } } } diff --git a/src/vs/workbench/electron-sandbox/window.ts b/src/vs/workbench/electron-sandbox/window.ts index 231433ee322..571cd3a5f0f 100644 --- a/src/vs/workbench/electron-sandbox/window.ts +++ b/src/vs/workbench/electron-sandbox/window.ts @@ -10,7 +10,7 @@ import { equals } from 'vs/base/common/objects'; import { EventType, EventHelper, addDisposableListener, scheduleAtNextAnimationFrame, ModifierKeyEmitter } from 'vs/base/browser/dom'; import { Separator, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions'; import { IFileService } from 'vs/platform/files/common/files'; -import { EditorResourceAccessor, IUntitledTextResourceEditorInput, SideBySideEditor, pathsToEditors, IResourceDiffEditorInput, IUntypedEditorInput } from 'vs/workbench/common/editor'; +import { EditorResourceAccessor, IUntitledTextResourceEditorInput, SideBySideEditor, pathsToEditors, IResourceDiffEditorInput, IUntypedEditorInput, IEditorPane } from 'vs/workbench/common/editor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { WindowMinimumSize, IOpenFileRequest, IWindowsConfiguration, getTitleBarStyle, IAddFoldersRequest, INativeRunActionInWindowRequest, INativeRunKeybindingInWindowRequest, INativeOpenFileRequest } from 'vs/platform/window/common/window'; @@ -215,7 +215,7 @@ export class NativeWindow extends Disposable { // Proxy Login Dialog ipcRenderer.on('vscode:openProxyAuthenticationDialog', async (event: unknown, payload: { authInfo: AuthInfo; username?: string; password?: string; replyChannel: string }) => { - const rememberCredentials = this.storageService.getBoolean(NativeWindow.REMEMBER_PROXY_CREDENTIALS_KEY, StorageScope.GLOBAL); + const rememberCredentials = this.storageService.getBoolean(NativeWindow.REMEMBER_PROXY_CREDENTIALS_KEY, StorageScope.APPLICATION); const result = await this.dialogService.input(Severity.Warning, localize('proxyAuthRequired', "Proxy Authentication Required"), [ localize({ key: 'loginButton', comment: ['&& denotes a mnemonic'] }, "&&Log In"), @@ -245,9 +245,9 @@ export class NativeWindow extends Disposable { // Update state based on checkbox if (result.checkboxChecked) { - this.storageService.store(NativeWindow.REMEMBER_PROXY_CREDENTIALS_KEY, true, StorageScope.GLOBAL, StorageTarget.MACHINE); + this.storageService.store(NativeWindow.REMEMBER_PROXY_CREDENTIALS_KEY, true, StorageScope.APPLICATION, StorageTarget.MACHINE); } else { - this.storageService.remove(NativeWindow.REMEMBER_PROXY_CREDENTIALS_KEY, StorageScope.GLOBAL); + this.storageService.remove(NativeWindow.REMEMBER_PROXY_CREDENTIALS_KEY, StorageScope.APPLICATION); } // Reply back to main side with credentials @@ -836,14 +836,23 @@ export class NativeWindow extends Disposable { } if (inputs.length) { - this.openResources(inputs, diffMode); - } + const openedEditorPanes = await this.openResources(inputs, diffMode); + + if (request.filesToWait) { + + // In wait mode, listen to changes to the editors and wait until the files + // are closed that the user wants to wait for. When this happens we delete + // the wait marker file to signal to the outside that editing is done. + // However, it is possible that opening of the editors failed, as such we + // check for whether editor panes got opened and otherwise delete the marker + // right away. - if (request.filesToWait && inputs.length) { - // In wait mode, listen to changes to the editors and wait until the files - // are closed that the user wants to wait for. When this happens we delete - // the wait marker file to signal to the outside that editing is done. - this.trackClosedWaitFiles(URI.revive(request.filesToWait.waitMarkerFileUri), coalesce(request.filesToWait.paths.map(path => URI.revive(path.fileUri)))); + if (openedEditorPanes.length) { + return this.trackClosedWaitFiles(URI.revive(request.filesToWait.waitMarkerFileUri), coalesce(request.filesToWait.paths.map(path => URI.revive(path.fileUri)))); + } else { + return this.fileService.del(URI.revive(request.filesToWait.waitMarkerFileUri)); + } + } } } @@ -856,7 +865,7 @@ export class NativeWindow extends Disposable { await this.fileService.del(waitMarkerFile); } - private async openResources(resources: Array<IResourceEditorInput | IUntitledTextResourceEditorInput>, diffMode: boolean): Promise<unknown> { + private async openResources(resources: Array<IResourceEditorInput | IUntitledTextResourceEditorInput>, diffMode: boolean): Promise<readonly IEditorPane[]> { const editors: IUntypedEditorInput[] = []; // In diffMode we open 2 resources as diff diff --git a/src/vs/workbench/services/accessibility/electron-sandbox/accessibilityService.ts b/src/vs/workbench/services/accessibility/electron-sandbox/accessibilityService.ts index af9a08af7c8..eb818e7fc16 100644 --- a/src/vs/workbench/services/accessibility/electron-sandbox/accessibilityService.ts +++ b/src/vs/workbench/services/accessibility/electron-sandbox/accessibilityService.ts @@ -22,6 +22,7 @@ interface AccessibilityMetrics { enabled: boolean; } type AccessibilityMetricsClassification = { + owner: 'isidorn'; enabled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; }; diff --git a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts index 7e06c120f95..c78225da320 100644 --- a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts +++ b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts @@ -62,6 +62,12 @@ const apiMenus: IAPIMenu[] = [ description: localize('menus.editorContextCopyAs', "'Copy as' submenu in the editor context menu") }, { + key: 'editor/context/share', + id: MenuId.EditorContextShare, + description: localize('menus.editorContextShare', "'Share' submenu in the editor context menu"), + proposed: 'contribShareMenu' + }, + { key: 'explorer/context', id: MenuId.ExplorerContext, description: localize('menus.explorerContext', "The file explorer context menu") @@ -172,6 +178,12 @@ const apiMenus: IAPIMenu[] = [ description: localize('notebook.toolbar', "The contributed notebook toolbar menu") }, { + key: 'notebook/kernelSource', + id: MenuId.NotebookKernelSource, + description: localize('notebook.kernelSource', "The contributed notebook kernel sources menu"), + proposed: 'notebookKernelSource' + }, + { key: 'notebook/cell/title', id: MenuId.NotebookCellTitle, description: localize('notebook.cell.title', "The contributed notebook cell title menu") @@ -191,13 +203,11 @@ const apiMenus: IAPIMenu[] = [ key: 'interactive/toolbar', id: MenuId.InteractiveToolbar, description: localize('interactive.toolbar', "The contributed interactive toolbar menu"), - proposed: 'notebookEditor' }, { key: 'interactive/cell/title', id: MenuId.InteractiveCellTitle, description: localize('interactive.cell.title', "The contributed interactive cell title menu"), - proposed: 'notebookEditor' }, { key: 'testing/item/context', @@ -246,12 +256,24 @@ const apiMenus: IAPIMenu[] = [ supportsSubmenus: false, }, { + key: 'file/share', + id: MenuId.MenubarShare, + description: localize('menus.share', "Share submenu shown in the top level File menu."), + proposed: 'contribShareMenu' + }, + { key: 'editor/inlineCompletions/actions', id: MenuId.InlineCompletionsActions, description: localize('inlineCompletions.actions', "The actions shown when hovering on an inline completion"), supportsSubmenus: false, - proposed: 'inlineCompletions' + proposed: 'inlineCompletionsAdditions' }, + { + key: 'merge/toolbar', + id: MenuId.MergeToolbar, + description: localize('merge.toolbar', "The prominent botton in the merge editor"), + proposed: 'contribMergeEditorToolbar' + } ]; namespace schema { @@ -325,7 +347,7 @@ namespace schema { return false; } - for (let item of items) { + for (const item of items) { if (isMenuItem(item)) { if (!isValidMenuItem(item, collector)) { return false; @@ -661,7 +683,7 @@ submenusExtensionPoint.setHandler(extensions => { _submenus.clear(); - for (let extension of extensions) { + for (const extension of extensions) { const { value, collector } = extension; forEach(value, entry => { @@ -674,7 +696,7 @@ submenusExtensionPoint.setHandler(extensions => { return; } if (_submenus.has(entry.value.id)) { - collector.warn(localize('submenuId.duplicate.id', "The `{0}` submenu was already previously registered.", entry.value.id)); + collector.info(localize('submenuId.duplicate.id', "The `{0}` submenu was already previously registered.", entry.value.id)); return; } if (!entry.value.label) { @@ -723,7 +745,7 @@ menusExtensionPoint.setHandler(extensions => { const items: { id: MenuId; item: IMenuItem | ISubmenuItem }[] = []; - for (let extension of extensions) { + for (const extension of extensions) { const { value, collector } = extension; forEach(value, entry => { diff --git a/src/vs/workbench/services/assignment/common/assignmentService.ts b/src/vs/workbench/services/assignment/common/assignmentService.ts index 8b960de52c0..942388eb947 100644 --- a/src/vs/workbench/services/assignment/common/assignmentService.ts +++ b/src/vs/workbench/services/assignment/common/assignmentService.ts @@ -24,7 +24,7 @@ export interface IWorkbenchAssignmentService extends IAssignmentService { class MementoKeyValueStorage implements IKeyValueStorage { private mementoObj: MementoObject; constructor(private memento: Memento) { - this.mementoObj = memento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); + this.mementoObj = memento.getMemento(StorageScope.APPLICATION, StorageTarget.MACHINE); } async getValue<T>(key: string, defaultValue?: T | undefined): Promise<T | undefined> { diff --git a/src/vs/workbench/services/authentication/browser/authenticationService.ts b/src/vs/workbench/services/authentication/browser/authenticationService.ts index f543021d924..6830c4c82fa 100644 --- a/src/vs/workbench/services/authentication/browser/authenticationService.ts +++ b/src/vs/workbench/services/authentication/browser/authenticationService.ts @@ -47,7 +47,7 @@ const FIRST_PARTY_ALLOWED_EXTENSIONS = [ export function readAccountUsages(storageService: IStorageService, providerId: string, accountName: string,): IAccountUsage[] { const accountKey = `${providerId}-${accountName}-usages`; - const storedUsages = storageService.get(accountKey, StorageScope.GLOBAL); + const storedUsages = storageService.get(accountKey, StorageScope.APPLICATION); let usages: IAccountUsage[] = []; if (storedUsages) { try { @@ -62,7 +62,7 @@ export function readAccountUsages(storageService: IStorageService, providerId: s export function removeAccountUsage(storageService: IStorageService, providerId: string, accountName: string): void { const accountKey = `${providerId}-${accountName}-usages`; - storageService.remove(accountKey, StorageScope.GLOBAL); + storageService.remove(accountKey, StorageScope.APPLICATION); } export function addAccountUsage(storageService: IStorageService, providerId: string, accountName: string, extensionId: string, extensionName: string) { @@ -84,7 +84,7 @@ export function addAccountUsage(storageService: IStorageService, providerId: str }); } - storageService.store(accountKey, JSON.stringify(usages), StorageScope.GLOBAL, StorageTarget.MACHINE); + storageService.store(accountKey, JSON.stringify(usages), StorageScope.APPLICATION, StorageTarget.MACHINE); } export type AuthenticationSessionInfo = { readonly id: string; readonly accessToken: string; readonly providerId: string; readonly canSignOut?: boolean }; @@ -112,7 +112,7 @@ export interface AllowedExtension { export function readAllowedExtensions(storageService: IStorageService, providerId: string, accountName: string): AllowedExtension[] { let trustedExtensions: AllowedExtension[] = []; try { - const trustedExtensionSrc = storageService.get(`${providerId}-${accountName}`, StorageScope.GLOBAL); + const trustedExtensionSrc = storageService.get(`${providerId}-${accountName}`, StorageScope.APPLICATION); if (trustedExtensionSrc) { trustedExtensions = JSON.parse(trustedExtensionSrc); } @@ -410,7 +410,7 @@ export class AuthenticationService extends Disposable implements IAuthentication allowList[index].allowed = isAllowed; } - await this.storageService.store(`${providerId}-${accountName}`, JSON.stringify(allowList), StorageScope.GLOBAL, StorageTarget.USER); + await this.storageService.store(`${providerId}-${accountName}`, JSON.stringify(allowList), StorageScope.APPLICATION, StorageTarget.USER); } async showGetSessionPrompt(providerId: string, accountName: string, extensionId: string, extensionName: string): Promise<boolean> { @@ -475,7 +475,7 @@ export class AuthenticationService extends Disposable implements IAuthentication this.updatedAllowedExtension(providerId, accountName, extensionId, extensionName, true); this.removeAccessRequest(providerId, extensionId); - this.storageService.store(`${extensionName}-${providerId}`, session.id, StorageScope.GLOBAL, StorageTarget.MACHINE); + this.storageService.store(`${extensionName}-${providerId}`, session.id, StorageScope.APPLICATION, StorageTarget.MACHINE); quickPick.dispose(); resolve(session); @@ -615,7 +615,7 @@ export class AuthenticationService extends Disposable implements IAuthentication this.updatedAllowedExtension(providerId, session.account.label, extensionId, extensionName, true); // And also set it as the preferred account for the extension - storageService.store(`${extensionName}-${providerId}`, session.id, StorageScope.GLOBAL, StorageTarget.MACHINE); + storageService.store(`${extensionName}-${providerId}`, session.id, StorageScope.APPLICATION, StorageTarget.MACHINE); } }); diff --git a/src/vs/workbench/services/commands/test/common/commandService.test.ts b/src/vs/workbench/services/commands/test/common/commandService.test.ts index 23110f4f75c..38c6f24d3f4 100644 --- a/src/vs/workbench/services/commands/test/common/commandService.test.ts +++ b/src/vs/workbench/services/commands/test/common/commandService.test.ts @@ -26,7 +26,7 @@ suite('CommandService', function () { let lastEvent: string; - let service = new CommandService(new InstantiationService(), new class extends NullExtensionService { + const service = new CommandService(new InstantiationService(), new class extends NullExtensionService { override activateByEvent(activationEvent: string): Promise<void> { lastEvent = activationEvent; return super.activateByEvent(activationEvent); @@ -51,7 +51,7 @@ suite('CommandService', function () { } }; - let service = new CommandService(new InstantiationService(), extensionService, new NullLogService()); + const service = new CommandService(new InstantiationService(), extensionService, new NullLogService()); await extensionService.whenInstalledExtensionsRegistered(); @@ -63,9 +63,9 @@ suite('CommandService', function () { test('!onReady, but executeCommand', function () { let callCounter = 0; - let reg = CommandsRegistry.registerCommand('bar', () => callCounter += 1); + const reg = CommandsRegistry.registerCommand('bar', () => callCounter += 1); - let service = new CommandService(new InstantiationService(), new class extends NullExtensionService { + const service = new CommandService(new InstantiationService(), new class extends NullExtensionService { override whenInstalledExtensionsRegistered() { return new Promise<boolean>(_resolve => { /*ignore*/ }); } @@ -82,16 +82,16 @@ suite('CommandService', function () { let resolveFunc: Function; const whenInstalledExtensionsRegistered = new Promise<boolean>(_resolve => { resolveFunc = _resolve; }); - let service = new CommandService(new InstantiationService(), new class extends NullExtensionService { + const service = new CommandService(new InstantiationService(), new class extends NullExtensionService { override whenInstalledExtensionsRegistered() { return whenInstalledExtensionsRegistered; } }, new NullLogService()); - let r = service.executeCommand('bar'); + const r = service.executeCommand('bar'); assert.strictEqual(callCounter, 0); - let reg = CommandsRegistry.registerCommand('bar', () => callCounter += 1); + const reg = CommandsRegistry.registerCommand('bar', () => callCounter += 1); resolveFunc!(true); return r.then(() => { @@ -104,8 +104,8 @@ suite('CommandService', function () { let callCounter = 0; const disposable = new DisposableStore(); - let events: string[] = []; - let service = new CommandService(new InstantiationService(), new class extends NullExtensionService { + const events: string[] = []; + const service = new CommandService(new InstantiationService(), new class extends NullExtensionService { override activateByEvent(event: string): Promise<void> { events.push(event); @@ -115,7 +115,7 @@ suite('CommandService', function () { if (event.indexOf('onCommand:') === 0) { return new Promise(resolve => { setTimeout(() => { - let reg = CommandsRegistry.registerCommand(event.substr('onCommand:'.length), () => { + const reg = CommandsRegistry.registerCommand(event.substr('onCommand:'.length), () => { callCounter += 1; }); disposable.add(reg); @@ -137,10 +137,10 @@ suite('CommandService', function () { }); test('issue #71471: wait for onCommand activation even if a command is registered', () => { - let expectedOrder: string[] = ['registering command', 'resolving activation event', 'executing command']; - let actualOrder: string[] = []; + const expectedOrder: string[] = ['registering command', 'resolving activation event', 'executing command']; + const actualOrder: string[] = []; const disposables = new DisposableStore(); - let service = new CommandService(new InstantiationService(), new class extends NullExtensionService { + const service = new CommandService(new InstantiationService(), new class extends NullExtensionService { override activateByEvent(event: string): Promise<void> { if (event === '*') { @@ -151,7 +151,7 @@ suite('CommandService', function () { setTimeout(() => { // Register the command after some time actualOrder.push('registering command'); - let reg = CommandsRegistry.registerCommand(event.substr('onCommand:'.length), () => { + const reg = CommandsRegistry.registerCommand(event.substr('onCommand:'.length), () => { actualOrder.push('executing command'); }); disposables.add(reg); diff --git a/src/vs/workbench/services/configuration/browser/configuration.ts b/src/vs/workbench/services/configuration/browser/configuration.ts index 2bd9b092c9a..e3c1a130ed4 100644 --- a/src/vs/workbench/services/configuration/browser/configuration.ts +++ b/src/vs/workbench/services/configuration/browser/configuration.ts @@ -9,7 +9,7 @@ import * as errors from 'vs/base/common/errors'; import { Disposable, IDisposable, dispose, toDisposable, MutableDisposable, combinedDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { RunOnceScheduler } from 'vs/base/common/async'; import { FileChangeType, FileChangesEvent, IFileService, whenProviderRegistered, FileOperationError, FileOperationResult, FileOperation, FileOperationEvent } from 'vs/platform/files/common/files'; -import { ConfigurationModel, ConfigurationModelParser, ConfigurationParseOptions, DefaultConfigurationModel, UserSettings } from 'vs/platform/configuration/common/configurationModels'; +import { ConfigurationModel, ConfigurationModelParser, ConfigurationParseOptions, UserSettings } from 'vs/platform/configuration/common/configurationModels'; import { WorkspaceConfigurationModelParser, StandaloneConfigurationModelParser } from 'vs/workbench/services/configuration/common/configurationModels'; import { TASKS_CONFIGURATION_KEY, FOLDER_SETTINGS_NAME, LAUNCH_CONFIGURATION_KEY, IConfigurationCache, ConfigurationKey, REMOTE_MACHINE_SCOPES, FOLDER_SCOPES, WORKSPACE_SCOPES } from 'vs/workbench/services/configuration/common/configuration'; import { IStoredWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces'; @@ -26,8 +26,9 @@ import { joinPath } from 'vs/base/common/resources'; import { Registry } from 'vs/platform/registry/common/platform'; import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; import { isObject } from 'vs/base/common/types'; +import { DefaultConfiguration as BaseDefaultConfiguration } from 'vs/platform/configuration/common/configurations'; -export class DefaultConfiguration extends Disposable { +export class DefaultConfiguration extends BaseDefaultConfiguration { static readonly DEFAULT_OVERRIDES_CACHE_EXISTS_KEY = 'DefaultOverridesCacheExists'; @@ -35,9 +36,6 @@ export class DefaultConfiguration extends Disposable { private cachedConfigurationDefaultsOverrides: IStringDictionary<any> = {}; private readonly cacheKey: ConfigurationKey = { type: 'defaults', key: 'configurationDefaultsOverrides' }; - private readonly _onDidChangeConfiguration = this._register(new Emitter<{ defaults: ConfigurationModel; properties: string[] }>()); - readonly onDidChangeConfiguration = this._onDidChangeConfiguration.event; - private updateCache: boolean = false; constructor( @@ -50,27 +48,20 @@ export class DefaultConfiguration extends Disposable { } } - private _configurationModel: ConfigurationModel | undefined; - get configurationModel(): ConfigurationModel { - if (!this._configurationModel) { - this._configurationModel = new DefaultConfigurationModel(this.cachedConfigurationDefaultsOverrides); - } - return this._configurationModel; + protected override getConfigurationDefaultOverrides(): IStringDictionary<any> { + return this.cachedConfigurationDefaultsOverrides; } - async initialize(): Promise<ConfigurationModel> { + override async initialize(): Promise<ConfigurationModel> { await this.initializeCachedConfigurationDefaultsOverrides(); - this._configurationModel = undefined; - this._register(this.configurationRegistry.onDidUpdateConfiguration(({ properties, defaultsOverrides }) => this.onDidUpdateConfiguration(properties, defaultsOverrides))); - return this.configurationModel; + return super.initialize(); } - reload(): ConfigurationModel { + override reload(): ConfigurationModel { this.updateCache = true; this.cachedConfigurationDefaultsOverrides = {}; - this._configurationModel = undefined; this.updateCachedConfigurationDefaultsOverrides(); - return this.configurationModel; + return super.reload(); } private initiaizeCachedConfigurationDefaultsOverridesPromise: Promise<void> | undefined; @@ -92,9 +83,8 @@ export class DefaultConfiguration extends Disposable { return this.initiaizeCachedConfigurationDefaultsOverridesPromise; } - private onDidUpdateConfiguration(properties: string[], defaultsOverrides?: boolean): void { - this._configurationModel = undefined; - this._onDidChangeConfiguration.fire({ defaults: this.configurationModel, properties }); + protected override onDidUpdateConfiguration(properties: string[], defaultsOverrides?: boolean): void { + super.onDidUpdateConfiguration(properties, defaultsOverrides); if (defaultsOverrides) { this.updateCachedConfigurationDefaultsOverrides(); } @@ -129,15 +119,17 @@ export class UserConfiguration extends Disposable { private readonly _onDidChangeConfiguration: Emitter<ConfigurationModel> = this._register(new Emitter<ConfigurationModel>()); readonly onDidChangeConfiguration: Event<ConfigurationModel> = this._onDidChangeConfiguration.event; - private readonly userConfiguration: MutableDisposable<UserSettings | FileServiceBasedConfiguration> = this._register(new MutableDisposable<UserSettings | FileServiceBasedConfiguration>()); + private readonly userConfiguration = this._register(new MutableDisposable<UserSettings | FileServiceBasedConfiguration>()); + private readonly userConfigurationChangeDisposable = this._register(new MutableDisposable<IDisposable>()); private readonly reloadConfigurationScheduler: RunOnceScheduler; - private readonly configurationParseOptions: ConfigurationParseOptions; + private configurationParseOptions: ConfigurationParseOptions; get hasTasksLoaded(): boolean { return this.userConfiguration.value instanceof FileServiceBasedConfiguration; } constructor( - private readonly userSettingsResource: URI, + private settingsResource: URI, + private tasksResource: URI | undefined, scopes: ConfigurationScope[] | undefined, private readonly fileService: IFileService, private readonly uriIdentityService: IUriIdentityService, @@ -145,9 +137,27 @@ export class UserConfiguration extends Disposable { ) { super(); this.configurationParseOptions = { scopes, skipRestricted: false }; - this.userConfiguration.value = new UserSettings(this.userSettingsResource, scopes, uriIdentityService.extUri, this.fileService); - this._register(this.userConfiguration.value.onDidChange(() => this.reloadConfigurationScheduler.schedule())); - this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this.reload().then(configurationModel => this._onDidChangeConfiguration.fire(configurationModel)), 50)); + this.userConfiguration.value = new UserSettings(settingsResource, scopes, uriIdentityService.extUri, this.fileService); + this.userConfigurationChangeDisposable.value = this.userConfiguration.value.onDidChange(() => this.reloadConfigurationScheduler.schedule()); + this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this.userConfiguration.value!.loadConfiguration().then(configurationModel => this._onDidChangeConfiguration.fire(configurationModel)), 50)); + } + + async reset(settingsResource: URI, tasksResource: URI | undefined, scopes: ConfigurationScope[] | undefined): Promise<ConfigurationModel> { + this.settingsResource = settingsResource; + this.tasksResource = tasksResource; + this.configurationParseOptions = { scopes, skipRestricted: false }; + const folder = this.uriIdentityService.extUri.dirname(this.settingsResource); + const standAloneConfigurationResources: [string, URI][] = this.tasksResource ? [[TASKS_CONFIGURATION_KEY, this.tasksResource]] : []; + const fileServiceBasedConfiguration = new FileServiceBasedConfiguration(folder.toString(), this.settingsResource, standAloneConfigurationResources, this.configurationParseOptions, this.fileService, this.uriIdentityService, this.logService); + const configurationModel = await fileServiceBasedConfiguration.loadConfiguration(); + this.userConfiguration.value = fileServiceBasedConfiguration; + + // Check for value because userConfiguration might have been disposed. + if (this.userConfigurationChangeDisposable.value) { + this.userConfigurationChangeDisposable.value = this.userConfiguration.value.onDidChange(() => this.reloadConfigurationScheduler.schedule()); + } + + return configurationModel; } async initialize(): Promise<ConfigurationModel> { @@ -158,19 +168,7 @@ export class UserConfiguration extends Disposable { if (this.hasTasksLoaded) { return this.userConfiguration.value!.loadConfiguration(); } - - const folder = this.uriIdentityService.extUri.dirname(this.userSettingsResource); - const standAloneConfigurationResources: [string, URI][] = [TASKS_CONFIGURATION_KEY].map(name => ([name, this.uriIdentityService.extUri.joinPath(folder, `${name}.json`)])); - const fileServiceBasedConfiguration = new FileServiceBasedConfiguration(folder.toString(), this.userSettingsResource, standAloneConfigurationResources, this.configurationParseOptions, this.fileService, this.uriIdentityService, this.logService); - const configurationModel = await fileServiceBasedConfiguration.loadConfiguration(); - this.userConfiguration.value = fileServiceBasedConfiguration; - - // Check for value because userConfiguration might have been disposed. - if (this.userConfiguration.value) { - this._register(this.userConfiguration.value.onDidChange(() => this.reloadConfigurationScheduler.schedule())); - } - - return configurationModel; + return this.reset(this.settingsResource, this.tasksResource, this.configurationParseOptions.scopes); } reparse(): ConfigurationModel { diff --git a/src/vs/workbench/services/configuration/browser/configurationService.ts b/src/vs/workbench/services/configuration/browser/configurationService.ts index 45af17cc175..72a1946338d 100644 --- a/src/vs/workbench/services/configuration/browser/configurationService.ts +++ b/src/vs/workbench/services/configuration/browser/configurationService.ts @@ -7,14 +7,15 @@ import { URI } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; import { ResourceMap } from 'vs/base/common/map'; import { equals } from 'vs/base/common/objects'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { Queue, Barrier, runWhenIdle, Promises } from 'vs/base/common/async'; import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import { IWorkspaceContextService, Workspace as BaseWorkspace, WorkbenchState, IWorkspaceFolder, IWorkspaceFoldersChangeEvent, WorkspaceFolder, toWorkspaceFolder, isWorkspaceFolder, IWorkspaceFoldersWillChangeEvent, IEmptyWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IWorkspaceIdentifier, IAnyWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; import { ConfigurationModel, ConfigurationChangeEvent, AllKeysConfigurationChangeEvent, mergeChanges } from 'vs/platform/configuration/common/configurationModels'; import { IConfigurationChangeEvent, ConfigurationTarget, IConfigurationOverrides, isConfigurationOverrides, IConfigurationData, IConfigurationValue, IConfigurationChange, ConfigurationTargetToString, IConfigurationUpdateOverrides, isConfigurationUpdateOverrides, IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IPolicyConfiguration, NullPolicyConfiguration, PolicyConfiguration } from 'vs/platform/configuration/common/configurations'; import { Configuration } from 'vs/workbench/services/configuration/common/configurationModels'; -import { FOLDER_CONFIG_FOLDER_NAME, defaultSettingsSchemaId, userSettingsSchemaId, workspaceSettingsSchemaId, folderSettingsSchemaId, IConfigurationCache, machineSettingsSchemaId, LOCAL_MACHINE_SCOPES, IWorkbenchConfigurationService, RestrictedSettings } from 'vs/workbench/services/configuration/common/configuration'; +import { FOLDER_CONFIG_FOLDER_NAME, defaultSettingsSchemaId, userSettingsSchemaId, workspaceSettingsSchemaId, folderSettingsSchemaId, IConfigurationCache, machineSettingsSchemaId, LOCAL_MACHINE_SCOPES, IWorkbenchConfigurationService, RestrictedSettings, PROFILE_SCOPES, LOCAL_MACHINE_PROFILE_SCOPES, profileSettingsSchemaId } from 'vs/workbench/services/configuration/common/configuration'; import { Registry } from 'vs/platform/registry/common/platform'; import { IConfigurationRegistry, Extensions, allSettings, windowSettings, resourceSettings, applicationSettings, machineSettings, machineOverridableSettings, ConfigurationScope, IConfigurationPropertySchema, keyFromOverrideIdentifiers, OVERRIDE_PROPERTY_PATTERN, resourceLanguageSettingsSchemaId, configurationDefaultsSchemaId } from 'vs/platform/configuration/common/configurationRegistry'; import { IStoredWorkspaceFolder, isStoredWorkspaceFolder, IWorkspaceFolderCreationData, useSlashForPath, getStoredWorkspaceFolder, toWorkspaceFolders } from 'vs/platform/workspaces/common/workspaces'; @@ -39,6 +40,15 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { IWorkbenchAssignmentService } from 'vs/workbench/services/assignment/common/assignmentService'; import { isUndefined } from 'vs/base/common/types'; import { localize } from 'vs/nls'; +import { DidChangeUserDataProfileEvent, IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { IPolicyService, NullPolicyService } from 'vs/platform/policy/common/policy'; +import { IUserDataProfile, IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; + +function getLocalUserConfigurationScopes(userDataProfile: IUserDataProfile, hasRemote: boolean): ConfigurationScope[] | undefined { + return userDataProfile.isDefault + ? hasRemote ? LOCAL_MACHINE_SCOPES : undefined + : hasRemote ? LOCAL_MACHINE_PROFILE_SCOPES : PROFILE_SCOPES; +} class Workspace extends BaseWorkspace { initialized: boolean = false; @@ -54,16 +64,15 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat private readonly configurationCache: IConfigurationCache; private _configuration: Configuration; private initialized: boolean = false; - private defaultConfiguration: DefaultConfiguration; - private localUserConfiguration: UserConfiguration; - private remoteUserConfiguration: RemoteUserConfiguration | null = null; - private workspaceConfiguration: WorkspaceConfiguration; + private readonly defaultConfiguration: DefaultConfiguration; + private readonly policyConfiguration: IPolicyConfiguration; + private applicationConfiguration: UserConfiguration | null = null; + private readonly applicationConfigurationDisposables: DisposableStore; + private readonly localUserConfiguration: UserConfiguration; + private readonly remoteUserConfiguration: RemoteUserConfiguration | null = null; + private readonly workspaceConfiguration: WorkspaceConfiguration; private cachedFolderConfigs: ResourceMap<FolderConfiguration>; - private workspaceEditingQueue: Queue<void>; - - private readonly logService: ILogService; - private readonly fileService: IFileService; - private readonly uriIdentityService: IUriIdentityService; + private readonly workspaceEditingQueue: Queue<void>; private readonly _onDidChangeConfiguration: Emitter<IConfigurationChangeEvent> = this._register(new Emitter<IConfigurationChangeEvent>()); public readonly onDidChangeConfiguration: Event<IConfigurationChangeEvent> = this._onDidChangeConfiguration.event; @@ -98,10 +107,13 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat constructor( { remoteAuthority, configurationCache }: { remoteAuthority?: string; configurationCache: IConfigurationCache }, environmentService: IWorkbenchEnvironmentService, - fileService: IFileService, + private readonly userDataProfileService: IUserDataProfileService, + private readonly userDataProfilesService: IUserDataProfilesService, + private readonly fileService: IFileService, remoteAgentService: IRemoteAgentService, - uriIdentityService: IUriIdentityService, - logService: ILogService, + private readonly uriIdentityService: IUriIdentityService, + private readonly logService: ILogService, + policyService: IPolicyService ) { super(); @@ -109,14 +121,14 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat this.initRemoteUserConfigurationBarrier = new Barrier(); this.completeWorkspaceBarrier = new Barrier(); - this.defaultConfiguration = new DefaultConfiguration(configurationCache, environmentService); + this.defaultConfiguration = this._register(new DefaultConfiguration(configurationCache, environmentService)); + this.policyConfiguration = policyService instanceof NullPolicyService ? new NullPolicyConfiguration() : this._register(new PolicyConfiguration(this.defaultConfiguration, policyService, logService)); this.configurationCache = configurationCache; - this.fileService = fileService; - this.uriIdentityService = uriIdentityService; - this.logService = logService; - this._configuration = new Configuration(this.defaultConfiguration.configurationModel, new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ResourceMap(), new ConfigurationModel(), new ResourceMap<ConfigurationModel>(), this.workspace); + this._configuration = new Configuration(this.defaultConfiguration.configurationModel, this.policyConfiguration.configurationModel, new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ResourceMap(), new ConfigurationModel(), new ResourceMap<ConfigurationModel>(), this.workspace); + this.applicationConfigurationDisposables = this._register(new DisposableStore()); + this.createApplicationConfiguration(); + this.localUserConfiguration = this._register(new UserConfiguration(userDataProfileService.currentProfile.settingsResource, userDataProfileService.currentProfile.tasksResource, getLocalUserConfigurationScopes(userDataProfileService.currentProfile, !!remoteAuthority), fileService, uriIdentityService, logService)); this.cachedFolderConfigs = new ResourceMap<FolderConfiguration>(); - this.localUserConfiguration = this._register(new UserConfiguration(environmentService.settingsResource, remoteAuthority ? LOCAL_MACHINE_SCOPES : undefined, fileService, uriIdentityService, logService)); this._register(this.localUserConfiguration.onDidChangeConfiguration(userConfiguration => this.onLocalUserConfigurationChanged(userConfiguration))); if (remoteAuthority) { const remoteUserConfiguration = this.remoteUserConfiguration = this._register(new RemoteUserConfiguration(remoteAuthority, configurationCache, fileService, uriIdentityService, remoteAgentService)); @@ -138,10 +150,22 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat })); this._register(this.defaultConfiguration.onDidChangeConfiguration(({ properties, defaults }) => this.onDefaultConfigurationChanged(defaults, properties))); + this._register(this.policyConfiguration.onDidChangeConfiguration(configurationModel => this.onPolicyConfigurationChanged(configurationModel))); + this._register(userDataProfileService.onDidChangeCurrentProfile(e => this.onUserDataProfileChanged(e))); this.workspaceEditingQueue = new Queue<void>(); } + private createApplicationConfiguration(): void { + this.applicationConfigurationDisposables.clear(); + if (this.userDataProfileService.currentProfile.isDefault) { + this.applicationConfiguration = null; + } else { + this.applicationConfiguration = this.applicationConfigurationDisposables.add(this._register(new UserConfiguration(this.userDataProfilesService.defaultProfile.settingsResource, undefined, [ConfigurationScope.APPLICATION], this.fileService, this.uriIdentityService, this.logService))); + this.applicationConfigurationDisposables.add(this.applicationConfiguration.onDidChangeConfiguration(configurationModel => this.onApplicationConfigurationChanged(configurationModel))); + } + } + // Workspace Context Service Impl public async getCompleteWorkspace(): Promise<Workspace> { @@ -338,9 +362,11 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat async reloadConfiguration(target?: ConfigurationTarget | IWorkspaceFolder): Promise<void> { if (target === undefined) { + this.reloadDefaultConfiguration(); + const application = await this.reloadApplicationConfiguration(true); const { local, remote } = await this.reloadUserConfiguration(); await this.reloadWorkspaceConfiguration(); - await this.loadConfiguration(local, remote); + await this.loadConfiguration(application, local, remote); return; } @@ -351,12 +377,12 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat switch (target) { case ConfigurationTarget.DEFAULT: - await this.reloadDefaultConfiguration(); + this.reloadDefaultConfiguration(); return; case ConfigurationTarget.USER: { const { local, remote } = await this.reloadUserConfiguration(); - await this.loadConfiguration(local, remote); + await this.loadConfiguration(this._configuration.applicationConfiguration, local, remote); return; } case ConfigurationTarget.USER_LOCAL: @@ -447,9 +473,7 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat if (this.restrictedSettings.workspace) { keys.push(...this.restrictedSettings.workspace); } - if (this.restrictedSettings.workspaceFolder) { - this.restrictedSettings.workspaceFolder.forEach((value) => keys.push(...value)); - } + this.restrictedSettings.workspaceFolder?.forEach((value) => keys.push(...value)); keys = distinct(keys); if (keys.length) { this.triggerConfigurationChange({ keys, overrides: [] }, { data, workspace: this.workspace }, ConfigurationTarget.WORKSPACE); @@ -555,7 +579,7 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat const result: IWorkspaceFoldersChangeEvent = { added: [], removed: [], changed: [] }; result.added = newFolders.filter(newFolder => !currentFolders.some(currentFolder => newFolder.uri.toString() === currentFolder.uri.toString())); for (let currentIndex = 0; currentIndex < currentFolders.length; currentIndex++) { - let currentFolder = currentFolders[currentIndex]; + const currentFolder = currentFolders[currentIndex]; let newIndex = 0; for (newIndex = 0; newIndex < newFolders.length && currentFolder.uri.toString() !== newFolders[newIndex].uri.toString(); newIndex++) { } if (newIndex < newFolders.length) { @@ -572,24 +596,46 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat private async initializeConfiguration(): Promise<void> { await this.defaultConfiguration.initialize(); - mark('code/willInitUserConfiguration'); - const { local, remote } = await this.initializeUserConfiguration(); - mark('code/didInitUserConfiguration'); + const [, application, user] = await Promise.all([ + this.policyConfiguration.initialize(), + this.initializeApplicationConfiguration(), + (async () => { + mark('code/willInitUserConfiguration'); + const result = await this.initializeUserConfiguration(); + mark('code/didInitUserConfiguration'); + return result; + })() + ]); mark('code/willInitWorkspaceConfiguration'); - await this.loadConfiguration(local, remote); + await this.loadConfiguration(application, user.local, user.remote); mark('code/didInitWorkspaceConfiguration'); } + private async initializeApplicationConfiguration(): Promise<ConfigurationModel> { + return this.applicationConfiguration ? this.applicationConfiguration.initialize() : Promise.resolve(new ConfigurationModel()); + } + private async initializeUserConfiguration(): Promise<{ local: ConfigurationModel; remote: ConfigurationModel }> { const [local, remote] = await Promise.all([this.localUserConfiguration.initialize(), this.remoteUserConfiguration ? this.remoteUserConfiguration.initialize() : Promise.resolve(new ConfigurationModel())]); return { local, remote }; } - private async reloadDefaultConfiguration(): Promise<void> { + private reloadDefaultConfiguration(): void { this.onDefaultConfigurationChanged(this.defaultConfiguration.reload()); } + private async reloadApplicationConfiguration(donotTrigger?: boolean): Promise<ConfigurationModel> { + if (!this.applicationConfiguration) { + return new ConfigurationModel(); + } + const model = await this.applicationConfiguration.reload(); + if (!donotTrigger) { + this.onApplicationConfigurationChanged(model); + } + return model; + } + private async reloadUserConfiguration(): Promise<{ local: ConfigurationModel; remote: ConfigurationModel }> { const [local, remote] = await Promise.all([this.reloadLocalUserConfiguration(true), this.reloadRemoteUserConfiguration(true)]); return { local, remote }; @@ -628,19 +674,19 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat return this.onWorkspaceFolderConfigurationChanged(folder); } - private async loadConfiguration(userConfigurationModel: ConfigurationModel, remoteUserConfigurationModel: ConfigurationModel): Promise<void> { + private async loadConfiguration(applicationConfigurationModel: ConfigurationModel, userConfigurationModel: ConfigurationModel, remoteUserConfigurationModel: ConfigurationModel): Promise<void> { // reset caches this.cachedFolderConfigs = new ResourceMap<FolderConfiguration>(); const folders = this.workspace.folders; const folderConfigurations = await this.loadFolderConfigurations(folders); - let workspaceConfiguration = this.getWorkspaceConfigurationModel(folderConfigurations); + const workspaceConfiguration = this.getWorkspaceConfigurationModel(folderConfigurations); const folderConfigurationModels = new ResourceMap<ConfigurationModel>(); folderConfigurations.forEach((folderConfiguration, index) => folderConfigurationModels.set(folders[index].uri, folderConfiguration)); const currentConfiguration = this._configuration; - this._configuration = new Configuration(this.defaultConfiguration.configurationModel, userConfigurationModel, remoteUserConfigurationModel, workspaceConfiguration, folderConfigurationModels, new ConfigurationModel(), new ResourceMap<ConfigurationModel>(), this.workspace); + this._configuration = new Configuration(this.defaultConfiguration.configurationModel, this.policyConfiguration.configurationModel, applicationConfigurationModel, userConfigurationModel, remoteUserConfigurationModel, workspaceConfiguration, folderConfigurationModels, new ConfigurationModel(), new ResourceMap<ConfigurationModel>(), this.workspace); if (this.initialized) { const change = this._configuration.compare(currentConfiguration); @@ -664,6 +710,21 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat } } + private onUserDataProfileChanged(e: DidChangeUserDataProfileEvent): void { + const promises: Promise<ConfigurationModel>[] = []; + promises.push(this.localUserConfiguration.reset(e.profile.settingsResource, e.profile.tasksResource, getLocalUserConfigurationScopes(e.profile, !!this.remoteUserConfiguration))); + if (e.previous.isDefault !== e.profile.isDefault) { + this.createApplicationConfiguration(); + if (this.applicationConfiguration) { + promises.push(this.reloadApplicationConfiguration(true)); + } + } + e.join((async () => { + const [localUser, application] = await Promise.all(promises); + await this.loadConfiguration(application ?? this._configuration.applicationConfiguration, localUser, this._configuration.remoteUserConfiguration); + })()); + } + private onDefaultConfigurationChanged(configurationModel: ConfigurationModel, properties?: string[]): void { if (this.workspace) { const previousData = this._configuration.toData(); @@ -692,6 +753,18 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat } } + private onPolicyConfigurationChanged(policyConfiguration: ConfigurationModel): void { + const previous = { data: this._configuration.toData(), workspace: this.workspace }; + const change = this._configuration.compareAndUpdatePolicyConfiguration(policyConfiguration); + this.triggerConfigurationChange(change, previous, ConfigurationTarget.DEFAULT); + } + + private onApplicationConfigurationChanged(applicationConfiguration: ConfigurationModel): void { + const previous = { data: this._configuration.toData(), workspace: this.workspace }; + const change = this._configuration.compareAndUpdateApplicationConfiguration(applicationConfiguration); + this.triggerConfigurationChange(change, previous, ConfigurationTarget.USER); + } + private onLocalUserConfigurationChanged(userConfiguration: ConfigurationModel): void { const previous = { data: this._configuration.toData(), workspace: this.workspace }; const change = this._configuration.compareAndUpdateLocalUserConfiguration(userConfiguration); @@ -897,7 +970,12 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat await this.configurationEditingService.writeConfiguration(editableConfigurationTarget, { key, value }, { scopes: overrides, donotNotifyError }); switch (editableConfigurationTarget) { case EditableConfigurationTarget.USER_LOCAL: - return this.reloadLocalUserConfiguration().then(() => undefined); + if (this.applicationConfiguration && this.configurationRegistry.getConfigurationProperties()[key].scope === ConfigurationScope.APPLICATION) { + await this.reloadApplicationConfiguration(); + } else { + await this.reloadLocalUserConfiguration(); + } + return; case EditableConfigurationTarget.USER_REMOTE: return this.reloadRemoteUserConfiguration().then(() => undefined); case EditableConfigurationTarget.WORKSPACE: @@ -1030,6 +1108,19 @@ class RegisterConfigurationSchemasContribution extends Disposable implements IWo } : allSettingsSchema; + const profileSettingsSchema: IJSONSchema = { + properties: { + ...machineSettings.properties, + ...machineOverridableSettings.properties, + ...windowSettings.properties, + ...resourceSettings.properties + }, + patternProperties: allSettings.patternProperties, + additionalProperties: true, + allowTrailingCommas: true, + allowComments: true + }; + const machineSettingsSchema: IJSONSchema = { properties: { ...machineSettings.properties, @@ -1075,6 +1166,7 @@ class RegisterConfigurationSchemasContribution extends Disposable implements IWo allowComments: true }); jsonRegistry.registerSchema(userSettingsSchemaId, userSettingsSchema); + jsonRegistry.registerSchema(profileSettingsSchemaId, profileSettingsSchema); jsonRegistry.registerSchema(machineSettingsSchemaId, machineSettingsSchema); if (WorkbenchState.WORKSPACE === this.workspaceContextService.getWorkbenchState()) { diff --git a/src/vs/workbench/services/configuration/common/configuration.ts b/src/vs/workbench/services/configuration/common/configuration.ts index c3d97f0c5e6..0b625da7565 100644 --- a/src/vs/workbench/services/configuration/common/configuration.ts +++ b/src/vs/workbench/services/configuration/common/configuration.ts @@ -9,6 +9,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { refineServiceDecorator } from 'vs/platform/instantiation/common/instantiation'; import { Event } from 'vs/base/common/event'; import { ResourceMap } from 'vs/base/common/map'; +import { IAnyWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; export const FOLDER_CONFIG_FOLDER_NAME = '.vscode'; export const FOLDER_SETTINGS_NAME = 'settings'; @@ -16,13 +17,16 @@ export const FOLDER_SETTINGS_PATH = `${FOLDER_CONFIG_FOLDER_NAME}/${FOLDER_SETTI export const defaultSettingsSchemaId = 'vscode://schemas/settings/default'; export const userSettingsSchemaId = 'vscode://schemas/settings/user'; +export const profileSettingsSchemaId = 'vscode://schemas/settings/profile'; export const machineSettingsSchemaId = 'vscode://schemas/settings/machine'; export const workspaceSettingsSchemaId = 'vscode://schemas/settings/workspace'; export const folderSettingsSchemaId = 'vscode://schemas/settings/folder'; export const launchSchemaId = 'vscode://schemas/launch'; export const tasksSchemaId = 'vscode://schemas/tasks'; -export const LOCAL_MACHINE_SCOPES = [ConfigurationScope.APPLICATION, ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE, ConfigurationScope.LANGUAGE_OVERRIDABLE]; +export const PROFILE_SCOPES = [ConfigurationScope.MACHINE, ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE, ConfigurationScope.LANGUAGE_OVERRIDABLE]; +export const LOCAL_MACHINE_PROFILE_SCOPES = [ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE, ConfigurationScope.LANGUAGE_OVERRIDABLE]; +export const LOCAL_MACHINE_SCOPES = [ConfigurationScope.APPLICATION, ...LOCAL_MACHINE_PROFILE_SCOPES]; export const REMOTE_MACHINE_SCOPES = [ConfigurationScope.MACHINE, ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE, ConfigurationScope.LANGUAGE_OVERRIDABLE, ConfigurationScope.MACHINE_OVERRIDABLE]; export const WORKSPACE_SCOPES = [ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE, ConfigurationScope.LANGUAGE_OVERRIDABLE, ConfigurationScope.MACHINE_OVERRIDABLE]; export const FOLDER_SCOPES = [ConfigurationScope.RESOURCE, ConfigurationScope.LANGUAGE_OVERRIDABLE, ConfigurationScope.MACHINE_OVERRIDABLE]; @@ -72,6 +76,12 @@ export interface IWorkbenchConfigurationService extends IConfigurationService { * The promise is resolved immediately if the window is not remote. */ whenRemoteConfigurationLoaded(): Promise<void>; + + /** + * Initialize configuration service for the given workspace + * @param arg workspace Identifier + */ + initialize(arg: IAnyWorkspaceIdentifier): Promise<void>; } export const TASKS_DEFAULT = '{\n\t\"version\": \"2.0.0\",\n\t\"tasks\": []\n}'; diff --git a/src/vs/workbench/services/configuration/common/configurationEditingService.ts b/src/vs/workbench/services/configuration/common/configurationEditingService.ts index 3e4e805c231..fc5491281b1 100644 --- a/src/vs/workbench/services/configuration/common/configurationEditingService.ts +++ b/src/vs/workbench/services/configuration/common/configurationEditingService.ts @@ -11,7 +11,6 @@ import { Queue } from 'vs/base/common/async'; import { Edit, FormattingOptions } from 'vs/base/common/jsonFormatter'; import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IConfigurationService, IConfigurationUpdateOverrides } from 'vs/platform/configuration/common/configuration'; import { FOLDER_SETTINGS_PATH, WORKSPACE_STANDALONE_CONFIGURATIONS, TASKS_CONFIGURATION_KEY, LAUNCH_CONFIGURATION_KEY, USER_STANDALONE_CONFIGURATIONS, TASKS_DEFAULT, FOLDER_SCOPES } from 'vs/workbench/services/configuration/common/configuration'; @@ -29,6 +28,8 @@ import { IReference } from 'vs/base/common/lifecycle'; import { Range } from 'vs/editor/common/core/range'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Selection } from 'vs/editor/common/core/selection'; +import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; export const enum ConfigurationEditingErrorCode { @@ -93,6 +94,11 @@ export const enum ConfigurationEditingErrorCode { ERROR_INVALID_CONFIGURATION, /** + * Error when trying to write a policy configuration + */ + ERROR_POLICY_CONFIGURATION, + + /** * Internal Error. */ ERROR_INTERNAL @@ -148,7 +154,8 @@ export class ConfigurationEditingService { constructor( @IConfigurationService private readonly configurationService: IConfigurationService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, - @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, + @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, @IFileService private readonly fileService: IFileService, @ITextModelService private readonly textModelResolverService: ITextModelService, @ITextFileService private readonly textFileService: ITextFileService, @@ -226,7 +233,7 @@ export class ConfigurationEditingService { const startPosition = model.getPositionAt(edit.offset); const endPosition = model.getPositionAt(edit.offset + edit.length); const range = new Range(startPosition.lineNumber, startPosition.column, endPosition.lineNumber, endPosition.column); - let currentText = model.getValueInRange(range); + const currentText = model.getValueInRange(range); if (edit.content !== currentText) { const editOperation = currentText ? EditOperation.replace(range, edit.content) : EditOperation.insert(startPosition, edit.content); model.pushEditOperations([new Selection(startPosition.lineNumber, startPosition.column, startPosition.lineNumber, startPosition.column)], [editOperation], () => []); @@ -359,6 +366,7 @@ export class ConfigurationEditingService { switch (error) { // API constraints + case ConfigurationEditingErrorCode.ERROR_POLICY_CONFIGURATION: return nls.localize('errorPolicyConfiguration', "Unable to write {0} because it is configured in system policy.", operation.key); case ConfigurationEditingErrorCode.ERROR_UNKNOWN_KEY: return nls.localize('errorUnknownKey', "Unable to write to {0} because {1} is not a registered configuration.", this.stringifyTarget(target), operation.key); case ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_CONFIGURATION_APPLICATION: return nls.localize('errorInvalidWorkspaceConfigurationApplication', "Unable to write {0} to Workspace Settings. This setting can be written only into User settings.", operation.key); case ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_CONFIGURATION_MACHINE: return nls.localize('errorInvalidWorkspaceConfigurationMachine', "Unable to write {0} to Workspace Settings. This setting can be written only into User settings.", operation.key); @@ -492,6 +500,10 @@ export class ConfigurationEditingService { private async validate(target: EditableConfigurationTarget, operation: IConfigurationEditOperation, checkDirty: boolean, overrides: IConfigurationUpdateOverrides): Promise<void> { + if (this.configurationService.inspect(operation.key).policyValue !== undefined) { + throw this.toConfigurationEditingError(ConfigurationEditingErrorCode.ERROR_POLICY_CONFIGURATION, target, operation); + } + const configurationProperties = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).getConfigurationProperties(); const configurationScope = configurationProperties[operation.key]?.scope; @@ -566,7 +578,7 @@ export class ConfigurationEditingService { const standaloneConfigurationMap = target === EditableConfigurationTarget.USER_LOCAL ? USER_STANDALONE_CONFIGURATIONS : WORKSPACE_STANDALONE_CONFIGURATIONS; const standaloneConfigurationKeys = Object.keys(standaloneConfigurationMap); for (const key of standaloneConfigurationKeys) { - const resource = this.getConfigurationFileResource(target, standaloneConfigurationMap[key], overrides.resource); + const resource = this.getConfigurationFileResource(target, key, standaloneConfigurationMap[key], overrides.resource, undefined); // Check for prefix if (config.key === key) { @@ -583,13 +595,15 @@ export class ConfigurationEditingService { } } - let key = config.key; + const key = config.key; + const configurationProperties = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).getConfigurationProperties(); + const configurationScope = configurationProperties[key]?.scope; let jsonPath = overrides.overrideIdentifiers?.length ? [keyFromOverrideIdentifiers(overrides.overrideIdentifiers), key] : [key]; if (target === EditableConfigurationTarget.USER_LOCAL || target === EditableConfigurationTarget.USER_REMOTE) { - return { key, jsonPath, value: config.value, resource: withNullAsUndefined(this.getConfigurationFileResource(target, '', null)), target }; + return { key, jsonPath, value: config.value, resource: withNullAsUndefined(this.getConfigurationFileResource(target, undefined, '', null, configurationScope)), target }; } - const resource = this.getConfigurationFileResource(target, FOLDER_SETTINGS_PATH, overrides.resource); + const resource = this.getConfigurationFileResource(target, undefined, FOLDER_SETTINGS_PATH, overrides.resource, configurationScope); if (this.isWorkspaceConfigurationResource(resource)) { jsonPath = ['settings', ...jsonPath]; } @@ -601,12 +615,15 @@ export class ConfigurationEditingService { return !!(workspace.configuration && resource && workspace.configuration.fsPath === resource.fsPath); } - private getConfigurationFileResource(target: EditableConfigurationTarget, relativePath: string, resource: URI | null | undefined): URI | null { + private getConfigurationFileResource(target: EditableConfigurationTarget, standAloneConfigurationKey: string | undefined, relativePath: string, resource: URI | null | undefined, scope: ConfigurationScope | undefined): URI | null { if (target === EditableConfigurationTarget.USER_LOCAL) { - if (relativePath) { - return this.uriIdentityService.extUri.joinPath(this.uriIdentityService.extUri.dirname(this.environmentService.settingsResource), relativePath); + if (standAloneConfigurationKey === TASKS_CONFIGURATION_KEY) { + return this.userDataProfileService.currentProfile.tasksResource; } else { - return this.environmentService.settingsResource; + if (scope === ConfigurationScope.APPLICATION && !this.userDataProfileService.currentProfile.isDefault) { + return this.userDataProfilesService.defaultProfile.settingsResource; + } + return this.userDataProfileService.currentProfile.settingsResource; } } if (target === EditableConfigurationTarget.USER_REMOTE) { diff --git a/src/vs/workbench/services/configuration/common/configurationModels.ts b/src/vs/workbench/services/configuration/common/configurationModels.ts index 95a1d40ad77..95eabebc306 100644 --- a/src/vs/workbench/services/configuration/common/configurationModels.ts +++ b/src/vs/workbench/services/configuration/common/configurationModels.ts @@ -98,6 +98,8 @@ export class Configuration extends BaseConfiguration { constructor( defaults: ConfigurationModel, + policy: ConfigurationModel, + application: ConfigurationModel, localUser: ConfigurationModel, remoteUser: ConfigurationModel, workspaceConfiguration: ConfigurationModel, @@ -105,7 +107,7 @@ export class Configuration extends BaseConfiguration { memoryConfiguration: ConfigurationModel, memoryConfigurationByResource: ResourceMap<ConfigurationModel>, private readonly _workspace?: Workspace) { - super(defaults, localUser, remoteUser, workspaceConfiguration, folders, memoryConfiguration, memoryConfigurationByResource); + super(defaults, policy, application, localUser, remoteUser, workspaceConfiguration, folders, memoryConfiguration, memoryConfigurationByResource); } override getValue(key: string | undefined, overrides: IConfigurationOverrides = {}): any { diff --git a/src/vs/workbench/services/configuration/common/jsonEditingService.ts b/src/vs/workbench/services/configuration/common/jsonEditingService.ts index cd00727f8d1..5251efc25b5 100644 --- a/src/vs/workbench/services/configuration/common/jsonEditingService.ts +++ b/src/vs/workbench/services/configuration/common/jsonEditingService.ts @@ -62,7 +62,7 @@ export class JSONEditingService implements IJSONEditingService { const startPosition = model.getPositionAt(edit.offset); const endPosition = model.getPositionAt(edit.offset + edit.length); const range = new Range(startPosition.lineNumber, startPosition.column, endPosition.lineNumber, endPosition.column); - let currentText = model.getValueInRange(range); + const currentText = model.getValueInRange(range); if (edit.content !== currentText) { const editOperation = currentText ? EditOperation.replace(range, edit.content) : EditOperation.insert(startPosition, edit.content); model.pushEditOperations([new Selection(startPosition.lineNumber, startPosition.column, startPosition.lineNumber, startPosition.column)], [editOperation], () => []); diff --git a/src/vs/workbench/services/configuration/test/browser/configurationEditingService.test.ts b/src/vs/workbench/services/configuration/test/browser/configurationEditingService.test.ts index 7d7e3cb1267..243add76058 100644 --- a/src/vs/workbench/services/configuration/test/browser/configurationEditingService.test.ts +++ b/src/vs/workbench/services/configuration/test/browser/configurationEditingService.test.ts @@ -6,6 +6,7 @@ import * as sinon from 'sinon'; import * as assert from 'assert'; import * as json from 'vs/base/common/json'; +import { Event } from 'vs/base/common/event'; import { Registry } from 'vs/platform/registry/common/platform'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; @@ -37,8 +38,14 @@ import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFil import { joinPath } from 'vs/base/common/resources'; import { VSBuffer } from 'vs/base/common/buffer'; import { RemoteAgentService } from 'vs/workbench/services/remote/browser/remoteAgentService'; -import { BrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; import { getSingleFolderWorkspaceIdentifier } from 'vs/workbench/services/workspaces/browser/workspaces'; +import { IUserDataProfilesService, UserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { hash } from 'vs/base/common/hash'; +import { FilePolicyService } from 'vs/platform/policy/common/filePolicyService'; +import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; +import { UserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfileService'; +import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; const ROOT = URI.file('tests').with({ scheme: 'vscode-tests' }); @@ -52,7 +59,8 @@ export class ConfigurationCache implements IConfigurationCache { suite('ConfigurationEditingService', () => { let instantiationService: TestInstantiationService; - let environmentService: BrowserWorkbenchEnvironmentService; + let userDataProfileService: IUserDataProfileService; + let environmentService: IWorkbenchEnvironmentService; let fileService: IFileService; let workspaceService: WorkspaceService; let testObject: ConfigurationEditingService; @@ -76,6 +84,14 @@ suite('ConfigurationEditingService', () => { 'configurationEditing.service.testSettingThree': { 'type': 'string', 'default': 'isSet' + }, + 'configurationEditing.service.policySetting': { + 'type': 'string', + 'default': 'isSet', + policy: { + name: 'configurationEditing.service.policySetting', + minimumVersion: '1.0.0', + } } } }); @@ -92,12 +108,19 @@ suite('ConfigurationEditingService', () => { instantiationService = <TestInstantiationService>workbenchInstantiationService(undefined, disposables); environmentService = TestEnvironmentService; + environmentService.policyFile = joinPath(workspaceFolder, 'policies.json'); instantiationService.stub(IEnvironmentService, environmentService); + const userDataProfilesService = instantiationService.stub(IUserDataProfilesService, new UserDataProfilesService(environmentService, fileService, logService)); + userDataProfileService = new UserDataProfileService(userDataProfilesService.defaultProfile); const remoteAgentService = disposables.add(instantiationService.createInstance(RemoteAgentService, null)); disposables.add(fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, logService)))); instantiationService.stub(IFileService, fileService); instantiationService.stub(IRemoteAgentService, remoteAgentService); - workspaceService = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService())); + workspaceService = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, userDataProfileService, userDataProfilesService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService(), new FilePolicyService(environmentService.policyFile, fileService, logService))); + await workspaceService.initialize({ + id: hash(workspaceFolder.toString()).toString(16), + uri: workspaceFolder + }); instantiationService.stub(IWorkspaceContextService, workspaceService); await workspaceService.initialize(getSingleFolderWorkspaceIdentifier(workspaceFolder)); @@ -133,7 +156,7 @@ suite('ConfigurationEditingService', () => { }); test('errors cases - invalid configuration', async () => { - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(',,,,,,,,,,,,,,')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString(',,,,,,,,,,,,,,')); try { await testObject.writeConfiguration(EditableConfigurationTarget.USER_LOCAL, { key: 'configurationEditing.service.testSetting', value: 'value' }, { donotNotifyError: true }); } catch (error) { @@ -169,7 +192,7 @@ suite('ConfigurationEditingService', () => { test('do not notify error', async () => { instantiationService.stub(ITextFileService, 'isDirty', true); const target = sinon.stub(); - instantiationService.stub(INotificationService, <INotificationService>{ prompt: target, _serviceBrand: undefined, onDidAddNotification: undefined!, onDidRemoveNotification: undefined!, notify: null!, error: null!, info: null!, warn: null!, status: null!, setFilter: null! }); + instantiationService.stub(INotificationService, <INotificationService>{ prompt: target, _serviceBrand: undefined, doNotDisturbMode: false, onDidAddNotification: undefined!, onDidRemoveNotification: undefined!, onDidChangeDoNotDisturbMode: undefined!, notify: null!, error: null!, info: null!, warn: null!, status: null! }); try { await testObject.writeConfiguration(EditableConfigurationTarget.USER_LOCAL, { key: 'configurationEditing.service.testSetting', value: 'value' }, { donotNotifyError: true }); } catch (error) { @@ -180,38 +203,60 @@ suite('ConfigurationEditingService', () => { assert.fail('Should fail with ERROR_CONFIGURATION_FILE_DIRTY error.'); }); + test('errors cases - ERROR_POLICY_CONFIGURATION', async () => { + await runWithFakedTimers({ useFakeTimers: true }, async () => { + const promise = Event.toPromise(instantiationService.get(IConfigurationService).onDidChangeConfiguration); + await fileService.writeFile(environmentService.policyFile!, VSBuffer.fromString('{ "configurationEditing.service.policySetting": "policyValue" }')); + await promise; + }); + try { + await testObject.writeConfiguration(EditableConfigurationTarget.USER_LOCAL, { key: 'configurationEditing.service.policySetting', value: 'value' }, { donotNotifyError: true }); + } catch (error) { + assert.strictEqual(error.code, ConfigurationEditingErrorCode.ERROR_POLICY_CONFIGURATION); + return; + } + assert.fail('Should fail with ERROR_POLICY_CONFIGURATION'); + }); + + test('write policy setting - when not set', async () => { + await testObject.writeConfiguration(EditableConfigurationTarget.USER_LOCAL, { key: 'configurationEditing.service.policySetting', value: 'value' }, { donotNotifyError: true }); + const contents = await fileService.readFile(userDataProfileService.currentProfile.settingsResource); + const parsed = json.parse(contents.value.toString()); + assert.strictEqual(parsed['configurationEditing.service.policySetting'], 'value'); + }); + test('write one setting - empty file', async () => { await testObject.writeConfiguration(EditableConfigurationTarget.USER_LOCAL, { key: 'configurationEditing.service.testSetting', value: 'value' }); - const contents = await fileService.readFile(environmentService.settingsResource); + const contents = await fileService.readFile(userDataProfileService.currentProfile.settingsResource); const parsed = json.parse(contents.value.toString()); assert.strictEqual(parsed['configurationEditing.service.testSetting'], 'value'); }); test('write one setting - existing file', async () => { - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "my.super.setting": "my.super.value" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "my.super.setting": "my.super.value" }')); await testObject.writeConfiguration(EditableConfigurationTarget.USER_LOCAL, { key: 'configurationEditing.service.testSetting', value: 'value' }); - const contents = await fileService.readFile(environmentService.settingsResource); + const contents = await fileService.readFile(userDataProfileService.currentProfile.settingsResource); const parsed = json.parse(contents.value.toString()); assert.strictEqual(parsed['configurationEditing.service.testSetting'], 'value'); assert.strictEqual(parsed['my.super.setting'], 'my.super.value'); }); test('remove an existing setting - existing file', async () => { - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "my.super.setting": "my.super.value", "configurationEditing.service.testSetting": "value" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "my.super.setting": "my.super.value", "configurationEditing.service.testSetting": "value" }')); await testObject.writeConfiguration(EditableConfigurationTarget.USER_LOCAL, { key: 'configurationEditing.service.testSetting', value: undefined }); - const contents = await fileService.readFile(environmentService.settingsResource); + const contents = await fileService.readFile(userDataProfileService.currentProfile.settingsResource); const parsed = json.parse(contents.value.toString()); assert.deepStrictEqual(Object.keys(parsed), ['my.super.setting']); assert.strictEqual(parsed['my.super.setting'], 'my.super.value'); }); test('remove non existing setting - existing file', async () => { - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "my.super.setting": "my.super.value" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "my.super.setting": "my.super.value" }')); await testObject.writeConfiguration(EditableConfigurationTarget.USER_LOCAL, { key: 'configurationEditing.service.testSetting', value: undefined }); - const contents = await fileService.readFile(environmentService.settingsResource); + const contents = await fileService.readFile(userDataProfileService.currentProfile.settingsResource); const parsed = json.parse(contents.value.toString()); assert.deepStrictEqual(Object.keys(parsed), ['my.super.setting']); assert.strictEqual(parsed['my.super.setting'], 'my.super.value'); @@ -222,7 +267,7 @@ suite('ConfigurationEditingService', () => { const value = { 'configurationEditing.service.testSetting': 'overridden value' }; await testObject.writeConfiguration(EditableConfigurationTarget.USER_LOCAL, { key, value }); - const contents = await fileService.readFile(environmentService.settingsResource); + const contents = await fileService.readFile(userDataProfileService.currentProfile.settingsResource); const parsed = json.parse(contents.value.toString()); assert.deepStrictEqual(parsed[key], value); }); diff --git a/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts b/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts index 352fd8e9d7e..3a1d3a6ca2b 100644 --- a/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts +++ b/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts @@ -44,6 +44,12 @@ import { RemoteAgentService } from 'vs/workbench/services/remote/browser/remoteA import { RemoteAuthorityResolverService } from 'vs/platform/remote/browser/remoteAuthorityResolverService'; import { hash } from 'vs/base/common/hash'; import { TestProductService } from 'vs/workbench/test/common/workbenchTestServices'; +import { IUserDataProfilesService, toUserDataProfile, UserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { NullPolicyService } from 'vs/platform/policy/common/policy'; +import { FilePolicyService } from 'vs/platform/policy/common/filePolicyService'; +import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; +import { UserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfileService'; +import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; function convertToWorkspacePayload(folder: URI): ISingleFolderWorkspaceIdentifier { return { @@ -63,7 +69,9 @@ const ROOT = URI.file('tests').with({ scheme: 'vscode-tests' }); suite('WorkspaceContextService - Folder', () => { - let folderName = 'Folder A', folder: URI, testObject: WorkspaceService; + const folderName = 'Folder A'; + let folder: URI; + let testObject: WorkspaceService; const disposables = new DisposableStore(); setup(async () => { @@ -77,7 +85,8 @@ suite('WorkspaceContextService - Folder', () => { const environmentService = TestEnvironmentService; fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService()))); - testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, fileService, new RemoteAgentService(null, environmentService, TestProductService, new RemoteAuthorityResolverService(TestProductService, undefined, undefined), new SignService(undefined), new NullLogService()), new UriIdentityService(fileService), new NullLogService())); + const userDataProfilesService = new UserDataProfilesService(environmentService, fileService, logService); + testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, new UserDataProfileService(userDataProfilesService.defaultProfile), userDataProfilesService, fileService, new RemoteAgentService(null, environmentService, TestProductService, new RemoteAuthorityResolverService(TestProductService, undefined, undefined), new SignService(undefined), new NullLogService()), new UriIdentityService(fileService), new NullLogService(), new NullPolicyService())); await (<WorkspaceService>testObject).initialize(convertToWorkspacePayload(folder)); }); @@ -117,7 +126,8 @@ suite('WorkspaceContextService - Folder', () => { const environmentService = TestEnvironmentService; fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService()))); - const testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, fileService, new RemoteAgentService(null, environmentService, TestProductService, new RemoteAuthorityResolverService(TestProductService, undefined, undefined), new SignService(undefined), new NullLogService()), new UriIdentityService(fileService), new NullLogService())); + const userDataProfilesService = new UserDataProfilesService(environmentService, fileService, logService); + const testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, new UserDataProfileService(userDataProfilesService.defaultProfile), userDataProfilesService, fileService, new RemoteAgentService(null, environmentService, TestProductService, new RemoteAuthorityResolverService(TestProductService, undefined, undefined), new SignService(undefined), new NullLogService()), new UriIdentityService(fileService), new NullLogService(), new NullPolicyService())); await (<WorkspaceService>testObject).initialize(convertToWorkspacePayload(folder)); const actual = testObject.getWorkspaceFolder(joinPath(folder, 'a')); @@ -137,7 +147,8 @@ suite('WorkspaceContextService - Folder', () => { const environmentService = TestEnvironmentService; fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService()))); - const testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, fileService, new RemoteAgentService(null, environmentService, TestProductService, new RemoteAuthorityResolverService(TestProductService, undefined, undefined), new SignService(undefined), new NullLogService()), new UriIdentityService(fileService), new NullLogService())); + const userDataProfilesService = new UserDataProfilesService(environmentService, fileService, logService); + const testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, new UserDataProfileService(userDataProfilesService.defaultProfile), userDataProfilesService, fileService, new RemoteAgentService(null, environmentService, TestProductService, new RemoteAuthorityResolverService(TestProductService, undefined, undefined), new SignService(undefined), new NullLogService()), new UriIdentityService(fileService), new NullLogService(), new NullPolicyService())); await (<WorkspaceService>testObject).initialize(convertToWorkspacePayload(folder)); @@ -184,7 +195,8 @@ suite('WorkspaceContextService - Workspace', () => { const remoteAgentService = disposables.add(instantiationService.createInstance(RemoteAgentService, null)); instantiationService.stub(IRemoteAgentService, remoteAgentService); fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService()))); - testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService())); + const userDataProfilesService = instantiationService.stub(IUserDataProfilesService, new UserDataProfilesService(environmentService, fileService, logService)); + testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, new UserDataProfileService(userDataProfilesService.defaultProfile), userDataProfilesService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService(), new NullPolicyService())); instantiationService.stub(IWorkspaceContextService, testObject); instantiationService.stub(IConfigurationService, testObject); @@ -242,7 +254,8 @@ suite('WorkspaceContextService - Workspace Editing', () => { const remoteAgentService = instantiationService.createInstance(RemoteAgentService, null); instantiationService.stub(IRemoteAgentService, remoteAgentService); fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService()))); - testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService())); + const userDataProfilesService = instantiationService.stub(IUserDataProfilesService, new UserDataProfilesService(environmentService, fileService, logService)); + testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, new UserDataProfileService(userDataProfilesService.defaultProfile), userDataProfilesService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService(), new NullPolicyService())); instantiationService.stub(IFileService, fileService); instantiationService.stub(IWorkspaceContextService, testObject); @@ -440,7 +453,7 @@ suite('WorkspaceContextService - Workspace Editing', () => { suite('WorkspaceService - Initialization', () => { - let configResource: URI, testObject: WorkspaceService, fileService: IFileService, environmentService: BrowserWorkbenchEnvironmentService; + let configResource: URI, testObject: WorkspaceService, fileService: IFileService, environmentService: BrowserWorkbenchEnvironmentService, userDataProfileService: IUserDataProfileService; const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration); const disposables = new DisposableStore(); @@ -485,7 +498,9 @@ suite('WorkspaceService - Initialization', () => { const remoteAgentService = instantiationService.createInstance(RemoteAgentService, null); instantiationService.stub(IRemoteAgentService, remoteAgentService); fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService()))); - testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService())); + const userDataProfilesService = instantiationService.stub(IUserDataProfilesService, new UserDataProfilesService(environmentService, fileService, logService)); + userDataProfileService = instantiationService.stub(IUserDataProfileService, new UserDataProfileService(userDataProfilesService.defaultProfile)); + testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, userDataProfileService, userDataProfilesService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService(), new NullPolicyService())); instantiationService.stub(IFileService, fileService); instantiationService.stub(IWorkspaceContextService, testObject); instantiationService.stub(IConfigurationService, testObject); @@ -501,7 +516,7 @@ suite('WorkspaceService - Initialization', () => { (isMacintosh ? test.skip : test)('initialize a folder workspace from an empty workspace with no configuration changes', async () => { - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "initialization.testSetting1": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "initialization.testSetting1": "userValue" }')); await testObject.reloadConfiguration(); const target = sinon.spy(); @@ -526,7 +541,7 @@ suite('WorkspaceService - Initialization', () => { (isMacintosh ? test.skip : test)('initialize a folder workspace from an empty workspace with configuration changes', async () => { - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "initialization.testSetting1": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "initialization.testSetting1": "userValue" }')); await testObject.reloadConfiguration(); const target = sinon.spy(); @@ -553,7 +568,7 @@ suite('WorkspaceService - Initialization', () => { (isMacintosh ? test.skip : test)('initialize a multi root workspace from an empty workspace with no configuration changes', async () => { - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "initialization.testSetting1": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "initialization.testSetting1": "userValue" }')); await testObject.reloadConfiguration(); const target = sinon.spy(); @@ -576,7 +591,7 @@ suite('WorkspaceService - Initialization', () => { (isMacintosh ? test.skip : test)('initialize a multi root workspace from an empty workspace with configuration changes', async () => { - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "initialization.testSetting1": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "initialization.testSetting1": "userValue" }')); await testObject.reloadConfiguration(); const target = sinon.spy(); @@ -603,7 +618,7 @@ suite('WorkspaceService - Initialization', () => { (isMacintosh ? test.skip : test)('initialize a folder workspace from a folder workspace with no configuration changes', async () => { await testObject.initialize(convertToWorkspacePayload(joinPath(ROOT, 'a'))); - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "initialization.testSetting1": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "initialization.testSetting1": "userValue" }')); await testObject.reloadConfiguration(); const target = sinon.spy(); testObject.onDidChangeWorkbenchState(target); @@ -669,7 +684,7 @@ suite('WorkspaceService - Initialization', () => { suite('WorkspaceConfigurationService - Folder', () => { - let testObject: WorkspaceService, workspaceService: WorkspaceService, fileService: IFileService, environmentService: BrowserWorkbenchEnvironmentService; + let testObject: WorkspaceService, workspaceService: WorkspaceService, fileService: IFileService, environmentService: IWorkbenchEnvironmentService, userDataProfileService: IUserDataProfileService, instantiationService: TestInstantiationService; const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration); const disposables: DisposableStore = new DisposableStore(); @@ -708,6 +723,14 @@ suite('WorkspaceConfigurationService - Folder', () => { 'default': 'isSet', restricted: true }, + 'configurationService.folder.policySetting': { + 'type': 'string', + 'default': 'isSet', + policy: { + name: 'configurationService.folder.policySetting', + minimumVersion: '1.0.0', + } + }, } }); @@ -729,12 +752,15 @@ suite('WorkspaceConfigurationService - Folder', () => { const folder = joinPath(ROOT, 'a'); await fileService.createFolder(folder); - const instantiationService = <TestInstantiationService>workbenchInstantiationService(undefined, disposables); + instantiationService = <TestInstantiationService>workbenchInstantiationService(undefined, disposables); environmentService = TestEnvironmentService; + environmentService.policyFile = joinPath(folder, 'policies.json'); const remoteAgentService = instantiationService.createInstance(RemoteAgentService, null); instantiationService.stub(IRemoteAgentService, remoteAgentService); fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService()))); - workspaceService = testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService())); + const userDataProfilesService = instantiationService.stub(IUserDataProfilesService, new UserDataProfilesService(environmentService, fileService, logService)); + userDataProfileService = instantiationService.stub(IUserDataProfileService, new UserDataProfileService(userDataProfilesService.defaultProfile)); + workspaceService = testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, userDataProfileService, userDataProfilesService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService(), new FilePolicyService(environmentService.policyFile, fileService, logService))); instantiationService.stub(IFileService, fileService); instantiationService.stub(IWorkspaceContextService, testObject); instantiationService.stub(IConfigurationService, testObject); @@ -750,17 +776,17 @@ suite('WorkspaceConfigurationService - Folder', () => { teardown(() => disposables.clear()); test('defaults', () => { - assert.deepStrictEqual(testObject.getValue('configurationService'), { 'folder': { 'applicationSetting': 'isSet', 'machineSetting': 'isSet', 'machineOverridableSetting': 'isSet', 'testSetting': 'isSet', 'languageSetting': 'isSet', 'restrictedSetting': 'isSet' } }); + assert.deepStrictEqual(testObject.getValue('configurationService'), { 'folder': { 'applicationSetting': 'isSet', 'machineSetting': 'isSet', 'machineOverridableSetting': 'isSet', 'testSetting': 'isSet', 'languageSetting': 'isSet', 'restrictedSetting': 'isSet', 'policySetting': 'isSet' } }); }); test('globals override defaults', async () => { - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "configurationService.folder.testSetting": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.folder.testSetting": "userValue" }')); await testObject.reloadConfiguration(); assert.strictEqual(testObject.getValue('configurationService.folder.testSetting'), 'userValue'); }); test('globals', async () => { - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "testworkbench.editor.tabs": true }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "testworkbench.editor.tabs": true }')); await testObject.reloadConfiguration(); assert.strictEqual(testObject.getValue('testworkbench.editor.tabs'), true); }); @@ -772,21 +798,21 @@ suite('WorkspaceConfigurationService - Folder', () => { }); test('workspace settings override user settings', async () => { - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "configurationService.folder.testSetting": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.folder.testSetting": "userValue" }')); await fileService.writeFile(joinPath(workspaceService.getWorkspace().folders[0].uri, '.vscode', 'settings.json'), VSBuffer.fromString('{ "configurationService.folder.testSetting": "workspaceValue" }')); await testObject.reloadConfiguration(); assert.strictEqual(testObject.getValue('configurationService.folder.testSetting'), 'workspaceValue'); }); test('machine overridable settings override user Settings', async () => { - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "configurationService.folder.machineOverridableSetting": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.folder.machineOverridableSetting": "userValue" }')); await fileService.writeFile(joinPath(workspaceService.getWorkspace().folders[0].uri, '.vscode', 'settings.json'), VSBuffer.fromString('{ "configurationService.folder.machineOverridableSetting": "workspaceValue" }')); await testObject.reloadConfiguration(); assert.strictEqual(testObject.getValue('configurationService.folder.machineOverridableSetting'), 'workspaceValue'); }); test('workspace settings override user settings after defaults are registered ', async () => { - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "configurationService.folder.newSetting": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.folder.newSetting": "userValue" }')); await fileService.writeFile(joinPath(workspaceService.getWorkspace().folders[0].uri, '.vscode', 'settings.json'), VSBuffer.fromString('{ "configurationService.folder.newSetting": "workspaceValue" }')); await testObject.reloadConfiguration(); configurationRegistry.registerConfiguration({ @@ -803,7 +829,7 @@ suite('WorkspaceConfigurationService - Folder', () => { }); test('machine overridable settings override user settings after defaults are registered ', async () => { - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "configurationService.folder.newMachineOverridableSetting": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.folder.newMachineOverridableSetting": "userValue" }')); await fileService.writeFile(joinPath(workspaceService.getWorkspace().folders[0].uri, '.vscode', 'settings.json'), VSBuffer.fromString('{ "configurationService.folder.newMachineOverridableSetting": "workspaceValue" }')); await testObject.reloadConfiguration(); configurationRegistry.registerConfiguration({ @@ -821,7 +847,7 @@ suite('WorkspaceConfigurationService - Folder', () => { }); test('application settings are not read from workspace', async () => { - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "configurationService.folder.applicationSetting": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.folder.applicationSetting": "userValue" }')); await fileService.writeFile(joinPath(workspaceService.getWorkspace().folders[0].uri, '.vscode', 'settings.json'), VSBuffer.fromString('{ "configurationService.folder.applicationSetting": "workspaceValue" }')); await testObject.reloadConfiguration(); @@ -830,7 +856,7 @@ suite('WorkspaceConfigurationService - Folder', () => { }); test('application settings are not read from workspace when workspace folder uri is passed', async () => { - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "configurationService.folder.applicationSetting": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.folder.applicationSetting": "userValue" }')); await fileService.writeFile(joinPath(workspaceService.getWorkspace().folders[0].uri, '.vscode', 'settings.json'), VSBuffer.fromString('{ "configurationService.folder.applicationSetting": "workspaceValue" }')); await testObject.reloadConfiguration(); @@ -839,7 +865,7 @@ suite('WorkspaceConfigurationService - Folder', () => { }); test('machine settings are not read from workspace', async () => { - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "configurationService.folder.machineSetting": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.folder.machineSetting": "userValue" }')); await fileService.writeFile(joinPath(workspaceService.getWorkspace().folders[0].uri, '.vscode', 'settings.json'), VSBuffer.fromString('{ "configurationService.folder.machineSetting": "workspaceValue" }')); await testObject.reloadConfiguration(); @@ -848,7 +874,7 @@ suite('WorkspaceConfigurationService - Folder', () => { }); test('machine settings are not read from workspace when workspace folder uri is passed', async () => { - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "configurationService.folder.machineSetting": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.folder.machineSetting": "userValue" }')); await fileService.writeFile(joinPath(workspaceService.getWorkspace().folders[0].uri, '.vscode', 'settings.json'), VSBuffer.fromString('{ "configurationService.folder.machineSetting": "workspaceValue" }')); await testObject.reloadConfiguration(); @@ -857,7 +883,7 @@ suite('WorkspaceConfigurationService - Folder', () => { }); test('get application scope settings are not loaded after defaults are registered', async () => { - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "configurationService.folder.applicationSetting-2": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.folder.applicationSetting-2": "userValue" }')); await fileService.writeFile(joinPath(workspaceService.getWorkspace().folders[0].uri, '.vscode', 'settings.json'), VSBuffer.fromString('{ "configurationService.folder.applicationSetting-2": "workspaceValue" }')); await testObject.reloadConfiguration(); @@ -882,7 +908,7 @@ suite('WorkspaceConfigurationService - Folder', () => { }); test('get application scope settings are not loaded after defaults are registered when workspace folder uri is passed', async () => { - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "configurationService.folder.applicationSetting-3": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.folder.applicationSetting-3": "userValue" }')); await fileService.writeFile(joinPath(workspaceService.getWorkspace().folders[0].uri, '.vscode', 'settings.json'), VSBuffer.fromString('{ "configurationService.folder.applicationSetting-3": "workspaceValue" }')); await testObject.reloadConfiguration(); @@ -907,7 +933,7 @@ suite('WorkspaceConfigurationService - Folder', () => { }); test('get machine scope settings are not loaded after defaults are registered', async () => { - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "configurationService.folder.machineSetting-2": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.folder.machineSetting-2": "userValue" }')); await fileService.writeFile(joinPath(workspaceService.getWorkspace().folders[0].uri, '.vscode', 'settings.json'), VSBuffer.fromString('{ "configurationService.folder.machineSetting-2": "workspaceValue" }')); await testObject.reloadConfiguration(); @@ -932,7 +958,7 @@ suite('WorkspaceConfigurationService - Folder', () => { }); test('get machine scope settings are not loaded after defaults are registered when workspace folder uri is passed', async () => { - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "configurationService.folder.machineSetting-3": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.folder.machineSetting-3": "userValue" }')); await fileService.writeFile(joinPath(workspaceService.getWorkspace().folders[0].uri, '.vscode', 'settings.json'), VSBuffer.fromString('{ "configurationService.folder.machineSetting-3": "workspaceValue" }')); await testObject.reloadConfiguration(); @@ -956,8 +982,27 @@ suite('WorkspaceConfigurationService - Folder', () => { assert.strictEqual(testObject.getValue('configurationService.folder.machineSetting-3', { resource: workspaceService.getWorkspace().folders[0].uri }), 'userValue'); }); + test('policy value override all', async () => { + const result = await runWithFakedTimers({ useFakeTimers: true }, async () => { + const promise = Event.toPromise(testObject.onDidChangeConfiguration); + await fileService.writeFile(environmentService.policyFile!, VSBuffer.fromString('{ "configurationService.folder.policySetting": "policyValue" }')); + return promise; + }); + assert.deepStrictEqual(result.affectedKeys, ['configurationService.folder.policySetting']); + assert.strictEqual(testObject.getValue('configurationService.folder.policySetting'), 'policyValue'); + assert.strictEqual(testObject.inspect('configurationService.folder.policySetting').policyValue, 'policyValue'); + }); + + test('policy settings when policy value is not set', async () => { + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.folder.policySetting": "userValue" }')); + await fileService.writeFile(joinPath(workspaceService.getWorkspace().folders[0].uri, '.vscode', 'settings.json'), VSBuffer.fromString('{ "configurationService.folder.policySetting": "workspaceValue" }')); + await testObject.reloadConfiguration(); + assert.strictEqual(testObject.getValue('configurationService.folder.policySetting'), 'workspaceValue'); + assert.strictEqual(testObject.inspect('configurationService.folder.policySetting').policyValue, undefined); + }); + test('reload configuration emits events after global configuraiton changes', async () => { - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "testworkbench.editor.tabs": true }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "testworkbench.editor.tabs": true }')); const target = sinon.spy(); testObject.onDidChangeConfiguration(target); await testObject.reloadConfiguration(); @@ -973,7 +1018,7 @@ suite('WorkspaceConfigurationService - Folder', () => { }); test('reload configuration should not emit event if no changes', async () => { - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "testworkbench.editor.tabs": true }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "testworkbench.editor.tabs": true }')); await fileService.writeFile(joinPath(workspaceService.getWorkspace().folders[0].uri, '.vscode', 'settings.json'), VSBuffer.fromString('{ "configurationService.folder.testSetting": "workspaceValue" }')); await testObject.reloadConfiguration(); const target = sinon.spy(); @@ -985,6 +1030,7 @@ suite('WorkspaceConfigurationService - Folder', () => { test('inspect', async () => { let actual = testObject.inspect('something.missing'); assert.strictEqual(actual.defaultValue, undefined); + assert.strictEqual(actual.application, undefined); assert.strictEqual(actual.userValue, undefined); assert.strictEqual(actual.workspaceValue, undefined); assert.strictEqual(actual.workspaceFolderValue, undefined); @@ -992,15 +1038,17 @@ suite('WorkspaceConfigurationService - Folder', () => { actual = testObject.inspect('configurationService.folder.testSetting'); assert.strictEqual(actual.defaultValue, 'isSet'); + assert.strictEqual(actual.application, undefined); assert.strictEqual(actual.userValue, undefined); assert.strictEqual(actual.workspaceValue, undefined); assert.strictEqual(actual.workspaceFolderValue, undefined); assert.strictEqual(actual.value, 'isSet'); - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "configurationService.folder.testSetting": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.folder.testSetting": "userValue" }')); await testObject.reloadConfiguration(); actual = testObject.inspect('configurationService.folder.testSetting'); assert.strictEqual(actual.defaultValue, 'isSet'); + assert.strictEqual(actual.application, undefined); assert.strictEqual(actual.userValue, 'userValue'); assert.strictEqual(actual.workspaceValue, undefined); assert.strictEqual(actual.workspaceFolderValue, undefined); @@ -1010,6 +1058,7 @@ suite('WorkspaceConfigurationService - Folder', () => { await testObject.reloadConfiguration(); actual = testObject.inspect('configurationService.folder.testSetting'); assert.strictEqual(actual.defaultValue, 'isSet'); + assert.strictEqual(actual.application, undefined); assert.strictEqual(actual.userValue, 'userValue'); assert.strictEqual(actual.workspaceValue, 'workspaceValue'); assert.strictEqual(actual.workspaceFolderValue, undefined); @@ -1023,7 +1072,7 @@ suite('WorkspaceConfigurationService - Folder', () => { assert.deepStrictEqual(actual.workspace, []); assert.deepStrictEqual(actual.workspaceFolder, []); - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "configurationService.folder.testSetting": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.folder.testSetting": "userValue" }')); await testObject.reloadConfiguration(); actual = testObject.keys(); assert.ok(actual.default.indexOf('configurationService.folder.testSetting') !== -1); @@ -1177,8 +1226,8 @@ suite('WorkspaceConfigurationService - Folder', () => { await promise; }); - test('creating workspace settings', async () => { - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "configurationService.folder.testSetting": "userValue" }')); + test('creating workspace settings', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.folder.testSetting": "userValue" }')); await testObject.reloadConfiguration(); await new Promise<void>((c, e) => { const disposable = testObject.onDidChangeConfiguration(e => { @@ -1189,10 +1238,10 @@ suite('WorkspaceConfigurationService - Folder', () => { }); fileService.writeFile(joinPath(workspaceService.getWorkspace().folders[0].uri, '.vscode', 'settings.json'), VSBuffer.fromString('{ "configurationService.folder.testSetting": "workspaceValue" }')).catch(e); }); - }); + })); - test('deleting workspace settings', async () => { - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "configurationService.folder.testSetting": "userValue" }')); + test('deleting workspace settings', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.folder.testSetting": "userValue" }')); const workspaceSettingsResource = joinPath(workspaceService.getWorkspace().folders[0].uri, '.vscode', 'settings.json'); await fileService.writeFile(workspaceSettingsResource, VSBuffer.fromString('{ "configurationService.folder.testSetting": "workspaceValue" }')); await testObject.reloadConfiguration(); @@ -1202,12 +1251,12 @@ suite('WorkspaceConfigurationService - Folder', () => { }); assert.ok(e.affectsConfiguration('configurationService.folder.testSetting')); assert.strictEqual(testObject.getValue('configurationService.folder.testSetting'), 'userValue'); - }); + })); test('restricted setting is read from workspace when workspace is trusted', async () => { testObject.updateWorkspaceTrust(true); - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "configurationService.folder.restrictedSetting": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.folder.restrictedSetting": "userValue" }')); await fileService.writeFile(joinPath(workspaceService.getWorkspace().folders[0].uri, '.vscode', 'settings.json'), VSBuffer.fromString('{ "configurationService.folder.restrictedSetting": "workspaceValue" }')); await testObject.reloadConfiguration(); @@ -1223,7 +1272,7 @@ suite('WorkspaceConfigurationService - Folder', () => { test('restricted setting is not read from workspace when workspace is changed to trusted', async () => { testObject.updateWorkspaceTrust(true); - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "configurationService.folder.restrictedSetting": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.folder.restrictedSetting": "userValue" }')); await fileService.writeFile(joinPath(workspaceService.getWorkspace().folders[0].uri, '.vscode', 'settings.json'), VSBuffer.fromString('{ "configurationService.folder.restrictedSetting": "workspaceValue" }')); await testObject.reloadConfiguration(); @@ -1241,7 +1290,7 @@ suite('WorkspaceConfigurationService - Folder', () => { test('change event is triggered when workspace is changed to untrusted', async () => { testObject.updateWorkspaceTrust(true); - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "configurationService.folder.restrictedSetting": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.folder.restrictedSetting": "userValue" }')); await fileService.writeFile(joinPath(workspaceService.getWorkspace().folders[0].uri, '.vscode', 'settings.json'), VSBuffer.fromString('{ "configurationService.folder.restrictedSetting": "workspaceValue" }')); await testObject.reloadConfiguration(); @@ -1256,7 +1305,7 @@ suite('WorkspaceConfigurationService - Folder', () => { test('restricted setting is not read from workspace when workspace is not trusted', async () => { testObject.updateWorkspaceTrust(false); - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "configurationService.folder.restrictedSetting": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.folder.restrictedSetting": "userValue" }')); await fileService.writeFile(joinPath(workspaceService.getWorkspace().folders[0].uri, '.vscode', 'settings.json'), VSBuffer.fromString('{ "configurationService.folder.restrictedSetting": "workspaceValue" }')); await testObject.reloadConfiguration(); @@ -1272,7 +1321,7 @@ suite('WorkspaceConfigurationService - Folder', () => { test('restricted setting is read when workspace is changed to trusted', async () => { testObject.updateWorkspaceTrust(false); - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "configurationService.folder.restrictedSetting": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.folder.restrictedSetting": "userValue" }')); await fileService.writeFile(joinPath(workspaceService.getWorkspace().folders[0].uri, '.vscode', 'settings.json'), VSBuffer.fromString('{ "configurationService.folder.restrictedSetting": "workspaceValue" }')); await testObject.reloadConfiguration(); @@ -1290,7 +1339,7 @@ suite('WorkspaceConfigurationService - Folder', () => { test('change event is triggered when workspace is changed to trusted', async () => { testObject.updateWorkspaceTrust(false); - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "configurationService.folder.restrictedSetting": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.folder.restrictedSetting": "userValue" }')); await fileService.writeFile(joinPath(workspaceService.getWorkspace().folders[0].uri, '.vscode', 'settings.json'), VSBuffer.fromString('{ "configurationService.folder.restrictedSetting": "workspaceValue" }')); await testObject.reloadConfiguration(); @@ -1302,19 +1351,19 @@ suite('WorkspaceConfigurationService - Folder', () => { assert.ok(event.affectsConfiguration('configurationService.folder.restrictedSetting')); }); - test('adding an restricted setting triggers change event', async () => { - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "configurationService.folder.restrictedSetting": "userValue" }')); + test('adding an restricted setting triggers change event', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.folder.restrictedSetting": "userValue" }')); testObject.updateWorkspaceTrust(false); const promise = Event.toPromise(testObject.onDidChangeRestrictedSettings); await fileService.writeFile(joinPath(workspaceService.getWorkspace().folders[0].uri, '.vscode', 'settings.json'), VSBuffer.fromString('{ "configurationService.folder.restrictedSetting": "workspaceValue" }')); return promise; - }); + })); test('remove an unregistered setting', async () => { const key = 'configurationService.folder.unknownSetting'; - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "configurationService.folder.unknownSetting": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.folder.unknownSetting": "userValue" }')); await fileService.writeFile(joinPath(workspaceService.getWorkspace().folders[0].uri, '.vscode', 'settings.json'), VSBuffer.fromString('{ "configurationService.folder.unknownSetting": "workspaceValue" }')); await testObject.reloadConfiguration(); @@ -1327,9 +1376,167 @@ suite('WorkspaceConfigurationService - Folder', () => { }); }); +suite('WorkspaceConfigurationService - Profiles', () => { + + let testObject: WorkspaceService, workspaceService: WorkspaceService, fileService: IFileService, environmentService: IWorkbenchEnvironmentService, userDataProfileService: IUserDataProfileService, instantiationService: TestInstantiationService; + const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration); + const disposables: DisposableStore = new DisposableStore(); + + suiteSetup(() => { + configurationRegistry.registerConfiguration({ + 'id': '_test', + 'type': 'object', + 'properties': { + 'configurationService.profiles.applicationSetting': { + 'type': 'string', + 'default': 'isSet', + scope: ConfigurationScope.APPLICATION + }, + 'configurationService.profiles.testSetting': { + 'type': 'string', + 'default': 'isSet', + }, + 'configurationService.profiles.applicationSetting2': { + 'type': 'string', + 'default': 'isSet', + scope: ConfigurationScope.APPLICATION + }, + 'configurationService.profiles.testSetting2': { + 'type': 'string', + 'default': 'isSet', + }, + } + }); + }); + + setup(async () => { + const logService = new NullLogService(); + fileService = disposables.add(new FileService(logService)); + const fileSystemProvider = disposables.add(new InMemoryFileSystemProvider()); + fileService.registerProvider(ROOT.scheme, fileSystemProvider); + + const folder = joinPath(ROOT, 'a'); + await fileService.createFolder(folder); + + instantiationService = <TestInstantiationService>workbenchInstantiationService(undefined, disposables); + environmentService = TestEnvironmentService; + environmentService.policyFile = joinPath(folder, 'policies.json'); + const remoteAgentService = instantiationService.createInstance(RemoteAgentService, null); + instantiationService.stub(IRemoteAgentService, remoteAgentService); + fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService()))); + const userDataProfilesService = instantiationService.stub(IUserDataProfilesService, new UserDataProfilesService(environmentService, fileService, logService)); + userDataProfileService = instantiationService.stub(IUserDataProfileService, new UserDataProfileService(toUserDataProfile('custom', joinPath(environmentService.userRoamingDataHome, 'profiles', 'temp')))); + workspaceService = testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, userDataProfileService, userDataProfilesService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService(), new FilePolicyService(environmentService.policyFile, fileService, logService))); + instantiationService.stub(IFileService, fileService); + instantiationService.stub(IWorkspaceContextService, testObject); + instantiationService.stub(IConfigurationService, testObject); + instantiationService.stub(IEnvironmentService, environmentService); + + await fileService.writeFile(userDataProfilesService.defaultProfile.settingsResource, VSBuffer.fromString('{ "configurationService.profiles.applicationSetting2": "applicationValue", "configurationService.profiles.testSetting2": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.profiles.applicationSetting2": "profileValue", "configurationService.profiles.testSetting2": "profileValue" }')); + await workspaceService.initialize(convertToWorkspacePayload(folder)); + instantiationService.stub(IKeybindingEditingService, instantiationService.createInstance(KeybindingsEditingService)); + instantiationService.stub(ITextFileService, instantiationService.createInstance(TestTextFileService)); + instantiationService.stub(ITextModelService, <ITextModelService>instantiationService.createInstance(TextModelResolverService)); + workspaceService.acquireInstantiationService(instantiationService); + }); + + teardown(() => disposables.clear()); + + test('initialize', async () => { + assert.strictEqual(testObject.getValue('configurationService.profiles.applicationSetting2'), 'applicationValue'); + assert.strictEqual(testObject.getValue('configurationService.profiles.testSetting2'), 'profileValue'); + }); + + test('inspect', async () => { + let actual = testObject.inspect('something.missing'); + assert.strictEqual(actual.defaultValue, undefined); + assert.strictEqual(actual.application, undefined); + assert.strictEqual(actual.userValue, undefined); + assert.strictEqual(actual.workspaceValue, undefined); + assert.strictEqual(actual.workspaceFolderValue, undefined); + assert.strictEqual(actual.value, undefined); + + actual = testObject.inspect('configurationService.profiles.applicationSetting'); + assert.strictEqual(actual.defaultValue, 'isSet'); + assert.strictEqual(actual.application, undefined); + assert.strictEqual(actual.userValue, undefined); + assert.strictEqual(actual.workspaceValue, undefined); + assert.strictEqual(actual.workspaceFolderValue, undefined); + assert.strictEqual(actual.value, 'isSet'); + + await fileService.writeFile(instantiationService.get(IUserDataProfilesService).defaultProfile.settingsResource, VSBuffer.fromString('{ "configurationService.profiles.applicationSetting": "applicationValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.profiles.applicationSetting": "profileValue" }')); + await testObject.reloadConfiguration(); + actual = testObject.inspect('configurationService.profiles.applicationSetting'); + assert.strictEqual(actual.defaultValue, 'isSet'); + assert.strictEqual(actual.applicationValue, 'applicationValue'); + assert.strictEqual(actual.userValue, undefined); + assert.strictEqual(actual.workspaceValue, undefined); + assert.strictEqual(actual.workspaceFolderValue, undefined); + assert.strictEqual(actual.value, 'applicationValue'); + + await fileService.writeFile(instantiationService.get(IUserDataProfilesService).defaultProfile.settingsResource, VSBuffer.fromString('{ "configurationService.profiles.testSetting": "applicationValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.profiles.testSetting": "profileValue" }')); + await testObject.reloadConfiguration(); + actual = testObject.inspect('configurationService.profiles.testSetting'); + assert.strictEqual(actual.defaultValue, 'isSet'); + assert.strictEqual(actual.applicationValue, undefined); + assert.strictEqual(actual.userValue, 'profileValue'); + assert.strictEqual(actual.workspaceValue, undefined); + assert.strictEqual(actual.workspaceFolderValue, undefined); + assert.strictEqual(actual.value, 'profileValue'); + }); + + test('update application scope setting', async () => { + await testObject.updateValue('configurationService.profiles.applicationSetting', 'applicationValue'); + + assert.deepStrictEqual(JSON.parse((await fileService.readFile(instantiationService.get(IUserDataProfilesService).defaultProfile.settingsResource)).value.toString()), { 'configurationService.profiles.applicationSetting': 'applicationValue', 'configurationService.profiles.applicationSetting2': 'applicationValue', 'configurationService.profiles.testSetting2': 'userValue' }); + assert.strictEqual(testObject.getValue('configurationService.profiles.applicationSetting'), 'applicationValue'); + }); + + test('update normal setting', async () => { + await testObject.updateValue('configurationService.profiles.testSetting', 'profileValue'); + + assert.deepStrictEqual(JSON.parse((await fileService.readFile(userDataProfileService.currentProfile.settingsResource)).value.toString()), { 'configurationService.profiles.testSetting': 'profileValue', 'configurationService.profiles.testSetting2': 'profileValue', 'configurationService.profiles.applicationSetting2': 'profileValue' }); + assert.strictEqual(testObject.getValue('configurationService.profiles.testSetting'), 'profileValue'); + }); + + test('switch to default profile', async () => { + await fileService.writeFile(instantiationService.get(IUserDataProfilesService).defaultProfile.settingsResource, VSBuffer.fromString('{ "configurationService.profiles.applicationSetting": "applicationValue", "configurationService.profiles.testSetting": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.profiles.applicationSetting": "profileValue", "configurationService.profiles.testSetting": "profileValue" }')); + await testObject.reloadConfiguration(); + + const promise = Event.toPromise(testObject.onDidChangeConfiguration); + await userDataProfileService.updateCurrentProfile(instantiationService.get(IUserDataProfilesService).defaultProfile, false); + + const changeEvent = await promise; + assert.deepStrictEqual(changeEvent.affectedKeys, ['configurationService.profiles.testSetting']); + assert.strictEqual(testObject.getValue('configurationService.profiles.applicationSetting'), 'applicationValue'); + assert.strictEqual(testObject.getValue('configurationService.profiles.testSetting'), 'userValue'); + }); + + test('switch to non default profile', async () => { + await fileService.writeFile(instantiationService.get(IUserDataProfilesService).defaultProfile.settingsResource, VSBuffer.fromString('{ "configurationService.profiles.applicationSetting": "applicationValue", "configurationService.profiles.testSetting": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.profiles.applicationSetting": "profileValue", "configurationService.profiles.testSetting": "profileValue" }')); + await testObject.reloadConfiguration(); + + const profile = toUserDataProfile('custom2', joinPath(environmentService.userRoamingDataHome, 'profiles', 'custom2')); + await fileService.writeFile(profile.settingsResource, VSBuffer.fromString('{ "configurationService.profiles.applicationSetting": "profileValue2", "configurationService.profiles.testSetting": "profileValue2" }')); + const promise = Event.toPromise(testObject.onDidChangeConfiguration); + await userDataProfileService.updateCurrentProfile(profile, false); + + const changeEvent = await promise; + assert.deepStrictEqual(changeEvent.affectedKeys, ['configurationService.profiles.testSetting']); + assert.strictEqual(testObject.getValue('configurationService.profiles.applicationSetting'), 'applicationValue'); + assert.strictEqual(testObject.getValue('configurationService.profiles.testSetting'), 'profileValue2'); + }); + +}); + suite('WorkspaceConfigurationService-Multiroot', () => { - let workspaceContextService: IWorkspaceContextService, jsonEditingServce: IJSONEditingService, testObject: WorkspaceService, fileService: IFileService, environmentService: BrowserWorkbenchEnvironmentService; + let workspaceContextService: IWorkspaceContextService, jsonEditingServce: IJSONEditingService, testObject: WorkspaceService, fileService: IFileService, environmentService: BrowserWorkbenchEnvironmentService, userDataProfileService: IUserDataProfileService; const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration); const disposables = new DisposableStore(); @@ -1405,7 +1612,9 @@ suite('WorkspaceConfigurationService-Multiroot', () => { const remoteAgentService = instantiationService.createInstance(RemoteAgentService, null); instantiationService.stub(IRemoteAgentService, remoteAgentService); fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService()))); - const workspaceService = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService())); + const userDataProfilesService = instantiationService.stub(IUserDataProfilesService, new UserDataProfilesService(environmentService, fileService, logService)); + userDataProfileService = instantiationService.stub(IUserDataProfileService, new UserDataProfileService(userDataProfilesService.defaultProfile)); + const workspaceService = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, userDataProfileService, userDataProfilesService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService(), new NullPolicyService())); instantiationService.stub(IFileService, fileService); instantiationService.stub(IWorkspaceContextService, workspaceService); @@ -1427,7 +1636,7 @@ suite('WorkspaceConfigurationService-Multiroot', () => { teardown(() => disposables.clear()); test('application settings are not read from workspace', async () => { - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "configurationService.folder.applicationSetting": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.folder.applicationSetting": "userValue" }')); await jsonEditingServce.write(workspaceContextService.getWorkspace().configuration!, [{ path: ['settings'], value: { 'configurationService.workspace.applicationSetting': 'workspaceValue' } }], true); await testObject.reloadConfiguration(); @@ -1436,7 +1645,7 @@ suite('WorkspaceConfigurationService-Multiroot', () => { }); test('application settings are not read from workspace when folder is passed', async () => { - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "configurationService.folder.applicationSetting": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.folder.applicationSetting": "userValue" }')); await jsonEditingServce.write(workspaceContextService.getWorkspace().configuration!, [{ path: ['settings'], value: { 'configurationService.workspace.applicationSetting': 'workspaceValue' } }], true); await testObject.reloadConfiguration(); @@ -1445,7 +1654,7 @@ suite('WorkspaceConfigurationService-Multiroot', () => { }); test('machine settings are not read from workspace', async () => { - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "configurationService.folder.machineSetting": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.folder.machineSetting": "userValue" }')); await jsonEditingServce.write(workspaceContextService.getWorkspace().configuration!, [{ path: ['settings'], value: { 'configurationService.workspace.machineSetting': 'workspaceValue' } }], true); await testObject.reloadConfiguration(); @@ -1454,7 +1663,7 @@ suite('WorkspaceConfigurationService-Multiroot', () => { }); test('machine settings are not read from workspace when folder is passed', async () => { - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "configurationService.folder.machineSetting": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.folder.machineSetting": "userValue" }')); await jsonEditingServce.write(workspaceContextService.getWorkspace().configuration!, [{ path: ['settings'], value: { 'configurationService.workspace.machineSetting': 'workspaceValue' } }], true); await testObject.reloadConfiguration(); @@ -1463,7 +1672,7 @@ suite('WorkspaceConfigurationService-Multiroot', () => { }); test('get application scope settings are not loaded after defaults are registered', async () => { - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "configurationService.workspace.newSetting": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.workspace.newSetting": "userValue" }')); await jsonEditingServce.write(workspaceContextService.getWorkspace().configuration!, [{ path: ['settings'], value: { 'configurationService.workspace.newSetting': 'workspaceValue' } }], true); await testObject.reloadConfiguration(); @@ -1488,7 +1697,7 @@ suite('WorkspaceConfigurationService-Multiroot', () => { }); test('get application scope settings are not loaded after defaults are registered when workspace folder is passed', async () => { - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "configurationService.workspace.newSetting-2": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.workspace.newSetting-2": "userValue" }')); await jsonEditingServce.write(workspaceContextService.getWorkspace().configuration!, [{ path: ['settings'], value: { 'configurationService.workspace.newSetting-2': 'workspaceValue' } }], true); await testObject.reloadConfiguration(); @@ -1513,7 +1722,7 @@ suite('WorkspaceConfigurationService-Multiroot', () => { }); test('workspace settings override user settings after defaults are registered for machine overridable settings ', async () => { - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "configurationService.workspace.newMachineOverridableSetting": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.workspace.newMachineOverridableSetting": "userValue" }')); await jsonEditingServce.write(workspaceContextService.getWorkspace().configuration!, [{ path: ['settings'], value: { 'configurationService.workspace.newMachineOverridableSetting': 'workspaceValue' } }], true); await testObject.reloadConfiguration(); @@ -1539,7 +1748,7 @@ suite('WorkspaceConfigurationService-Multiroot', () => { }); test('application settings are not read from workspace folder', async () => { - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "configurationService.workspace.applicationSetting": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.workspace.applicationSetting": "userValue" }')); await fileService.writeFile(workspaceContextService.getWorkspace().folders[0].toResource('.vscode/settings.json'), VSBuffer.fromString('{ "configurationService.workspace.applicationSetting": "workspaceFolderValue" }')); await testObject.reloadConfiguration(); @@ -1548,7 +1757,7 @@ suite('WorkspaceConfigurationService-Multiroot', () => { }); test('application settings are not read from workspace folder when workspace folder is passed', async () => { - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "configurationService.workspace.applicationSetting": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.workspace.applicationSetting": "userValue" }')); await fileService.writeFile(workspaceContextService.getWorkspace().folders[0].toResource('.vscode/settings.json'), VSBuffer.fromString('{ "configurationService.workspace.applicationSetting": "workspaceFolderValue" }')); await testObject.reloadConfiguration(); @@ -1557,7 +1766,7 @@ suite('WorkspaceConfigurationService-Multiroot', () => { }); test('machine settings are not read from workspace folder', async () => { - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "configurationService.workspace.machineSetting": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.workspace.machineSetting": "userValue" }')); await fileService.writeFile(workspaceContextService.getWorkspace().folders[0].toResource('.vscode/settings.json'), VSBuffer.fromString('{ "configurationService.workspace.machineSetting": "workspaceFolderValue" }')); await testObject.reloadConfiguration(); @@ -1566,7 +1775,7 @@ suite('WorkspaceConfigurationService-Multiroot', () => { }); test('machine settings are not read from workspace folder when workspace folder is passed', async () => { - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "configurationService.workspace.machineSetting": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.workspace.machineSetting": "userValue" }')); await fileService.writeFile(workspaceContextService.getWorkspace().folders[0].toResource('.vscode/settings.json'), VSBuffer.fromString('{ "configurationService.workspace.machineSetting": "workspaceFolderValue" }')); await testObject.reloadConfiguration(); @@ -1575,7 +1784,7 @@ suite('WorkspaceConfigurationService-Multiroot', () => { }); test('application settings are not read from workspace folder after defaults are registered', async () => { - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "configurationService.workspace.testNewApplicationSetting": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.workspace.testNewApplicationSetting": "userValue" }')); await fileService.writeFile(workspaceContextService.getWorkspace().folders[0].toResource('.vscode/settings.json'), VSBuffer.fromString('{ "configurationService.workspace.testNewApplicationSetting": "workspaceFolderValue" }')); await testObject.reloadConfiguration(); @@ -1600,7 +1809,7 @@ suite('WorkspaceConfigurationService-Multiroot', () => { }); test('application settings are not read from workspace folder after defaults are registered', async () => { - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "configurationService.workspace.testNewMachineSetting": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.workspace.testNewMachineSetting": "userValue" }')); await fileService.writeFile(workspaceContextService.getWorkspace().folders[0].toResource('.vscode/settings.json'), VSBuffer.fromString('{ "configurationService.workspace.testNewMachineSetting": "workspaceFolderValue" }')); await testObject.reloadConfiguration(); @@ -1693,7 +1902,7 @@ suite('WorkspaceConfigurationService-Multiroot', () => { assert.strictEqual(actual.workspaceFolderValue, undefined); assert.strictEqual(actual.value, 'isSet'); - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "configurationService.workspace.testResourceSetting": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.workspace.testResourceSetting": "userValue" }')); await testObject.reloadConfiguration(); actual = testObject.inspect('configurationService.workspace.testResourceSetting'); assert.strictEqual(actual.defaultValue, 'isSet'); @@ -1948,7 +2157,7 @@ suite('WorkspaceConfigurationService-Multiroot', () => { test('restricted setting is read from workspace folders when workspace is trusted', async () => { testObject.updateWorkspaceTrust(true); - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "configurationService.workspace.testRestrictedSetting1": "userValue", "configurationService.workspace.testRestrictedSetting2": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.workspace.testRestrictedSetting1": "userValue", "configurationService.workspace.testRestrictedSetting2": "userValue" }')); await jsonEditingServce.write((workspaceContextService.getWorkspace().configuration!), [{ path: ['settings'], value: { 'configurationService.workspace.testRestrictedSetting1': 'workspaceValue' } }], true); await fileService.writeFile(joinPath(testObject.getWorkspace().folders[1].uri, '.vscode', 'settings.json'), VSBuffer.fromString('{ "configurationService.workspace.testRestrictedSetting2": "workspaceFolder2Value" }')); await testObject.reloadConfiguration(); @@ -1968,7 +2177,7 @@ suite('WorkspaceConfigurationService-Multiroot', () => { test('restricted setting is not read from workspace when workspace is not trusted', async () => { testObject.updateWorkspaceTrust(false); - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "configurationService.workspace.testRestrictedSetting1": "userValue", "configurationService.workspace.testRestrictedSetting2": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.workspace.testRestrictedSetting1": "userValue", "configurationService.workspace.testRestrictedSetting2": "userValue" }')); await jsonEditingServce.write((workspaceContextService.getWorkspace().configuration!), [{ path: ['settings'], value: { 'configurationService.workspace.testRestrictedSetting1': 'workspaceValue' } }], true); await fileService.writeFile(joinPath(testObject.getWorkspace().folders[1].uri, '.vscode', 'settings.json'), VSBuffer.fromString('{ "configurationService.workspace.testRestrictedSetting2": "workspaceFolder2Value" }')); await testObject.reloadConfiguration(); @@ -1987,7 +2196,7 @@ suite('WorkspaceConfigurationService-Multiroot', () => { test('remove an unregistered setting', async () => { const key = 'configurationService.workspace.unknownSetting'; - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "configurationService.workspace.unknownSetting": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.workspace.unknownSetting": "userValue" }')); await jsonEditingServce.write((workspaceContextService.getWorkspace().configuration!), [{ path: ['settings'], value: { 'configurationService.workspace.unknownSetting': 'workspaceValue' } }], true); await fileService.writeFile(joinPath(workspaceContextService.getWorkspace().folders[0].uri, '.vscode', 'settings.json'), VSBuffer.fromString('{ "configurationService.workspace.unknownSetting": "workspaceFolderValue1" }')); await fileService.writeFile(joinPath(workspaceContextService.getWorkspace().folders[1].uri, '.vscode', 'settings.json'), VSBuffer.fromString('{ "configurationService.workspace.unknownSetting": "workspaceFolderValue2" }')); @@ -2013,7 +2222,7 @@ suite('WorkspaceConfigurationService - Remote Folder', () => { let testObject: WorkspaceService, folder: URI, machineSettingsResource: URI, remoteSettingsResource: URI, fileSystemProvider: InMemoryFileSystemProvider, resolveRemoteEnvironment: () => void, - instantiationService: TestInstantiationService, fileService: IFileService, environmentService: BrowserWorkbenchEnvironmentService; + instantiationService: TestInstantiationService, fileService: IFileService, environmentService: BrowserWorkbenchEnvironmentService, userDataProfileService: IUserDataProfileService; const remoteAuthority = 'configuraiton-tests'; const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration); const disposables = new DisposableStore(); @@ -2066,7 +2275,9 @@ suite('WorkspaceConfigurationService - Remote Folder', () => { const remoteAgentService = instantiationService.stub(IRemoteAgentService, <Partial<IRemoteAgentService>>{ getEnvironment: () => remoteEnvironmentPromise }); fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService()))); const configurationCache: IConfigurationCache = { read: () => Promise.resolve(''), write: () => Promise.resolve(), remove: () => Promise.resolve(), needsCaching: () => false }; - testObject = disposables.add(new WorkspaceService({ configurationCache, remoteAuthority }, environmentService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService())); + const userDataProfilesService = instantiationService.stub(IUserDataProfilesService, new UserDataProfilesService(environmentService, fileService, logService)); + userDataProfileService = instantiationService.stub(IUserDataProfileService, new UserDataProfileService(userDataProfilesService.defaultProfile)); + testObject = disposables.add(new WorkspaceService({ configurationCache, remoteAuthority }, environmentService, userDataProfileService, userDataProfilesService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService(), new NullPolicyService())); instantiationService.stub(IWorkspaceContextService, testObject); instantiationService.stub(IConfigurationService, testObject); instantiationService.stub(IEnvironmentService, environmentService); @@ -2152,7 +2363,7 @@ suite('WorkspaceConfigurationService - Remote Folder', () => { }); test('machine settings in local user settings does not override defaults', async () => { - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "configurationService.remote.machineSetting": "globalValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.remote.machineSetting": "globalValue" }')); registerRemoteFileSystemProvider(); resolveRemoteEnvironment(); await initialize(); @@ -2160,7 +2371,7 @@ suite('WorkspaceConfigurationService - Remote Folder', () => { }); test('machine overridable settings in local user settings does not override defaults', async () => { - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "configurationService.remote.machineOverridableSetting": "globalValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.remote.machineOverridableSetting": "globalValue" }')); registerRemoteFileSystemProvider(); resolveRemoteEnvironment(); await initialize(); @@ -2195,7 +2406,7 @@ suite('WorkspaceConfigurationService - Remote Folder', () => { }); test('machine settings in local user settings does not override defaults after defalts are registered ', async () => { - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "configurationService.remote.newMachineSetting": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.remote.newMachineSetting": "userValue" }')); registerRemoteFileSystemProvider(); resolveRemoteEnvironment(); await initialize(); @@ -2214,7 +2425,7 @@ suite('WorkspaceConfigurationService - Remote Folder', () => { }); test('machine overridable settings in local user settings does not override defaults after defaults are registered ', async () => { - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "configurationService.remote.newMachineOverridableSetting": "userValue" }')); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.remote.newMachineOverridableSetting": "userValue" }')); registerRemoteFileSystemProvider(); resolveRemoteEnvironment(); await initialize(); diff --git a/src/vs/workbench/services/configuration/test/common/configurationModels.test.ts b/src/vs/workbench/services/configuration/test/common/configurationModels.test.ts index 228cc13113a..72486f21370 100644 --- a/src/vs/workbench/services/configuration/test/common/configurationModels.test.ts +++ b/src/vs/workbench/services/configuration/test/common/configurationModels.test.ts @@ -155,14 +155,14 @@ suite('Workspace Configuration', () => { test('Test compare same configurations', () => { const workspace = new Workspace('a', [new WorkspaceFolder({ index: 0, name: 'a', uri: URI.file('folder1') }), new WorkspaceFolder({ index: 1, name: 'b', uri: URI.file('folder2') }), new WorkspaceFolder({ index: 2, name: 'c', uri: URI.file('folder3') })]); - const configuration1 = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ResourceMap<ConfigurationModel>(), new ConfigurationModel(), new ResourceMap<ConfigurationModel>(), workspace); + const configuration1 = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ResourceMap<ConfigurationModel>(), new ConfigurationModel(), new ResourceMap<ConfigurationModel>(), workspace); configuration1.updateDefaultConfiguration(defaultConfigurationModel); configuration1.updateLocalUserConfiguration(toConfigurationModel({ 'window.title': 'native', '[typescript]': { 'editor.insertSpaces': false } })); configuration1.updateWorkspaceConfiguration(toConfigurationModel({ 'editor.lineNumbers': 'on' })); configuration1.updateFolderConfiguration(URI.file('folder1'), toConfigurationModel({ 'editor.fontSize': 14 })); configuration1.updateFolderConfiguration(URI.file('folder2'), toConfigurationModel({ 'editor.wordWrap': 'on' })); - const configuration2 = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ResourceMap<ConfigurationModel>(), new ConfigurationModel(), new ResourceMap<ConfigurationModel>(), workspace); + const configuration2 = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ResourceMap<ConfigurationModel>(), new ConfigurationModel(), new ResourceMap<ConfigurationModel>(), workspace); configuration2.updateDefaultConfiguration(defaultConfigurationModel); configuration2.updateLocalUserConfiguration(toConfigurationModel({ 'window.title': 'native', '[typescript]': { 'editor.insertSpaces': false } })); configuration2.updateWorkspaceConfiguration(toConfigurationModel({ 'editor.lineNumbers': 'on' })); @@ -176,14 +176,14 @@ suite('Workspace Configuration', () => { test('Test compare different configurations', () => { const workspace = new Workspace('a', [new WorkspaceFolder({ index: 0, name: 'a', uri: URI.file('folder1') }), new WorkspaceFolder({ index: 1, name: 'b', uri: URI.file('folder2') }), new WorkspaceFolder({ index: 2, name: 'c', uri: URI.file('folder3') })]); - const configuration1 = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ResourceMap<ConfigurationModel>(), new ConfigurationModel(), new ResourceMap<ConfigurationModel>(), workspace); + const configuration1 = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ResourceMap<ConfigurationModel>(), new ConfigurationModel(), new ResourceMap<ConfigurationModel>(), workspace); configuration1.updateDefaultConfiguration(defaultConfigurationModel); configuration1.updateLocalUserConfiguration(toConfigurationModel({ 'window.title': 'native', '[typescript]': { 'editor.insertSpaces': false } })); configuration1.updateWorkspaceConfiguration(toConfigurationModel({ 'editor.lineNumbers': 'on' })); configuration1.updateFolderConfiguration(URI.file('folder1'), toConfigurationModel({ 'editor.fontSize': 14 })); configuration1.updateFolderConfiguration(URI.file('folder2'), toConfigurationModel({ 'editor.wordWrap': 'on' })); - const configuration2 = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ResourceMap<ConfigurationModel>(), new ConfigurationModel(), new ResourceMap<ConfigurationModel>(), workspace); + const configuration2 = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ResourceMap<ConfigurationModel>(), new ConfigurationModel(), new ResourceMap<ConfigurationModel>(), workspace); configuration2.updateDefaultConfiguration(defaultConfigurationModel); configuration2.updateLocalUserConfiguration(toConfigurationModel({ 'workbench.enableTabs': true, '[typescript]': { 'editor.insertSpaces': true } })); configuration2.updateWorkspaceConfiguration(toConfigurationModel({ 'editor.fontSize': 11 })); diff --git a/src/vs/workbench/services/configurationResolver/browser/baseConfigurationResolverService.ts b/src/vs/workbench/services/configurationResolver/browser/baseConfigurationResolverService.ts index b4f3c050e7e..059d8e46456 100644 --- a/src/vs/workbench/services/configurationResolver/browser/baseConfigurationResolverService.ts +++ b/src/vs/workbench/services/configurationResolver/browser/baseConfigurationResolverService.ts @@ -7,7 +7,7 @@ import * as nls from 'vs/nls'; import * as Types from 'vs/base/common/types'; import { Schemas } from 'vs/base/common/network'; import { SideBySideEditor, EditorResourceAccessor } from 'vs/workbench/common/editor'; -import { IStringDictionary, forEach, fromMap } from 'vs/base/common/collections'; +import { IStringDictionary, forEach } from 'vs/base/common/collections'; import { IConfigurationService, IConfigurationOverrides, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IWorkspaceFolder, IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; @@ -128,7 +128,7 @@ export abstract class BaseConfigurationResolverService extends AbstractVariableR if (!mapping) { return null; } else if (mapping.size > 0) { - return this.resolveAnyAsync(folder, config, fromMap(mapping)); + return this.resolveAnyAsync(folder, config, Object.fromEntries(mapping)); } else { return config; } @@ -181,7 +181,7 @@ export abstract class BaseConfigurationResolverService extends AbstractVariableR let inputs: ConfiguredInput[] = []; if (this.workspaceContextService.getWorkbenchState() !== WorkbenchState.EMPTY && section) { const overrides: IConfigurationOverrides = folder ? { resource: folder.uri } : {}; - let result = this.configurationService.inspect(section, overrides); + const result = this.configurationService.inspect(section, overrides); if (result && (result.userValue || result.workspaceValue || result.workspaceFolderValue)) { switch (target) { case ConfigurationTarget.USER: inputs = (<any>result.userValue)?.inputs; break; diff --git a/src/vs/workbench/services/configurationResolver/common/variableResolver.ts b/src/vs/workbench/services/configurationResolver/common/variableResolver.ts index 7e557d33120..8cbb4be2848 100644 --- a/src/vs/workbench/services/configurationResolver/common/variableResolver.ts +++ b/src/vs/workbench/services/configurationResolver/common/variableResolver.ts @@ -141,7 +141,7 @@ export class AbstractVariableResolverService implements IConfigurationResolverSe } else if (types.isArray(value)) { return Promise.all(value.map(s => this.recursiveResolve(environment, folderUri, s, commandValueMapping, resolvedVariables))); } else if (types.isObject(value)) { - let result: IStringDictionary<string | IStringDictionary<string> | string[]> = Object.create(null); + const result: IStringDictionary<string | IStringDictionary<string> | string[]> = Object.create(null); const replaced = await Promise.all(Object.keys(value).map(async key => { const replaced = await this.resolveString(environment, folderUri, key, commandValueMapping, resolvedVariables); return [replaced, await this.recursiveResolve(environment, folderUri, value[key], commandValueMapping, resolvedVariables)] as const; @@ -165,9 +165,7 @@ export class AbstractVariableResolverService implements IConfigurationResolverSe let resolvedValue = await this.evaluateSingleVariable(environment, match, variable, folderUri, commandValueMapping); - if (resolvedVariables) { - resolvedVariables.set(variable, resolvedValue); - } + resolvedVariables?.set(variable, resolvedValue); if ((resolvedValue !== match) && types.isString(resolvedValue) && resolvedValue.match(AbstractVariableResolverService.VARIABLE_REGEXP)) { resolvedValue = await this.resolveString(environment, folderUri, resolvedValue, commandValueMapping, resolvedVariables); diff --git a/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts b/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts index e486565ab36..a17e27f7917 100644 --- a/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts +++ b/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts @@ -62,7 +62,7 @@ const nullContext = { suite('Configuration Resolver Service', () => { let configurationResolverService: IConfigurationResolverService | null; - let envVariables: { [key: string]: string } = { key1: 'Value for key1', key2: 'Value for key2' }; + const envVariables: { [key: string]: string } = { key1: 'Value for key1', key2: 'Value for key2' }; let environmentService: MockWorkbenchEnvironmentService; let mockCommandService: MockCommandService; let editorService: TestEditorServiceWithActiveEditor; @@ -218,7 +218,7 @@ suite('Configuration Resolver Service', () => { }); test('substitute one configuration variable', async () => { - let configurationService: IConfigurationService = new TestConfigurationService({ + const configurationService: IConfigurationService = new TestConfigurationService({ editor: { fontFamily: 'foo' }, @@ -229,24 +229,23 @@ suite('Configuration Resolver Service', () => { } }); - let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService, extensionService); + const service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService, extensionService); assert.strictEqual(await service.resolveAsync(workspace, 'abc ${config:editor.fontFamily} xyz'), 'abc foo xyz'); }); test('substitute configuration variable with undefined workspace folder', async () => { - let configurationService: IConfigurationService = new TestConfigurationService({ + const configurationService: IConfigurationService = new TestConfigurationService({ editor: { fontFamily: 'foo' } }); - let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService, extensionService); + const service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService, extensionService); assert.strictEqual(await service.resolveAsync(undefined, 'abc ${config:editor.fontFamily} xyz'), 'abc foo xyz'); }); test('substitute many configuration variables', async () => { - let configurationService: IConfigurationService; - configurationService = new TestConfigurationService({ + const configurationService = new TestConfigurationService({ editor: { fontFamily: 'foo' }, @@ -257,13 +256,12 @@ suite('Configuration Resolver Service', () => { } }); - let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService, extensionService); + const service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService, extensionService); assert.strictEqual(await service.resolveAsync(workspace, 'abc ${config:editor.fontFamily} ${config:terminal.integrated.fontFamily} xyz'), 'abc foo bar xyz'); }); test('substitute one env variable and a configuration variable', async () => { - let configurationService: IConfigurationService; - configurationService = new TestConfigurationService({ + const configurationService = new TestConfigurationService({ editor: { fontFamily: 'foo' }, @@ -274,7 +272,7 @@ suite('Configuration Resolver Service', () => { } }); - let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService, extensionService); + const service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService, extensionService); if (platform.isWindows) { assert.strictEqual(await service.resolveAsync(workspace, 'abc ${config:editor.fontFamily} ${workspaceFolder} ${env:key1} xyz'), 'abc foo \\VSCode\\workspaceLocation Value for key1 xyz'); } else { @@ -283,8 +281,7 @@ suite('Configuration Resolver Service', () => { }); test('substitute many env variable and a configuration variable', async () => { - let configurationService: IConfigurationService; - configurationService = new TestConfigurationService({ + const configurationService = new TestConfigurationService({ editor: { fontFamily: 'foo' }, @@ -295,7 +292,7 @@ suite('Configuration Resolver Service', () => { } }); - let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService, extensionService); + const service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService, extensionService); if (platform.isWindows) { assert.strictEqual(await service.resolveAsync(workspace, '${config:editor.fontFamily} ${config:terminal.integrated.fontFamily} ${workspaceFolder} - ${workspaceFolder} ${env:key1} - ${env:key2}'), 'foo bar \\VSCode\\workspaceLocation - \\VSCode\\workspaceLocation Value for key1 - Value for key2'); } else { @@ -304,8 +301,7 @@ suite('Configuration Resolver Service', () => { }); test('mixed types of configuration variables', async () => { - let configurationService: IConfigurationService; - configurationService = new TestConfigurationService({ + const configurationService = new TestConfigurationService({ editor: { fontFamily: 'foo', lineNumbers: 123, @@ -329,30 +325,28 @@ suite('Configuration Resolver Service', () => { } }); - let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService, extensionService); + const service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService, extensionService); assert.strictEqual(await service.resolveAsync(workspace, 'abc ${config:editor.fontFamily} ${config:editor.lineNumbers} ${config:editor.insertSpaces} xyz'), 'abc foo 123 false xyz'); }); test('uses original variable as fallback', async () => { - let configurationService: IConfigurationService; - configurationService = new TestConfigurationService({ + const configurationService = new TestConfigurationService({ editor: {} }); - let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService, extensionService); + const service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService, extensionService); assert.strictEqual(await service.resolveAsync(workspace, 'abc ${unknownVariable} xyz'), 'abc ${unknownVariable} xyz'); assert.strictEqual(await service.resolveAsync(workspace, 'abc ${env:unknownVariable} xyz'), 'abc xyz'); }); test('configuration variables with invalid accessor', () => { - let configurationService: IConfigurationService; - configurationService = new TestConfigurationService({ + const configurationService = new TestConfigurationService({ editor: { fontFamily: 'foo' } }); - let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService, extensionService); + const service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService, extensionService); assert.rejects(async () => await service.resolveAsync(workspace, 'abc ${env} xyz')); assert.rejects(async () => await service.resolveAsync(workspace, 'abc ${env:} xyz')); diff --git a/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts b/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts index b4c0e11cbeb..fe6eff44dd5 100644 --- a/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts +++ b/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts @@ -83,9 +83,7 @@ class NativeContextMenuService extends Disposable implements IContextMenuService const actions = delegate.getActions(); if (actions.length) { const onHide = once(() => { - if (delegate.onHide) { - delegate.onHide(false); - } + delegate.onHide?.(false); dom.ModifierKeyEmitter.getInstance().resetKeyStatus(); this._onDidHideContextMenu.fire(); diff --git a/src/vs/workbench/services/decorations/browser/decorationsService.ts b/src/vs/workbench/services/decorations/browser/decorationsService.ts index 75886dc3b76..42a7850a675 100644 --- a/src/vs/workbench/services/decorations/browser/decorationsService.ts +++ b/src/vs/workbench/services/decorations/browser/decorationsService.ts @@ -90,10 +90,10 @@ class DecorationRule { createCSSRule(`.${this.itemColorClassName}`, `color: ${getColor(color)};`, element); // badge or icon - let letters: string[] = []; + const letters: string[] = []; let icon: ThemeIcon | undefined; - for (let d of data) { + for (const d of data) { if (ThemeIcon.isThemeIcon(d.letter)) { icon = d.letter; break; @@ -174,7 +174,7 @@ class DecorationStyles { // sort by weight data.sort((a, b) => (b.weight || 0) - (a.weight || 0)); - let key = DecorationRule.keyOf(data); + const key = DecorationRule.keyOf(data); let rule = this._decorationRules.get(key); if (!rule) { @@ -186,11 +186,11 @@ class DecorationStyles { rule.acquire(); - let labelClassName = rule.itemColorClassName; + const labelClassName = rule.itemColorClassName; let badgeClassName = rule.itemBadgeClassName; - let iconClassName = rule.iconBadgeClassName; + const iconClassName = rule.iconBadgeClassName; let tooltip = distinct(data.filter(d => !isFalsyOrWhitespace(d.tooltip)).map(d => d.tooltip)).join(' • '); - let strikethrough = data.some(d => d.strikethrough); + const strikethrough = data.some(d => d.strikethrough); if (onlyChildren) { // show items from its children only @@ -280,7 +280,7 @@ export class DecorationsService implements IDecorationsService { // remove everything what came from this provider const removeAll = () => { const uris: URI[] = []; - for (let [uri, map] of this._data) { + for (const [uri, map] of this._data) { if (map.delete(provider)) { uris.push(uri); } @@ -323,7 +323,7 @@ export class DecorationsService implements IDecorationsService { getDecoration(uri: URI, includeChildren: boolean): IDecoration | undefined { - let all: IDecorationData[] = []; + const all: IDecorationData[] = []; let containsChildren: boolean = false; const map = this._ensureEntry(uri); @@ -398,7 +398,8 @@ export class DecorationsService implements IDecorationsService { private _keepItem(map: DecorationEntry, provider: IDecorationsProvider, uri: URI, data: IDecorationData | undefined): IDecorationData | null { const deco = data ? data : null; - const old = map.set(provider, deco); + const old = map.get(provider); + map.set(provider, deco); if (deco || old) { // only fire event when something changed this._onDidChangeDecorationsDelayed.fire(uri); diff --git a/src/vs/workbench/services/decorations/test/browser/decorationsService.test.ts b/src/vs/workbench/services/decorations/test/browser/decorationsService.test.ts index 326d138b70f..c98a7d69195 100644 --- a/src/vs/workbench/services/decorations/test/browser/decorationsService.test.ts +++ b/src/vs/workbench/services/decorations/test/browser/decorationsService.test.ts @@ -13,6 +13,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { mock } from 'vs/base/test/common/mock'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; +import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; suite('DecorationsService', function () { @@ -32,32 +33,33 @@ suite('DecorationsService', function () { test('Async provider, async/evented result', function () { - let uri = URI.parse('foo:bar'); - let callCounter = 0; - - service.registerDecorationsProvider(new class implements IDecorationsProvider { - readonly label: string = 'Test'; - readonly onDidChange: Event<readonly URI[]> = Event.None; - provideDecorations(uri: URI) { - callCounter += 1; - return new Promise<IDecorationData>(resolve => { - setTimeout(() => resolve({ - color: 'someBlue', - tooltip: 'T', - strikethrough: true - })); - }); - } - }); + return runWithFakedTimers({}, async function () { + + const uri = URI.parse('foo:bar'); + let callCounter = 0; + + service.registerDecorationsProvider(new class implements IDecorationsProvider { + readonly label: string = 'Test'; + readonly onDidChange: Event<readonly URI[]> = Event.None; + provideDecorations(uri: URI) { + callCounter += 1; + return new Promise<IDecorationData>(resolve => { + setTimeout(() => resolve({ + color: 'someBlue', + tooltip: 'T', + strikethrough: true + })); + }); + } + }); - // trigger -> async - assert.strictEqual(service.getDecoration(uri, false), undefined); - assert.strictEqual(callCounter, 1); + // trigger -> async + assert.strictEqual(service.getDecoration(uri, false), undefined); + assert.strictEqual(callCounter, 1); - // event when result is computed - return Event.toPromise(service.onDidChangeDecorations).then(e => { + // event when result is computed + const e = await Event.toPromise(service.onDidChangeDecorations); assert.strictEqual(e.affectsResource(uri), true); - // sync result assert.deepStrictEqual(service.getDecoration(uri, false)!.tooltip, 'T'); assert.deepStrictEqual(service.getDecoration(uri, false)!.strikethrough, true); @@ -67,7 +69,7 @@ suite('DecorationsService', function () { test('Sync provider, sync result', function () { - let uri = URI.parse('foo:bar'); + const uri = URI.parse('foo:bar'); let callCounter = 0; service.registerDecorationsProvider(new class implements IDecorationsProvider { @@ -86,36 +88,40 @@ suite('DecorationsService', function () { }); test('Clear decorations on provider dispose', async function () { - let uri = URI.parse('foo:bar'); - let callCounter = 0; + return runWithFakedTimers({}, async function () { - let reg = service.registerDecorationsProvider(new class implements IDecorationsProvider { - readonly label: string = 'Test'; - readonly onDidChange: Event<readonly URI[]> = Event.None; - provideDecorations(uri: URI) { - callCounter += 1; - return { color: 'someBlue', tooltip: 'J' }; - } - }); + const uri = URI.parse('foo:bar'); + let callCounter = 0; - // trigger -> sync - assert.deepStrictEqual(service.getDecoration(uri, false)!.tooltip, 'J'); - assert.strictEqual(callCounter, 1); + const reg = service.registerDecorationsProvider(new class implements IDecorationsProvider { + readonly label: string = 'Test'; + readonly onDidChange: Event<readonly URI[]> = Event.None; + provideDecorations(uri: URI) { + callCounter += 1; + return { color: 'someBlue', tooltip: 'J' }; + } + }); - // un-register -> ensure good event - let didSeeEvent = false; - let p = new Promise<void>(resolve => { - service.onDidChangeDecorations(e => { - assert.strictEqual(e.affectsResource(uri), true); - assert.deepStrictEqual(service.getDecoration(uri, false), undefined); - assert.strictEqual(callCounter, 1); - didSeeEvent = true; - resolve(); + // trigger -> sync + assert.deepStrictEqual(service.getDecoration(uri, false)!.tooltip, 'J'); + assert.strictEqual(callCounter, 1); + + // un-register -> ensure good event + let didSeeEvent = false; + const p = new Promise<void>(resolve => { + service.onDidChangeDecorations(e => { + assert.strictEqual(e.affectsResource(uri), true); + assert.deepStrictEqual(service.getDecoration(uri, false), undefined); + assert.strictEqual(callCounter, 1); + didSeeEvent = true; + resolve(); + }); }); + reg.dispose(); // will clear all data + await p; + assert.strictEqual(didSeeEvent, true); + }); - reg.dispose(); // will clear all data - await p; - assert.strictEqual(didSeeEvent, true); }); test('No default bubbling', function () { @@ -130,7 +136,7 @@ suite('DecorationsService', function () { } }); - let childUri = URI.parse('file:///some/path/some/file.txt'); + const childUri = URI.parse('file:///some/path/some/file.txt'); let deco = service.getDecoration(childUri, false)!; assert.strictEqual(deco.tooltip, '.txt'); @@ -160,10 +166,10 @@ suite('DecorationsService', function () { test('Decorations not showing up for second root folder #48502', async function () { let cancelCount = 0; - let winjsCancelCount = 0; + const winjsCancelCount = 0; let callCount = 0; - let provider = new class implements IDecorationsProvider { + const provider = new class implements IDecorationsProvider { _onDidChange = new Emitter<URI[]>(); onDidChange: Event<readonly URI[]> = this._onDidChange.event; @@ -185,7 +191,7 @@ suite('DecorationsService', function () { } }; - let reg = service.registerDecorationsProvider(provider); + const reg = service.registerDecorationsProvider(provider); const uri = URI.parse('foo://bar'); service.getDecoration(uri, false); @@ -202,7 +208,7 @@ suite('DecorationsService', function () { test('Decorations not bubbling... #48745', function () { - let reg = service.registerDecorationsProvider({ + const reg = service.registerDecorationsProvider({ label: 'Test', onDidChange: Event.None, provideDecorations(uri: URI) { @@ -214,13 +220,13 @@ suite('DecorationsService', function () { } }); - let data1 = service.getDecoration(URI.parse('a:b/'), true); + const data1 = service.getDecoration(URI.parse('a:b/'), true); assert.ok(!data1); - let data2 = service.getDecoration(URI.parse('a:b/c.hello'), false)!; + const data2 = service.getDecoration(URI.parse('a:b/c.hello'), false)!; assert.ok(data2.tooltip); - let data3 = service.getDecoration(URI.parse('a:b/'), true); + const data3 = service.getDecoration(URI.parse('a:b/'), true); assert.ok(data3); @@ -229,9 +235,9 @@ suite('DecorationsService', function () { test('Folder decorations don\'t go away when file with problems is deleted #61919 (part1)', function () { - let emitter = new Emitter<URI[]>(); + const emitter = new Emitter<URI[]>(); let gone = false; - let reg = service.registerDecorationsProvider({ + const reg = service.registerDecorationsProvider({ label: 'Test', onDidChange: emitter.event, provideDecorations(uri: URI) { @@ -242,8 +248,8 @@ suite('DecorationsService', function () { } }); - let uri = URI.parse('foo:/folder/file.ts'); - let uri2 = URI.parse('foo:/folder/'); + const uri = URI.parse('foo:/folder/file.ts'); + const uri2 = URI.parse('foo:/folder/'); let data = service.getDecoration(uri, true)!; assert.strictEqual(data.tooltip, 'FOO'); @@ -264,42 +270,45 @@ suite('DecorationsService', function () { test('Folder decorations don\'t go away when file with problems is deleted #61919 (part2)', function () { - let emitter = new Emitter<URI[]>(); - let gone = false; - let reg = service.registerDecorationsProvider({ - label: 'Test', - onDidChange: emitter.event, - provideDecorations(uri: URI) { - if (!gone && uri.path.match(/file.ts$/)) { - return { tooltip: 'FOO', weight: 17, bubble: true }; + return runWithFakedTimers({}, async function () { + + const emitter = new Emitter<URI[]>(); + let gone = false; + const reg = service.registerDecorationsProvider({ + label: 'Test', + onDidChange: emitter.event, + provideDecorations(uri: URI) { + if (!gone && uri.path.match(/file.ts$/)) { + return { tooltip: 'FOO', weight: 17, bubble: true }; + } + return undefined; } - return undefined; - } - }); - - let uri = URI.parse('foo:/folder/file.ts'); - let uri2 = URI.parse('foo:/folder/'); - let data = service.getDecoration(uri, true)!; - assert.strictEqual(data.tooltip, 'FOO'); - - data = service.getDecoration(uri2, true)!; - assert.ok(data.tooltip); // emphazied items... + }); - return new Promise<void>((resolve, reject) => { - let l = service.onDidChangeDecorations(e => { - l.dispose(); - try { - assert.ok(e.affectsResource(uri)); - assert.ok(e.affectsResource(uri2)); - resolve(); - reg.dispose(); - } catch (err) { - reject(err); - reg.dispose(); - } + const uri = URI.parse('foo:/folder/file.ts'); + const uri2 = URI.parse('foo:/folder/'); + let data = service.getDecoration(uri, true)!; + assert.strictEqual(data.tooltip, 'FOO'); + + data = service.getDecoration(uri2, true)!; + assert.ok(data.tooltip); // emphazied items... + + return new Promise<void>((resolve, reject) => { + const l = service.onDidChangeDecorations(e => { + l.dispose(); + try { + assert.ok(e.affectsResource(uri)); + assert.ok(e.affectsResource(uri2)); + resolve(); + reg.dispose(); + } catch (err) { + reject(err); + reg.dispose(); + } + }); + gone = true; + emitter.fire([uri]); }); - gone = true; - emitter.fire([uri]); }); }); diff --git a/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts b/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts index 435d8273715..8dbfc92efa9 100644 --- a/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts +++ b/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts @@ -255,7 +255,7 @@ export class SimpleFileDialog { this.isWindows = await this.checkIsWindowsOS(); let homedir: URI = this.options.defaultUri ? this.options.defaultUri : this.workspaceContextService.getWorkspace().folders[0].uri; let stat: IFileStatWithPartialMetadata | undefined; - let ext: string = resources.extname(homedir); + const ext: string = resources.extname(homedir); if (this.options.defaultUri) { try { stat = await this.fileService.stat(this.options.defaultUri); @@ -556,7 +556,7 @@ export class SimpleFileDialog { private async tryUpdateItems(value: string, valueUri: URI): Promise<UpdateResult> { if ((value.length > 0) && (value[0] === '~')) { - let newDir = this.tildaReplace(value); + const newDir = this.tildaReplace(value); return await this.updateItems(newDir, true) ? UpdateResult.UpdatedWithTrailing : UpdateResult.Updated; } else if (value === '\\') { valueUri = this.root(this.currentFolder); @@ -951,7 +951,7 @@ export class SimpleFileDialog { folder = await this.fileService.resolve(currentFolder); } const items = folder.children ? await Promise.all(folder.children.map(child => this.createItem(child, currentFolder, token))) : []; - for (let item of items) { + for (const item of items) { if (item) { result.push(item); } diff --git a/src/vs/workbench/services/editor/browser/editorResolverService.ts b/src/vs/workbench/services/editor/browser/editorResolverService.ts index 481d60b1b3f..21ea8e0dcac 100644 --- a/src/vs/workbench/services/editor/browser/editorResolverService.ts +++ b/src/vs/workbench/services/editor/browser/editorResolverService.ts @@ -69,8 +69,8 @@ export class EditorResolverService extends Disposable implements IEditorResolver ) { super(); // Read in the cache on statup - this.cache = new Set<string>(JSON.parse(this.storageService.get(EditorResolverService.cacheStorageID, StorageScope.GLOBAL, JSON.stringify([])))); - this.storageService.remove(EditorResolverService.cacheStorageID, StorageScope.GLOBAL); + this.cache = new Set<string>(JSON.parse(this.storageService.get(EditorResolverService.cacheStorageID, StorageScope.PROFILE, JSON.stringify([])))); + this.storageService.remove(EditorResolverService.cacheStorageID, StorageScope.PROFILE); this.convertOldAssociationFormat(); this._register(this.storageService.onWillSaveState(() => { @@ -140,7 +140,7 @@ export class EditorResolverService extends Disposable implements IEditorResolver } let resource = EditorResourceAccessor.getCanonicalUri(untypedEditor, { supportSideBySide: SideBySideEditor.PRIMARY }); - let options = untypedEditor.options; + const options = untypedEditor.options; // If it was resolved before we await for the extensions to activate and then proceed with resolution or else the backing extensions won't be registered if (this.cache && resource && this.resourceMatchesCache(resource)) { @@ -281,7 +281,7 @@ export class EditorResolverService extends Disposable implements IEditorResolver if (!Array.isArray(rawAssociations)) { return; } - let newSettingObject = Object.create(null); + const newSettingObject = Object.create(null); // Make the correctly formatted object from the array and then set that object for (const association of rawAssociations) { if (association.filenamePattern) { @@ -303,7 +303,7 @@ export class EditorResolverService extends Disposable implements IEditorResolver rawAssociations[key] = value; } } - let associations = []; + const associations = []; for (const [key, value] of Object.entries(rawAssociations)) { const association: EditorAssociation = { filenamePattern: key, @@ -337,7 +337,7 @@ export class EditorResolverService extends Disposable implements IEditorResolver private findMatchingEditors(resource: URI): RegisteredEditor[] { // The user setting should be respected even if the editor doesn't specify that resource in package.json const userSettings = this.getAssociationsForResource(resource); - let matchingEditors: RegisteredEditor[] = []; + const matchingEditors: RegisteredEditor[] = []; // Then all glob patterns for (const [key, editors] of this._editors) { for (const editor of editors) { @@ -396,7 +396,7 @@ export class EditorResolverService extends Disposable implements IEditorResolver }; } - let editors = this.findMatchingEditors(resource); + const editors = this.findMatchingEditors(resource); const associationsFromSetting = this.getAssociationsForResource(resource); // We only want minPriority+ if no user defined setting is found, else we won't resolve an editor @@ -517,7 +517,7 @@ export class EditorResolverService extends Disposable implements IEditorResolver } /** - * Given a resource and an editorId, returns all editors open for that resouce and editorId. + * Given a resource and an editorId, returns all editors open for that resource and editorId. * @param resource The resource specified * @param editorId The editorID * @returns A list of editors @@ -546,13 +546,13 @@ export class EditorResolverService extends Disposable implements IEditorResolver [key: string]: string[]; }; const editors = this.findMatchingEditors(resource); - const storedChoices: StoredChoice = JSON.parse(this.storageService.get(EditorResolverService.conflictingDefaultsStorageID, StorageScope.GLOBAL, '{}')); + const storedChoices: StoredChoice = JSON.parse(this.storageService.get(EditorResolverService.conflictingDefaultsStorageID, StorageScope.PROFILE, '{}')); const globForResource = `*${extname(resource)}`; // Writes to the storage service that a choice has been made for the currently installed editors const writeCurrentEditorsToStorage = () => { storedChoices[globForResource] = []; editors.forEach(editor => storedChoices[globForResource].push(editor.editorInfo.id)); - this.storageService.store(EditorResolverService.conflictingDefaultsStorageID, JSON.stringify(storedChoices), StorageScope.GLOBAL, StorageTarget.MACHINE); + this.storageService.store(EditorResolverService.conflictingDefaultsStorageID, JSON.stringify(storedChoices), StorageScope.PROFILE, StorageTarget.MACHINE); }; // If the user has already made a choice for this editor we don't want to ask them again @@ -671,7 +671,7 @@ export class EditorResolverService extends Disposable implements IEditorResolver // Create the editor picker const editorPicker = this.quickInputService.createQuickPick<IQuickPickItem>(); const placeHolderMessage = showDefaultPicker ? - localize('prompOpenWith.updateDefaultPlaceHolder', "Select new default editor for '{0}'", `*${extname(resource)}`) : + localize('promptOpenWith.updateDefaultPlaceHolder', "Select new default editor for '{0}'", `*${extname(resource)}`) : localize('promptOpenWith.placeHolder', "Select editor for '{0}'", basename(resource)); editorPicker.placeholder = placeHolderMessage; editorPicker.canAcceptInBackground = true; @@ -746,7 +746,7 @@ export class EditorResolverService extends Disposable implements IEditorResolver private sendEditorResolutionTelemetry(chosenInput: EditorInput): void { type editorResolutionClassification = { - viewType: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'The id of the editor opened. Used to gain an undertsanding of what editors are most popular' }; + viewType: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'The id of the editor opened. Used to gain an understanding of what editors are most popular' }; owner: 'lramos15'; comment: 'An event that fires when an editor type is picked'; }; @@ -783,7 +783,7 @@ export class EditorResolverService extends Disposable implements IEditorResolver cacheStorage.add(association.filenamePattern); } } - this.storageService.store(EditorResolverService.cacheStorageID, JSON.stringify(Array.from(cacheStorage)), StorageScope.GLOBAL, StorageTarget.MACHINE); + this.storageService.store(EditorResolverService.cacheStorageID, JSON.stringify(Array.from(cacheStorage)), StorageScope.PROFILE, StorageTarget.MACHINE); } private resourceMatchesCache(resource: URI): boolean { diff --git a/src/vs/workbench/services/editor/browser/editorService.ts b/src/vs/workbench/services/editor/browser/editorService.ts index 4b5689f2e11..c31eab43d77 100644 --- a/src/vs/workbench/services/editor/browser/editorService.ts +++ b/src/vs/workbench/services/editor/browser/editorService.ts @@ -233,7 +233,7 @@ export class EditorService extends Disposable implements EditorServiceImpl { private async handleMovedFile(source: URI, target: URI): Promise<void> { for (const group of this.editorGroupService.groups) { - let replacements: (IUntypedEditorReplacement | IEditorReplacement)[] = []; + const replacements: (IUntypedEditorReplacement | IEditorReplacement)[] = []; for (const editor of group.editors) { const resource = editor.resource; diff --git a/src/vs/workbench/services/editor/common/editorGroupFinder.ts b/src/vs/workbench/services/editor/common/editorGroupFinder.ts index 40780dd9ca6..b0c70ee48ba 100644 --- a/src/vs/workbench/services/editor/common/editorGroupFinder.ts +++ b/src/vs/workbench/services/editor/common/editorGroupFinder.ts @@ -52,8 +52,8 @@ export function findGroup(accessor: ServicesAccessor, editor: EditorInputWithOpt function doFindGroup(input: EditorInputWithOptions | IUntypedEditorInput, preferredGroup: PreferredGroup | undefined, editorGroupService: IEditorGroupsService, configurationService: IConfigurationService): IEditorGroup { let group: IEditorGroup | undefined; - let editor = isEditorInputWithOptions(input) ? input.editor : input; - let options = input.options; + const editor = isEditorInputWithOptions(input) ? input.editor : input; + const options = input.options; // Group: Instance of Group if (preferredGroup && typeof preferredGroup !== 'number') { diff --git a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts index 83497325fa8..807dfa9d228 100644 --- a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts @@ -201,9 +201,9 @@ suite('EditorGroupsService', () => { const rootGroup = part.activeGroup; - let input1 = new TestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); - let input2 = new TestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); - let input3 = new TestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID); + const input1 = new TestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); + const input2 = new TestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); + const input3 = new TestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID); await rootGroup.openEditor(input1, { pinned: true }); await part.sideGroup.openEditor(input2, { pinned: true }); @@ -215,7 +215,7 @@ suite('EditorGroupsService', () => { }); test('save & restore state', async function () { - let [part, instantiationService] = await createPart(); + const [part, instantiationService] = await createPart(); const rootGroup = part.groups[0]; const rightGroup = part.addGroup(rootGroup, GroupDirection.RIGHT); @@ -232,7 +232,7 @@ suite('EditorGroupsService', () => { part.saveState(); part.dispose(); - let [restoredPart] = await createPart(instantiationService); + const [restoredPart] = await createPart(instantiationService); assert.strictEqual(restoredPart.groups.length, 3); assert.ok(restoredPart.getGroup(rootGroup.id)); diff --git a/src/vs/workbench/services/editor/test/browser/editorService.test.ts b/src/vs/workbench/services/editor/test/browser/editorService.test.ts index 12042e67927..e4da27e5ebc 100644 --- a/src/vs/workbench/services/editor/test/browser/editorService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorService.test.ts @@ -168,8 +168,8 @@ suite('EditorService', () => { test('openEditor() - multiple calls are cancelled and indicated as such', async () => { const [, service] = await createEditorService(); - let input = new TestFileEditorInput(URI.parse('my://resource-basics'), TEST_EDITOR_INPUT_ID); - let otherInput = new TestFileEditorInput(URI.parse('my://resource2-basics'), TEST_EDITOR_INPUT_ID); + const input = new TestFileEditorInput(URI.parse('my://resource-basics'), TEST_EDITOR_INPUT_ID); + const otherInput = new TestFileEditorInput(URI.parse('my://resource2-basics'), TEST_EDITOR_INPUT_ID); let activeEditorChangeEventCounter = 0; const activeEditorChangeListener = service.onDidActiveEditorChange(() => { @@ -215,7 +215,7 @@ suite('EditorService', () => { await editor2.group.closeAllEditors(); input = new TestFileEditorInput(URI.parse('my://resource-basics'), TEST_EDITOR_INPUT_ID); - let inputSame = new TestFileEditorInput(URI.parse('my://resource-basics'), TEST_EDITOR_INPUT_ID); + const inputSame = new TestFileEditorInput(URI.parse('my://resource-basics'), TEST_EDITOR_INPUT_ID); editorP1 = service.openEditor(input, { pinned: true }); editorP2 = service.openEditor(inputSame, { pinned: true }); @@ -230,8 +230,8 @@ suite('EditorService', () => { test('openEditor() - singleton typed editors reveal instead of split', async () => { const [part, service] = await createEditorService(); - let input1 = new TestSingletonFileEditorInput(URI.parse('my://resource-basics1'), TEST_EDITOR_INPUT_ID); - let input2 = new TestSingletonFileEditorInput(URI.parse('my://resource-basics2'), TEST_EDITOR_INPUT_ID); + const input1 = new TestSingletonFileEditorInput(URI.parse('my://resource-basics1'), TEST_EDITOR_INPUT_ID); + const input2 = new TestSingletonFileEditorInput(URI.parse('my://resource-basics2'), TEST_EDITOR_INPUT_ID); const input1Group = (await service.openEditor(input1, { pinned: true }))?.group; const input2Group = (await service.openEditor(input2, { pinned: true }, SIDE_GROUP))?.group; @@ -255,16 +255,16 @@ suite('EditorService', () => { editor => ({ editor: new TestFileEditorInput(editor.resource, TEST_EDITOR_INPUT_ID) }) )); - let input1: IResourceEditorInput = { resource: URI.parse('file://resource-basics.editor-service-locked-group-tests'), options: { pinned: true } }; - let input2: IResourceEditorInput = { resource: URI.parse('file://resource2-basics.editor-service-locked-group-tests'), options: { pinned: true } }; - let input3: IResourceEditorInput = { resource: URI.parse('file://resource3-basics.editor-service-locked-group-tests'), options: { pinned: true } }; - let input4: IResourceEditorInput = { resource: URI.parse('file://resource4-basics.editor-service-locked-group-tests'), options: { pinned: true } }; - let input5: IResourceEditorInput = { resource: URI.parse('file://resource5-basics.editor-service-locked-group-tests'), options: { pinned: true } }; - let input6: IResourceEditorInput = { resource: URI.parse('file://resource6-basics.editor-service-locked-group-tests'), options: { pinned: true } }; - let input7: IResourceEditorInput = { resource: URI.parse('file://resource7-basics.editor-service-locked-group-tests'), options: { pinned: true } }; + const input1: IResourceEditorInput = { resource: URI.parse('file://resource-basics.editor-service-locked-group-tests'), options: { pinned: true } }; + const input2: IResourceEditorInput = { resource: URI.parse('file://resource2-basics.editor-service-locked-group-tests'), options: { pinned: true } }; + const input3: IResourceEditorInput = { resource: URI.parse('file://resource3-basics.editor-service-locked-group-tests'), options: { pinned: true } }; + const input4: IResourceEditorInput = { resource: URI.parse('file://resource4-basics.editor-service-locked-group-tests'), options: { pinned: true } }; + const input5: IResourceEditorInput = { resource: URI.parse('file://resource5-basics.editor-service-locked-group-tests'), options: { pinned: true } }; + const input6: IResourceEditorInput = { resource: URI.parse('file://resource6-basics.editor-service-locked-group-tests'), options: { pinned: true } }; + const input7: IResourceEditorInput = { resource: URI.parse('file://resource7-basics.editor-service-locked-group-tests'), options: { pinned: true } }; - let editor1 = await service.openEditor(input1, { pinned: true }); - let editor2 = await service.openEditor(input2, { pinned: true }, SIDE_GROUP); + const editor1 = await service.openEditor(input1, { pinned: true }); + const editor2 = await service.openEditor(input2, { pinned: true }, SIDE_GROUP); const group1 = editor1?.group; assert.strictEqual(group1?.count, 1); @@ -299,7 +299,7 @@ suite('EditorService', () => { // Will open a new group because side group is locked part.activateGroup(group1.id); - let editor3 = await service.openEditor(input4, { pinned: true }, SIDE_GROUP); + const editor3 = await service.openEditor(input4, { pinned: true }, SIDE_GROUP); assert.strictEqual(part.count, 3); const group3 = editor3?.group; @@ -317,7 +317,7 @@ suite('EditorService', () => { group3.lock(true); part.activateGroup(group1.id); - let editor5 = await service.openEditor(input5, { pinned: true }); + const editor5 = await service.openEditor(input5, { pinned: true }); const group4 = editor5?.group; assert.strictEqual(group4?.count, 1); assert.strictEqual(group4.activeEditor?.resource?.toString(), input5.resource.toString()); @@ -392,14 +392,14 @@ suite('EditorService', () => { )); const rootGroup = part.activeGroup; - let rightGroup = part.addGroup(rootGroup, GroupDirection.RIGHT); + const rightGroup = part.addGroup(rootGroup, GroupDirection.RIGHT); part.activateGroup(rootGroup); - let input1: IResourceEditorInput = { resource: URI.parse('file://resource-basics.editor-service-locked-group-tests'), options: { pinned: true } }; - let input2: IResourceEditorInput = { resource: URI.parse('file://resource2-basics.editor-service-locked-group-tests'), options: { pinned: true } }; - let input3: IResourceEditorInput = { resource: URI.parse('file://resource3-basics.editor-service-locked-group-tests'), options: { pinned: true } }; - let input4: IResourceEditorInput = { resource: URI.parse('file://resource4-basics.editor-service-locked-group-tests'), options: { pinned: true } }; + const input1: IResourceEditorInput = { resource: URI.parse('file://resource-basics.editor-service-locked-group-tests'), options: { pinned: true } }; + const input2: IResourceEditorInput = { resource: URI.parse('file://resource2-basics.editor-service-locked-group-tests'), options: { pinned: true } }; + const input3: IResourceEditorInput = { resource: URI.parse('file://resource3-basics.editor-service-locked-group-tests'), options: { pinned: true } }; + const input4: IResourceEditorInput = { resource: URI.parse('file://resource4-basics.editor-service-locked-group-tests'), options: { pinned: true } }; await service.openEditor(input1, rootGroup.id); await service.openEditor(input2, rootGroup.id); @@ -440,14 +440,14 @@ suite('EditorService', () => { )); const rootGroup = part.activeGroup; - let rightGroup = part.addGroup(rootGroup, GroupDirection.RIGHT); + const rightGroup = part.addGroup(rootGroup, GroupDirection.RIGHT); part.activateGroup(rootGroup); - let input1: IResourceEditorInput = { resource: URI.parse('file://resource-basics.editor-service-locked-group-tests'), options: { pinned: true } }; - let input2: IResourceEditorInput = { resource: URI.parse('file://resource2-basics.editor-service-locked-group-tests'), options: { pinned: true } }; - let input3: IResourceEditorInput = { resource: URI.parse('file://resource3-basics.editor-service-locked-group-tests'), options: { pinned: true } }; - let input4: IResourceEditorInput = { resource: URI.parse('file://resource4-basics.editor-service-locked-group-tests'), options: { pinned: true } }; + const input1: IResourceEditorInput = { resource: URI.parse('file://resource-basics.editor-service-locked-group-tests'), options: { pinned: true } }; + const input2: IResourceEditorInput = { resource: URI.parse('file://resource2-basics.editor-service-locked-group-tests'), options: { pinned: true } }; + const input3: IResourceEditorInput = { resource: URI.parse('file://resource3-basics.editor-service-locked-group-tests'), options: { pinned: true } }; + const input4: IResourceEditorInput = { resource: URI.parse('file://resource4-basics.editor-service-locked-group-tests'), options: { pinned: true } }; await service.openEditor(input1, rootGroup.id); await service.openEditor(input2, rootGroup.id); @@ -488,14 +488,14 @@ suite('EditorService', () => { )); const rootGroup = part.activeGroup; - let rightGroup = part.addGroup(rootGroup, GroupDirection.RIGHT); + const rightGroup = part.addGroup(rootGroup, GroupDirection.RIGHT); part.activateGroup(rootGroup); - let input1: IResourceEditorInput = { resource: URI.parse('file://resource-basics.editor-service-locked-group-tests'), options: { pinned: true } }; - let input2: IResourceEditorInput = { resource: URI.parse('file://resource2-basics.editor-service-locked-group-tests'), options: { pinned: true } }; - let input3: IResourceEditorInput = { resource: URI.parse('file://resource3-basics.editor-service-locked-group-tests'), options: { pinned: true } }; - let input4: IResourceEditorInput = { resource: URI.parse('file://resource4-basics.editor-service-locked-group-tests'), options: { pinned: true } }; + const input1: IResourceEditorInput = { resource: URI.parse('file://resource-basics.editor-service-locked-group-tests'), options: { pinned: true } }; + const input2: IResourceEditorInput = { resource: URI.parse('file://resource2-basics.editor-service-locked-group-tests'), options: { pinned: true } }; + const input3: IResourceEditorInput = { resource: URI.parse('file://resource3-basics.editor-service-locked-group-tests'), options: { pinned: true } }; + const input4: IResourceEditorInput = { resource: URI.parse('file://resource4-basics.editor-service-locked-group-tests'), options: { pinned: true } }; await service.openEditor(input1, rootGroup.id); await service.openEditor(input2, rootGroup.id); @@ -608,8 +608,8 @@ suite('EditorService', () => { { // untyped resource editor, no options, no group { - let untypedEditor: IResourceEditorInput = { resource: URI.file('file.editor-service-override-tests') }; - let pane = await openEditor(untypedEditor); + const untypedEditor: IResourceEditorInput = { resource: URI.file('file.editor-service-override-tests') }; + const pane = await openEditor(untypedEditor); let typedEditor = pane?.input; assert.strictEqual(pane?.group, rootGroup); @@ -630,7 +630,7 @@ suite('EditorService', () => { assert.strictEqual(pane?.group.activeEditor, typedEditor); // replaceEditors should work too - let untypedEditorReplacement: IResourceEditorInput = { resource: URI.file('file-replaced.editor-service-override-tests') }; + const untypedEditorReplacement: IResourceEditorInput = { resource: URI.file('file-replaced.editor-service-override-tests') }; await service.replaceEditors([{ editor: typedEditor, replacement: untypedEditorReplacement @@ -654,9 +654,9 @@ suite('EditorService', () => { // untyped resource editor, options (override disabled), no group { - let untypedEditor: IResourceEditorInput = { resource: URI.file('file.editor-service-override-tests'), options: { override: EditorResolution.DISABLED } }; - let pane = await openEditor(untypedEditor); - let typedEditor = pane?.input; + const untypedEditor: IResourceEditorInput = { resource: URI.file('file.editor-service-override-tests'), options: { override: EditorResolution.DISABLED } }; + const pane = await openEditor(untypedEditor); + const typedEditor = pane?.input; assert.strictEqual(pane?.group, rootGroup); assert.ok(typedEditor instanceof FileEditorInput); @@ -680,8 +680,8 @@ suite('EditorService', () => { // untyped resource editor, options (override disabled, sticky: true, preserveFocus: true), no group { - let untypedEditor: IResourceEditorInput = { resource: URI.file('file.editor-service-override-tests'), options: { sticky: true, preserveFocus: true, override: EditorResolution.DISABLED } }; - let pane = await openEditor(untypedEditor); + const untypedEditor: IResourceEditorInput = { resource: URI.file('file.editor-service-override-tests'), options: { sticky: true, preserveFocus: true, override: EditorResolution.DISABLED } }; + const pane = await openEditor(untypedEditor); assert.strictEqual(pane?.group, rootGroup); assert.ok(pane.input instanceof FileEditorInput); @@ -702,8 +702,8 @@ suite('EditorService', () => { // untyped resource editor, options (override default), no group { - let untypedEditor: IResourceEditorInput = { resource: URI.file('file.editor-service-override-tests'), options: { override: DEFAULT_EDITOR_ASSOCIATION.id } }; - let pane = await openEditor(untypedEditor); + const untypedEditor: IResourceEditorInput = { resource: URI.file('file.editor-service-override-tests'), options: { override: DEFAULT_EDITOR_ASSOCIATION.id } }; + const pane = await openEditor(untypedEditor); assert.strictEqual(pane?.group, rootGroup); assert.ok(pane.input instanceof FileEditorInput); @@ -722,8 +722,8 @@ suite('EditorService', () => { // untyped resource editor, options (override: TEST_EDITOR_INPUT_ID), no group { - let untypedEditor: IResourceEditorInput = { resource: URI.file('file.editor-service-override-tests'), options: { override: TEST_EDITOR_INPUT_ID } }; - let pane = await openEditor(untypedEditor); + const untypedEditor: IResourceEditorInput = { resource: URI.file('file.editor-service-override-tests'), options: { override: TEST_EDITOR_INPUT_ID } }; + const pane = await openEditor(untypedEditor); assert.strictEqual(pane?.group, rootGroup); assert.ok(pane.input instanceof TestFileEditorInput); @@ -742,8 +742,8 @@ suite('EditorService', () => { // untyped resource editor, options (sticky: true, preserveFocus: true), no group { - let untypedEditor: IResourceEditorInput = { resource: URI.file('file.editor-service-override-tests'), options: { sticky: true, preserveFocus: true } }; - let pane = await openEditor(untypedEditor); + const untypedEditor: IResourceEditorInput = { resource: URI.file('file.editor-service-override-tests'), options: { sticky: true, preserveFocus: true } }; + const pane = await openEditor(untypedEditor); assert.strictEqual(pane?.group, rootGroup); assert.ok(pane.input instanceof TestFileEditorInput); @@ -765,8 +765,8 @@ suite('EditorService', () => { // untyped resource editor, options (override: TEST_EDITOR_INPUT_ID, sticky: true, preserveFocus: true), no group { - let untypedEditor: IResourceEditorInput = { resource: URI.file('file.editor-service-override-tests'), options: { sticky: true, preserveFocus: true, override: TEST_EDITOR_INPUT_ID } }; - let pane = await openEditor(untypedEditor); + const untypedEditor: IResourceEditorInput = { resource: URI.file('file.editor-service-override-tests'), options: { sticky: true, preserveFocus: true, override: TEST_EDITOR_INPUT_ID } }; + const pane = await openEditor(untypedEditor); assert.strictEqual(pane?.group, rootGroup); assert.ok(pane.input instanceof TestFileEditorInput); @@ -788,8 +788,8 @@ suite('EditorService', () => { // untyped resource editor, no options, SIDE_GROUP { - let untypedEditor: IResourceEditorInput = { resource: URI.file('file.editor-service-override-tests') }; - let pane = await openEditor(untypedEditor, SIDE_GROUP); + const untypedEditor: IResourceEditorInput = { resource: URI.file('file.editor-service-override-tests') }; + const pane = await openEditor(untypedEditor, SIDE_GROUP); assert.strictEqual(accessor.editorGroupService.groups.length, 2); assert.notStrictEqual(pane?.group, rootGroup); @@ -809,8 +809,8 @@ suite('EditorService', () => { // untyped resource editor, options (override disabled), SIDE_GROUP { - let untypedEditor: IResourceEditorInput = { resource: URI.file('file.editor-service-override-tests'), options: { override: EditorResolution.DISABLED } }; - let pane = await openEditor(untypedEditor, SIDE_GROUP); + const untypedEditor: IResourceEditorInput = { resource: URI.file('file.editor-service-override-tests'), options: { override: EditorResolution.DISABLED } }; + const pane = await openEditor(untypedEditor, SIDE_GROUP); assert.strictEqual(accessor.editorGroupService.groups.length, 2); assert.notStrictEqual(pane?.group, rootGroup); @@ -833,8 +833,8 @@ suite('EditorService', () => { { // typed editor, no options, no group { - let typedEditor = new TestFileEditorInput(URI.file('file.editor-service-override-tests'), TEST_EDITOR_INPUT_ID); - let pane = await openEditor({ editor: typedEditor }); + const typedEditor = new TestFileEditorInput(URI.file('file.editor-service-override-tests'), TEST_EDITOR_INPUT_ID); + const pane = await openEditor({ editor: typedEditor }); let typedInput = pane?.input; assert.strictEqual(pane?.group, rootGroup); @@ -855,7 +855,7 @@ suite('EditorService', () => { assert.strictEqual(pane?.group.activeEditor, typedInput); // replaceEditors should work too - let typedEditorReplacement = new TestFileEditorInput(URI.file('file-replaced.editor-service-override-tests'), TEST_EDITOR_INPUT_ID); + const typedEditorReplacement = new TestFileEditorInput(URI.file('file-replaced.editor-service-override-tests'), TEST_EDITOR_INPUT_ID); await service.replaceEditors([{ editor: typedEditor, replacement: typedEditorReplacement @@ -879,9 +879,9 @@ suite('EditorService', () => { // typed editor, options (override disabled), no group { - let typedEditor = new TestFileEditorInput(URI.file('file.editor-service-override-tests'), TEST_EDITOR_INPUT_ID); - let pane = await openEditor({ editor: typedEditor, options: { override: EditorResolution.DISABLED } }); - let typedInput = pane?.input; + const typedEditor = new TestFileEditorInput(URI.file('file.editor-service-override-tests'), TEST_EDITOR_INPUT_ID); + const pane = await openEditor({ editor: typedEditor, options: { override: EditorResolution.DISABLED } }); + const typedInput = pane?.input; assert.strictEqual(pane?.group, rootGroup); assert.ok(typedInput instanceof TestFileEditorInput); @@ -905,8 +905,8 @@ suite('EditorService', () => { // typed editor, options (override disabled, sticky: true, preserveFocus: true), no group { - let typedEditor = new TestFileEditorInput(URI.file('file.editor-service-override-tests'), TEST_EDITOR_INPUT_ID); - let pane = await openEditor({ editor: typedEditor, options: { sticky: true, preserveFocus: true, override: EditorResolution.DISABLED } }); + const typedEditor = new TestFileEditorInput(URI.file('file.editor-service-override-tests'), TEST_EDITOR_INPUT_ID); + const pane = await openEditor({ editor: typedEditor, options: { sticky: true, preserveFocus: true, override: EditorResolution.DISABLED } }); assert.strictEqual(pane?.group, rootGroup); assert.ok(pane.input instanceof TestFileEditorInput); @@ -927,8 +927,8 @@ suite('EditorService', () => { // typed editor, options (override default), no group { - let typedEditor = new TestFileEditorInput(URI.file('file.editor-service-override-tests'), TEST_EDITOR_INPUT_ID); - let pane = await openEditor({ editor: typedEditor, options: { override: DEFAULT_EDITOR_ASSOCIATION.id } }); + const typedEditor = new TestFileEditorInput(URI.file('file.editor-service-override-tests'), TEST_EDITOR_INPUT_ID); + const pane = await openEditor({ editor: typedEditor, options: { override: DEFAULT_EDITOR_ASSOCIATION.id } }); assert.strictEqual(pane?.group, rootGroup); assert.ok(pane.input instanceof FileEditorInput); @@ -947,8 +947,8 @@ suite('EditorService', () => { // typed editor, options (override: TEST_EDITOR_INPUT_ID), no group { - let typedEditor = new TestFileEditorInput(URI.file('file.editor-service-override-tests'), TEST_EDITOR_INPUT_ID); - let pane = await openEditor({ editor: typedEditor, options: { override: TEST_EDITOR_INPUT_ID } }); + const typedEditor = new TestFileEditorInput(URI.file('file.editor-service-override-tests'), TEST_EDITOR_INPUT_ID); + const pane = await openEditor({ editor: typedEditor, options: { override: TEST_EDITOR_INPUT_ID } }); assert.strictEqual(pane?.group, rootGroup); assert.ok(pane.input instanceof TestFileEditorInput); @@ -967,8 +967,8 @@ suite('EditorService', () => { // typed editor, options (sticky: true, preserveFocus: true), no group { - let typedEditor = new TestFileEditorInput(URI.file('file.editor-service-override-tests'), TEST_EDITOR_INPUT_ID); - let pane = await openEditor({ editor: typedEditor, options: { sticky: true, preserveFocus: true } }); + const typedEditor = new TestFileEditorInput(URI.file('file.editor-service-override-tests'), TEST_EDITOR_INPUT_ID); + const pane = await openEditor({ editor: typedEditor, options: { sticky: true, preserveFocus: true } }); assert.strictEqual(pane?.group, rootGroup); assert.ok(pane.input instanceof TestFileEditorInput); @@ -990,8 +990,8 @@ suite('EditorService', () => { // typed editor, options (override: TEST_EDITOR_INPUT_ID, sticky: true, preserveFocus: true), no group { - let typedEditor = new TestFileEditorInput(URI.file('file.editor-service-override-tests'), TEST_EDITOR_INPUT_ID); - let pane = await openEditor({ editor: typedEditor, options: { sticky: true, preserveFocus: true, override: TEST_EDITOR_INPUT_ID } }); + const typedEditor = new TestFileEditorInput(URI.file('file.editor-service-override-tests'), TEST_EDITOR_INPUT_ID); + const pane = await openEditor({ editor: typedEditor, options: { sticky: true, preserveFocus: true, override: TEST_EDITOR_INPUT_ID } }); assert.strictEqual(pane?.group, rootGroup); assert.ok(pane.input instanceof TestFileEditorInput); @@ -1013,8 +1013,8 @@ suite('EditorService', () => { // typed editor, no options, SIDE_GROUP { - let typedEditor = new TestFileEditorInput(URI.file('file.editor-service-override-tests'), TEST_EDITOR_INPUT_ID); - let pane = await openEditor({ editor: typedEditor }, SIDE_GROUP); + const typedEditor = new TestFileEditorInput(URI.file('file.editor-service-override-tests'), TEST_EDITOR_INPUT_ID); + const pane = await openEditor({ editor: typedEditor }, SIDE_GROUP); assert.strictEqual(accessor.editorGroupService.groups.length, 2); assert.notStrictEqual(pane?.group, rootGroup); @@ -1034,8 +1034,8 @@ suite('EditorService', () => { // typed editor, options (override disabled), SIDE_GROUP { - let typedEditor = new TestFileEditorInput(URI.file('file.editor-service-override-tests'), TEST_EDITOR_INPUT_ID); - let pane = await openEditor({ editor: typedEditor, options: { override: EditorResolution.DISABLED } }, SIDE_GROUP); + const typedEditor = new TestFileEditorInput(URI.file('file.editor-service-override-tests'), TEST_EDITOR_INPUT_ID); + const pane = await openEditor({ editor: typedEditor, options: { override: EditorResolution.DISABLED } }, SIDE_GROUP); assert.strictEqual(accessor.editorGroupService.groups.length, 2); assert.notStrictEqual(pane?.group, rootGroup); @@ -1058,8 +1058,8 @@ suite('EditorService', () => { { // untyped untitled editor, no options, no group { - let untypedEditor: IUntitledTextResourceEditorInput = { resource: undefined, options: { override: TEST_EDITOR_INPUT_ID } }; - let pane = await openEditor(untypedEditor); + const untypedEditor: IUntitledTextResourceEditorInput = { resource: undefined, options: { override: TEST_EDITOR_INPUT_ID } }; + const pane = await openEditor(untypedEditor); assert.strictEqual(pane?.group, rootGroup); assert.ok(pane.input instanceof TestFileEditorInput); @@ -1078,8 +1078,8 @@ suite('EditorService', () => { // untyped untitled editor, no options, SIDE_GROUP { - let untypedEditor: IUntitledTextResourceEditorInput = { resource: undefined, options: { override: TEST_EDITOR_INPUT_ID } }; - let pane = await openEditor(untypedEditor, SIDE_GROUP); + const untypedEditor: IUntitledTextResourceEditorInput = { resource: undefined, options: { override: TEST_EDITOR_INPUT_ID } }; + const pane = await openEditor(untypedEditor, SIDE_GROUP); assert.strictEqual(accessor.editorGroupService.groups.length, 2); assert.notStrictEqual(pane?.group, rootGroup); @@ -1099,9 +1099,9 @@ suite('EditorService', () => { // untyped untitled editor with associated resource, no options, no group { - let untypedEditor: IUntitledTextResourceEditorInput = { resource: URI.file('file-original.editor-service-override-tests').with({ scheme: 'untitled' }) }; - let pane = await openEditor(untypedEditor); - let typedEditor = pane?.input; + const untypedEditor: IUntitledTextResourceEditorInput = { resource: URI.file('file-original.editor-service-override-tests').with({ scheme: 'untitled' }) }; + const pane = await openEditor(untypedEditor); + const typedEditor = pane?.input; assert.strictEqual(pane?.group, rootGroup); assert.ok(typedEditor instanceof TestFileEditorInput); @@ -1125,8 +1125,8 @@ suite('EditorService', () => { // untyped untitled editor, options (sticky: true, preserveFocus: true), no group { - let untypedEditor: IUntitledTextResourceEditorInput = { resource: undefined, options: { sticky: true, preserveFocus: true, override: TEST_EDITOR_INPUT_ID } }; - let pane = await openEditor(untypedEditor); + const untypedEditor: IUntitledTextResourceEditorInput = { resource: undefined, options: { sticky: true, preserveFocus: true, override: TEST_EDITOR_INPUT_ID } }; + const pane = await openEditor(untypedEditor); assert.strictEqual(pane?.group, rootGroup); assert.ok(pane.input instanceof TestFileEditorInput); @@ -1151,13 +1151,13 @@ suite('EditorService', () => { { // untyped diff editor, no options, no group { - let untypedEditor: IResourceDiffEditorInput = { + const untypedEditor: IResourceDiffEditorInput = { original: { resource: URI.file('file-original.editor-service-override-tests') }, modified: { resource: URI.file('file-modified.editor-service-override-tests') }, options: { override: TEST_EDITOR_INPUT_ID } }; - let pane = await openEditor(untypedEditor); - let typedEditor = pane?.input; + const pane = await openEditor(untypedEditor); + const typedEditor = pane?.input; assert.strictEqual(pane?.group, rootGroup); assert.ok(typedEditor instanceof TestFileEditorInput); @@ -1175,12 +1175,12 @@ suite('EditorService', () => { // untyped diff editor, no options, SIDE_GROUP { - let untypedEditor: IResourceDiffEditorInput = { + const untypedEditor: IResourceDiffEditorInput = { original: { resource: URI.file('file-original.editor-service-override-tests') }, modified: { resource: URI.file('file-modified.editor-service-override-tests') }, options: { override: TEST_EDITOR_INPUT_ID } }; - let pane = await openEditor(untypedEditor, SIDE_GROUP); + const pane = await openEditor(untypedEditor, SIDE_GROUP); assert.strictEqual(accessor.editorGroupService.groups.length, 2); assert.notStrictEqual(pane?.group, rootGroup); @@ -1199,14 +1199,14 @@ suite('EditorService', () => { // untyped diff editor, options (sticky: true, preserveFocus: true), no group { - let untypedEditor: IResourceDiffEditorInput = { + const untypedEditor: IResourceDiffEditorInput = { original: { resource: URI.file('file-original.editor-service-override-tests') }, modified: { resource: URI.file('file-modified.editor-service-override-tests') }, options: { override: TEST_EDITOR_INPUT_ID, sticky: true, preserveFocus: true } }; - let pane = await openEditor(untypedEditor); + const pane = await openEditor(untypedEditor); assert.strictEqual(pane?.group, rootGroup); assert.ok(pane.input instanceof TestFileEditorInput); @@ -1230,8 +1230,8 @@ suite('EditorService', () => { // no options, no group { - let typedEditor = new TestFileEditorInput(URI.file('file.something'), TEST_EDITOR_INPUT_ID); - let pane = await openEditor({ editor: typedEditor }); + const typedEditor = new TestFileEditorInput(URI.file('file.something'), TEST_EDITOR_INPUT_ID); + const pane = await openEditor({ editor: typedEditor }); assert.strictEqual(pane?.group, rootGroup); assert.ok(pane.input instanceof TestFileEditorInput); @@ -1250,8 +1250,8 @@ suite('EditorService', () => { // no options, SIDE_GROUP { - let typedEditor = new TestFileEditorInput(URI.file('file.something'), TEST_EDITOR_INPUT_ID); - let pane = await openEditor({ editor: typedEditor }, SIDE_GROUP); + const typedEditor = new TestFileEditorInput(URI.file('file.something'), TEST_EDITOR_INPUT_ID); + const pane = await openEditor({ editor: typedEditor }, SIDE_GROUP); assert.strictEqual(accessor.editorGroupService.groups.length, 2); assert.notStrictEqual(pane?.group, rootGroup); @@ -1275,9 +1275,9 @@ suite('EditorService', () => { // no options, no group { - let typedEditor = new TestFileEditorInput(URI.file('file.something'), TEST_EDITOR_INPUT_ID); + const typedEditor = new TestFileEditorInput(URI.file('file.something'), TEST_EDITOR_INPUT_ID); typedEditor.disableToUntyped = true; - let pane = await openEditor({ editor: typedEditor }); + const pane = await openEditor({ editor: typedEditor }); assert.strictEqual(pane?.group, rootGroup); assert.ok(pane.input instanceof TestFileEditorInput); @@ -1296,9 +1296,9 @@ suite('EditorService', () => { // no options, SIDE_GROUP { - let typedEditor = new TestFileEditorInput(URI.file('file.something'), TEST_EDITOR_INPUT_ID); + const typedEditor = new TestFileEditorInput(URI.file('file.something'), TEST_EDITOR_INPUT_ID); typedEditor.disableToUntyped = true; - let pane = await openEditor({ editor: typedEditor }, SIDE_GROUP); + const pane = await openEditor({ editor: typedEditor }, SIDE_GROUP); assert.strictEqual(accessor.editorGroupService.groups.length, 2); assert.notStrictEqual(pane?.group, rootGroup); @@ -1322,12 +1322,12 @@ suite('EditorService', () => { // mix of untyped and typed editors { - let untypedEditor1: IResourceEditorInput = { resource: URI.file('file1.editor-service-override-tests') }; - let untypedEditor2: IResourceEditorInput = { resource: URI.file('file2.editor-service-override-tests'), options: { override: EditorResolution.DISABLED } }; - let untypedEditor3: EditorInputWithOptions = { editor: new TestFileEditorInput(URI.file('file3.editor-service-override-tests'), TEST_EDITOR_INPUT_ID) }; - let untypedEditor4: EditorInputWithOptions = { editor: new TestFileEditorInput(URI.file('file4.editor-service-override-tests'), TEST_EDITOR_INPUT_ID), options: { override: EditorResolution.DISABLED } }; - let untypedEditor5: IResourceEditorInput = { resource: URI.file('file5.editor-service-override-tests') }; - let pane = (await service.openEditors([untypedEditor1, untypedEditor2, untypedEditor3, untypedEditor4, untypedEditor5]))[0]; + const untypedEditor1: IResourceEditorInput = { resource: URI.file('file1.editor-service-override-tests') }; + const untypedEditor2: IResourceEditorInput = { resource: URI.file('file2.editor-service-override-tests'), options: { override: EditorResolution.DISABLED } }; + const untypedEditor3: EditorInputWithOptions = { editor: new TestFileEditorInput(URI.file('file3.editor-service-override-tests'), TEST_EDITOR_INPUT_ID) }; + const untypedEditor4: EditorInputWithOptions = { editor: new TestFileEditorInput(URI.file('file4.editor-service-override-tests'), TEST_EDITOR_INPUT_ID), options: { override: EditorResolution.DISABLED } }; + const untypedEditor5: IResourceEditorInput = { resource: URI.file('file5.editor-service-override-tests') }; + const pane = (await service.openEditors([untypedEditor1, untypedEditor2, untypedEditor3, untypedEditor4, untypedEditor5]))[0]; assert.strictEqual(pane?.group, rootGroup); assert.strictEqual(pane?.group.count, 5); @@ -1348,11 +1348,11 @@ suite('EditorService', () => { { // untyped default editor, options: revealIfVisible { - let untypedEditor1: IResourceEditorInput = { resource: URI.file('file-1'), options: { revealIfVisible: true, pinned: true } }; - let untypedEditor2: IResourceEditorInput = { resource: URI.file('file-2'), options: { pinned: true } }; + const untypedEditor1: IResourceEditorInput = { resource: URI.file('file-1'), options: { revealIfVisible: true, pinned: true } }; + const untypedEditor2: IResourceEditorInput = { resource: URI.file('file-2'), options: { pinned: true } }; - let rootPane = await openEditor(untypedEditor1); - let sidePane = await openEditor(untypedEditor2, SIDE_GROUP); + const rootPane = await openEditor(untypedEditor1); + const sidePane = await openEditor(untypedEditor2, SIDE_GROUP); assert.strictEqual(rootPane?.group?.count, 1); assert.strictEqual(sidePane?.group?.count, 1); @@ -1369,13 +1369,13 @@ suite('EditorService', () => { // untyped default editor, options: revealIfOpened { - let untypedEditor1: IResourceEditorInput = { resource: URI.file('file-1'), options: { revealIfOpened: true, pinned: true } }; - let untypedEditor2: IResourceEditorInput = { resource: URI.file('file-2'), options: { pinned: true } }; + const untypedEditor1: IResourceEditorInput = { resource: URI.file('file-1'), options: { revealIfOpened: true, pinned: true } }; + const untypedEditor2: IResourceEditorInput = { resource: URI.file('file-2'), options: { pinned: true } }; - let rootPane = await openEditor(untypedEditor1); + const rootPane = await openEditor(untypedEditor1); await openEditor(untypedEditor2); assert.strictEqual(rootPane?.group?.activeEditor?.resource?.toString(), untypedEditor2.resource.toString()); - let sidePane = await openEditor(untypedEditor2, SIDE_GROUP); + const sidePane = await openEditor(untypedEditor2, SIDE_GROUP); assert.strictEqual(rootPane?.group?.count, 2); assert.strictEqual(sidePane?.group?.count, 1); @@ -1957,7 +1957,7 @@ suite('EditorService', () => { test('two active editor change events when opening editor to the side', async function () { const [, service] = await createEditorService(); - let input = new TestFileEditorInput(URI.parse('my://resource-active'), TEST_EDITOR_INPUT_ID); + const input = new TestFileEditorInput(URI.parse('my://resource-active'), TEST_EDITOR_INPUT_ID); let activeEditorChangeEvents = 0; const activeEditorChangeListener = service.onDidActiveEditorChange(() => { @@ -1991,7 +1991,7 @@ suite('EditorService', () => { const [, service] = await createEditorService(); // Open untitled input - let editor = await service.openEditor({ resource: undefined }); + const editor = await service.openEditor({ resource: undefined }); assert.strictEqual(service.activeEditorPane, editor); assert.strictEqual(service.activeTextEditorControl, editor?.getControl()); @@ -2004,10 +2004,10 @@ suite('EditorService', () => { const input = new TestFileEditorInput(URI.parse('my://resource-active'), TEST_EDITOR_INPUT_ID); const otherInput = new TestFileEditorInput(URI.parse('my://resource2-inactive'), TEST_EDITOR_INPUT_ID); - let editor = await service.openEditor(input, { pinned: true }); + const editor = await service.openEditor(input, { pinned: true }); assert.ok(editor); - let otherEditor = await service.openEditor(otherInput, { inactive: true }); + const otherEditor = await service.openEditor(otherInput, { inactive: true }); assert.ok(!otherEditor); }); @@ -2017,7 +2017,7 @@ suite('EditorService', () => { const failingInput = new TestFileEditorInput(URI.parse('my://resource-failing'), TEST_EDITOR_INPUT_ID); failingInput.setFailToOpen(); - let failingEditor = await service.openEditor(failingInput); + const failingEditor = await service.openEditor(failingInput); assert.ok(failingEditor instanceof ErrorPlaceholderEditor); }); @@ -2031,7 +2031,7 @@ suite('EditorService', () => { await service.openEditor(failingInput, { inactive: true }); failingInput.setFailToOpen(); - let failingEditor = await service.openEditor(failingInput); + const failingEditor = await service.openEditor(failingInput); assert.ok(failingEditor instanceof ErrorPlaceholderEditor); }); diff --git a/src/vs/workbench/services/environment/browser/environmentService.ts b/src/vs/workbench/services/environment/browser/environmentService.ts index f205de47189..631b3db439e 100644 --- a/src/vs/workbench/services/environment/browser/environmentService.ts +++ b/src/vs/workbench/services/environment/browser/environmentService.ts @@ -56,26 +56,20 @@ export class BrowserWorkbenchEnvironmentService implements IBrowserWorkbenchEnvi get userRoamingDataHome(): URI { return URI.file('/User').with({ scheme: Schemas.vscodeUserData }); } @memoize - get settingsResource(): URI { return joinPath(this.userRoamingDataHome, 'settings.json'); } - - @memoize get argvResource(): URI { return joinPath(this.userRoamingDataHome, 'argv.json'); } @memoize - get snippetsHome(): URI { return joinPath(this.userRoamingDataHome, 'snippets'); } - - @memoize get cacheHome(): URI { return joinPath(this.userRoamingDataHome, 'caches'); } @memoize - get globalStorageHome(): URI { return joinPath(this.userRoamingDataHome, 'globalStorage'); } - - @memoize get workspaceStorageHome(): URI { return joinPath(this.userRoamingDataHome, 'workspaceStorage'); } @memoize get localHistoryHome(): URI { return joinPath(this.userRoamingDataHome, 'History'); } + @memoize + get stateResource(): URI { return joinPath(this.userRoamingDataHome, 'State', 'storage.json'); } + /** * In Web every workspace can potentially have scoped user-data * and/or extensions and if Sync state is shared then it can make @@ -93,9 +87,6 @@ export class BrowserWorkbenchEnvironmentService implements IBrowserWorkbenchEnvi get sync(): 'on' | 'off' | undefined { return undefined; } @memoize - get keybindingsResource(): URI { return joinPath(this.userRoamingDataHome, 'keybindings.json'); } - - @memoize get keyboardLayoutResource(): URI { return joinPath(this.userRoamingDataHome, 'keyboardLayout.json'); } @memoize @@ -214,6 +205,8 @@ export class BrowserWorkbenchEnvironmentService implements IBrowserWorkbenchEnvi @memoize get disableWorkspaceTrust(): boolean { return !this.options.enableWorkspaceTrust; } + editSessionId: string | undefined = this.options.editSessionId; + private payload: Map<string, string> | undefined; constructor( diff --git a/src/vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.ts b/src/vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.ts index 089434c2a84..320aab963d5 100644 --- a/src/vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.ts +++ b/src/vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.ts @@ -3,14 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IBuiltinExtensionsScannerService, ExtensionType, IExtensionManifest, IExtension, TargetPlatform } from 'vs/platform/extensions/common/extensions'; -import { isWeb } from 'vs/base/common/platform'; +import { IBuiltinExtensionsScannerService, ExtensionType, IExtensionManifest, TargetPlatform, IExtension } from 'vs/platform/extensions/common/extensions'; +import { isWeb, Language } from 'vs/base/common/platform'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { FileAccess } from 'vs/base/common/network'; -import { localizeManifest } from 'vs/platform/extensionManagement/common/extensionNls'; +import { URI } from 'vs/base/common/uri'; +import { IExtensionResourceLoaderService } from 'vs/workbench/services/extensionResourceLoader/common/extensionResourceLoader'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { ITranslations, localizeManifest } from 'vs/platform/extensionManagement/common/extensionNls'; +import { ILogService } from 'vs/platform/log/common/log'; interface IBundledExtension { extensionPath: string; @@ -24,13 +28,24 @@ export class BuiltinExtensionsScannerService implements IBuiltinExtensionsScanne declare readonly _serviceBrand: undefined; - private readonly builtinExtensions: IExtension[] = []; + private readonly builtinExtensionsPromises: Promise<IExtension>[] = []; + + private nlsUrl: URI | undefined; constructor( @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, @IUriIdentityService uriIdentityService: IUriIdentityService, + @IExtensionResourceLoaderService private readonly extensionResourceLoaderService: IExtensionResourceLoaderService, + @IProductService productService: IProductService, + @ILogService private readonly logService: ILogService ) { if (isWeb) { + const nlsBaseUrl = productService.extensionsGallery?.nlsBaseUrl; + // Only use the nlsBaseUrl if we are using a language other than the default, English. + if (nlsBaseUrl && productService.commit && !Language.isDefaultVariant()) { + this.nlsUrl = URI.joinPath(URI.parse(nlsBaseUrl), productService.commit, productService.version, Language.value()); + } + const builtinExtensionsServiceUrl = FileAccess.asBrowserUri('../../../../../../extensions', require); if (builtinExtensionsServiceUrl) { let bundledExtensions: IBundledExtension[] = []; @@ -49,24 +64,43 @@ export class BuiltinExtensionsScannerService implements IBuiltinExtensionsScanne } } - this.builtinExtensions = bundledExtensions.map(e => ({ - identifier: { id: getGalleryExtensionId(e.packageJSON.publisher, e.packageJSON.name) }, - location: uriIdentityService.extUri.joinPath(builtinExtensionsServiceUrl!, e.extensionPath), - type: ExtensionType.System, - isBuiltin: true, - manifest: e.packageNLS ? localizeManifest(e.packageJSON, e.packageNLS) : e.packageJSON, - readmeUrl: e.readmePath ? uriIdentityService.extUri.joinPath(builtinExtensionsServiceUrl!, e.readmePath) : undefined, - changelogUrl: e.changelogPath ? uriIdentityService.extUri.joinPath(builtinExtensionsServiceUrl!, e.changelogPath) : undefined, - targetPlatform: TargetPlatform.WEB, - validations: [], - isValid: true - })); + this.builtinExtensionsPromises = bundledExtensions.map(async e => { + const id = getGalleryExtensionId(e.packageJSON.publisher, e.packageJSON.name); + return { + identifier: { id }, + location: uriIdentityService.extUri.joinPath(builtinExtensionsServiceUrl!, e.extensionPath), + type: ExtensionType.System, + isBuiltin: true, + manifest: e.packageNLS ? await this.localizeManifest(id, e.packageJSON, e.packageNLS) : e.packageJSON, + readmeUrl: e.readmePath ? uriIdentityService.extUri.joinPath(builtinExtensionsServiceUrl!, e.readmePath) : undefined, + changelogUrl: e.changelogPath ? uriIdentityService.extUri.joinPath(builtinExtensionsServiceUrl!, e.changelogPath) : undefined, + targetPlatform: TargetPlatform.WEB, + validations: [], + isValid: true + }; + }); } } } async scanBuiltinExtensions(): Promise<IExtension[]> { - return [...this.builtinExtensions]; + return [...await Promise.all(this.builtinExtensionsPromises)]; + } + + private async localizeManifest(extensionId: string, manifest: IExtensionManifest, fallbackTranslations: ITranslations): Promise<IExtensionManifest> { + if (!this.nlsUrl) { + return localizeManifest(manifest, fallbackTranslations); + } + // the `package` endpoint returns the translations in a key-value format similar to the package.nls.json file. + const uri = URI.joinPath(this.nlsUrl, extensionId, 'package'); + try { + const res = await this.extensionResourceLoaderService.readExtensionResource(uri); + const json = JSON.parse(res.toString()); + return localizeManifest(manifest, json, fallbackTranslations); + } catch (e) { + this.logService.error(e); + return localizeManifest(manifest, fallbackTranslations); + } } } diff --git a/src/vs/workbench/services/extensionManagement/browser/extensionBisect.ts b/src/vs/workbench/services/extensionManagement/browser/extensionBisect.ts index 844d3432be0..ad8c116d0b3 100644 --- a/src/vs/workbench/services/extensionManagement/browser/extensionBisect.ts +++ b/src/vs/workbench/services/extensionManagement/browser/extensionBisect.ts @@ -78,7 +78,7 @@ class ExtensionBisectService implements IExtensionBisectService { @IStorageService private readonly _storageService: IStorageService, @IWorkbenchEnvironmentService private readonly _envService: IWorkbenchEnvironmentService ) { - const raw = _storageService.get(ExtensionBisectService._storageKey, StorageScope.GLOBAL); + const raw = _storageService.get(ExtensionBisectService._storageKey, StorageScope.APPLICATION); this._state = BisectState.fromJSON(raw); if (this._state) { @@ -126,7 +126,7 @@ class ExtensionBisectService implements IExtensionBisectService { } const extensionIds = extensions.map(ext => ext.identifier.id); const newState = new BisectState(extensionIds, 0, extensionIds.length, 0); - this._storageService.store(ExtensionBisectService._storageKey, JSON.stringify(newState), StorageScope.GLOBAL, StorageTarget.MACHINE); + this._storageService.store(ExtensionBisectService._storageKey, JSON.stringify(newState), StorageScope.APPLICATION, StorageTarget.MACHINE); await this._storageService.flush(); } @@ -150,13 +150,13 @@ class ExtensionBisectService implements IExtensionBisectService { seeingBad ? this._state.low : this._state.mid, seeingBad ? this._state.mid : this._state.high, ); - this._storageService.store(ExtensionBisectService._storageKey, JSON.stringify(nextState), StorageScope.GLOBAL, StorageTarget.MACHINE); + this._storageService.store(ExtensionBisectService._storageKey, JSON.stringify(nextState), StorageScope.APPLICATION, StorageTarget.MACHINE); await this._storageService.flush(); return undefined; } async reset(): Promise<void> { - this._storageService.remove(ExtensionBisectService._storageKey, StorageScope.GLOBAL); + this._storageService.remove(ExtensionBisectService._storageKey, StorageScope.APPLICATION); await this._storageService.flush(); } } diff --git a/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts b/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts index 1040b35beff..7c9d42c8621 100644 --- a/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts +++ b/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts @@ -147,7 +147,11 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench throw new Error(localize('cannot change enablement environment', "Cannot change enablement of {0} extension because it is enabled in environment", extension.manifest.displayName || extension.identifier.id)); } - switch (this.getEnablementState(extension)) { + this.throwErrorIfEnablementStateCannotBeChanged(extension, this.getEnablementState(extension), donotCheckDependencies); + } + + private throwErrorIfEnablementStateCannotBeChanged(extension: IExtension, enablementStateOfExtension: EnablementState, donotCheckDependencies?: boolean): void { + switch (enablementStateOfExtension) { case EnablementState.DisabledByEnvironment: throw new Error(localize('cannot change disablement environment', "Cannot change enablement of {0} extension because it is disabled in environment", extension.manifest.displayName || extension.identifier.id)); case EnablementState.DisabledByVirtualWorkspace: @@ -219,30 +223,60 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench } private getExtensionsToEnableRecursively(extensions: IExtension[], allExtensions: ReadonlyArray<IExtension>, enablementState: EnablementState, options: { dependencies: boolean; pack: boolean }, checked: IExtension[] = []): IExtension[] { + if (!options.dependencies && !options.pack) { + return []; + } + const toCheck = extensions.filter(e => checked.indexOf(e) === -1); - if (toCheck.length) { - for (const extension of toCheck) { - checked.push(extension); + if (!toCheck.length) { + return []; + } + + for (const extension of toCheck) { + checked.push(extension); + } + + const extensionsToDisable: IExtension[] = []; + for (const extension of allExtensions) { + // Extension is already checked + if (checked.some(e => areSameExtensions(e.identifier, extension.identifier))) { + continue; + } + + const enablementStateOfExtension = this.getEnablementState(extension); + // Extension enablement state is same as the end enablement state + if (enablementStateOfExtension === enablementState) { + continue; } - const extensionsToDisable = allExtensions.filter(i => { - if (checked.indexOf(i) !== -1) { - return false; + + // Check if the extension is a dependency or in extension pack + if (extensions.some(e => + (options.dependencies && e.manifest.extensionDependencies?.some(id => areSameExtensions({ id }, extension.identifier))) + || (options.pack && e.manifest.extensionPack?.some(id => areSameExtensions({ id }, extension.identifier))))) { + + const index = extensionsToDisable.findIndex(e => areSameExtensions(e.identifier, extension.identifier)); + + // Extension is not aded to the disablement list so add it + if (index === -1) { + extensionsToDisable.push(extension); } - if (this.getEnablementState(i) === enablementState) { - return false; + + // Extension is there already in the disablement list. + else { + try { + // Replace only if the enablement state can be changed + this.throwErrorIfEnablementStateCannotBeChanged(extension, enablementStateOfExtension, true); + extensionsToDisable.splice(index, 1, extension); + } catch (error) { /*Do not add*/ } } - return (options.dependencies || options.pack) - && extensions.some(extension => - (options.dependencies && extension.manifest.extensionDependencies?.some(id => areSameExtensions({ id }, i.identifier))) - || (options.pack && extension.manifest.extensionPack?.some(id => areSameExtensions({ id }, i.identifier))) - ); - }); - if (extensionsToDisable.length) { - extensionsToDisable.push(...this.getExtensionsToEnableRecursively(extensionsToDisable, allExtensions, enablementState, options, checked)); } - return extensionsToDisable; } - return []; + + if (extensionsToDisable.length) { + extensionsToDisable.push(...this.getExtensionsToEnableRecursively(extensionsToDisable, allExtensions, enablementState, options, checked)); + } + + return extensionsToDisable; } private _setUserEnablementState(extension: IExtension, newState: EnablementState): Promise<boolean> { @@ -490,7 +524,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench if (!this.hasWorkspace) { return Promise.resolve(false); } - let disabledExtensions = this._getWorkspaceDisabledExtensions(); + const disabledExtensions = this._getWorkspaceDisabledExtensions(); if (disabledExtensions.every(e => !areSameExtensions(e, identifier))) { disabledExtensions.push(identifier); this._setDisabledExtensions(disabledExtensions); @@ -503,7 +537,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench if (!this.hasWorkspace) { return false; } - let disabledExtensions = this._getWorkspaceDisabledExtensions(); + const disabledExtensions = this._getWorkspaceDisabledExtensions(); for (let index = 0; index < disabledExtensions.length; index++) { const disabledExtension = disabledExtensions[index]; if (areSameExtensions(disabledExtension, identifier)) { @@ -519,7 +553,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench if (!this.hasWorkspace) { return false; } - let enabledExtensions = this._getWorkspaceEnabledExtensions(); + const enabledExtensions = this._getWorkspaceEnabledExtensions(); if (enabledExtensions.every(e => !areSameExtensions(e, identifier))) { enabledExtensions.push(identifier); this._setEnabledExtensions(enabledExtensions); @@ -532,7 +566,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench if (!this.hasWorkspace) { return false; } - let enabledExtensions = this._getWorkspaceEnabledExtensions(); + const enabledExtensions = this._getWorkspaceEnabledExtensions(); for (let index = 0; index < enabledExtensions.length; index++) { const disabledExtension = enabledExtensions[index]; if (areSameExtensions(disabledExtension, identifier)) { @@ -580,9 +614,9 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench } private _onDidChangeExtensions(added: ReadonlyArray<IExtension>, removed: ReadonlyArray<IExtension>): void { - const disabledByTrustExtensions = added.filter(e => this.getEnablementState(e) === EnablementState.DisabledByTrustRequirement); - if (disabledByTrustExtensions.length) { - this._onEnablementChanged.fire(disabledByTrustExtensions); + const disabledExtensions = added.filter(e => !this.isEnabledEnablementState(this.getEnablementState(e))); + if (disabledExtensions.length) { + this._onEnablementChanged.fire(disabledExtensions); } removed.forEach(({ identifier }) => this._reset(identifier)); } @@ -652,7 +686,8 @@ class ExtensionsManager extends Disposable { this.logService.error(error); } this._register(this.extensionManagementService.onDidInstallExtensions(e => this.onDidInstallExtensions(e.reduce<IExtension[]>((result, { local, operation }) => { if (local && operation !== InstallOperation.Migrate) { result.push(local); } return result; }, [])))); - this._register(Event.filter(this.extensionManagementService.onDidUninstallExtension, (e => !e.error))(e => this.onDidUninstallExtension(e.identifier, e.server))); + this._register(Event.filter(this.extensionManagementService.onDidUninstallExtension, (e => !e.error))(e => this.onDidUninstallExtensions([e.identifier], e.server))); + this._register(this.extensionManagementService.onDidChangeProfileExtensions(({ added, removed, server }) => { this.onDidInstallExtensions(added); this.onDidUninstallExtensions(removed.map(({ identifier }) => identifier), server); })); } private onDidInstallExtensions(extensions: IExtension[]): void { @@ -662,10 +697,15 @@ class ExtensionsManager extends Disposable { } } - private onDidUninstallExtension(identifier: IExtensionIdentifier, server: IExtensionManagementServer): void { - const index = this._extensions.findIndex(e => areSameExtensions(e.identifier, identifier) && this.extensionManagementServerService.getExtensionManagementServer(e) === server); - if (index !== -1) { - const removed = this._extensions.splice(index, 1); + private onDidUninstallExtensions(identifiers: IExtensionIdentifier[], server: IExtensionManagementServer): void { + const removed: IExtension[] = []; + for (const identifier of identifiers) { + const index = this._extensions.findIndex(e => areSameExtensions(e.identifier, identifier) && this.extensionManagementServerService.getExtensionManagementServer(e) === server); + if (index !== -1) { + removed.push(...this._extensions.splice(index, 1)); + } + } + if (removed.length) { this._onDidChangeExtensions.fire({ added: [], removed }); } } diff --git a/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts b/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts index 1124b843b01..faf1645bb6d 100644 --- a/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts +++ b/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts @@ -6,7 +6,7 @@ import { IBuiltinExtensionsScannerService, ExtensionType, IExtensionIdentifier, IExtension, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions'; import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; import { IScannedExtension, IWebExtensionsScannerService, ScanOptions } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; -import { isWeb } from 'vs/base/common/platform'; +import { isWeb, Language } from 'vs/base/common/platform'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { joinPath } from 'vs/base/common/resources'; import { URI, UriComponents } from 'vs/base/common/uri'; @@ -39,6 +39,7 @@ import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storag import { IProductService } from 'vs/platform/product/common/productService'; import { validateExtensionManifest } from 'vs/platform/extensions/common/extensionValidator'; import Severity from 'vs/base/common/severity'; +import { IStringDictionary, forEach } from 'vs/base/common/collections'; type GalleryExtensionInfo = { readonly id: string; preRelease?: boolean; migrateStorageFrom?: string }; type ExtensionInfo = { readonly id: string; preRelease: boolean }; @@ -56,7 +57,10 @@ interface IStoredWebExtension { readonly location: UriComponents; readonly readmeUri?: UriComponents; readonly changelogUri?: UriComponents; + // deprecated in favor of packageNLSUris & fallbackPackageNLSUri readonly packageNLSUri?: UriComponents; + readonly packageNLSUris?: IStringDictionary<UriComponents>; + readonly fallbackPackageNLSUri?: UriComponents; readonly metadata?: Metadata; } @@ -66,7 +70,10 @@ interface IWebExtension { location: URI; readmeUri?: URI; changelogUri?: URI; + // deprecated in favor of packageNLSUris & fallbackPackageNLSUri packageNLSUri?: URI; + packageNLSUris?: Map<string, URI>; + fallbackPackageNLSUri?: URI; metadata?: Metadata; } @@ -108,7 +115,8 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten private readCustomBuiltinExtensionsInfoFromEnv(): Promise<{ extensions: ExtensionInfo[]; extensionsToMigrate: [string, string][]; extensionLocations: URI[] }> { if (!this._customBuiltinExtensionsInfoPromise) { this._customBuiltinExtensionsInfoPromise = (async () => { - let extensions: ExtensionInfo[] = [], extensionLocations: URI[] = []; + let extensions: ExtensionInfo[] = []; + const extensionLocations: URI[] = []; const extensionsToMigrate: [string, string][] = []; const customBuiltinExtensionsInfo = this.environmentService.options && Array.isArray(this.environmentService.options.additionalBuiltinExtensions) ? this.environmentService.options.additionalBuiltinExtensions.map(additionalBuiltinExtension => isString(additionalBuiltinExtension) ? { id: additionalBuiltinExtension } : additionalBuiltinExtension) @@ -217,7 +225,7 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten } const result: IScannedExtension[] = []; try { - const useCache = this.storageService.get('additionalBuiltinExtensions', StorageScope.GLOBAL, '[]') === JSON.stringify(extensions); + const useCache = this.storageService.get('additionalBuiltinExtensions', StorageScope.APPLICATION, '[]') === JSON.stringify(extensions); const webExtensions = await (useCache ? this.getCustomBuiltinExtensionsFromCache() : this.updateCustomBuiltinExtensionsCache()); if (webExtensions.length) { await Promise.all(webExtensions.map(async webExtension => { @@ -231,7 +239,7 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten } })); } - this.storageService.store('additionalBuiltinExtensions', JSON.stringify(extensions), StorageScope.GLOBAL, StorageTarget.MACHINE); + this.storageService.store('additionalBuiltinExtensions', JSON.stringify(extensions), StorageScope.APPLICATION, StorageTarget.MACHINE); } catch (error) { this.logService.info('Ignoring following additional builtin extensions as there is an error while fetching them from gallery', extensions.map(({ id }) => id), getErrorMessage(error)); } @@ -400,7 +408,7 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten return result; } - async scanExistingExtension(extensionLocation: URI, extensionType: ExtensionType): Promise<IExtension | null> { + async scanExistingExtension(extensionLocation: URI, extensionType: ExtensionType): Promise<IScannedExtension | null> { if (extensionType === ExtensionType.System) { const systemExtensions = await this.scanSystemExtensions(); return systemExtensions.find(e => e.location.toString() === extensionLocation.toString()) || null; @@ -409,6 +417,11 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten return userExtensions.find(e => e.location.toString() === extensionLocation.toString()) || null; } + async scanMetadata(extensionLocation: URI): Promise<Metadata | undefined> { + const extension = await this.scanExistingExtension(extensionLocation, ExtensionType.User); + return extension?.metadata; + } + async scanExtensionManifest(extensionLocation: URI): Promise<IExtensionManifest | null> { const packageJSONUri = joinPath(extensionLocation, 'package.json'); try { @@ -428,7 +441,7 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten } async addExtension(location: URI, metadata?: Metadata): Promise<IExtension> { - const webExtension = await this.toWebExtension(location, undefined, undefined, undefined, undefined, metadata); + const webExtension = await this.toWebExtension(location, undefined, undefined, undefined, undefined, undefined, metadata); return this.addWebExtension(webExtension); } @@ -483,7 +496,7 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten } private async scanInstalledExtensions(scanOptions?: ScanOptions): Promise<IScannedExtension[]> { - let installedExtensions = await this.readInstalledExtensions(); + const installedExtensions = await this.readInstalledExtensions(); installedExtensions.sort((a, b) => a.identifier.id < b.identifier.id ? -1 : a.identifier.id > b.identifier.id ? 1 : semver.rcompare(a.version, b.version)); const result = new Map<string, IScannedExtension>(); for (const webExtension of installedExtensions) { @@ -506,11 +519,33 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten } extensionLocation = galleryExtension.properties.targetPlatform === TargetPlatform.WEB ? extensionLocation.with({ query: `${extensionLocation.query ? `${extensionLocation.query}&` : ''}target=${galleryExtension.properties.targetPlatform}` }) : extensionLocation; const extensionResources = await this.listExtensionResources(extensionLocation); - const packageNLSResource = extensionResources.find(e => basename(e) === 'package.nls.json'); - return this.toWebExtension(extensionLocation, galleryExtension.identifier, packageNLSResource ? URI.parse(packageNLSResource) : null, galleryExtension.assets.readme ? URI.parse(galleryExtension.assets.readme.uri) : undefined, galleryExtension.assets.changelog ? URI.parse(galleryExtension.assets.changelog.uri) : undefined, metadata); + const packageNLSResources = this.getNLSResourceMapFromResources(extensionResources); + + // The fallback, in English, will fill in any gaps missing in the localized file. + const fallbackPackageNLSResource = extensionResources.find(e => basename(e) === 'package.nls.json'); + return this.toWebExtension( + extensionLocation, + galleryExtension.identifier, + packageNLSResources, + fallbackPackageNLSResource ? URI.parse(fallbackPackageNLSResource) : null, + galleryExtension.assets.readme ? URI.parse(galleryExtension.assets.readme.uri) : undefined, + galleryExtension.assets.changelog ? URI.parse(galleryExtension.assets.changelog.uri) : undefined, + metadata); + } + + private getNLSResourceMapFromResources(extensionResources: string[]): Map<string, URI> { + const packageNLSResources = new Map<string, URI>(); + extensionResources.forEach(e => { + // Grab all package.nls.{language}.json files + const regexResult = /package\.nls\.([\w-]+)\.json/.exec(basename(e)); + if (regexResult?.[1]) { + packageNLSResources.set(regexResult[1], URI.parse(e)); + } + }); + return packageNLSResources; } - private async toWebExtension(extensionLocation: URI, identifier?: IExtensionIdentifier, packageNLSUri?: URI | null, readmeUri?: URI, changelogUri?: URI, metadata?: Metadata): Promise<IWebExtension> { + private async toWebExtension(extensionLocation: URI, identifier?: IExtensionIdentifier, packageNLSUris?: Map<string, URI>, fallbackPackageNLSUri?: URI | null, readmeUri?: URI, changelogUri?: URI, metadata?: Metadata): Promise<IWebExtension> { let packageJSONContent; try { packageJSONContent = await this.extensionResourceLoaderService.readExtensionResource(joinPath(extensionLocation, 'package.json')); @@ -527,12 +562,12 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten throw new Error(localize('not a web extension', "Cannot add '{0}' because this extension is not a web extension.", manifest.displayName || manifest.name)); } - if (packageNLSUri === undefined) { + if (fallbackPackageNLSUri === undefined) { try { - packageNLSUri = joinPath(extensionLocation, 'package.nls.json'); - await this.extensionResourceLoaderService.readExtensionResource(packageNLSUri); + fallbackPackageNLSUri = joinPath(extensionLocation, 'package.nls.json'); + await this.extensionResourceLoaderService.readExtensionResource(fallbackPackageNLSUri); } catch (error) { - packageNLSUri = undefined; + fallbackPackageNLSUri = undefined; } } @@ -542,7 +577,8 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten location: extensionLocation, readmeUri, changelogUri, - packageNLSUri: packageNLSUri ? packageNLSUri : undefined, + packageNLSUris, + fallbackPackageNLSUri: fallbackPackageNLSUri ? fallbackPackageNLSUri : undefined, metadata, }; } @@ -580,8 +616,11 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten }; } - if (webExtension.packageNLSUri) { - manifest = await this.translateManifest(manifest, webExtension.packageNLSUri); + const packageNLSUri = webExtension.packageNLSUris?.get(Language.value()); + if (packageNLSUri || webExtension.fallbackPackageNLSUri) { + manifest = packageNLSUri + ? await this.translateManifest(manifest, packageNLSUri, webExtension.fallbackPackageNLSUri) + : await this.translateManifest(manifest, webExtension.fallbackPackageNLSUri!); } const uuid = (<IGalleryMetadata | undefined>webExtension.metadata)?.id; @@ -620,20 +659,45 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten return []; } - private async translateManifest(manifest: IExtensionManifest, nlsURL: URI): Promise<IExtensionManifest> { + private async translateManifest(manifest: IExtensionManifest, nlsURL: URI, fallbackNlsURL?: URI): Promise<IExtensionManifest> { try { const content = await this.extensionResourceLoaderService.readExtensionResource(nlsURL); + const fallbackContent = fallbackNlsURL ? await this.extensionResourceLoaderService.readExtensionResource(fallbackNlsURL) : undefined; if (content) { - manifest = localizeManifest(manifest, JSON.parse(content)); + manifest = localizeManifest(manifest, JSON.parse(content), fallbackContent ? JSON.parse(fallbackContent) : undefined); } } catch (error) { /* ignore */ } return manifest; } - private readInstalledExtensions(): Promise<IWebExtension[]> { + private async readInstalledExtensions(): Promise<IWebExtension[]> { + await this.migratePackageNLSUris(); return this.withWebExtensions(this.installedExtensionsResource); } + // TODO: @TylerLeonhardt/@Sandy081: Delete after 6 months + private _migratePackageNLSUrisPromise: Promise<void> | undefined; + private migratePackageNLSUris(): Promise<void> { + if (!this._migratePackageNLSUrisPromise) { + this._migratePackageNLSUrisPromise = (async () => { + const webExtensions = await this.withWebExtensions(this.installedExtensionsResource); + if (webExtensions.some(e => !e.packageNLSUris && e.packageNLSUri)) { + const migratedExtensions = await Promise.all(webExtensions.map(async e => { + if (!e.packageNLSUris && e.packageNLSUri) { + e.fallbackPackageNLSUri = e.packageNLSUri; + const extensionResources = await this.listExtensionResources(e.location); + e.packageNLSUris = this.getNLSResourceMapFromResources(extensionResources); + e.packageNLSUri = undefined; + } + return e; + })); + await this.withWebExtensions(this.installedExtensionsResource, () => migratedExtensions); + } + })(); + } + return this._migratePackageNLSUrisPromise; + } + private writeInstalledExtensions(updateFn: (extensions: IWebExtension[]) => IWebExtension[]): Promise<IWebExtension[]> { return this.withWebExtensions(this.installedExtensionsResource, updateFn); } @@ -670,12 +734,20 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten this.logService.info('Ignoring invalid extension while scanning', storedWebExtensions); continue; } + let packageNLSUris: Map<string, URI> | undefined; + if (e.packageNLSUris) { + packageNLSUris = new Map<string, URI>(); + forEach<UriComponents>(e.packageNLSUris, (entry) => packageNLSUris!.set(entry.key, URI.revive(entry.value))); + } + webExtensions.push({ identifier: e.identifier, version: e.version, location: URI.revive(e.location), readmeUri: URI.revive(e.readmeUri), changelogUri: URI.revive(e.changelogUri), + packageNLSUris, + fallbackPackageNLSUri: URI.revive(e.fallbackPackageNLSUri), packageNLSUri: URI.revive(e.packageNLSUri), metadata: e.metadata, }); @@ -690,13 +762,22 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten // Update if (updateFn) { webExtensions = updateFn(webExtensions); + function toStringDictionary(dictionary: Map<string, URI> | undefined): IStringDictionary<UriComponents> | undefined { + if (!dictionary) { + return undefined; + } + const result: IStringDictionary<UriComponents> = Object.create(null); + dictionary.forEach((value, key) => result[key] = value.toJSON()); + return result; + } const storedWebExtensions: IStoredWebExtension[] = webExtensions.map(e => ({ identifier: e.identifier, version: e.version, location: e.location.toJSON(), readmeUri: e.readmeUri?.toJSON(), changelogUri: e.changelogUri?.toJSON(), - packageNLSUri: e.packageNLSUri?.toJSON(), + packageNLSUris: toStringDictionary(e.packageNLSUris), + fallbackPackageNLSUri: e.fallbackPackageNLSUri?.toJSON(), metadata: e.metadata })); await this.fileService.writeFile(file, VSBuffer.fromString(JSON.stringify(storedWebExtensions))); diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts index 5c374e3e103..5e3c46b1a5b 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts @@ -6,14 +6,21 @@ import { Event } from 'vs/base/common/event'; import { createDecorator, refineServiceDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IExtension, ExtensionType, IExtensionManifest } from 'vs/platform/extensions/common/extensions'; -import { IExtensionManagementService, IGalleryExtension, IExtensionIdentifier, ILocalExtension, InstallOptions, InstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionResult, Metadata, InstallVSIXOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionManagementService, IGalleryExtension, IExtensionIdentifier, ILocalExtension, InstallOptions, InstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionResult, Metadata, InstallVSIXOptions, UninstallExtensionEvent } from 'vs/platform/extensionManagement/common/extensionManagement'; import { URI } from 'vs/base/common/uri'; import { FileAccess } from 'vs/base/common/network'; +export type DidChangeProfileExtensionsEvent = { readonly added: ILocalExtension[]; readonly removed: ILocalExtension[] }; + +export interface IProfileAwareExtensionManagementService extends IExtensionManagementService { + onDidChangeProfileExtensions: Event<DidChangeProfileExtensionsEvent>; + switchExtensionsProfile(extensionsProfileResource: URI | undefined): Promise<void>; +} + export interface IExtensionManagementServer { readonly id: string; readonly label: string; - readonly extensionManagementService: IExtensionManagementService; + readonly extensionManagementService: IProfileAwareExtensionManagementService; } export const enum ExtensionInstallLocation { @@ -35,8 +42,9 @@ export interface IExtensionManagementServerService { export const DefaultIconPath = FileAccess.asBrowserUri('./media/defaultIcon.png', require).toString(true); export type InstallExtensionOnServerEvent = InstallExtensionEvent & { server: IExtensionManagementServer }; -export type UninstallExtensionOnServerEvent = IExtensionIdentifier & { server: IExtensionManagementServer }; +export type UninstallExtensionOnServerEvent = UninstallExtensionEvent & { server: IExtensionManagementServer }; export type DidUninstallExtensionOnServerEvent = DidUninstallExtensionEvent & { server: IExtensionManagementServer }; +export type DidChangeProfileExtensionsOnServerEvent = DidChangeProfileExtensionsEvent & { server: IExtensionManagementServer }; export const IWorkbenchExtensionManagementService = refineServiceDecorator<IExtensionManagementService, IWorkbenchExtensionManagementService>(IExtensionManagementService); export interface IWorkbenchExtensionManagementService extends IExtensionManagementService { @@ -46,6 +54,7 @@ export interface IWorkbenchExtensionManagementService extends IExtensionManageme onDidInstallExtensions: Event<readonly InstallExtensionResult[]>; onUninstallExtension: Event<UninstallExtensionOnServerEvent>; onDidUninstallExtension: Event<DidUninstallExtensionOnServerEvent>; + onDidChangeProfileExtensions: Event<DidChangeProfileExtensionsOnServerEvent>; installVSIX(location: URI, manifest: IExtensionManifest, installOptions?: InstallVSIXOptions): Promise<ILocalExtension>; installWebExtension(location: URI): Promise<ILocalExtension>; @@ -152,11 +161,12 @@ export interface IWebExtensionsScannerService { scanSystemExtensions(): Promise<IExtension[]>; scanUserExtensions(options?: ScanOptions): Promise<IScannedExtension[]>; scanExtensionsUnderDevelopment(): Promise<IExtension[]>; - scanExistingExtension(extensionLocation: URI, extensionType: ExtensionType): Promise<IExtension | null>; + scanExistingExtension(extensionLocation: URI, extensionType: ExtensionType): Promise<IScannedExtension | null>; addExtension(location: URI, metadata?: Metadata): Promise<IExtension>; addExtensionFromGallery(galleryExtension: IGalleryExtension, metadata?: Metadata): Promise<IExtension>; removeExtension(identifier: IExtensionIdentifier, version?: string): Promise<void>; + scanMetadata(extensionLocation: URI): Promise<Metadata | undefined>; scanExtensionManifest(extensionLocation: URI): Promise<IExtensionManifest | null>; } diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementServerService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementServerService.ts index 4a42f77188a..233e0bc56c2 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementServerService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementServerService.ts @@ -14,8 +14,7 @@ import { isWeb } from 'vs/base/common/platform'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { WebExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/webExtensionManagementService'; import { IExtension } from 'vs/platform/extensions/common/extensions'; -import { ExtensionManagementChannelClient } from 'vs/platform/extensionManagement/common/extensionManagementIpc'; -import { ILogService } from 'vs/platform/log/common/log'; +import { NativeProfileAwareExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/profileAwareExtensionManagementService'; export class ExtensionManagementServerService implements IExtensionManagementServerService { @@ -29,11 +28,10 @@ export class ExtensionManagementServerService implements IExtensionManagementSer @IRemoteAgentService remoteAgentService: IRemoteAgentService, @ILabelService labelService: ILabelService, @IInstantiationService instantiationService: IInstantiationService, - @ILogService logService: ILogService, ) { const remoteAgentConnection = remoteAgentService.getConnection(); if (remoteAgentConnection) { - const extensionManagementService = new ExtensionManagementChannelClient(remoteAgentConnection.getChannel<IChannel>('extensions')); + const extensionManagementService = instantiationService.createInstance(NativeProfileAwareExtensionManagementService, remoteAgentConnection.getChannel<IChannel>('extensions'), undefined); this.remoteExtensionManagementServer = { id: 'remote', extensionManagementService, diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index 46c26ac1795..1205ab9b412 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -5,9 +5,9 @@ import { Event, EventMultiplexer } from 'vs/base/common/event'; import { - ILocalExtension, IGalleryExtension, IExtensionIdentifier, IExtensionsControlManifest, IGalleryMetadata, IExtensionGalleryService, InstallOptions, UninstallOptions, InstallVSIXOptions, InstallExtensionResult, ExtensionManagementError, ExtensionManagementErrorCode + ILocalExtension, IGalleryExtension, IExtensionIdentifier, IExtensionsControlManifest, IGalleryMetadata, IExtensionGalleryService, InstallOptions, UninstallOptions, InstallVSIXOptions, InstallExtensionResult, ExtensionManagementError, ExtensionManagementErrorCode, Metadata } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { DidUninstallExtensionOnServerEvent, IExtensionManagementServer, IExtensionManagementServerService, InstallExtensionOnServerEvent, IWorkbenchExtensionManagementService, UninstallExtensionOnServerEvent } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { DidChangeProfileExtensionsOnServerEvent, DidUninstallExtensionOnServerEvent, IExtensionManagementServer, IExtensionManagementServerService, InstallExtensionOnServerEvent, IWorkbenchExtensionManagementService, UninstallExtensionOnServerEvent } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { ExtensionType, isLanguagePackExtension, IExtensionManifest, getWorkspaceSupportTypeMessage, TargetPlatform } from 'vs/platform/extensions/common/extensions'; import { URI } from 'vs/base/common/uri'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -40,6 +40,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench readonly onDidInstallExtensions: Event<readonly InstallExtensionResult[]>; readonly onUninstallExtension: Event<UninstallExtensionOnServerEvent>; readonly onDidUninstallExtension: Event<DidUninstallExtensionOnServerEvent>; + readonly onDidChangeProfileExtensions: Event<DidChangeProfileExtensionsOnServerEvent>; protected readonly servers: IExtensionManagementServer[] = []; @@ -72,6 +73,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench this.onDidInstallExtensions = this._register(this.servers.reduce((emitter: EventMultiplexer<readonly InstallExtensionResult[]>, server) => { emitter.add(server.extensionManagementService.onDidInstallExtensions); return emitter; }, new EventMultiplexer<readonly InstallExtensionResult[]>())).event; this.onUninstallExtension = this._register(this.servers.reduce((emitter: EventMultiplexer<UninstallExtensionOnServerEvent>, server) => { emitter.add(Event.map(server.extensionManagementService.onUninstallExtension, e => ({ ...e, server }))); return emitter; }, new EventMultiplexer<UninstallExtensionOnServerEvent>())).event; this.onDidUninstallExtension = this._register(this.servers.reduce((emitter: EventMultiplexer<DidUninstallExtensionOnServerEvent>, server) => { emitter.add(Event.map(server.extensionManagementService.onDidUninstallExtension, e => ({ ...e, server }))); return emitter; }, new EventMultiplexer<DidUninstallExtensionOnServerEvent>())).event; + this.onDidChangeProfileExtensions = this._register(this.servers.reduce((emitter: EventMultiplexer<DidChangeProfileExtensionsOnServerEvent>, server) => { emitter.add(Event.map(server.extensionManagementService.onDidChangeProfileExtensions, e => ({ ...e, server }))); return emitter; }, new EventMultiplexer<DidChangeProfileExtensionsOnServerEvent>())).event; } async getInstalled(type?: ExtensionType): Promise<ILocalExtension[]> { @@ -485,5 +487,13 @@ export class ExtensionManagementService extends Disposable implements IWorkbench return this._targetPlatformPromise; } + async getMetadata(extension: ILocalExtension): Promise<Metadata | undefined> { + const server = this.getServer(extension); + if (!server) { + return undefined; + } + return server.extensionManagementService.getMetadata(extension); + } + registerParticipant() { throw new Error('Not Supported'); } } diff --git a/src/vs/workbench/services/extensionManagement/common/profileAwareExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/profileAwareExtensionManagementService.ts new file mode 100644 index 00000000000..b8a639d4e9e --- /dev/null +++ b/src/vs/workbench/services/extensionManagement/common/profileAwareExtensionManagementService.ts @@ -0,0 +1,73 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IChannel } from 'vs/base/parts/ipc/common/ipc'; +import { IProfileAwareExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { ExtensionManagementChannelClient } from 'vs/platform/extensionManagement/common/extensionManagementIpc'; +import { URI } from 'vs/base/common/uri'; +import { IGalleryExtension, ILocalExtension, InstallOptions, InstallVSIXOptions, UninstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ExtensionIdentifier, ExtensionType } from 'vs/platform/extensions/common/extensions'; +import { Emitter, Event } from 'vs/base/common/event'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { delta } from 'vs/base/common/arrays'; +import { compare } from 'vs/base/common/strings'; +import { DisposableStore } from 'vs/base/common/lifecycle'; + +export class NativeProfileAwareExtensionManagementService extends ExtensionManagementChannelClient implements IProfileAwareExtensionManagementService { + + private readonly disposables = this._register(new DisposableStore()); + + override get onInstallExtension() { return Event.filter(super.onInstallExtension, e => this.filterEvent(e), this.disposables); } + override get onDidInstallExtensions() { + return Event.filter( + Event.map(super.onDidInstallExtensions, results => results.filter(e => this.filterEvent(e)), this.disposables), + results => results.length > 0, this.disposables); + } + override get onUninstallExtension() { return Event.filter(super.onUninstallExtension, e => this.filterEvent(e), this.disposables); } + override get onDidUninstallExtension() { return Event.filter(super.onDidUninstallExtension, e => this.filterEvent(e), this.disposables); } + + private readonly _onDidChangeProfileExtensions = this._register(new Emitter<{ readonly added: ILocalExtension[]; readonly removed: ILocalExtension[] }>()); + readonly onDidChangeProfileExtensions = this._onDidChangeProfileExtensions.event; + + constructor(channel: IChannel, public extensionsProfileResource: URI | undefined, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + ) { + super(channel); + } + + private filterEvent({ profileLocation, applicationScoped }: { profileLocation?: URI; applicationScoped?: boolean }): boolean { + return applicationScoped || this.uriIdentityService.extUri.isEqual(this.extensionsProfileResource, profileLocation); + } + + override install(vsix: URI, options?: InstallVSIXOptions): Promise<ILocalExtension> { + return super.install(vsix, { ...options, profileLocation: this.extensionsProfileResource }); + } + + override installFromGallery(extension: IGalleryExtension, installOptions?: InstallOptions): Promise<ILocalExtension> { + return super.installFromGallery(extension, { ...installOptions, profileLocation: this.extensionsProfileResource }); + } + + override uninstall(extension: ILocalExtension, options?: UninstallOptions): Promise<void> { + return super.uninstall(extension, { ...options, profileLocation: this.extensionsProfileResource }); + } + + override getInstalled(type: ExtensionType | null = null): Promise<ILocalExtension[]> { + return super.getInstalled(type, this.extensionsProfileResource); + } + + async switchExtensionsProfile(extensionsProfileResource: URI | undefined): Promise<void> { + if (this.uriIdentityService.extUri.isEqual(extensionsProfileResource, this.extensionsProfileResource)) { + return; + } + const oldExtensions = await this.getInstalled(ExtensionType.User); + this.extensionsProfileResource = extensionsProfileResource; + const newExtensions = await this.getInstalled(ExtensionType.User); + const { added, removed } = delta(oldExtensions, newExtensions, (a, b) => compare(`${ExtensionIdentifier.toKey(a.identifier.id)}@${a.manifest.version}`, `${ExtensionIdentifier.toKey(b.identifier.id)}@${b.manifest.version}`)); + if (added.length || removed.length) { + this._onDidChangeProfileExtensions.fire({ added, removed }); + } + } + +} diff --git a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts index 28a5b281506..147936d4468 100644 --- a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts @@ -4,31 +4,40 @@ *--------------------------------------------------------------------------------------------*/ import { ExtensionType, IExtension, IExtensionIdentifier, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions'; -import { IExtensionManagementService, ILocalExtension, IGalleryExtension, IGalleryMetadata, InstallOperation, IExtensionGalleryService, InstallOptions, Metadata } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionManagementService, ILocalExtension, IGalleryExtension, IGalleryMetadata, InstallOperation, IExtensionGalleryService, InstallOptions, Metadata, UninstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; import { URI } from 'vs/base/common/uri'; +import { Event } from 'vs/base/common/event'; import { areSameExtensions, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { IScannedExtension, IWebExtensionsScannerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { IProfileAwareExtensionManagementService, IScannedExtension, IWebExtensionsScannerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { ILogService } from 'vs/platform/log/common/log'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { AbstractExtensionManagementService, AbstractExtensionTask, IInstallExtensionTask, IUninstallExtensionTask, UninstallExtensionTaskOptions } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService'; +import { AbstractExtensionManagementService, AbstractExtensionTask, IInstallExtensionTask, IUninstallExtensionTask } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; import { IProductService } from 'vs/platform/product/common/productService'; import { isBoolean, isUndefined } from 'vs/base/common/types'; +import { IExtensionsProfileScannerService } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService'; +import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; -export class WebExtensionManagementService extends AbstractExtensionManagementService implements IExtensionManagementService { +export class WebExtensionManagementService extends AbstractExtensionManagementService implements IExtensionManagementService, IProfileAwareExtensionManagementService { declare readonly _serviceBrand: undefined; + readonly onDidChangeProfileExtensions = Event.None; + constructor( @IExtensionGalleryService extensionGalleryService: IExtensionGalleryService, @ITelemetryService telemetryService: ITelemetryService, @ILogService logService: ILogService, @IWebExtensionsScannerService private readonly webExtensionsScannerService: IWebExtensionsScannerService, @IExtensionManifestPropertiesService private readonly extensionManifestPropertiesService: IExtensionManifestPropertiesService, - @IProductService productService: IProductService + @IExtensionsProfileScannerService extensionsProfileScannerService: IExtensionsProfileScannerService, + @IProductService productService: IProductService, + @IUserDataProfilesService userDataProfilesService: IUserDataProfilesService, + @IUriIdentityService uriIdentityService: IUriIdentityService, ) { - super(extensionGalleryService, telemetryService, logService, productService); + super(userDataProfilesService, uriIdentityService, extensionGalleryService, extensionsProfileScannerService, telemetryService, logService, productService); } async getTargetPlatform(): Promise<TargetPlatform> { @@ -67,8 +76,12 @@ export class WebExtensionManagementService extends AbstractExtensionManagementSe return this.installExtension(manifest, location, options); } - protected override async getCompatibleVersion(extension: IGalleryExtension, fetchCompatibleVersion: boolean, includePreRelease: boolean): Promise<IGalleryExtension | null> { - const compatibleExtension = await super.getCompatibleVersion(extension, fetchCompatibleVersion, includePreRelease); + getMetadata(extension: ILocalExtension): Promise<Metadata | undefined> { + return this.webExtensionsScannerService.scanMetadata(extension.location); + } + + protected override async getCompatibleVersion(extension: IGalleryExtension, sameVersion: boolean, includePreRelease: boolean): Promise<IGalleryExtension | null> { + const compatibleExtension = await super.getCompatibleVersion(extension, sameVersion, includePreRelease); if (compatibleExtension) { return compatibleExtension; } @@ -87,11 +100,13 @@ export class WebExtensionManagementService extends AbstractExtensionManagementSe return local; } - protected createInstallExtensionTask(manifest: IExtensionManifest, extension: URI | IGalleryExtension, options: InstallOptions): IInstallExtensionTask { + async switchExtensionsProfile(extensionsProfileResource: URI | undefined): Promise<void> { } + + protected createDefaultInstallExtensionTask(manifest: IExtensionManifest, extension: URI | IGalleryExtension, options: InstallOptions): IInstallExtensionTask { return new InstallExtensionTask(manifest, extension, options, this.webExtensionsScannerService); } - protected createUninstallExtensionTask(extension: ILocalExtension, options: UninstallExtensionTaskOptions): IUninstallExtensionTask { + protected createDefaultUninstallExtensionTask(extension: ILocalExtension, options: UninstallOptions): IUninstallExtensionTask { return new UninstallExtensionTask(extension, options, this.webExtensionsScannerService); } @@ -105,8 +120,9 @@ function toLocalExtension(extension: IExtension): ILocalExtension { const metadata = getMetadata(undefined, extension); return { ...extension, - identifier: { id: extension.identifier.id, uuid: metadata.id }, + identifier: { id: extension.identifier.id, uuid: metadata.id ?? extension.identifier.uuid }, isMachineScoped: !!metadata.isMachineScoped, + isApplicationScoped: !!metadata.isApplicationScoped, publisherId: metadata.publisherId || null, publisherDisplayName: metadata.publisherDisplayName || null, installedTimestamp: metadata.installedTimestamp, @@ -123,7 +139,7 @@ function getMetadata(options?: InstallOptions, existingExtension?: IExtension): return metadata; } -class InstallExtensionTask extends AbstractExtensionTask<ILocalExtension> implements IInstallExtensionTask { +class InstallExtensionTask extends AbstractExtensionTask<{ local: ILocalExtension; metadata: Metadata }> implements IInstallExtensionTask { readonly identifier: IExtensionIdentifier; readonly source: URI | IGalleryExtension; @@ -142,7 +158,7 @@ class InstallExtensionTask extends AbstractExtensionTask<ILocalExtension> implem this.source = extension; } - protected async doRun(token: CancellationToken): Promise<ILocalExtension> { + protected async doRun(token: CancellationToken): Promise<{ local: ILocalExtension; metadata: Metadata }> { const userExtensions = await this.webExtensionsScannerService.scanUserExtensions(); const existingExtension = userExtensions.find(e => areSameExtensions(e.identifier, this.identifier)); if (existingExtension) { @@ -167,7 +183,7 @@ class InstallExtensionTask extends AbstractExtensionTask<ILocalExtension> implem const scannedExtension = URI.isUri(this.extension) ? await this.webExtensionsScannerService.addExtension(this.extension, metadata) : await this.webExtensionsScannerService.addExtensionFromGallery(this.extension, metadata); - return toLocalExtension(scannedExtension); + return { local: toLocalExtension(scannedExtension), metadata }; } } @@ -175,7 +191,7 @@ class UninstallExtensionTask extends AbstractExtensionTask<void> implements IUni constructor( readonly extension: ILocalExtension, - options: UninstallExtensionTaskOptions, + options: UninstallOptions, private readonly webExtensionsScannerService: IWebExtensionsScannerService, ) { super(); diff --git a/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementServerService.ts b/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementServerService.ts index 007e0a3fc73..762987822ee 100644 --- a/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementServerService.ts +++ b/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementServerService.ts @@ -14,14 +14,16 @@ import { NativeRemoteExtensionManagementService } from 'vs/workbench/services/ex import { ILabelService } from 'vs/platform/label/common/label'; import { IExtension } from 'vs/platform/extensions/common/extensions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ExtensionManagementChannelClient } from 'vs/platform/extensionManagement/common/extensionManagementIpc'; +import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { NativeProfileAwareExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/profileAwareExtensionManagementService'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; -export class ExtensionManagementServerService implements IExtensionManagementServerService { +export class ExtensionManagementServerService extends Disposable implements IExtensionManagementServerService { declare readonly _serviceBrand: undefined; - private readonly _localExtensionManagementServer: IExtensionManagementServer; - public get localExtensionManagementServer(): IExtensionManagementServer { return this._localExtensionManagementServer; } + readonly localExtensionManagementServer: IExtensionManagementServer; readonly remoteExtensionManagementServer: IExtensionManagementServer | null = null; readonly webExtensionManagementServer: IExtensionManagementServer | null = null; @@ -29,11 +31,19 @@ export class ExtensionManagementServerService implements IExtensionManagementSer @ISharedProcessService sharedProcessService: ISharedProcessService, @IRemoteAgentService remoteAgentService: IRemoteAgentService, @ILabelService labelService: ILabelService, + @IUserDataProfilesService userDataProfilesService: IUserDataProfilesService, + @IUserDataProfileService userDataProfileService: IUserDataProfileService, @IInstantiationService instantiationService: IInstantiationService, ) { - const localExtensionManagementService = new ExtensionManagementChannelClient(sharedProcessService.getChannel('extensions')); - - this._localExtensionManagementServer = { extensionManagementService: localExtensionManagementService, id: 'local', label: localize('local', "Local") }; + super(); + const localExtensionManagementService = this._register(instantiationService.createInstance(NativeProfileAwareExtensionManagementService, sharedProcessService.getChannel('extensions'), userDataProfileService.currentProfile.extensionsResource)); + this.localExtensionManagementServer = { extensionManagementService: localExtensionManagementService, id: 'local', label: localize('local', "Local") }; + this._register(userDataProfilesService.onDidChangeProfiles(e => { + if (userDataProfileService.currentProfile.isDefault) { + localExtensionManagementService.extensionsProfileResource = userDataProfilesService.defaultProfile.extensionsResource; + } + })); + this._register(userDataProfileService.onDidChangeCurrentProfile(e => e.join(localExtensionManagementService.switchExtensionsProfile(e.profile.extensionsResource)))); const remoteAgentConnection = remoteAgentService.getConnection(); if (remoteAgentConnection) { const extensionManagementService = instantiationService.createInstance(NativeRemoteExtensionManagementService, remoteAgentConnection.getChannel<IChannel>('extensions'), this.localExtensionManagementServer); @@ -43,6 +53,7 @@ export class ExtensionManagementServerService implements IExtensionManagementSer get label() { return labelService.getHostLabel(Schemas.vscodeRemote, remoteAgentConnection!.remoteAuthority) || localize('remote', "Remote"); }, }; } + } getExtensionManagementServer(extension: IExtension): IExtensionManagementServer { diff --git a/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts index 9888caf5571..53c178a68f6 100644 --- a/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts @@ -21,9 +21,10 @@ import { IExtensionManagementServer } from 'vs/workbench/services/extensionManag import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; import { Promises } from 'vs/base/common/async'; import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; -import { ExtensionManagementChannelClient } from 'vs/platform/extensionManagement/common/extensionManagementIpc'; +import { NativeProfileAwareExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/profileAwareExtensionManagementService'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; -export class NativeRemoteExtensionManagementService extends ExtensionManagementChannelClient implements IExtensionManagementService { +export class NativeRemoteExtensionManagementService extends NativeProfileAwareExtensionManagementService implements IExtensionManagementService { constructor( channel: IChannel, @@ -34,8 +35,9 @@ export class NativeRemoteExtensionManagementService extends ExtensionManagementC @IProductService private readonly productService: IProductService, @INativeWorkbenchEnvironmentService private readonly environmentService: INativeWorkbenchEnvironmentService, @IExtensionManifestPropertiesService private readonly extensionManifestPropertiesService: IExtensionManifestPropertiesService, + @IUriIdentityService uriIdentityService: IUriIdentityService, ) { - super(channel); + super(channel, undefined, uriIdentityService); } override async install(vsix: URI, options?: InstallVSIXOptions): Promise<ILocalExtension> { @@ -101,9 +103,23 @@ export class NativeRemoteExtensionManagementService extends ExtensionManagementC } private async checkAndGetCompatible(extension: IGalleryExtension, includePreRelease: boolean): Promise<IGalleryExtension> { - const compatible = await this.galleryService.getCompatibleExtension(extension, includePreRelease, await this.getTargetPlatform()); - if (compatible) { - if (includePreRelease && !compatible.properties.isPreReleaseVersion && extension.hasPreReleaseVersion) { + const targetPlatform = await this.getTargetPlatform(); + let compatibleExtension: IGalleryExtension | null = null; + + if (extension.hasPreReleaseVersion && extension.properties.isPreReleaseVersion !== includePreRelease) { + compatibleExtension = (await this.galleryService.getExtensions([{ ...extension.identifier, preRelease: includePreRelease }], { targetPlatform, compatible: true }, CancellationToken.None))[0] || null; + } + + if (!compatibleExtension && await this.galleryService.isExtensionCompatible(extension, includePreRelease, targetPlatform)) { + compatibleExtension = extension; + } + + if (!compatibleExtension) { + compatibleExtension = await this.galleryService.getCompatibleExtension(extension, includePreRelease, targetPlatform); + } + + if (compatibleExtension) { + if (includePreRelease && !compatibleExtension.properties.isPreReleaseVersion && extension.hasPreReleaseVersion) { throw new ExtensionManagementError(localize('notFoundCompatiblePrereleaseDependency', "Can't install pre-release version of '{0}' extension because it is not compatible with the current version of {1} (version {2}).", extension.identifier.id, this.productService.nameLong, this.productService.version), ExtensionManagementErrorCode.IncompatiblePreRelease); } } else { @@ -114,7 +130,7 @@ export class NativeRemoteExtensionManagementService extends ExtensionManagementC throw new ExtensionManagementError(localize('notFoundCompatibleDependency', "Can't install '{0}' extension because it is not compatible with the current version of {1} (version {2}).", extension.identifier.id, this.productService.nameLong, this.productService.version), ExtensionManagementErrorCode.Incompatible); } - return compatible; + return compatibleExtension; } private async installUIDependenciesAndPackedExtensions(local: ILocalExtension): Promise<void> { diff --git a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts index 660c2fcd2e7..1bfaa725a8a 100644 --- a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts +++ b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts @@ -4,15 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; import * as sinon from 'sinon'; -import { IExtensionManagementService, DidUninstallExtensionEvent, ILocalExtension, InstallExtensionEvent, InstallExtensionResult } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService, ExtensionInstallLocation } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { IExtensionManagementService, DidUninstallExtensionEvent, ILocalExtension, InstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService, ExtensionInstallLocation, IProfileAwareExtensionManagementService, DidChangeProfileExtensionsEvent } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { ExtensionEnablementService } from 'vs/workbench/services/extensionManagement/browser/extensionEnablementService'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { Emitter } from 'vs/base/common/event'; import { IWorkspace, IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IStorageService, InMemoryStorageService } from 'vs/platform/storage/common/storage'; -import { IExtensionContributions, ExtensionType, IExtension, IExtensionManifest, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { IExtensionContributions, ExtensionType, IExtension, IExtensionManifest } from 'vs/platform/extensions/common/extensions'; import { isUndefinedOrNull } from 'vs/base/common/types'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -59,11 +59,12 @@ export class TestExtensionEnablementService extends ExtensionEnablementService { instantiationService.stub(IExtensionManagementServerService, anExtensionManagementServerService({ id: 'local', label: 'local', - extensionManagementService: <IExtensionManagementService>{ + extensionManagementService: <IProfileAwareExtensionManagementService>{ onInstallExtension: new Emitter<InstallExtensionEvent>().event, onDidInstallExtensions: new Emitter<readonly InstallExtensionResult[]>().event, - onUninstallExtension: new Emitter<IExtensionIdentifier>().event, + onUninstallExtension: new Emitter<UninstallExtensionEvent>().event, onDidUninstallExtension: new Emitter<DidUninstallExtensionEvent>().event, + onDidChangeProfileExtensions: new Emitter<DidChangeProfileExtensionsEvent>().event, }, }, null, null)); const extensionManagementService = instantiationService.createInstance(ExtensionManagementService); @@ -116,6 +117,7 @@ suite('ExtensionEnablementService Test', () => { const didInstallEvent = new Emitter<readonly InstallExtensionResult[]>(); const didUninstallEvent = new Emitter<DidUninstallExtensionEvent>(); + const didChangeProfileExtensionsEvent = new Emitter<DidChangeProfileExtensionsEvent>(); const installed: ILocalExtension[] = []; setup(() => { @@ -125,9 +127,10 @@ suite('ExtensionEnablementService Test', () => { instantiationService.stub(IExtensionManagementServerService, anExtensionManagementServerService({ id: 'local', label: 'local', - extensionManagementService: <IExtensionManagementService>{ + extensionManagementService: <IProfileAwareExtensionManagementService>{ onDidInstallExtensions: didInstallEvent.event, onDidUninstallExtension: didUninstallEvent.event, + onDidChangeProfileExtensions: didChangeProfileExtensionsEvent.event, getInstalled: () => Promise.resolve(installed) }, }, null, null)); @@ -621,6 +624,38 @@ suite('ExtensionEnablementService Test', () => { assert.deepStrictEqual(testObject.getEnablementState(extension), EnablementState.DisabledByVirtualWorkspace); }); + test('test enable a remote workspace extension and local ui extension that is a dependency of remote', async () => { + instantiationService.stub(IExtensionManagementServerService, anExtensionManagementServerService(anExtensionManagementServer('vscode-local', instantiationService), anExtensionManagementServer('vscode-remote', instantiationService), null)); + const localUIExtension = aLocalExtension2('pub.a', { main: 'main.js', extensionKind: ['ui'] }, { location: URI.file(`pub.a`) }); + const remoteUIExtension = aLocalExtension2('pub.a', { main: 'main.js', extensionKind: ['ui'] }, { location: URI.file(`pub.a`).with({ scheme: 'vscode-remote' }) }); + const target = aLocalExtension2('pub.b', { main: 'main.js', extensionDependencies: ['pub.a'] }, { location: URI.file(`pub.b`).with({ scheme: 'vscode-remote' }) }); + testObject = new TestExtensionEnablementService(instantiationService); + + installed.push(localUIExtension, remoteUIExtension, target); + await testObject.setEnablement([target, localUIExtension], EnablementState.DisabledGlobally); + await testObject.setEnablement([target, localUIExtension], EnablementState.EnabledGlobally); + assert.ok(testObject.isEnabled(target)); + assert.ok(testObject.isEnabled(localUIExtension)); + assert.strictEqual(testObject.getEnablementState(target), EnablementState.EnabledGlobally); + assert.strictEqual(testObject.getEnablementState(localUIExtension), EnablementState.EnabledGlobally); + }); + + test('test enable a remote workspace extension also enables its dependency in local', async () => { + instantiationService.stub(IExtensionManagementServerService, anExtensionManagementServerService(anExtensionManagementServer('vscode-local', instantiationService), anExtensionManagementServer('vscode-remote', instantiationService), null)); + const localUIExtension = aLocalExtension2('pub.a', { main: 'main.js', extensionKind: ['ui'] }, { location: URI.file(`pub.a`) }); + const remoteUIExtension = aLocalExtension2('pub.a', { main: 'main.js', extensionKind: ['ui'] }, { location: URI.file(`pub.a`).with({ scheme: 'vscode-remote' }) }); + const target = aLocalExtension2('pub.b', { main: 'main.js', extensionDependencies: ['pub.a'] }, { location: URI.file(`pub.b`).with({ scheme: 'vscode-remote' }) }); + testObject = new TestExtensionEnablementService(instantiationService); + + installed.push(localUIExtension, remoteUIExtension, target); + await testObject.setEnablement([target, localUIExtension], EnablementState.DisabledGlobally); + await testObject.setEnablement([target], EnablementState.EnabledGlobally); + assert.ok(testObject.isEnabled(target)); + assert.ok(testObject.isEnabled(localUIExtension)); + assert.strictEqual(testObject.getEnablementState(target), EnablementState.EnabledGlobally); + assert.strictEqual(testObject.getEnablementState(localUIExtension), EnablementState.EnabledGlobally); + }); + test('test canChangeEnablement return false when extension is disabled in virtual workspace', () => { const extension = aLocalExtension2('pub.a', { capabilities: { virtualWorkspaces: false } }); instantiationService.stub(IWorkspaceContextService, 'getWorkspace', <IWorkspace>{ folders: [{ uri: URI.file('worskapceA').with(({ scheme: 'virtual' })) }] }); @@ -949,13 +984,29 @@ suite('ExtensionEnablementService Test', () => { assert.deepStrictEqual((<IExtension>target.args[0][0][1]).identifier, { id: 'pub.c' }); }); + test('test adding an extension that was disabled', async () => { + const extension = aLocalExtension('pub.a'); + installed.push(extension); + testObject = new TestExtensionEnablementService(instantiationService); + await testObject.setEnablement([extension], EnablementState.DisabledGlobally); + + const target = sinon.spy(); + testObject.onEnablementChanged(target); + didChangeProfileExtensionsEvent.fire({ added: [extension], removed: [] }); + + assert.ok(!testObject.isEnabled(extension)); + assert.strictEqual(testObject.getEnablementState(extension), EnablementState.DisabledGlobally); + assert.strictEqual(target.args[0][0].length, 1); + assert.deepStrictEqual((<IExtension>target.args[0][0][0]).identifier, { id: 'pub.a' }); + }); + }); function anExtensionManagementServer(authority: string, instantiationService: TestInstantiationService): IExtensionManagementServer { return { id: authority, label: authority, - extensionManagementService: instantiationService.get(IExtensionManagementService), + extensionManagementService: instantiationService.get(IExtensionManagementService) as IProfileAwareExtensionManagementService, }; } diff --git a/src/vs/workbench/services/extensionRecommendations/common/extensionIgnoredRecommendationsService.ts b/src/vs/workbench/services/extensionRecommendations/common/extensionIgnoredRecommendationsService.ts index 6ef76eca4b5..7f5c70fbff1 100644 --- a/src/vs/workbench/services/extensionRecommendations/common/extensionIgnoredRecommendationsService.ts +++ b/src/vs/workbench/services/extensionRecommendations/common/extensionIgnoredRecommendationsService.ts @@ -70,7 +70,7 @@ export class ExtensionIgnoredRecommendationsService extends Disposable implement } private onDidStorageChange(e: IStorageValueChangeEvent): void { - if (e.key === ignoredRecommendationsStorageKey && e.scope === StorageScope.GLOBAL + if (e.key === ignoredRecommendationsStorageKey && e.scope === StorageScope.PROFILE && this.ignoredRecommendationsValue !== this.getStoredIgnoredRecommendationsValue() /* This checks if current window changed the value or not */) { this._ignoredRecommendationsValue = undefined; this._globalIgnoredRecommendations = this.getCachedIgnoredRecommendations(); @@ -99,11 +99,11 @@ export class ExtensionIgnoredRecommendationsService extends Disposable implement } private getStoredIgnoredRecommendationsValue(): string { - return this.storageService.get(ignoredRecommendationsStorageKey, StorageScope.GLOBAL, '[]'); + return this.storageService.get(ignoredRecommendationsStorageKey, StorageScope.PROFILE, '[]'); } private setStoredIgnoredRecommendationsValue(value: string): void { - this.storageService.store(ignoredRecommendationsStorageKey, value, StorageScope.GLOBAL, StorageTarget.USER); + this.storageService.store(ignoredRecommendationsStorageKey, value, StorageScope.PROFILE, StorageTarget.USER); } } diff --git a/src/vs/workbench/services/extensions/browser/extensionService.ts b/src/vs/workbench/services/extensions/browser/extensionService.ts index 1384f29e5ac..acdf6ca2308 100644 --- a/src/vs/workbench/services/extensions/browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/browser/extensionService.ts @@ -5,7 +5,7 @@ import * as nls from 'vs/nls'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { IWorkbenchExtensionEnablementService, IWebExtensionsScannerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { IWorkbenchExtensionEnablementService, IWebExtensionsScannerService, IWorkbenchExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -25,7 +25,6 @@ import { Schemas } from 'vs/base/common/network'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; import { IUserDataInitializationService } from 'vs/workbench/services/userData/browser/userDataInit'; @@ -45,15 +44,15 @@ export class ExtensionService extends AbstractExtensionService implements IExten @IWorkbenchExtensionEnablementService extensionEnablementService: IWorkbenchExtensionEnablementService, @IFileService fileService: IFileService, @IProductService productService: IProductService, - @IExtensionManagementService extensionManagementService: IExtensionManagementService, + @IWorkbenchExtensionManagementService extensionManagementService: IWorkbenchExtensionManagementService, @IWorkspaceContextService contextService: IWorkspaceContextService, @IConfigurationService configurationService: IConfigurationService, @IExtensionManifestPropertiesService extensionManifestPropertiesService: IExtensionManifestPropertiesService, @IWebExtensionsScannerService webExtensionsScannerService: IWebExtensionsScannerService, @ILogService logService: ILogService, @IRemoteAgentService remoteAgentService: IRemoteAgentService, + @ILifecycleService lifecycleService: ILifecycleService, @IRemoteAuthorityResolverService private readonly _remoteAuthorityResolverService: IRemoteAuthorityResolverService, - @ILifecycleService private readonly _lifecycleService: ILifecycleService, @IUserDataInitializationService private readonly _userDataInitializationService: IUserDataInitializationService, ) { super( @@ -70,11 +69,12 @@ export class ExtensionService extends AbstractExtensionService implements IExten extensionManifestPropertiesService, webExtensionsScannerService, logService, - remoteAgentService + remoteAgentService, + lifecycleService ); // Initialize installed extensions first and do it only after workbench is ready - this._lifecycleService.when(LifecyclePhase.Ready).then(async () => { + lifecycleService.when(LifecyclePhase.Ready).then(async () => { await this._userDataInitializationService.initializeInstalledExtensions(this._instantiationService); this._initialize(); }); diff --git a/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts b/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts index 5c045163b1b..41645b62f84 100644 --- a/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts +++ b/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts @@ -45,7 +45,7 @@ function isExtensionId(value: string): boolean { class UserTrustedExtensionIdStorage { get extensions(): string[] { - const userTrustedExtensionIdsJson = this.storageService.get(USER_TRUSTED_EXTENSIONS_STORAGE_KEY, StorageScope.GLOBAL, '[]'); + const userTrustedExtensionIdsJson = this.storageService.get(USER_TRUSTED_EXTENSIONS_STORAGE_KEY, StorageScope.PROFILE, '[]'); try { return JSON.parse(userTrustedExtensionIdsJson); @@ -65,7 +65,7 @@ class UserTrustedExtensionIdStorage { } set(ids: string[]): void { - this.storageService.store(USER_TRUSTED_EXTENSIONS_STORAGE_KEY, JSON.stringify(ids), StorageScope.GLOBAL, StorageTarget.MACHINE); + this.storageService.store(USER_TRUSTED_EXTENSIONS_STORAGE_KEY, JSON.stringify(ids), StorageScope.PROFILE, StorageTarget.MACHINE); } } @@ -82,6 +82,7 @@ export interface ExtensionUrlHandlerEvent { } export interface ExtensionUrlHandlerClassification extends GDPRClassification<ExtensionUrlHandlerEvent> { + owner: 'joaomoreno'; readonly extensionId: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight' }; } diff --git a/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts b/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts index 31b1091efce..90f4cd0ec56 100644 --- a/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts +++ b/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts @@ -30,6 +30,7 @@ import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { FileAccess } from 'vs/base/common/network'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { parentOriginHash } from 'vs/workbench/browser/webview'; +import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; export interface IWebWorkerExtensionHostInitData { readonly autoStart: boolean; @@ -66,6 +67,7 @@ export class WebWorkerExtensionHost extends Disposable implements IExtensionHost @ILabelService private readonly _labelService: ILabelService, @ILogService private readonly _logService: ILogService, @IBrowserWorkbenchEnvironmentService private readonly _environmentService: IBrowserWorkbenchEnvironmentService, + @IUserDataProfilesService private readonly _userDataProfilesService: IUserDataProfilesService, @IProductService private readonly _productService: IProductService, @ILayoutService private readonly _layoutService: ILayoutService, @IStorageService private readonly _storageService: IStorageService, @@ -130,6 +132,7 @@ export class WebWorkerExtensionHost extends Disposable implements IExtensionHost const iframe = document.createElement('iframe'); iframe.setAttribute('class', 'web-worker-ext-host-iframe'); iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin'); + iframe.setAttribute('allow', 'usb; serial; hid; cross-origin-isolated;'); iframe.setAttribute('aria-hidden', 'true'); iframe.style.display = 'none'; @@ -251,9 +254,7 @@ export class WebWorkerExtensionHost extends Disposable implements IExtensionHost return; } this._isTerminating = true; - if (this._protocol) { - this._protocol.send(createMessageOfType(MessageType.Terminate)); - } + this._protocol?.send(createMessageOfType(MessageType.Terminate)); super.dispose(); } @@ -281,7 +282,7 @@ export class WebWorkerExtensionHost extends Disposable implements IExtensionHost appLanguage: platform.language, extensionDevelopmentLocationURI: this._environmentService.extensionDevelopmentLocationURI, extensionTestsLocationURI: this._environmentService.extensionTestsLocationURI, - globalStorageHome: this._environmentService.globalStorageHome, + globalStorageHome: this._userDataProfilesService.defaultProfile.globalStorageHome, workspaceStorageHome: this._environmentService.workspaceStorageHome, }, workspace: this._contextService.getWorkbenchState() === WorkbenchState.EMPTY ? undefined : { @@ -290,6 +291,10 @@ export class WebWorkerExtensionHost extends Disposable implements IExtensionHost name: this._labelService.getWorkspaceLabel(workspace), transient: workspace.transient }, + consoleForward: { + includeStack: false, + logNative: this._environmentService.debugRenderer + }, allExtensions: deltaExtensions.toAdd, myExtensions: deltaExtensions.myToAdd, telemetryInfo, diff --git a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts index e8aee60a675..5314122757d 100644 --- a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts +++ b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts @@ -11,7 +11,7 @@ import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle' import * as perf from 'vs/base/common/performance'; import { isEqualOrParent } from 'vs/base/common/resources'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { IWebExtensionsScannerService, IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { IWebExtensionsScannerService, IWorkbenchExtensionEnablementService, IWorkbenchExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -25,7 +25,7 @@ import { ExtensionKind } from 'vs/platform/environment/common/environment'; import { IFileService } from 'vs/platform/files/common/files'; import { parseExtensionDevOptions } from 'vs/workbench/services/extensions/common/extensionDevOptions'; import { IProductService } from 'vs/platform/product/common/productService'; -import { IExtensionManagementService, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IExtensionActivationHost as IWorkspaceContainsActivationHost, checkGlobFileExists, checkActivateWorkspaceContainsExtension } from 'vs/workbench/services/extensions/common/workspaceContains'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -37,6 +37,7 @@ import { ApiProposalName, allApiProposals } from 'vs/workbench/services/extensio import { forEach } from 'vs/base/common/collections'; import { ILogService } from 'vs/platform/log/common/log'; import { IExtensionHostExitInfo, IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; const hasOwnProperty = Object.hasOwnProperty; const NO_OP_VOID_PROMISE = Promise.resolve<void>(undefined); @@ -108,7 +109,7 @@ class Lock { this._isLocked = true; let customerHoldsLock = true; - let logLongRunningCustomerTimeout = setTimeout(() => { + const logLongRunningCustomerTimeout = setTimeout(() => { if (customerHoldsLock) { console.warn(`The customer named ${customer.name} has been holding on to the lock for 30s. This might be a problem.`); } @@ -181,13 +182,14 @@ export abstract class AbstractExtensionService extends Disposable implements IEx @IWorkbenchExtensionEnablementService protected readonly _extensionEnablementService: IWorkbenchExtensionEnablementService, @IFileService protected readonly _fileService: IFileService, @IProductService protected readonly _productService: IProductService, - @IExtensionManagementService protected readonly _extensionManagementService: IExtensionManagementService, + @IWorkbenchExtensionManagementService protected readonly _extensionManagementService: IWorkbenchExtensionManagementService, @IWorkspaceContextService private readonly _contextService: IWorkspaceContextService, @IConfigurationService protected readonly _configurationService: IConfigurationService, @IExtensionManifestPropertiesService protected readonly _extensionManifestPropertiesService: IExtensionManifestPropertiesService, @IWebExtensionsScannerService protected readonly _webExtensionsScannerService: IWebExtensionsScannerService, @ILogService protected readonly _logService: ILogService, @IRemoteAgentService protected readonly _remoteAgentService: IRemoteAgentService, + @ILifecycleService private readonly _lifecycleService: ILifecycleService, ) { super(); @@ -221,8 +223,8 @@ export abstract class AbstractExtensionService extends Disposable implements IEx this._runningLocation = new Map<string, ExtensionRunningLocation>(); this._register(this._extensionEnablementService.onEnablementChanged((extensions) => { - let toAdd: IExtension[] = []; - let toRemove: IExtension[] = []; + const toAdd: IExtension[] = []; + const toRemove: IExtension[] = []; for (const extension of extensions) { if (this._safeInvokeIsEnabled(extension)) { // an extension has been enabled @@ -235,6 +237,8 @@ export abstract class AbstractExtensionService extends Disposable implements IEx this._handleDeltaExtensions(new DeltaExtensionsQueueItem(toAdd, toRemove)); })); + this._register(this._extensionManagementService.onDidChangeProfileExtensions(({ added, removed }) => this._handleDeltaExtensions(new DeltaExtensionsQueueItem(added, removed)))); + this._register(this._extensionManagementService.onDidInstallExtensions((result) => { const extensions: IExtension[] = []; for (const { local, operation } of result) { @@ -253,6 +257,10 @@ export abstract class AbstractExtensionService extends Disposable implements IEx this._handleDeltaExtensions(new DeltaExtensionsQueueItem([], [event.identifier.id])); } })); + + this._register(this._lifecycleService.onDidShutdown(() => { + this.stopExtensionHosts(); + })); } private _getExtensionKind(extensionDescription: IExtensionDescription): ExtensionKind[] { @@ -538,23 +546,6 @@ export abstract class AbstractExtensionService extends Disposable implements IEx } private async _deltaExtensions(_toAdd: IExtension[], _toRemove: string[] | IExtension[]): Promise<void> { - let toAdd: IExtensionDescription[] = []; - for (let i = 0, len = _toAdd.length; i < len; i++) { - const extension = _toAdd[i]; - - const extensionDescription = await this._scanSingleExtension(extension); - if (!extensionDescription) { - // could not scan extension... - continue; - } - - if (!this.canAddExtension(extensionDescription)) { - continue; - } - - toAdd.push(extensionDescription); - } - let toRemove: IExtensionDescription[] = []; for (let i = 0, len = _toRemove.length; i < len; i++) { const extensionOrId = _toRemove[i]; @@ -579,6 +570,23 @@ export abstract class AbstractExtensionService extends Disposable implements IEx toRemove.push(extensionDescription); } + const toAdd: IExtensionDescription[] = []; + for (let i = 0, len = _toAdd.length; i < len; i++) { + const extension = _toAdd[i]; + + const extensionDescription = await this._scanSingleExtension(extension); + if (!extensionDescription) { + // could not scan extension... + continue; + } + + if (!this._canAddExtension(extensionDescription, toRemove)) { + continue; + } + + toAdd.push(extensionDescription); + } + if (toAdd.length === 0 && toRemove.length === 0) { return; } @@ -635,15 +643,19 @@ export abstract class AbstractExtensionService extends Disposable implements IEx } public canAddExtension(extension: IExtensionDescription): boolean { - const existing = this._registry.getExtensionDescription(extension.identifier); - if (existing) { - // this extension is already running (most likely at a different version) - return false; - } + return this._canAddExtension(extension, []); + } - // Check if extension is renamed - if (extension.uuid && this._registry.getAllExtensionDescriptions().some(e => e.uuid === extension.uuid)) { - return false; + private _canAddExtension(extension: IExtensionDescription, extensionsBeingRemoved: IExtensionDescription[]): boolean { + // (Also check for renamed extensions) + const existing = this._registry.getExtensionDescriptionByIdOrUUID(extension.identifier, extension.id); + if (existing) { + // This extension is already known (most likely at a different version) + // so it cannot be added again unless it is removed first + const isBeingRemoved = extensionsBeingRemoved.some((extensionDescription) => ExtensionIdentifier.equals(extension.identifier, extensionDescription.identifier)); + if (!isBeingRemoved) { + return false; + } } const extensionKind = this._getExtensionKind(extension); @@ -659,7 +671,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx public canRemoveExtension(extension: IExtensionDescription): boolean { const extensionDescription = this._registry.getExtensionDescription(extension.identifier); if (!extensionDescription) { - // ignore removing an extension which is not running + // Can't remove an extension that is unknown! return false; } @@ -773,7 +785,6 @@ export abstract class AbstractExtensionService extends Disposable implements IEx exitCode = 1 /* ERROR */; } - await extensionHostManager.extensionTestsSendExit(exitCode); this._onExtensionHostExit(exitCode); } @@ -813,13 +824,16 @@ export abstract class AbstractExtensionService extends Disposable implements IEx //#region Stopping / Starting / Restarting public stopExtensionHosts(): void { - let previouslyActivatedExtensionIds: ExtensionIdentifier[] = []; + const previouslyActivatedExtensionIds: ExtensionIdentifier[] = []; this._extensionHostActiveExtensions.forEach((value) => { previouslyActivatedExtensionIds.push(value); }); - for (const manager of this._extensionHostManagers) { - manager.dispose(); + // See https://github.com/microsoft/vscode/issues/152204 + // Dispose extension hosts in reverse creation order because the local extension host + // might be critical in sustaining a connection to the remote extension host + for (let i = this._extensionHostManagers.length - 1; i >= 0; i--) { + this._extensionHostManagers[i].dispose(); } this._extensionHostManagers = []; this._extensionHostActiveExtensions = new Map<string, ExtensionIdentifier>(); @@ -857,7 +871,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx } const extensionHostId = String(++this._lastExtensionHostId); - const processManager: IExtensionHostManager = createExtensionHostManager(this._instantiationService, extensionHostId, extensionHost, isInitialStart, initialActivationEvents, this._acquireInternalAPI()); + const processManager: IExtensionHostManager = this._doCreateExtensionHostManager(extensionHostId, extensionHost, isInitialStart, initialActivationEvents); processManager.onDidExit(([code, signal]) => this._onExtensionHostCrashOrExit(processManager, code, signal)); processManager.onDidChangeResponsiveState((responsiveState) => { this._onDidChangeResponsiveChange.fire({ @@ -869,6 +883,10 @@ export abstract class AbstractExtensionService extends Disposable implements IEx return processManager; } + protected _doCreateExtensionHostManager(extensionHostId: string, extensionHost: IExtensionHost, isInitialStart: boolean, initialActivationEvents: string[]): IExtensionHostManager { + return createExtensionHostManager(this._instantiationService, extensionHostId, extensionHost, isInitialStart, initialActivationEvents, this._acquireInternalAPI()); + } + private _onExtensionHostCrashOrExit(extensionHost: IExtensionHostManager, code: number, signal: string | null): void { // Unexpected termination @@ -1056,7 +1074,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx } public getExtensionsStatus(): { [id: string]: IExtensionsStatus } { - let result: { [id: string]: IExtensionsStatus } = Object.create(null); + const result: { [id: string]: IExtensionsStatus } = Object.create(null); if (this._registry) { const extensions = this._registry.getAllExtensionDescriptions(); for (const extension of extensions) { @@ -1099,7 +1117,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx // --- impl protected _checkEnableProposedApi(extensions: IExtensionDescription[]): void { - for (let extension of extensions) { + for (const extension of extensions) { this._proposedApiController.updateEnabledApiProposals(extension); } } @@ -1157,9 +1175,9 @@ export abstract class AbstractExtensionService extends Disposable implements IEx protected _doHandleExtensionPoints(affectedExtensions: IExtensionDescription[]): void { const affectedExtensionPoints: { [extPointName: string]: boolean } = Object.create(null); - for (let extensionDescription of affectedExtensions) { + for (const extensionDescription of affectedExtensions) { if (extensionDescription.contributes) { - for (let extPointName in extensionDescription.contributes) { + for (const extPointName in extensionDescription.contributes) { if (hasOwnProperty.call(extensionDescription.contributes, extPointName)) { affectedExtensionPoints[extPointName] = true; } diff --git a/src/vs/workbench/services/extensions/common/extensionDescriptionRegistry.ts b/src/vs/workbench/services/extensions/common/extensionDescriptionRegistry.ts index 274bc9c396e..0277a064de7 100644 --- a/src/vs/workbench/services/extensions/common/extensionDescriptionRegistry.ts +++ b/src/vs/workbench/services/extensions/common/extensionDescriptionRegistry.ts @@ -68,19 +68,16 @@ export class ExtensionDescriptionRegistry { } public deltaExtensions(toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[]): DeltaExtensionsResult { - if (toAdd.length > 0) { - this._extensionDescriptions = this._extensionDescriptions.concat(toAdd); - } + // It is possible that an extension is removed, only to be added again at a different version + // so we will first handle removals + this._extensionDescriptions = removeExtensions(this._extensionDescriptions, toRemove); + + // Then, handle the extensions to add + this._extensionDescriptions = this._extensionDescriptions.concat(toAdd); // Immediately remove looping extensions! const looping = ExtensionDescriptionRegistry._findLoopingExtensions(this._extensionDescriptions); - toRemove = toRemove.concat(looping.map(ext => ext.identifier)); - - if (toRemove.length > 0) { - const toRemoveSet = new Set<string>(); - toRemove.forEach(extensionId => toRemoveSet.add(ExtensionIdentifier.toKey(extensionId))); - this._extensionDescriptions = this._extensionDescriptions.filter(extension => !toRemoveSet.has(ExtensionIdentifier.toKey(extension.identifier))); - } + this._extensionDescriptions = removeExtensions(this._extensionDescriptions, looping.map(ext => ext.identifier)); this._initialize(); this._onDidChange.fire(undefined); @@ -133,12 +130,12 @@ export class ExtensionDescriptionRegistry { } }; - let descs = new Map<string, IExtensionDescription>(); - for (let extensionDescription of extensionDescriptions) { + const descs = new Map<string, IExtensionDescription>(); + for (const extensionDescription of extensionDescriptions) { const extensionId = ExtensionIdentifier.toKey(extensionDescription.identifier); descs.set(extensionId, extensionDescription); if (extensionDescription.extensionDependencies) { - for (let _depId of extensionDescription.extensionDependencies) { + for (const _depId of extensionDescription.extensionDependencies) { const depId = ExtensionIdentifier.toKey(_depId); G.addArc(extensionId, depId); } @@ -146,11 +143,11 @@ export class ExtensionDescriptionRegistry { } // initialize with all extensions with no dependencies. - let good = new Set<string>(); + const good = new Set<string>(); G.getNodes().filter(id => G.getArcs(id).length === 0).forEach(id => good.add(id)); // all other extensions will be processed below. - let nodes = G.getNodes().filter(id => !good.has(id)); + const nodes = G.getNodes().filter(id => !good.has(id)); let madeProgress: boolean; do { @@ -194,6 +191,22 @@ export class ExtensionDescriptionRegistry { const extension = this._extensionsMap.get(ExtensionIdentifier.toKey(extensionId)); return extension ? extension : undefined; } + + public getExtensionDescriptionByUUID(uuid: string): IExtensionDescription | undefined { + for (const extensionDescription of this._extensionsArr) { + if (extensionDescription.uuid === uuid) { + return extensionDescription; + } + } + return undefined; + } + + public getExtensionDescriptionByIdOrUUID(extensionId: ExtensionIdentifier | string, uuid: string | undefined): IExtensionDescription | undefined { + return ( + this.getExtensionDescription(extensionId) + ?? (uuid ? this.getExtensionDescriptionByUUID(uuid) : undefined) + ); + } } const enum SortBucket { @@ -226,3 +239,9 @@ function extensionCmp(a: IExtensionDescription, b: IExtensionDescription): numbe } return 0; } + +function removeExtensions(arr: IExtensionDescription[], toRemove: ExtensionIdentifier[]): IExtensionDescription[] { + const toRemoveSet = new Set<string>(); + toRemove.forEach(extensionId => toRemoveSet.add(ExtensionIdentifier.toKey(extensionId))); + return arr.filter(extension => !toRemoveSet.has(ExtensionIdentifier.toKey(extension.identifier))); +} diff --git a/src/vs/workbench/services/extensions/common/extensionDevOptions.ts b/src/vs/workbench/services/extensions/common/extensionDevOptions.ts index 436f3e98773..1d9adf60100 100644 --- a/src/vs/workbench/services/extensions/common/extensionDevOptions.ts +++ b/src/vs/workbench/services/extensions/common/extensionDevOptions.ts @@ -15,21 +15,21 @@ export interface IExtensionDevOptions { export function parseExtensionDevOptions(environmentService: IEnvironmentService): IExtensionDevOptions { // handle extension host lifecycle a bit special when we know we are developing an extension that runs inside - let isExtensionDevHost = environmentService.isExtensionDevelopment; + const isExtensionDevHost = environmentService.isExtensionDevelopment; let debugOk = true; - let extDevLocs = environmentService.extensionDevelopmentLocationURI; + const extDevLocs = environmentService.extensionDevelopmentLocationURI; if (extDevLocs) { - for (let x of extDevLocs) { + for (const x of extDevLocs) { if (x.scheme !== Schemas.file) { debugOk = false; } } } - let isExtensionDevDebug = debugOk && typeof environmentService.debugExtensionHost.port === 'number'; - let isExtensionDevDebugBrk = debugOk && !!environmentService.debugExtensionHost.break; - let isExtensionDevTestFromCli = isExtensionDevHost && !!environmentService.extensionTestsLocationURI && !environmentService.debugExtensionHost.debugId; + const isExtensionDevDebug = debugOk && typeof environmentService.debugExtensionHost.port === 'number'; + const isExtensionDevDebugBrk = debugOk && !!environmentService.debugExtensionHost.break; + const isExtensionDevTestFromCli = isExtensionDevHost && !!environmentService.extensionTestsLocationURI && !environmentService.debugExtensionHost.debugId; return { isExtensionDevHost, isExtensionDevDebug, diff --git a/src/vs/workbench/services/extensions/common/extensionHostEnv.ts b/src/vs/workbench/services/extensions/common/extensionHostEnv.ts new file mode 100644 index 00000000000..9b952930ecd --- /dev/null +++ b/src/vs/workbench/services/extensions/common/extensionHostEnv.ts @@ -0,0 +1,93 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IProcessEnvironment } from 'vs/base/common/platform'; + +export const enum ExtHostConnectionType { + IPC = 1, + Socket = 2, + MessagePort = 3 +} + +/** + * The extension host will connect via named pipe / domain socket to its renderer. + */ +export class IPCExtHostConnection { + public static ENV_KEY = 'VSCODE_EXTHOST_IPC_HOOK'; + + public readonly type = ExtHostConnectionType.IPC; + + constructor( + public readonly pipeName: string + ) { } + + public serialize(env: IProcessEnvironment): void { + env[IPCExtHostConnection.ENV_KEY] = this.pipeName; + } +} + +/** + * The extension host will receive via nodejs IPC the socket to its renderer. + */ +export class SocketExtHostConnection { + public static ENV_KEY = 'VSCODE_EXTHOST_WILL_SEND_SOCKET'; + + public readonly type = ExtHostConnectionType.Socket; + + public serialize(env: IProcessEnvironment): void { + env[SocketExtHostConnection.ENV_KEY] = '1'; + } +} + +/** + * The extension host will receive via nodejs IPC the MessagePort to its renderer. + */ +export class MessagePortExtHostConnection { + public static ENV_KEY = 'VSCODE_WILL_SEND_MESSAGE_PORT'; + + public readonly type = ExtHostConnectionType.MessagePort; + + public serialize(env: IProcessEnvironment): void { + env[MessagePortExtHostConnection.ENV_KEY] = '1'; + } +} + +export type ExtHostConnection = IPCExtHostConnection | SocketExtHostConnection | MessagePortExtHostConnection; + +function clean(env: IProcessEnvironment): void { + delete env[IPCExtHostConnection.ENV_KEY]; + delete env[SocketExtHostConnection.ENV_KEY]; + delete env[MessagePortExtHostConnection.ENV_KEY]; +} + +/** + * Write `connection` into `env` and clean up `env`. + */ +export function writeExtHostConnection(connection: ExtHostConnection, env: IProcessEnvironment): void { + // Avoid having two different keys that might introduce amiguity or problems. + clean(env); + connection.serialize(env); +} + +/** + * Read `connection` from `env` and clean up `env`. + */ +export function readExtHostConnection(env: IProcessEnvironment): ExtHostConnection { + if (env[IPCExtHostConnection.ENV_KEY]) { + return cleanAndReturn(env, new IPCExtHostConnection(env[IPCExtHostConnection.ENV_KEY]!)); + } + if (env[SocketExtHostConnection.ENV_KEY]) { + return cleanAndReturn(env, new SocketExtHostConnection()); + } + if (env[MessagePortExtHostConnection.ENV_KEY]) { + return cleanAndReturn(env, new MessagePortExtHostConnection()); + } + throw new Error(`No connection information defined in environment!`); +} + +function cleanAndReturn(env: IProcessEnvironment, result: ExtHostConnection): ExtHostConnection { + clean(env); + return result; +} diff --git a/src/vs/workbench/services/extensions/common/extensionHostManager.ts b/src/vs/workbench/services/extensions/common/extensionHostManager.ts index 2f912754968..7366addd3dd 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostManager.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostManager.ts @@ -21,7 +21,7 @@ import { StopWatch } from 'vs/base/common/stopwatch'; import { VSBuffer } from 'vs/base/common/buffer'; import { IExtensionHost, ExtensionHostKind, ActivationKind, extensionHostKindToString, ExtensionActivationReason, IInternalExtensionService, ExtensionRunningLocation, ExtensionHostExtensions } from 'vs/workbench/services/extensions/common/extensions'; import { CATEGORIES } from 'vs/workbench/common/actions'; -import { Barrier, timeout } from 'vs/base/common/async'; +import { Barrier } from 'vs/base/common/async'; import { URI } from 'vs/base/common/uri'; import { ILogService } from 'vs/platform/log/common/log'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -53,7 +53,6 @@ export interface IExtensionHostManager { getCanonicalURI(remoteAuthority: string, uri: URI): Promise<URI | null>; start(allExtensions: IExtensionDescription[], myExtensions: ExtensionIdentifier[]): Promise<void>; extensionTestsExecute(): Promise<number>; - extensionTestsSendExit(exitCode: number): Promise<void>; setRemoteEnvironment(env: { [key: string]: string | null }): Promise<void>; } @@ -132,7 +131,7 @@ class ExtensionHostManager extends Disposable implements IExtensionHostManager { }; this._telemetryService.publicLog2<ExtensionHostStartupEvent, ExtensionHostStartupClassification>('extensionHostStartup', startingTelemetryEvent); - this._proxy = this._extensionHost.start()!.then( + this._proxy = this._extensionHost.start().then( (protocol) => { this._hasStarted = true; @@ -239,8 +238,8 @@ class ExtensionHostManager extends Disposable implements IExtensionHostManager { private async _measureUp(proxy: IExtensionHostProxy): Promise<number> { const SIZE = 10 * 1024 * 1024; // 10MB - let buff = VSBuffer.alloc(SIZE); - let value = Math.ceil(Math.random() * 256); + const buff = VSBuffer.alloc(SIZE); + const value = Math.ceil(Math.random() * 256); for (let i = 0; i < buff.byteLength; i++) { buff.writeUInt8(i, value); } @@ -294,16 +293,27 @@ class ExtensionHostManager extends Disposable implements IExtensionHostManager { const namedCustomers = ExtHostCustomersRegistry.getNamedCustomers(); for (let i = 0, len = namedCustomers.length; i < len; i++) { const [id, ctor] = namedCustomers[i]; - const instance = this._instantiationService.createInstance(ctor, extHostContext); - this._customers.push(instance); - this._rpcProtocol.set(id, instance); + try { + const instance = this._instantiationService.createInstance(ctor, extHostContext); + this._customers.push(instance); + this._rpcProtocol.set(id, instance); + } catch (err) { + this._logService.critical(`Cannot instantiate named customer: '${id.sid}'`); + this._logService.critical(err); + errors.onUnexpectedError(err); + } } // Customers const customers = ExtHostCustomersRegistry.getCustomers(); for (const ctor of customers) { - const instance = this._instantiationService.createInstance(ctor, extHostContext); - this._customers.push(instance); + try { + const instance = this._instantiationService.createInstance(ctor, extHostContext); + this._customers.push(instance); + } catch (err) { + this._logService.critical(err); + errors.onUnexpectedError(err); + } } if (!extensionHostProxy) { @@ -358,7 +368,7 @@ class ExtensionHostManager extends Disposable implements IExtensionHostManager { if (tryEnableInspector) { await this._extensionHost.enableInspectPort(); } - let port = this._extensionHost.getInspectPort(); + const port = this._extensionHost.getInspectPort(); if (port) { return port; } @@ -418,20 +428,6 @@ class ExtensionHostManager extends Disposable implements IExtensionHostManager { return proxy.extensionTestsExecute(); } - public async extensionTestsSendExit(exitCode: number): Promise<void> { - const proxy = await this._proxy; - if (!proxy) { - return; - } - // This method does not wait for the actual RPC to be confirmed - // It waits for the socket to drain (i.e. the message has been sent) - // It also times out after 5s in case drain takes too long - proxy.extensionTestsExit(exitCode); - if (this._rpcProtocol) { - await Promise.race([this._rpcProtocol.drain(), timeout(5000)]); - } - } - public representsRunningLocation(runningLocation: ExtensionRunningLocation): boolean { return this._extensionHost.runningLocation.equals(runningLocation); } @@ -609,11 +605,6 @@ class LazyStartExtensionHostManager extends Disposable implements IExtensionHost const actual = await this._getOrCreateActualAndStart(`execute tests.`); return actual.extensionTestsExecute(); } - public async extensionTestsSendExit(exitCode: number): Promise<void> { - await this._startCalled.wait(); - const actual = await this._getOrCreateActualAndStart(`execute tests.`); - return actual.extensionTestsSendExit(exitCode); - } public async setRemoteEnvironment(env: { [key: string]: string | null }): Promise<void> { await this._startCalled.wait(); if (this._actual) { @@ -632,7 +623,7 @@ function prettyWithoutArrays(data: any): any { return data; } if (data && typeof data === 'object' && typeof data.toString === 'function') { - let result = data.toString(); + const result = data.toString(); if (result !== '[object Object]') { return result; } @@ -689,7 +680,7 @@ interface ExtHostLatencyProvider { measure(): Promise<ExtHostLatencyResult | null>; } -let providers: ExtHostLatencyProvider[] = []; +const providers: ExtHostLatencyProvider[] = []; function registerLatencyTestProvider(provider: ExtHostLatencyProvider): IDisposable { providers.push(provider); return { diff --git a/src/vs/workbench/services/extensions/common/extensionHostProtocol.ts b/src/vs/workbench/services/extensions/common/extensionHostProtocol.ts index ca72df6f92f..cc654df1787 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostProtocol.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostProtocol.ts @@ -31,6 +31,7 @@ export interface IExtensionHostInitData { logFile: URI; autoStart: boolean; remote: { isRemote: boolean; authority: string | undefined; connectionData: IRemoteConnectionData | null }; + consoleForward: { includeStack: boolean; logNative: boolean }; uiKind: UIKind; messagePorts?: ReadonlyMap<string, MessagePortLike>; } @@ -122,3 +123,8 @@ export function isMessageOfType(message: VSBuffer, type: MessageType): boolean { default: return false; } } + +export const enum NativeLogMarkers { + Start = 'START_NATIVE_LOG', + End = 'END_NATIVE_LOG', +} diff --git a/src/vs/workbench/services/extensions/common/extensionHostProxy.ts b/src/vs/workbench/services/extensions/common/extensionHostProxy.ts index b528989a727..e1c962a8b40 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostProxy.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostProxy.ts @@ -34,7 +34,6 @@ export interface IExtensionHostProxy { getCanonicalURI(remoteAuthority: string, uri: URI): Promise<URI | null>; startExtensionHost(extensionsDelta: IExtensionDescriptionDelta): Promise<void>; extensionTestsExecute(): Promise<number>; - extensionTestsExit(code: number): Promise<void>; activateByEvent(activationEvent: string, activationKind: ActivationKind): Promise<void>; activate(extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise<boolean>; setRemoteEnvironment(env: { [key: string]: string | null }): Promise<void>; diff --git a/src/vs/workbench/services/extensions/common/extensionStorageMigration.ts b/src/vs/workbench/services/extensions/common/extensionStorageMigration.ts index 2e5a0bb6e4a..fbb28a3f4cc 100644 --- a/src/vs/workbench/services/extensions/common/extensionStorageMigration.ts +++ b/src/vs/workbench/services/extensions/common/extensionStorageMigration.ts @@ -12,6 +12,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ILogService } from 'vs/platform/log/common/log'; import { IStorageService, 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 { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; /** @@ -22,6 +23,7 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace export async function migrateExtensionStorage(fromExtensionId: string, toExtensionId: string, global: boolean, instantionService: IInstantiationService): Promise<void> { return instantionService.invokeFunction(async serviceAccessor => { const environmentService = serviceAccessor.get(IEnvironmentService); + const userDataProfilesService = serviceAccessor.get(IUserDataProfilesService); const extensionStorageService = serviceAccessor.get(IExtensionStorageService); const storageService = serviceAccessor.get(IStorageService); const uriIdentityService = serviceAccessor.get(IUriIdentityService); @@ -37,12 +39,12 @@ export async function migrateExtensionStorage(fromExtensionId: string, toExtensi const getExtensionStorageLocation = (extensionId: string, global: boolean): URI => { if (global) { - return uriIdentityService.extUri.joinPath(environmentService.globalStorageHome, extensionId.toLowerCase() /* Extension id is lower cased for global storage */); + return uriIdentityService.extUri.joinPath(userDataProfilesService.defaultProfile.globalStorageHome, extensionId.toLowerCase() /* Extension id is lower cased for global storage */); } return uriIdentityService.extUri.joinPath(environmentService.workspaceStorageHome, workspaceContextService.getWorkspace().id, extensionId); }; - const storageScope = global ? StorageScope.GLOBAL : StorageScope.WORKSPACE; + const storageScope = global ? StorageScope.PROFILE : StorageScope.WORKSPACE; if (!storageService.getBoolean(storageMigratedKey, storageScope, false) && !(migrateLowerCaseStorageKey && storageService.getBoolean(migrateLowerCaseStorageKey, storageScope, false))) { logService.info(`Migrating ${global ? 'global' : 'workspace'} extension storage from ${fromExtensionId} to ${toExtensionId}...`); // Migrate state diff --git a/src/vs/workbench/services/extensions/common/extensions.ts b/src/vs/workbench/services/extensions/common/extensions.ts index 722dba8dd6b..17d96c5614d 100644 --- a/src/vs/workbench/services/extensions/common/extensions.ts +++ b/src/vs/workbench/services/extensions/common/extensions.ts @@ -157,7 +157,7 @@ export interface IExtensionHost { readonly extensions: ExtensionHostExtensions; readonly onExit: Event<[number, string | null]>; - start(): Promise<IMessagePassingProtocol> | null; + start(): Promise<IMessagePassingProtocol>; getInspectPort(): number | undefined; enableInspectPort(): Promise<boolean>; dispose(): void; @@ -324,7 +324,7 @@ export class ExtensionIdentifierSet implements Set<ExtensionIdentifier> { } *entries(): IterableIterator<[ExtensionIdentifier, ExtensionIdentifier]> { - for (let [_key, value] of this._map) { + for (const [_key, value] of this._map) { yield [value, value]; } } diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index c562da5671f..f735af27335 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -9,15 +9,18 @@ export const allApiProposals = Object.freeze({ authSession: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authSession.d.ts', badges: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.badges.d.ts', commentsResolvedState: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.commentsResolvedState.d.ts', + contribEditSessions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribEditSessions.d.ts', contribLabelFormatterWorkspaceTooltip: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribLabelFormatterWorkspaceTooltip.d.ts', contribMenuBarHome: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribMenuBarHome.d.ts', + contribMergeEditorToolbar: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribMergeEditorToolbar.d.ts', contribRemoteHelp: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribRemoteHelp.d.ts', + contribShareMenu: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribShareMenu.d.ts', contribViewsRemote: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribViewsRemote.d.ts', contribViewsWelcome: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribViewsWelcome.d.ts', customEditorMove: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.customEditorMove.d.ts', - dataTransferFiles: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.dataTransferFiles.d.ts', diffCommand: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.diffCommand.d.ts', documentFiltersExclusive: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.documentFiltersExclusive.d.ts', + documentPaste: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.documentPaste.d.ts', editorInsets: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.editorInsets.d.ts', extensionRuntime: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.extensionRuntime.d.ts', extensionsAny: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.extensionsAny.d.ts', @@ -26,7 +29,6 @@ export const allApiProposals = Object.freeze({ findTextInFiles: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.findTextInFiles.d.ts', fsChunks: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.fsChunks.d.ts', idToken: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.idToken.d.ts', - inlineCompletions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.inlineCompletions.d.ts', inlineCompletionsAdditions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts', inlineCompletionsNew: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.inlineCompletionsNew.d.ts', ipc: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.ipc.d.ts', @@ -36,12 +38,11 @@ export const allApiProposals = Object.freeze({ notebookDebugOptions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookDebugOptions.d.ts', notebookDeprecated: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookDeprecated.d.ts', notebookEditor: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookEditor.d.ts', - notebookEditorDecorationType: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookEditorDecorationType.d.ts', notebookEditorEdit: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookEditorEdit.d.ts', + notebookKernelSource: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookKernelSource.d.ts', notebookLiveShare: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookLiveShare.d.ts', notebookMessaging: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookMessaging.d.ts', notebookMime: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookMime.d.ts', - notebookProxyController: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookProxyController.d.ts', notebookWorkspaceEdit: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookWorkspaceEdit.d.ts', portsAttributes: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.portsAttributes.d.ts', quickPickSortByLabel: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.quickPickSortByLabel.d.ts', @@ -49,6 +50,7 @@ export const allApiProposals = Object.freeze({ scmActionButton: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmActionButton.d.ts', scmSelectedProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmSelectedProvider.d.ts', scmValidation: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmValidation.d.ts', + snippetWorkspaceEdit: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.snippetWorkspaceEdit.d.ts', taskPresentationGroup: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.taskPresentationGroup.d.ts', telemetry: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.telemetry.d.ts', terminalDataWriteEvent: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalDataWriteEvent.d.ts', diff --git a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts index 504f8b5eb47..33eaef0922b 100644 --- a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts +++ b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts @@ -14,7 +14,6 @@ import { IMessage } from 'vs/workbench/services/extensions/common/extensions'; import { ExtensionIdentifier, IExtensionDescription, EXTENSION_CATEGORIES } from 'vs/platform/extensions/common/extensions'; import { ExtensionKind } from 'vs/platform/environment/common/environment'; import { allApiProposals } from 'vs/workbench/services/extensions/common/extensionsApiProposals'; -import { values } from 'vs/base/common/collections'; import { productSchemaId } from 'vs/platform/product/common/productService'; const schemaRegistry = Registry.as<IJSONContributionRegistry>(Extensions.JSONContribution); @@ -92,8 +91,8 @@ export class ExtensionPointUserDelta<T> { const previousSet = this._toSet(previous); const currentSet = this._toSet(current); - let added = current.filter(user => !previousSet.has(ExtensionIdentifier.toKey(user.description.identifier))); - let removed = previous.filter(user => !currentSet.has(ExtensionIdentifier.toKey(user.description.identifier))); + const added = current.filter(user => !previousSet.has(ExtensionIdentifier.toKey(user.description.identifier))); + const removed = previous.filter(user => !currentSet.has(ExtensionIdentifier.toKey(user.description.identifier))); return new ExtensionPointUserDelta<T>(added, removed); } @@ -236,7 +235,7 @@ export const schema: IJSONSchema = { items: { type: 'string', enum: Object.keys(allApiProposals), - markdownEnumDescriptions: values(allApiProposals) + markdownEnumDescriptions: Object.values(allApiProposals) } }, activationEvents: { @@ -517,6 +516,19 @@ export const schema: IJSONSchema = { } } }, + sponsor: { + description: nls.localize('vscode.extension.contributes.sponsor', "Specify the location from where users can sponsor your extension."), + type: 'object', + defaultSnippets: [ + { body: { url: '${1:https:}' } }, + ], + properties: { + 'url': { + description: nls.localize('vscode.extension.contributes.sponsor.url', "URL from where users can sponsor your extension. It must be a valid URL with a HTTP or HTTPS protocol. Example value: https://github.com/sponsors/nvaccess"), + type: 'string', + } + } + }, scripts: { type: 'object', properties: { @@ -588,7 +600,7 @@ schemaRegistry.registerSchema(productSchemaId, { items: { type: 'string', enum: Object.keys(allApiProposals), - markdownEnumDescriptions: values(allApiProposals) + markdownEnumDescriptions: Object.values(allApiProposals) } }] } diff --git a/src/vs/workbench/services/extensions/common/extensionsUtil.ts b/src/vs/workbench/services/extensions/common/extensionsUtil.ts index 977052ebcfc..0a65c761e81 100644 --- a/src/vs/workbench/services/extensions/common/extensionsUtil.ts +++ b/src/vs/workbench/services/extensions/common/extensionsUtil.ts @@ -6,9 +6,11 @@ import { ExtensionIdentifier, IExtensionDescription, IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { localize } from 'vs/nls'; import { ILogService } from 'vs/platform/log/common/log'; +import * as semver from 'vs/base/common/semver/semver'; +// TODO: @sandy081 merge this with deduping in extensionsScannerService.ts export function dedupExtensions(system: IExtensionDescription[], user: IExtensionDescription[], development: IExtensionDescription[], logService: ILogService): IExtensionDescription[] { - let result = new Map<string, IExtensionDescription>(); + const result = new Map<string, IExtensionDescription>(); system.forEach((systemExtension) => { const extensionKey = ExtensionIdentifier.toKey(systemExtension.identifier); const extension = result.get(extensionKey); @@ -22,11 +24,18 @@ export function dedupExtensions(system: IExtensionDescription[], user: IExtensio const extension = result.get(extensionKey); if (extension) { if (extension.isBuiltin) { + if (semver.gte(extension.version, userExtension.version)) { + logService.warn(`Skipping extension ${userExtension.extensionLocation.path} in favour of the builtin extension ${extension.extensionLocation.path}.`); + return; + } // Overwriting a builtin extension inherits the `isBuiltin` property and it doesn't show a warning (<IRelaxedExtensionDescription>userExtension).isBuiltin = true; } else { logService.warn(localize('overwritingExtension', "Overwriting extension {0} with {1}.", extension.extensionLocation.fsPath, userExtension.extensionLocation.fsPath)); } + } else if (userExtension.isBuiltin) { + logService.warn(`Skipping obsolete builtin extension ${userExtension.extensionLocation.path}`); + return; } result.set(extensionKey, userExtension); }); @@ -42,7 +51,7 @@ export function dedupExtensions(system: IExtensionDescription[], user: IExtensio } result.set(extensionKey, developedExtension); }); - let r: IExtensionDescription[] = []; + const r: IExtensionDescription[] = []; result.forEach((value) => r.push(value)); return r; } diff --git a/src/vs/workbench/services/extensions/common/lazyPromise.ts b/src/vs/workbench/services/extensions/common/lazyPromise.ts index 0f6c07064a3..5860ffc2b5e 100644 --- a/src/vs/workbench/services/extensions/common/lazyPromise.ts +++ b/src/vs/workbench/services/extensions/common/lazyPromise.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { onUnexpectedError } from 'vs/base/common/errors'; +import { CancellationError, onUnexpectedError } from 'vs/base/common/errors'; export class LazyPromise implements Promise<any> { @@ -14,8 +14,8 @@ export class LazyPromise implements Promise<any> { private _hasValue: boolean; private _value: any; - private _hasErr: boolean; - private _err: any; + protected _hasErr: boolean; + protected _err: any; constructor() { this._actual = null; @@ -91,3 +91,11 @@ export class LazyPromise implements Promise<any> { return this._ensureActual().finally(callback); } } + +export class CanceledLazyPromise extends LazyPromise { + constructor() { + super(); + this._hasErr = true; + this._err = new CancellationError(); + } +} diff --git a/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts b/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts index a699e459da3..62eea683c18 100644 --- a/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts +++ b/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts @@ -28,7 +28,6 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/ import { parseExtensionDevOptions } from 'vs/workbench/services/extensions/common/extensionDevOptions'; import { createMessageOfType, isMessageOfType, MessageType, IExtensionHostInitData, UIKind } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; import { ExtensionHostExtensions, ExtensionHostLogFileName, IExtensionHost, RemoteRunningLocation } from 'vs/workbench/services/extensions/common/extensions'; -import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { Extensions, IOutputChannelRegistry } from 'vs/workbench/services/output/common/output'; export interface IRemoteExtensionHostInitData { @@ -68,7 +67,6 @@ export class RemoteExtensionHost extends Disposable implements IExtensionHost { @IWorkspaceContextService private readonly _contextService: IWorkspaceContextService, @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, @ITelemetryService private readonly _telemetryService: ITelemetryService, - @ILifecycleService private readonly _lifecycleService: ILifecycleService, @ILogService private readonly _logService: ILogService, @ILabelService private readonly _labelService: ILabelService, @IRemoteAuthorityResolverService private readonly remoteAuthorityResolverService: IRemoteAuthorityResolverService, @@ -82,8 +80,6 @@ export class RemoteExtensionHost extends Disposable implements IExtensionHost { this._hasLostConnection = false; this._terminating = false; - this._register(this._lifecycleService.onDidShutdown(() => this.dispose())); - const devOpts = parseExtensionDevOptions(this._environmentService); this._isExtensionDevHost = devOpts.isExtensionDevHost; } @@ -129,7 +125,7 @@ export class RemoteExtensionHost extends Disposable implements IExtensionHost { return connectRemoteAgentExtensionHost(options, startParams).then(result => { this._register(result); - let { protocol, debugPort, reconnectionToken } = result; + const { protocol, debugPort, reconnectionToken } = result; const isExtensionDevelopmentDebug = typeof debugPort === 'number'; if (debugOk && this._environmentService.isExtensionDevelopment && this._environmentService.debugExtensionHost.debugId && debugPort) { this._extensionHostDebugService.attachSession(this._environmentService.debugExtensionHost.debugId, debugPort, this._initDataProvider.remoteAuthority); @@ -149,7 +145,7 @@ export class RemoteExtensionHost extends Disposable implements IExtensionHost { // 2) wait for the incoming `initialized` event. return new Promise<IMessagePassingProtocol>((resolve, reject) => { - let handle = setTimeout(() => { + const handle = setTimeout(() => { reject('The remote extenion host took longer than 60s to send its ready message.'); }, 60 * 1000); @@ -242,6 +238,10 @@ export class RemoteExtensionHost extends Disposable implements IExtensionHost { authority: this._initDataProvider.remoteAuthority, connectionData: remoteInitData.connectionData }, + consoleForward: { + includeStack: false, + logNative: Boolean(this._environmentService.debugExtensionHost.debugId) + }, allExtensions: deltaExtensions.toAdd, myExtensions: deltaExtensions.myToAdd, telemetryInfo, @@ -269,12 +269,16 @@ export class RemoteExtensionHost extends Disposable implements IExtensionHost { if (this._protocol) { // Send the extension host a request to terminate itself // (graceful termination) + // setTimeout(() => { + // console.log(`SENDING TERMINATE TO REMOTE EXT HOST!`); const socket = this._protocol.getSocket(); this._protocol.send(createMessageOfType(MessageType.Terminate)); this._protocol.sendDisconnect(); this._protocol.dispose(); + // this._protocol.drain(); socket.end(); this._protocol = null; + // }, 1000); } } } diff --git a/src/vs/workbench/services/extensions/common/rpcProtocol.ts b/src/vs/workbench/services/extensions/common/rpcProtocol.ts index f44eae7e257..db6aa3ad6d8 100644 --- a/src/vs/workbench/services/extensions/common/rpcProtocol.ts +++ b/src/vs/workbench/services/extensions/common/rpcProtocol.ts @@ -14,7 +14,7 @@ import { MarshalledObject } from 'vs/base/common/marshalling'; import { MarshalledId } from 'vs/base/common/marshallingIds'; import { IURITransformer, transformIncomingURIs } from 'vs/base/common/uriIpc'; import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; -import { LazyPromise } from 'vs/workbench/services/extensions/common/lazyPromise'; +import { CanceledLazyPromise, LazyPromise } from 'vs/workbench/services/extensions/common/lazyPromise'; import { getStringIdentifierForProxy, IRPCProtocol, Proxied, ProxyIdentifier, SerializableObjectWithBuffers } from 'vs/workbench/services/extensions/common/proxyIdentifier'; export interface JSONStringifyReplacer { @@ -246,7 +246,7 @@ export class RPCProtocol extends Disposable implements IRPCProtocol { } private _createProxy<T>(rpcId: number, debugName: string): T { - let handler = { + const handler = { get: (target: any, name: PropertyKey) => { if (typeof name === 'string' && !target[name] && name.charCodeAt(0) === CharCode.DollarSign) { target[name] = (...myArgs: any[]) => { @@ -306,9 +306,7 @@ export class RPCProtocol extends Disposable implements IRPCProtocol { break; } case MessageType.Acknowledged: { - if (this._logger) { - this._logger.logIncoming(msgLength, req, RequestInitiator.LocalSide, `ack`); - } + this._logger?.logIncoming(msgLength, req, RequestInitiator.LocalSide, `ack`); this._onDidReceiveAcknowledge(req); break; } @@ -334,7 +332,7 @@ export class RPCProtocol extends Disposable implements IRPCProtocol { break; } case MessageType.ReplyOKVSBuffer: { - let value = MessageIO.deserializeReplyOKVSBuffer(buff); + const value = MessageIO.deserializeReplyOKVSBuffer(buff); this._receiveReply(msgLength, req, value); break; } @@ -357,9 +355,7 @@ export class RPCProtocol extends Disposable implements IRPCProtocol { } private _receiveRequest(msgLength: number, req: number, rpcId: number, method: string, args: any[], usesCancellationToken: boolean): void { - if (this._logger) { - this._logger.logIncoming(msgLength, req, RequestInitiator.OtherSide, `receiveRequest ${getStringIdentifierForProxy(rpcId)}.${method}(`, args); - } + this._logger?.logIncoming(msgLength, req, RequestInitiator.OtherSide, `receiveRequest ${getStringIdentifierForProxy(rpcId)}.${method}(`, args); const callId = String(req); let promise: Promise<any>; @@ -379,42 +375,30 @@ export class RPCProtocol extends Disposable implements IRPCProtocol { // Acknowledge the request const msg = MessageIO.serializeAcknowledged(req); - if (this._logger) { - this._logger.logOutgoing(msg.byteLength, req, RequestInitiator.OtherSide, `ack`); - } + this._logger?.logOutgoing(msg.byteLength, req, RequestInitiator.OtherSide, `ack`); this._protocol.send(msg); promise.then((r) => { delete this._cancelInvokedHandlers[callId]; const msg = MessageIO.serializeReplyOK(req, r, this._uriReplacer); - if (this._logger) { - this._logger.logOutgoing(msg.byteLength, req, RequestInitiator.OtherSide, `reply:`, r); - } + this._logger?.logOutgoing(msg.byteLength, req, RequestInitiator.OtherSide, `reply:`, r); this._protocol.send(msg); }, (err) => { delete this._cancelInvokedHandlers[callId]; const msg = MessageIO.serializeReplyErr(req, err); - if (this._logger) { - this._logger.logOutgoing(msg.byteLength, req, RequestInitiator.OtherSide, `replyErr:`, err); - } + this._logger?.logOutgoing(msg.byteLength, req, RequestInitiator.OtherSide, `replyErr:`, err); this._protocol.send(msg); }); } private _receiveCancel(msgLength: number, req: number): void { - if (this._logger) { - this._logger.logIncoming(msgLength, req, RequestInitiator.OtherSide, `receiveCancel`); - } + this._logger?.logIncoming(msgLength, req, RequestInitiator.OtherSide, `receiveCancel`); const callId = String(req); - if (this._cancelInvokedHandlers[callId]) { - this._cancelInvokedHandlers[callId](); - } + this._cancelInvokedHandlers[callId]?.(); } private _receiveReply(msgLength: number, req: number, value: any): void { - if (this._logger) { - this._logger.logIncoming(msgLength, req, RequestInitiator.LocalSide, `receiveReply:`, value); - } + this._logger?.logIncoming(msgLength, req, RequestInitiator.LocalSide, `receiveReply:`, value); const callId = String(req); if (!this._pendingRPCReplies.hasOwnProperty(callId)) { return; @@ -427,9 +411,7 @@ export class RPCProtocol extends Disposable implements IRPCProtocol { } private _receiveReplyErr(msgLength: number, req: number, value: any): void { - if (this._logger) { - this._logger.logIncoming(msgLength, req, RequestInitiator.LocalSide, `receiveReplyErr:`, value); - } + this._logger?.logIncoming(msgLength, req, RequestInitiator.LocalSide, `receiveReplyErr:`, value); const callId = String(req); if (!this._pendingRPCReplies.hasOwnProperty(callId)) { @@ -466,7 +448,7 @@ export class RPCProtocol extends Disposable implements IRPCProtocol { if (!actor) { throw new Error('Unknown actor ' + getStringIdentifierForProxy(rpcId)); } - let method = actor[methodName]; + const method = actor[methodName]; if (typeof method !== 'function') { throw new Error('Unknown method ' + methodName + ' on actor ' + getStringIdentifierForProxy(rpcId)); } @@ -475,7 +457,7 @@ export class RPCProtocol extends Disposable implements IRPCProtocol { private _remoteCall(rpcId: number, methodName: string, args: any[]): Promise<any> { if (this._isDisposed) { - return Promise.reject<any>(errors.canceled()); + return new CanceledLazyPromise(); } let cancellationToken: CancellationToken | null = null; if (args.length > 0 && CancellationToken.isCancellationToken(args[args.length - 1])) { @@ -496,9 +478,7 @@ export class RPCProtocol extends Disposable implements IRPCProtocol { if (cancellationToken) { cancellationToken.onCancellationRequested(() => { const msg = MessageIO.serializeCancel(req); - if (this._logger) { - this._logger.logOutgoing(msg.byteLength, req, RequestInitiator.LocalSide, `cancel`); - } + this._logger?.logOutgoing(msg.byteLength, req, RequestInitiator.LocalSide, `cancel`); this._protocol.send(MessageIO.serializeCancel(req)); }); } @@ -506,9 +486,7 @@ export class RPCProtocol extends Disposable implements IRPCProtocol { this._pendingRPCReplies[callId] = result; this._onWillSendRequest(req); const msg = MessageIO.serializeRequest(req, rpcId, methodName, serializedRequestArguments, !!cancellationToken); - if (this._logger) { - this._logger.logOutgoing(msg.byteLength, req, RequestInitiator.LocalSide, `request: ${getStringIdentifierForProxy(rpcId)}.${methodName}(`, args); - } + this._logger?.logOutgoing(msg.byteLength, req, RequestInitiator.LocalSide, `request: ${getStringIdentifierForProxy(rpcId)}.${methodName}(`, args); this._protocol.send(msg); return result; } @@ -517,7 +495,7 @@ export class RPCProtocol extends Disposable implements IRPCProtocol { class MessageBuffer { public static alloc(type: MessageType, req: number, messageSize: number): MessageBuffer { - let result = new MessageBuffer(VSBuffer.alloc(messageSize + 1 /* type */ + 4 /* req */), 0); + const result = new MessageBuffer(VSBuffer.alloc(messageSize + 1 /* type */ + 4 /* req */), 0); result.writeUInt8(type); result.writeUInt32(req); return result; @@ -673,7 +651,7 @@ class MessageBuffer { public readMixedArray(): Array<string | VSBuffer | SerializableObjectWithBuffers<any> | undefined> { const arrLen = this._buff.readUInt8(this._offset); this._offset += 1; - let arr: Array<string | VSBuffer | SerializableObjectWithBuffers<any> | undefined> = new Array(arrLen); + const arr: Array<string | VSBuffer | SerializableObjectWithBuffers<any> | undefined> = new Array(arrLen); for (let i = 0; i < arrLen; i++) { const argType = <ArgType>this.readUInt8(); switch (argType) { @@ -774,7 +752,7 @@ class MessageIO { len += MessageBuffer.sizeShortString(methodBuff); len += MessageBuffer.sizeLongString(argsBuff); - let result = MessageBuffer.alloc(usesCancellationToken ? MessageType.RequestJSONArgsWithCancellation : MessageType.RequestJSONArgs, req, len); + const result = MessageBuffer.alloc(usesCancellationToken ? MessageType.RequestJSONArgsWithCancellation : MessageType.RequestJSONArgs, req, len); result.writeUInt8(rpcId); result.writeShortString(methodBuff); result.writeLongString(argsBuff); @@ -800,7 +778,7 @@ class MessageIO { len += MessageBuffer.sizeShortString(methodBuff); len += MessageBuffer.sizeMixedArray(args); - let result = MessageBuffer.alloc(usesCancellationToken ? MessageType.RequestMixedArgsWithCancellation : MessageType.RequestMixedArgs, req, len); + const result = MessageBuffer.alloc(usesCancellationToken ? MessageType.RequestMixedArgsWithCancellation : MessageType.RequestMixedArgs, req, len); result.writeUInt8(rpcId); result.writeShortString(methodBuff); result.writeMixedArray(args); @@ -856,7 +834,7 @@ class MessageIO { let len = 0; len += MessageBuffer.sizeVSBuffer(res); - let result = MessageBuffer.alloc(MessageType.ReplyOKVSBuffer, req, len); + const result = MessageBuffer.alloc(MessageType.ReplyOKVSBuffer, req, len); result.writeVSBuffer(res); return result.buffer; } @@ -871,7 +849,7 @@ class MessageIO { let len = 0; len += MessageBuffer.sizeLongString(resBuff); - let result = MessageBuffer.alloc(MessageType.ReplyOKJSON, req, len); + const result = MessageBuffer.alloc(MessageType.ReplyOKJSON, req, len); result.writeLongString(resBuff); return result.buffer; } @@ -886,7 +864,7 @@ class MessageIO { len += MessageBuffer.sizeVSBuffer(buffer); } - let result = MessageBuffer.alloc(MessageType.ReplyOKJSONWithBuffers, req, len); + const result = MessageBuffer.alloc(MessageType.ReplyOKJSONWithBuffers, req, len); result.writeUInt32(buffers.length); result.writeLongString(resBuff); for (const buffer of buffers) { @@ -923,7 +901,7 @@ class MessageIO { let len = 0; len += MessageBuffer.sizeLongString(errBuff); - let result = MessageBuffer.alloc(MessageType.ReplyErrError, req, len); + const result = MessageBuffer.alloc(MessageType.ReplyErrError, req, len); result.writeLongString(errBuff); return result.buffer; } diff --git a/src/vs/workbench/services/extensions/electron-browser/nativeExtensionService.ts b/src/vs/workbench/services/extensions/electron-browser/nativeExtensionService.ts new file mode 100644 index 00000000000..2e4c745a9f8 --- /dev/null +++ b/src/vs/workbench/services/extensions/electron-browser/nativeExtensionService.ts @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { ExtensionHostKind, ExtensionRunningLocation, IExtensionHost, IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { NativeLocalProcessExtensionHost } from 'vs/workbench/services/extensions/electron-browser/nativeLocalProcessExtensionHost'; +import { ElectronExtensionService } from 'vs/workbench/services/extensions/electron-sandbox/electronExtensionService'; + +export class NativeExtensionService extends ElectronExtensionService { + protected override _createExtensionHost(runningLocation: ExtensionRunningLocation, isInitialStart: boolean): IExtensionHost | null { + if (runningLocation.kind === ExtensionHostKind.LocalProcess) { + return this._instantiationService.createInstance(NativeLocalProcessExtensionHost, runningLocation, this._createLocalExtensionHostDataProvider(isInitialStart, runningLocation)); + } + return super._createExtensionHost(runningLocation, isInitialStart); + } +} + +registerSingleton(IExtensionService, NativeExtensionService); diff --git a/src/vs/workbench/services/extensions/electron-browser/nativeLocalProcessExtensionHost.ts b/src/vs/workbench/services/extensions/electron-browser/nativeLocalProcessExtensionHost.ts new file mode 100644 index 00000000000..888db160609 --- /dev/null +++ b/src/vs/workbench/services/extensions/electron-browser/nativeLocalProcessExtensionHost.ts @@ -0,0 +1,119 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createServer, Server } from 'net'; +import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import * as platform from 'vs/base/common/platform'; +import { StopWatch } from 'vs/base/common/stopwatch'; +import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; +import { PersistentProtocol } from 'vs/base/parts/ipc/common/ipc.net'; +import { createRandomIPCHandle, NodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; +import { IExtensionHostProcessOptions } from 'vs/platform/extensions/common/extensionHostStarter'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IPCExtHostConnection, writeExtHostConnection } from 'vs/workbench/services/extensions/common/extensionHostEnv'; +import { createMessageOfType, MessageType } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; +import { ExtensionHostProcess, ExtHostMessagePortCommunication, IExtHostCommunication, SandboxLocalProcessExtensionHost } from 'vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost'; + +export class NativeLocalProcessExtensionHost extends SandboxLocalProcessExtensionHost { + protected override async _start(): Promise<IMessagePassingProtocol> { + const canUseUtilityProcess = await this._extensionHostStarter.canUseUtilityProcess(); + if (canUseUtilityProcess && this._configurationService.getValue<boolean | undefined>('extensions.experimental.useUtilityProcess')) { + const communication = this._toDispose.add(new ExtHostMessagePortCommunication(this._logService)); + return this._startWithCommunication(communication); + } else { + const communication = this._toDispose.add(new ExtHostNamedPipeCommunication(this._logService)); + return this._startWithCommunication(communication); + } + } +} + +interface INamedPipePreparedData { + pipeName: string; + namedPipeServer: Server; +} + +class ExtHostNamedPipeCommunication extends Disposable implements IExtHostCommunication<INamedPipePreparedData> { + + readonly useUtilityProcess = false; + + constructor( + @ILogService private readonly _logService: ILogService + ) { + super(); + } + + prepare(): Promise<INamedPipePreparedData> { + return new Promise<{ pipeName: string; namedPipeServer: Server }>((resolve, reject) => { + const pipeName = createRandomIPCHandle(); + + const namedPipeServer = createServer(); + namedPipeServer.on('error', reject); + namedPipeServer.listen(pipeName, () => { + namedPipeServer?.removeListener('error', reject); + resolve({ pipeName, namedPipeServer }); + }); + this._register(toDisposable(() => { + if (namedPipeServer.listening) { + namedPipeServer.close(); + } + })); + }); + } + + establishProtocol(prepared: INamedPipePreparedData, extensionHostProcess: ExtensionHostProcess, opts: IExtensionHostProcessOptions): Promise<IMessagePassingProtocol> { + const { namedPipeServer, pipeName } = prepared; + + writeExtHostConnection(new IPCExtHostConnection(pipeName), opts.env); + + return new Promise<PersistentProtocol>((resolve, reject) => { + + // Wait for the extension host to connect to our named pipe + // and wrap the socket in the message passing protocol + const handle = setTimeout(() => { + if (namedPipeServer.listening) { + namedPipeServer.close(); + } + reject('The local extension host took longer than 60s to connect.'); + }, 60 * 1000); + + namedPipeServer.on('connection', (socket) => { + + clearTimeout(handle); + if (namedPipeServer.listening) { + namedPipeServer.close(); + } + + const nodeSocket = new NodeSocket(socket, 'renderer-exthost'); + const protocol = new PersistentProtocol(nodeSocket); + + this._register(toDisposable(() => { + // Send the extension host a request to terminate itself + // (graceful termination) + protocol.send(createMessageOfType(MessageType.Terminate)); + protocol.flush(); + + socket.end(); + nodeSocket.dispose(); + protocol.dispose(); + })); + + resolve(protocol); + }); + + // Now that the named pipe listener is installed, start the ext host process + const sw = StopWatch.create(false); + extensionHostProcess.start(opts).then(() => { + const duration = sw.elapsed(); + if (platform.isCI) { + this._logService.info(`IExtensionHostStarter.start() took ${duration} ms.`); + } + }, (err) => { + // Starting the ext host process resulted in an error + reject(err); + }); + + }); + } +} diff --git a/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts b/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts index 852ea7702e9..dd9be154068 100644 --- a/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts +++ b/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts @@ -15,6 +15,7 @@ import { localize } from 'vs/nls'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { timeout } from 'vs/base/common/async'; +import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; export class CachedExtensionScanner { @@ -26,6 +27,7 @@ export class CachedExtensionScanner { @INotificationService private readonly _notificationService: INotificationService, @IHostService private readonly _hostService: IHostService, @IExtensionsScannerService private readonly _extensionsScannerService: IExtensionsScannerService, + @IUserDataProfileService private readonly _userDataProfileService: IUserDataProfileService, @ILogService private readonly _logService: ILogService, ) { this.scannedExtensions = new Promise<IExtensionDescription[]>((resolve, reject) => { @@ -41,24 +43,24 @@ export class CachedExtensionScanner { public async startScanningExtensions(): Promise<void> { try { - const { system, user, development } = await this._scanInstalledExtensions(); - const r = dedupExtensions(system, user, development, this._logService); - this._scannedExtensionsResolve(r); + const extensions = await this._scanInstalledExtensions(); + this._scannedExtensionsResolve(extensions); } catch (err) { this._scannedExtensionsReject(err); } } - private async _scanInstalledExtensions(): Promise<{ system: IExtensionDescription[]; user: IExtensionDescription[]; development: IExtensionDescription[] }> { + private async _scanInstalledExtensions(): Promise<IExtensionDescription[]> { try { const language = platform.language; const [scannedSystemExtensions, scannedUserExtensions] = await Promise.all([ this._extensionsScannerService.scanSystemExtensions({ language, useCache: true, checkControlFile: true }), - this._extensionsScannerService.scanUserExtensions({ language, useCache: true })]); + this._extensionsScannerService.scanUserExtensions({ language, profileLocation: this._userDataProfileService.currentProfile.extensionsResource, useCache: true })]); const scannedDevelopedExtensions = await this._extensionsScannerService.scanExtensionsUnderDevelopment({ language }, [...scannedSystemExtensions, ...scannedUserExtensions]); const system = scannedSystemExtensions.map(e => toExtensionDescription(e, false)); const user = scannedUserExtensions.map(e => toExtensionDescription(e, false)); const development = scannedDevelopedExtensions.map(e => toExtensionDescription(e, true)); + const r = dedupExtensions(system, user, development, this._logService); const disposable = this._extensionsScannerService.onDidChangeCache(() => { disposable.dispose(); this._notificationService.prompt( @@ -71,11 +73,11 @@ export class CachedExtensionScanner { ); }); timeout(5000).then(() => disposable.dispose()); - return { system, user, development }; + return r; } catch (err) { this._logService.error(`Error scanning installed extensions:`); this._logService.error(err); - return { system: [], user: [], development: [] }; + return []; } } diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts b/src/vs/workbench/services/extensions/electron-sandbox/electronExtensionService.ts index 388033c90a1..3c267c40962 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/electron-sandbox/electronExtensionService.ts @@ -3,16 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ILocalProcessExtensionHostDataProvider, ILocalProcessExtensionHostInitData, LocalProcessExtensionHost } from 'vs/workbench/services/extensions/electron-browser/localProcessExtensionHost'; - import { CachedExtensionScanner } from 'vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner'; -import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { AbstractExtensionService, ExtensionHostCrashTracker, ExtensionRunningPreference, extensionRunningPreferenceToString, filterByRunningLocation } from 'vs/workbench/services/extensions/common/abstractExtensionService'; import * as nls from 'vs/nls'; import { runWhenIdle } from 'vs/base/common/async'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { IExtensionManagementService, IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { IWorkbenchExtensionEnablementService, EnablementState, IWebExtensionsScannerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IWorkbenchExtensionEnablementService, EnablementState, IWebExtensionsScannerService, IWorkbenchExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IRemoteExtensionHostDataProvider, RemoteExtensionHost, IRemoteExtensionHostInitData } from 'vs/workbench/services/extensions/common/remoteExtensionHost'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; @@ -33,7 +30,7 @@ import { flatten } from 'vs/base/common/arrays'; import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; import { IRemoteExplorerService } from 'vs/workbench/services/remote/common/remoteExplorerService'; import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; -import { getRemoteName } from 'vs/platform/remote/common/remoteHosts'; +import { getRemoteName, parseAuthorityWithPort } from 'vs/platform/remote/common/remoteHosts'; import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment'; import { IWebWorkerExtensionHostDataProvider, IWebWorkerExtensionHostInitData, WebWorkerExtensionHost } from 'vs/workbench/services/extensions/browser/webWorkerExtensionHost'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; @@ -50,8 +47,9 @@ import { StopWatch } from 'vs/base/common/stopwatch'; import { isCI } from 'vs/base/common/platform'; import { IResolveAuthorityErrorResult } from 'vs/workbench/services/extensions/common/extensionHostProxy'; import { URI } from 'vs/base/common/uri'; +import { ILocalProcessExtensionHostDataProvider, ILocalProcessExtensionHostInitData, SandboxLocalProcessExtensionHost } from 'vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost'; -export class ExtensionService extends AbstractExtensionService implements IExtensionService { +export abstract class ElectronExtensionService extends AbstractExtensionService implements IExtensionService { private readonly _enableLocalWebWorker: boolean; private readonly _lazyLocalWebWorker: boolean; @@ -68,15 +66,15 @@ export class ExtensionService extends AbstractExtensionService implements IExten @IWorkbenchExtensionEnablementService extensionEnablementService: IWorkbenchExtensionEnablementService, @IFileService fileService: IFileService, @IProductService productService: IProductService, - @IExtensionManagementService extensionManagementService: IExtensionManagementService, + @IWorkbenchExtensionManagementService extensionManagementService: IWorkbenchExtensionManagementService, @IWorkspaceContextService contextService: IWorkspaceContextService, @IConfigurationService configurationService: IConfigurationService, @IExtensionManifestPropertiesService extensionManifestPropertiesService: IExtensionManifestPropertiesService, @IWebExtensionsScannerService webExtensionsScannerService: IWebExtensionsScannerService, @ILogService logService: ILogService, @IRemoteAgentService remoteAgentService: IRemoteAgentService, + @ILifecycleService lifecycleService: ILifecycleService, @IRemoteAuthorityResolverService private readonly _remoteAuthorityResolverService: IRemoteAuthorityResolverService, - @ILifecycleService private readonly _lifecycleService: ILifecycleService, @INativeHostService private readonly _nativeHostService: INativeHostService, @IHostService private readonly _hostService: IHostService, @IRemoteExplorerService private readonly _remoteExplorerService: IRemoteExplorerService, @@ -97,7 +95,8 @@ export class ExtensionService extends AbstractExtensionService implements IExten extensionManifestPropertiesService, webExtensionsScannerService, logService, - remoteAgentService + remoteAgentService, + lifecycleService ); [this._enableLocalWebWorker, this._lazyLocalWebWorker] = this._isLocalWebWorkerEnabled(); @@ -110,7 +109,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten // some editors require the extension host to restore // and this would result in a deadlock // see https://github.com/microsoft/vscode/issues/41322 - this._lifecycleService.when(LifecyclePhase.Ready).then(() => { + lifecycleService.when(LifecyclePhase.Ready).then(() => { // reschedule to ensure this runs after restoring viewlets, panels, and editors runWhenIdle(() => { this._initialize(); @@ -155,7 +154,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten ])); } - private _createLocalExtensionHostDataProvider(isInitialStart: boolean, desiredRunningLocation: ExtensionRunningLocation): ILocalProcessExtensionHostDataProvider & IWebWorkerExtensionHostDataProvider { + protected _createLocalExtensionHostDataProvider(isInitialStart: boolean, desiredRunningLocation: ExtensionRunningLocation): ILocalProcessExtensionHostDataProvider & IWebWorkerExtensionHostDataProvider { return { getInitData: async (): Promise<ILocalProcessExtensionHostInitData & IWebWorkerExtensionHostInitData> => { if (isInitialStart) { @@ -193,7 +192,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten } protected _pickExtensionHostKind(extensionId: ExtensionIdentifier, extensionKinds: ExtensionKind[], isInstalledLocally: boolean, isInstalledRemotely: boolean, preference: ExtensionRunningPreference): ExtensionHostKind | null { - const result = ExtensionService.pickExtensionHostKind(extensionKinds, isInstalledLocally, isInstalledRemotely, preference, Boolean(this._environmentService.remoteAuthority), this._enableLocalWebWorker); + const result = ElectronExtensionService.pickExtensionHostKind(extensionKinds, isInstalledLocally, isInstalledRemotely, preference, Boolean(this._environmentService.remoteAuthority), this._enableLocalWebWorker); this._logService.trace(`pickRunningLocation for ${extensionId.value}, extension kinds: [${extensionKinds.join(', ')}], isInstalledLocally: ${isInstalledLocally}, isInstalledRemotely: ${isInstalledRemotely}, preference: ${extensionRunningPreferenceToString(preference)} => ${extensionHostKindToString(result)}`); return result; } @@ -240,7 +239,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten protected _createExtensionHost(runningLocation: ExtensionRunningLocation, isInitialStart: boolean): IExtensionHost | null { switch (runningLocation.kind) { case ExtensionHostKind.LocalProcess: { - return this._instantiationService.createInstance(LocalProcessExtensionHost, runningLocation, this._createLocalExtensionHostDataProvider(isInitialStart, runningLocation)); + return this._instantiationService.createInstance(SandboxLocalProcessExtensionHost, runningLocation, this._createLocalExtensionHostDataProvider(isInitialStart, runningLocation)); } case ExtensionHostKind.LocalWebWorker: { if (this._enableLocalWebWorker) { @@ -351,12 +350,12 @@ export class ExtensionService extends AbstractExtensionService implements IExten const authorityPlusIndex = remoteAuthority.indexOf('+'); if (authorityPlusIndex === -1) { // This authority does not need to be resolved, simply parse the port number - const lastColon = remoteAuthority.lastIndexOf(':'); + const { host, port } = parseAuthorityWithPort(remoteAuthority); return { authority: { authority: remoteAuthority, - host: remoteAuthority.substring(0, lastColon), - port: parseInt(remoteAuthority.substring(lastColon + 1), 10), + host, + port, connectionToken: undefined } }; @@ -596,6 +595,12 @@ export class ExtensionService extends AbstractExtensionService implements IExten // Dispose everything associated with the extension host this.stopExtensionHosts(); + // Dispose the management connection to avoid reconnecting after the extension host exits + const connection = this._remoteAgentService.getConnection(); + if (connection) { + connection.dispose(); + } + if (this._isExtensionDevTestFromCli) { // When CLI testing make sure to exit with proper exit code this._nativeHostService.exit(code); @@ -614,6 +619,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten const sendTelemetry = (userReaction: 'install' | 'enable' | 'cancel') => { /* __GDPR__ "remoteExtensionRecommendations:popup" : { + "owner": "sandy081", "userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "extensionId": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" } } @@ -676,8 +682,6 @@ function getRemoteAuthorityPrefix(remoteAuthority: string): string { return remoteAuthority.substring(0, plusIndex); } -registerSingleton(IExtensionService, ExtensionService); - class RestartExtensionHostAction extends Action2 { constructor() { diff --git a/src/vs/workbench/services/extensions/electron-sandbox/extensionHostProfiler.ts b/src/vs/workbench/services/extensions/electron-sandbox/extensionHostProfiler.ts index a78d85ff212..f2a029bf0a2 100644 --- a/src/vs/workbench/services/extensions/electron-sandbox/extensionHostProfiler.ts +++ b/src/vs/workbench/services/extensions/electron-sandbox/extensionHostProfiler.ts @@ -35,17 +35,17 @@ export class ExtensionHostProfiler { } private _distill(profile: IV8Profile, extensions: IExtensionDescription[]): IExtensionHostProfile { - let searchTree = TernarySearchTree.forUris<IExtensionDescription>(); - for (let extension of extensions) { + const searchTree = TernarySearchTree.forUris<IExtensionDescription>(); + for (const extension of extensions) { if (extension.extensionLocation.scheme === Schemas.file) { searchTree.set(URI.file(extension.extensionLocation.fsPath), extension); } } - let nodes = profile.nodes; - let idsToNodes = new Map<number, IV8ProfileNode>(); - let idsToSegmentId = new Map<number, ProfileSegmentId | null>(); - for (let node of nodes) { + const nodes = profile.nodes; + const idsToNodes = new Map<number, IV8ProfileNode>(); + const idsToSegmentId = new Map<number, ProfileSegmentId | null>(); + for (const node of nodes) { idsToNodes.set(node.id, node); } @@ -89,15 +89,15 @@ export class ExtensionHostProfiler { visit(nodes[0], null); const samples = profile.samples || []; - let timeDeltas = profile.timeDeltas || []; - let distilledDeltas: number[] = []; - let distilledIds: ProfileSegmentId[] = []; + const timeDeltas = profile.timeDeltas || []; + const distilledDeltas: number[] = []; + const distilledIds: ProfileSegmentId[] = []; let currSegmentTime = 0; let currSegmentId: string | undefined; for (let i = 0; i < samples.length; i++) { - let id = samples[i]; - let segmentId = idsToSegmentId.get(id); + const id = samples[i]; + const segmentId = idsToSegmentId.get(id); if (segmentId !== currSegmentId) { if (currSegmentId) { distilledIds.push(currSegmentId); @@ -120,9 +120,9 @@ export class ExtensionHostProfiler { ids: distilledIds, data: profile, getAggregatedTimes: () => { - let segmentsToTime = new Map<ProfileSegmentId, number>(); + const segmentsToTime = new Map<ProfileSegmentId, number>(); for (let i = 0; i < distilledIds.length; i++) { - let id = distilledIds[i]; + const id = distilledIds[i]; segmentsToTime.set(id, (segmentsToTime.get(id) || 0) + distilledDeltas[i]); } return segmentsToTime; diff --git a/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts b/src/vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost.ts index 0f7f60bbc7d..97eeb78e28e 100644 --- a/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts +++ b/src/vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost.ts @@ -3,21 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Server, Socket, createServer } from 'net'; -import { createRandomIPCHandle, NodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; - import * as nls from 'vs/nls'; import { timeout } from 'vs/base/common/async'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { Emitter, Event } from 'vs/base/common/event'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import * as objects from 'vs/base/common/objects'; import * as platform from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; -import { IRemoteConsoleLog, log } from 'vs/base/common/console'; -import { logRemoteEntry, logRemoteEntryIfError } from 'vs/workbench/services/extensions/common/remoteConsoleUtil'; import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; -import { PersistentProtocol } from 'vs/base/parts/ipc/common/ipc.net'; +import { BufferedEmitter } from 'vs/base/parts/ipc/common/ipc.net'; import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; import { ILabelService } from 'vs/platform/label/common/label'; import { ILifecycleService, WillShutdownEvent } from 'vs/workbench/services/lifecycle/common/lifecycle'; @@ -27,7 +22,7 @@ import { INotificationService, Severity } from 'vs/platform/notification/common/ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; import { isUntitledWorkspace, IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { MessageType, createMessageOfType, isMessageOfType, IExtensionHostInitData, UIKind } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; +import { MessageType, isMessageOfType, IExtensionHostInitData, UIKind, NativeLogMarkers } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; import { withNullAsUndefined } from 'vs/base/common/types'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { parseExtensionDevOptions } from '../common/extensionDevOptions'; @@ -40,10 +35,15 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IOutputChannelRegistry, Extensions } from 'vs/workbench/services/output/common/output'; import { IShellEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/shellEnvironmentService'; import { IExtensionHostProcessOptions, IExtensionHostStarter } from 'vs/platform/extensions/common/extensionHostStarter'; -import { SerializedError } from 'vs/base/common/errors'; +import { CancellationError, SerializedError } from 'vs/base/common/errors'; import { removeDangerousEnvVariables } from 'vs/base/common/processes'; import { StopWatch } from 'vs/base/common/stopwatch'; import { process } from 'vs/base/parts/sandbox/electron-sandbox/globals'; +import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { generateUuid } from 'vs/base/common/uuid'; +import { acquirePort } from 'vs/base/parts/ipc/electron-sandbox/ipc.mp'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { MessagePortExtHostConnection, writeExtHostConnection } from 'vs/workbench/services/extensions/common/extensionHostEnv'; export interface ILocalProcessExtensionHostInitData { readonly autoStart: boolean; @@ -55,12 +55,7 @@ export interface ILocalProcessExtensionHostDataProvider { getInitData(): Promise<ILocalProcessExtensionHostInitData>; } -const enum NativeLogMarkers { - Start = 'START_NATIVE_LOG', - End = 'END_NATIVE_LOG', -} - -class ExtensionHostProcess { +export class ExtensionHostProcess { private readonly _id: string; @@ -91,7 +86,7 @@ class ExtensionHostProcess { this._id = id; } - public start(opts: IExtensionHostProcessOptions): Promise<{ pid: number }> { + public start(opts: IExtensionHostProcessOptions): Promise<void> { return this._extensionHostStarter.start(this._id, opts); } @@ -104,7 +99,7 @@ class ExtensionHostProcess { } } -export class LocalProcessExtensionHost implements IExtensionHost { +export class SandboxLocalProcessExtensionHost implements IExtensionHost { public readonly remoteAuthority = null; public readonly lazyStart = false; @@ -115,7 +110,7 @@ export class LocalProcessExtensionHost implements IExtensionHost { private readonly _onDidSetInspectPort = new Emitter<void>(); - private readonly _toDispose = new DisposableStore(); + protected readonly _toDispose = new DisposableStore(); private readonly _isExtensionDevHost: boolean; private readonly _isExtensionDevDebug: boolean; @@ -127,11 +122,9 @@ export class LocalProcessExtensionHost implements IExtensionHost { private _terminating: boolean; // Resources, in order they get acquired/created when .start() is called: - private _namedPipeServer: Server | null; private _inspectPort: number | null; private _extensionHostProcess: ExtensionHostProcess | null; - private _extensionHostConnection: Socket | null; - private _messageProtocol: Promise<PersistentProtocol> | null; + private _messageProtocol: Promise<IMessagePassingProtocol> | null; private readonly _extensionHostLogFile: URI; @@ -143,14 +136,16 @@ export class LocalProcessExtensionHost implements IExtensionHost { @INativeHostService private readonly _nativeHostService: INativeHostService, @ILifecycleService private readonly _lifecycleService: ILifecycleService, @INativeWorkbenchEnvironmentService private readonly _environmentService: INativeWorkbenchEnvironmentService, + @IUserDataProfilesService private readonly _userDataProfilesService: IUserDataProfilesService, @ITelemetryService private readonly _telemetryService: ITelemetryService, - @ILogService private readonly _logService: ILogService, + @ILogService protected readonly _logService: ILogService, @ILabelService private readonly _labelService: ILabelService, @IExtensionHostDebugService private readonly _extensionHostDebugService: IExtensionHostDebugService, @IHostService private readonly _hostService: IHostService, @IProductService private readonly _productService: IProductService, @IShellEnvironmentService private readonly _shellEnvironmentService: IShellEnvironmentService, - @IExtensionHostStarter private readonly _extensionHostStarter: IExtensionHostStarter, + @IExtensionHostStarter protected readonly _extensionHostStarter: IExtensionHostStarter, + @IConfigurationService protected readonly _configurationService: IConfigurationService, ) { const devOpts = parseExtensionDevOptions(this._environmentService); this._isExtensionDevHost = devOpts.isExtensionDevHost; @@ -161,17 +156,14 @@ export class LocalProcessExtensionHost implements IExtensionHost { this._lastExtensionHostError = null; this._terminating = false; - this._namedPipeServer = null; this._inspectPort = null; this._extensionHostProcess = null; - this._extensionHostConnection = null; this._messageProtocol = null; this._extensionHostLogFile = joinPath(this._environmentService.extHostLogsPath, `${ExtensionHostLogFileName}.log`); this._toDispose.add(this._onExit); this._toDispose.add(this._lifecycleService.onWillShutdown(e => this._onWillShutdown(e))); - this._toDispose.add(this._lifecycleService.onDidShutdown(() => this.terminate())); this._toDispose.add(this._extensionHostDebugService.onClose(event => { if (this._isExtensionDevHost && this._environmentService.debugExtensionHost.debugId === event.sessionId) { this._nativeHostService.closeWindow(); @@ -185,183 +177,170 @@ export class LocalProcessExtensionHost implements IExtensionHost { } public dispose(): void { - this.terminate(); + if (this._terminating) { + return; + } + this._terminating = true; + + this._toDispose.dispose(); } - public start(): Promise<IMessagePassingProtocol> | null { + public start(): Promise<IMessagePassingProtocol> { if (this._terminating) { // .terminate() was called - return null; + throw new CancellationError(); } if (!this._messageProtocol) { - this._messageProtocol = Promise.all([ - this._extensionHostStarter.createExtensionHost(), - this._tryListenOnPipe(), - this._tryFindDebugPort(), - this._shellEnvironmentService.getShellEnv(), - ]).then(([extensionHostCreationResult, pipeName, portNumber, processEnv]) => { - - this._extensionHostProcess = new ExtensionHostProcess(extensionHostCreationResult.id, this._extensionHostStarter); - - const env = objects.mixin(processEnv, { - VSCODE_AMD_ENTRYPOINT: 'vs/workbench/api/node/extensionHostProcess', - VSCODE_PIPE_LOGGING: 'true', - VSCODE_VERBOSE_LOGGING: true, - VSCODE_LOG_NATIVE: this._isExtensionDevHost, - VSCODE_IPC_HOOK_EXTHOST: pipeName, - VSCODE_HANDLES_UNCAUGHT_ERRORS: true, - VSCODE_LOG_STACK: !this._isExtensionDevTestFromCli && (this._isExtensionDevHost || !this._environmentService.isBuilt || this._productService.quality !== 'stable' || this._environmentService.verbose) - }); + this._messageProtocol = this._start(); + } - if (this._environmentService.debugExtensionHost.env) { - objects.mixin(env, this._environmentService.debugExtensionHost.env); - } + return this._messageProtocol; + } - removeDangerousEnvVariables(env); + protected async _start(): Promise<IMessagePassingProtocol> { + const communication = this._toDispose.add(new ExtHostMessagePortCommunication(this._logService)); + return this._startWithCommunication(communication); + } - if (this._isExtensionDevHost) { - // Unset `VSCODE_CODE_CACHE_PATH` when developing extensions because it might - // be that dependencies, that otherwise would be cached, get modified. - delete env['VSCODE_CODE_CACHE_PATH']; - } + protected async _startWithCommunication<T>(communication: IExtHostCommunication<T>): Promise<IMessagePassingProtocol> { - const opts = { - env, - // We only detach the extension host on windows. Linux and Mac orphan by default - // and detach under Linux and Mac create another process group. - // We detach because we have noticed that when the renderer exits, its child processes - // (i.e. extension host) are taken down in a brutal fashion by the OS - detached: !!platform.isWindows, - execArgv: undefined as string[] | undefined, - silent: true - }; - - if (portNumber !== 0) { - opts.execArgv = [ - '--nolazy', - (this._isExtensionDevDebugBrk ? '--inspect-brk=' : '--inspect=') + portNumber - ]; - } else { - opts.execArgv = ['--inspect-port=0']; - } + const [extensionHostCreationResult, communicationPreparedData, portNumber, processEnv] = await Promise.all([ + this._extensionHostStarter.createExtensionHost(communication.useUtilityProcess), + communication.prepare(), + this._tryFindDebugPort(), + this._shellEnvironmentService.getShellEnv(), + ]); - if (this._environmentService.extensionTestsLocationURI) { - opts.execArgv.unshift('--expose-gc'); - } + this._extensionHostProcess = new ExtensionHostProcess(extensionHostCreationResult.id, this._extensionHostStarter); - if (this._environmentService.args['prof-v8-extensions']) { - opts.execArgv.unshift('--prof'); - } + const env = objects.mixin(processEnv, { + VSCODE_AMD_ENTRYPOINT: 'vs/workbench/api/node/extensionHostProcess', + VSCODE_HANDLES_UNCAUGHT_ERRORS: true + }); - if (this._environmentService.args['max-memory']) { - opts.execArgv.unshift(`--max-old-space-size=${this._environmentService.args['max-memory']}`); - } + if (this._environmentService.debugExtensionHost.env) { + objects.mixin(env, this._environmentService.debugExtensionHost.env); + } - // Catch all output coming from the extension host process - type Output = { data: string; format: string[] }; - const onStdout = this._handleProcessOutputStream(this._extensionHostProcess.onStdout); - const onStderr = this._handleProcessOutputStream(this._extensionHostProcess.onStderr); - const onOutput = Event.any( - Event.map(onStdout.event, o => ({ data: `%c${o}`, format: [''] })), - Event.map(onStderr.event, o => ({ data: `%c${o}`, format: ['color: red'] })) - ); + removeDangerousEnvVariables(env); - // Debounce all output, so we can render it in the Chrome console as a group - const onDebouncedOutput = Event.debounce<Output>(onOutput, (r, o) => { - return r - ? { data: r.data + o.data, format: [...r.format, ...o.format] } - : { data: o.data, format: o.format }; - }, 100); - - // Print out extension host output - onDebouncedOutput(output => { - const inspectorUrlMatch = output.data && output.data.match(/ws:\/\/([^\s]+:(\d+)\/[^\s]+)/); - if (inspectorUrlMatch) { - if (!this._environmentService.isBuilt && !this._isExtensionDevTestFromCli) { - console.log(`%c[Extension Host] %cdebugger inspector at chrome-devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=${inspectorUrlMatch[1]}`, 'color: blue', 'color:'); - } - if (!this._inspectPort) { - this._inspectPort = Number(inspectorUrlMatch[2]); - this._onDidSetInspectPort.fire(); - } - } else { - if (!this._isExtensionDevTestFromCli) { - console.group('Extension Host'); - console.log(output.data, ...output.format); - console.groupEnd(); - } - } - }); + if (this._isExtensionDevHost) { + // Unset `VSCODE_CODE_CACHE_PATH` when developing extensions because it might + // be that dependencies, that otherwise would be cached, get modified. + delete env['VSCODE_CODE_CACHE_PATH']; + } - // Support logging from extension host - this._extensionHostProcess.onMessage(msg => { - if (msg && (<IRemoteConsoleLog>msg).type === '__$console') { - this._logExtensionHostMessage(<IRemoteConsoleLog>msg); - } - }); + const opts: IExtensionHostProcessOptions = { + responseWindowId: this._environmentService.window.id, + responseChannel: 'vscode:startExtensionHostMessagePortResult', + responseNonce: generateUuid(), + env, + // We only detach the extension host on windows. Linux and Mac orphan by default + // and detach under Linux and Mac create another process group. + // We detach because we have noticed that when the renderer exits, its child processes + // (i.e. extension host) are taken down in a brutal fashion by the OS + detached: !!platform.isWindows, + execArgv: undefined as string[] | undefined, + silent: true + }; - // Lifecycle + if (portNumber !== 0) { + opts.execArgv = [ + '--nolazy', + (this._isExtensionDevDebugBrk ? '--inspect-brk=' : '--inspect=') + portNumber + ]; + } else { + opts.execArgv = ['--inspect-port=0']; + } - this._extensionHostProcess.onError((e) => this._onExtHostProcessError(e.error)); - this._extensionHostProcess.onExit(({ code, signal }) => this._onExtHostProcessExit(code, signal)); + if (this._environmentService.extensionTestsLocationURI) { + opts.execArgv.unshift('--expose-gc'); + } - // Notify debugger that we are ready to attach to the process if we run a development extension - if (portNumber) { - if (this._isExtensionDevHost && portNumber && this._isExtensionDevDebug && this._environmentService.debugExtensionHost.debugId) { - this._extensionHostDebugService.attachSession(this._environmentService.debugExtensionHost.debugId, portNumber); - } - this._inspectPort = portNumber; + if (this._environmentService.args['prof-v8-extensions']) { + opts.execArgv.unshift('--prof'); + } + + if (this._environmentService.args['max-memory']) { + opts.execArgv.unshift(`--max-old-space-size=${this._environmentService.args['max-memory']}`); + } + + // Catch all output coming from the extension host process + type Output = { data: string; format: string[] }; + const onStdout = this._handleProcessOutputStream(this._extensionHostProcess.onStdout); + const onStderr = this._handleProcessOutputStream(this._extensionHostProcess.onStderr); + const onOutput = Event.any( + Event.map(onStdout.event, o => ({ data: `%c${o}`, format: [''] })), + Event.map(onStderr.event, o => ({ data: `%c${o}`, format: ['color: red'] })) + ); + + // Debounce all output, so we can render it in the Chrome console as a group + const onDebouncedOutput = Event.debounce<Output>(onOutput, (r, o) => { + return r + ? { data: r.data + o.data, format: [...r.format, ...o.format] } + : { data: o.data, format: o.format }; + }, 100); + + // Print out extension host output + onDebouncedOutput(output => { + const inspectorUrlMatch = output.data && output.data.match(/ws:\/\/([^\s]+:(\d+)\/[^\s]+)/); + if (inspectorUrlMatch) { + if (!this._environmentService.isBuilt && !this._isExtensionDevTestFromCli) { + console.log(`%c[Extension Host] %cdebugger inspector at chrome-devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=${inspectorUrlMatch[1]}`, 'color: blue', 'color:'); + } + if (!this._inspectPort) { + this._inspectPort = Number(inspectorUrlMatch[2]); this._onDidSetInspectPort.fire(); } - - // Help in case we fail to start it - let startupTimeoutHandle: any; - if (!this._environmentService.isBuilt && !this._environmentService.remoteAuthority || this._isExtensionDevHost) { - startupTimeoutHandle = setTimeout(() => { - this._logService.error(`[LocalProcessExtensionHost]: Extension host did not start in 10 seconds (debugBrk: ${this._isExtensionDevDebugBrk})`); - - const msg = this._isExtensionDevDebugBrk - ? nls.localize('extensionHost.startupFailDebug', "Extension host did not start in 10 seconds, it might be stopped on the first line and needs a debugger to continue.") - : nls.localize('extensionHost.startupFail', "Extension host did not start in 10 seconds, that might be a problem."); - - this._notificationService.prompt(Severity.Warning, msg, - [{ - label: nls.localize('reloadWindow', "Reload Window"), - run: () => this._hostService.reload() - }], - { sticky: true } - ); - }, 10000); + } else { + if (!this._isExtensionDevTestFromCli) { + console.group('Extension Host'); + console.log(output.data, ...output.format); + console.groupEnd(); } + } + }); - // Initialize extension host process with hand shakes - return this._tryExtHostHandshake(opts).then((protocol) => { - clearTimeout(startupTimeoutHandle); - return protocol; - }); - }); + // Lifecycle + + this._extensionHostProcess.onError((e) => this._onExtHostProcessError(e.error)); + this._extensionHostProcess.onExit(({ code, signal }) => this._onExtHostProcessExit(code, signal)); + + // Notify debugger that we are ready to attach to the process if we run a development extension + if (portNumber) { + if (this._isExtensionDevHost && portNumber && this._isExtensionDevDebug && this._environmentService.debugExtensionHost.debugId) { + this._extensionHostDebugService.attachSession(this._environmentService.debugExtensionHost.debugId, portNumber); + } + this._inspectPort = portNumber; + this._onDidSetInspectPort.fire(); } - return this._messageProtocol; - } + // Help in case we fail to start it + let startupTimeoutHandle: any; + if (!this._environmentService.isBuilt && !this._environmentService.remoteAuthority || this._isExtensionDevHost) { + startupTimeoutHandle = setTimeout(() => { + this._logService.error(`[LocalProcessExtensionHost]: Extension host did not start in 10 seconds (debugBrk: ${this._isExtensionDevDebugBrk})`); + + const msg = this._isExtensionDevDebugBrk + ? nls.localize('extensionHost.startupFailDebug', "Extension host did not start in 10 seconds, it might be stopped on the first line and needs a debugger to continue.") + : nls.localize('extensionHost.startupFail', "Extension host did not start in 10 seconds, that might be a problem."); + + this._notificationService.prompt(Severity.Warning, msg, + [{ + label: nls.localize('reloadWindow', "Reload Window"), + run: () => this._hostService.reload() + }], + { sticky: true } + ); + }, 10000); + } - /** - * Start a server (`this._namedPipeServer`) that listens on a named pipe and return the named pipe name. - */ - private _tryListenOnPipe(): Promise<string> { - return new Promise<string>((resolve, reject) => { - const pipeName = createRandomIPCHandle(); - - this._namedPipeServer = createServer(); - this._namedPipeServer.on('error', reject); - this._namedPipeServer.listen(pipeName, () => { - if (this._namedPipeServer) { - this._namedPipeServer.removeListener('error', reject); - } - resolve(pipeName); - }); - }); + // Initialize extension host process with hand shakes + const protocol = await communication.establishProtocol(communicationPreparedData, this._extensionHostProcess, opts); + await this._performHandshake(protocol); + clearTimeout(startupTimeoutHandle); + return protocol; } /** @@ -394,102 +373,58 @@ export class LocalProcessExtensionHost implements IExtensionHost { return port || 0; } - private _tryExtHostHandshake(opts: IExtensionHostProcessOptions): Promise<PersistentProtocol> { + private _performHandshake(protocol: IMessagePassingProtocol): Promise<void> { + // 1) wait for the incoming `ready` event and send the initialization data. + // 2) wait for the incoming `initialized` event. + return new Promise<void>((resolve, reject) => { - return new Promise<PersistentProtocol>((resolve, reject) => { + let timeoutHandle: any; + const installTimeoutCheck = () => { + timeoutHandle = setTimeout(() => { + reject('The local extenion host took longer than 60s to send its ready message.'); + }, 60 * 1000); + }; + const uninstallTimeoutCheck = () => { + clearTimeout(timeoutHandle); + }; - // Wait for the extension host to connect to our named pipe - // and wrap the socket in the message passing protocol - let handle = setTimeout(() => { - if (this._namedPipeServer) { - this._namedPipeServer.close(); - this._namedPipeServer = null; - } - reject('The local extension host took longer than 60s to connect.'); - }, 60 * 1000); + // Wait 60s for the ready message + installTimeoutCheck(); - this._namedPipeServer!.on('connection', socket => { + const disposable = protocol.onMessage(msg => { - clearTimeout(handle); - if (this._namedPipeServer) { - this._namedPipeServer.close(); - this._namedPipeServer = null; - } - this._extensionHostConnection = socket; + if (isMessageOfType(msg, MessageType.Ready)) { - // using a buffered message protocol here because between now - // and the first time a `then` executes some messages might be lost - // unless we immediately register a listener for `onMessage`. - resolve(new PersistentProtocol(new NodeSocket(this._extensionHostConnection, 'renderer-exthost'))); - }); + // 1) Extension Host is ready to receive messages, initialize it + uninstallTimeoutCheck(); - // Now that the named pipe listener is installed, start the ext host process - const sw = StopWatch.create(false); - this._extensionHostProcess!.start(opts).then(() => { - const duration = sw.elapsed(); - if (platform.isCI) { - this._logService.info(`IExtensionHostStarter.start() took ${duration} ms.`); - } - }, (err) => { - // Starting the ext host process resulted in an error - reject(err); - }); - - }).then((protocol) => { - - // 1) wait for the incoming `ready` event and send the initialization data. - // 2) wait for the incoming `initialized` event. - return new Promise<PersistentProtocol>((resolve, reject) => { - - let timeoutHandle: NodeJS.Timer; - const installTimeoutCheck = () => { - timeoutHandle = setTimeout(() => { - reject('The local extenion host took longer than 60s to send its ready message.'); - }, 60 * 1000); - }; - const uninstallTimeoutCheck = () => { - clearTimeout(timeoutHandle); - }; - - // Wait 60s for the ready message - installTimeoutCheck(); - - const disposable = protocol.onMessage(msg => { - - if (isMessageOfType(msg, MessageType.Ready)) { - - // 1) Extension Host is ready to receive messages, initialize it - uninstallTimeoutCheck(); - - this._createExtHostInitData().then(data => { + this._createExtHostInitData().then(data => { - // Wait 60s for the initialized message - installTimeoutCheck(); + // Wait 60s for the initialized message + installTimeoutCheck(); - protocol.send(VSBuffer.fromString(JSON.stringify(data))); - }); - return; - } + protocol.send(VSBuffer.fromString(JSON.stringify(data))); + }); + return; + } - if (isMessageOfType(msg, MessageType.Initialized)) { + if (isMessageOfType(msg, MessageType.Initialized)) { - // 2) Extension Host is initialized - uninstallTimeoutCheck(); + // 2) Extension Host is initialized + uninstallTimeoutCheck(); - // stop listening for messages here - disposable.dispose(); + // stop listening for messages here + disposable.dispose(); - // Register log channel for exthost log - Registry.as<IOutputChannelRegistry>(Extensions.OutputChannels).registerChannel({ id: 'extHostLog', label: nls.localize('extension host Log', "Extension Host"), file: this._extensionHostLogFile, log: true }); + // Register log channel for exthost log + Registry.as<IOutputChannelRegistry>(Extensions.OutputChannels).registerChannel({ id: 'extHostLog', label: nls.localize('extension host Log', "Extension Host"), file: this._extensionHostLogFile, log: true }); - // release this promise - resolve(protocol); - return; - } - - console.error(`received unexpected message during handshake phase from the extension host: `, msg); - }); + // release this promise + resolve(); + return; + } + console.error(`received unexpected message during handshake phase from the extension host: `, msg); }); }); @@ -512,7 +447,7 @@ export class LocalProcessExtensionHost implements IExtensionHost { appLanguage: platform.language, extensionDevelopmentLocationURI: this._environmentService.extensionDevelopmentLocationURI, extensionTestsLocationURI: this._environmentService.extensionTestsLocationURI, - globalStorageHome: this._environmentService.globalStorageHome, + globalStorageHome: this._userDataProfilesService.defaultProfile.globalStorageHome, workspaceStorageHome: this._environmentService.workspaceStorageHome, }, workspace: this._contextService.getWorkbenchState() === WorkbenchState.EMPTY ? undefined : { @@ -527,6 +462,10 @@ export class LocalProcessExtensionHost implements IExtensionHost { connectionData: null, isRemote: false }, + consoleForward: { + includeStack: !this._isExtensionDevTestFromCli && (this._isExtensionDevHost || !this._environmentService.isBuilt || this._productService.quality !== 'stable' || this._environmentService.verbose), + logNative: !this._isExtensionDevTestFromCli && this._isExtensionDevHost + }, allExtensions: deltaExtensions.toAdd, myExtensions: deltaExtensions.myToAdd, telemetryInfo, @@ -538,17 +477,6 @@ export class LocalProcessExtensionHost implements IExtensionHost { }; } - private _logExtensionHostMessage(entry: IRemoteConsoleLog) { - if (this._isExtensionDevTestFromCli) { - // If running tests from cli, log to the log service everything - logRemoteEntry(this._logService, entry); - } else { - // Log to the log service only errors and log everything to local console - logRemoteEntryIfError(this._logService, entry, 'Extension Host'); - log(entry, 'Extension Host'); - } - } - private _onExtHostProcessError(_err: SerializedError): void { let err: any = _err; if (_err && _err.$isError) { @@ -558,7 +486,7 @@ export class LocalProcessExtensionHost implements IExtensionHost { err.stack = _err.stack; } - let errorMessage = toErrorMessage(err); + const errorMessage = toErrorMessage(err); if (errorMessage === this._lastExtensionHostError) { return; // prevent error spam } @@ -585,7 +513,7 @@ export class LocalProcessExtensionHost implements IExtensionHost { // not a fancy approach, but this is the same approach used by the split2 // module which is well-optimized (https://github.com/mcollina/split2) last += chunk; - let lines = last.split(/\r?\n/g); + const lines = last.split(/\r?\n/g); last = lines.pop()!; // protected against an extension spamming and leaking memory if no new line is written. @@ -632,63 +560,76 @@ export class LocalProcessExtensionHost implements IExtensionHost { return withNullAsUndefined(this._inspectPort); } - private terminate(): void { - if (this._terminating) { - return; + private _onWillShutdown(event: WillShutdownEvent): void { + // If the extension development host was started without debugger attached we need + // to communicate this back to the main side to terminate the debug session + if (this._isExtensionDevHost && !this._isExtensionDevTestFromCli && !this._isExtensionDevDebug && this._environmentService.debugExtensionHost.debugId) { + this._extensionHostDebugService.terminateSession(this._environmentService.debugExtensionHost.debugId); + event.join(timeout(100 /* wait a bit for IPC to get delivered */), { id: 'join.extensionDevelopment', label: nls.localize('join.extensionDevelopment', "Terminating extension debug session") }); } - this._terminating = true; + } +} - this._toDispose.dispose(); +export interface IExtHostCommunication<T> { + readonly useUtilityProcess: boolean; + prepare(): Promise<T>; + establishProtocol(prepared: T, extensionHostProcess: ExtensionHostProcess, opts: IExtensionHostProcessOptions): Promise<IMessagePassingProtocol>; +} - if (!this._messageProtocol) { - // .start() was not called - return; - } +export class ExtHostMessagePortCommunication extends Disposable implements IExtHostCommunication<void> { - this._messageProtocol.then((protocol) => { + readonly useUtilityProcess = true; - // Send the extension host a request to terminate itself - // (graceful termination) - protocol.send(createMessageOfType(MessageType.Terminate)); + constructor( + @ILogService private readonly _logService: ILogService + ) { + super(); + } - protocol.getSocket().dispose(); + async prepare(): Promise<void> { + } - protocol.dispose(); + establishProtocol(prepared: void, extensionHostProcess: ExtensionHostProcess, opts: IExtensionHostProcessOptions): Promise<IMessagePassingProtocol> { - // Give the extension host 10s, after which we will - // try to kill the process and release any resources - setTimeout(() => this._cleanResources(), 10 * 1000); + writeExtHostConnection(new MessagePortExtHostConnection(), opts.env); - }, (err) => { + // Get ready to acquire the message port from the shared process worker + const portPromise = acquirePort(undefined /* we trigger the request via service call! */, opts.responseChannel, opts.responseNonce); - // Establishing a protocol with the extension host failed, so - // try to kill the process and release any resources. - this._cleanResources(); - }); - } + return new Promise<IMessagePassingProtocol>((resolve, reject) => { - private _cleanResources(): void { - if (this._namedPipeServer) { - this._namedPipeServer.close(); - this._namedPipeServer = null; - } - if (this._extensionHostConnection) { - this._extensionHostConnection.end(); - this._extensionHostConnection = null; - } - if (this._extensionHostProcess) { - this._extensionHostProcess.kill(); - this._extensionHostProcess = null; - } - } + const handle = setTimeout(() => { + reject('The local extension host took longer than 60s to connect.'); + }, 60 * 1000); - private _onWillShutdown(event: WillShutdownEvent): void { + portPromise.then((port) => { + this._register(toDisposable(() => { + // Close the message port when the extension host is disposed + port.close(); + })); + clearTimeout(handle); - // If the extension development host was started without debugger attached we need - // to communicate this back to the main side to terminate the debug session - if (this._isExtensionDevHost && !this._isExtensionDevTestFromCli && !this._isExtensionDevDebug && this._environmentService.debugExtensionHost.debugId) { - this._extensionHostDebugService.terminateSession(this._environmentService.debugExtensionHost.debugId); - event.join(timeout(100 /* wait a bit for IPC to get delivered */), { id: 'join.extensionDevelopment', label: nls.localize('join.extensionDevelopment', "Terminating extension debug session") }); - } + const onMessage = new BufferedEmitter<VSBuffer>(); + port.onmessage = ((e) => onMessage.fire(VSBuffer.wrap(e.data))); + port.start(); + + resolve({ + onMessage: onMessage.event, + send: message => port.postMessage(message.buffer), + }); + }); + + // Now that the message port listener is installed, start the ext host process + const sw = StopWatch.create(false); + extensionHostProcess.start(opts).then(() => { + const duration = sw.elapsed(); + if (platform.isCI) { + this._logService.info(`IExtensionHostStarter.start() took ${duration} ms.`); + } + }, (err) => { + // Starting the ext host process resulted in an error + reject(err); + }); + }); } } diff --git a/src/vs/workbench/services/extensions/electron-sandbox/sandboxExtensionService.ts b/src/vs/workbench/services/extensions/electron-sandbox/sandboxExtensionService.ts new file mode 100644 index 00000000000..ea51d7abc8f --- /dev/null +++ b/src/vs/workbench/services/extensions/electron-sandbox/sandboxExtensionService.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 { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { ElectronExtensionService } from 'vs/workbench/services/extensions/electron-sandbox/electronExtensionService'; + +export class SandboxExtensionService extends ElectronExtensionService { +} + +registerSingleton(IExtensionService, SandboxExtensionService); diff --git a/src/vs/workbench/services/extensions/test/browser/extensionService.test.ts b/src/vs/workbench/services/extensions/test/browser/extensionService.test.ts index ecc2877518c..18d684cacaa 100644 --- a/src/vs/workbench/services/extensions/test/browser/extensionService.test.ts +++ b/src/vs/workbench/services/extensions/test/browser/extensionService.test.ts @@ -5,8 +5,34 @@ import * as assert from 'assert'; import { ExtensionService as BrowserExtensionService } from 'vs/workbench/services/extensions/browser/extensionService'; -import { ExtensionRunningPreference } from 'vs/workbench/services/extensions/common/abstractExtensionService'; -import { ExtensionHostKind } from 'vs/workbench/services/extensions/common/extensions'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { Event } from 'vs/base/common/event'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { ExtensionKind, IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { ExtensionIdentifier, IExtension, IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { IFileService } from 'vs/platform/files/common/files'; +import { createServices, TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { ILogService, NullLogService } from 'vs/platform/log/common/log'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; +import product from 'vs/platform/product/common/product'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IWorkspaceTrustEnablementService } from 'vs/platform/workspace/common/workspaceTrust'; +import { WorkspaceTrustEnablementService } from 'vs/workbench/services/workspaces/common/workspaceTrust'; +import { IWebExtensionsScannerService, IWorkbenchExtensionEnablementService, IWorkbenchExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { AbstractExtensionService, ExtensionRunningPreference } from 'vs/workbench/services/extensions/common/abstractExtensionService'; +import { ExtensionManifestPropertiesService, IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; +import { ExtensionHostKind, ExtensionRunningLocation, IExtensionHost, IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { TestEnvironmentService, TestFileService, TestLifecycleService, TestRemoteAgentService, TestWebExtensionsScannerService, TestWorkbenchExtensionEnablementService, TestWorkbenchExtensionManagementService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices'; +import { mock } from 'vs/base/test/common/mock'; +import { IExtensionHostManager } from 'vs/workbench/services/extensions/common/extensionHostManager'; suite('BrowserExtensionService', () => { test('pickRunningLocation', () => { @@ -87,3 +113,80 @@ suite('BrowserExtensionService', () => { assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['workspace', 'web', 'ui'], true, true, ExtensionRunningPreference.None), ExtensionHostKind.Remote); }); }); + +suite('ExtensionService', () => { + + class MyTestExtensionService extends AbstractExtensionService { + public readonly order: string[] = []; + protected _pickExtensionHostKind(extensionId: ExtensionIdentifier, extensionKinds: ExtensionKind[], isInstalledLocally: boolean, isInstalledRemotely: boolean, preference: ExtensionRunningPreference): ExtensionHostKind | null { + throw new Error('Method not implemented.'); + } + protected override _doCreateExtensionHostManager(extensionHostId: string, extensionHost: IExtensionHost, isInitialStart: boolean, initialActivationEvents: string[]): IExtensionHostManager { + const order = this.order; + order.push(`create ${extensionHostId}`); + return new class extends mock<IExtensionHostManager>() { + override onDidExit = Event.None; + override onDidChangeResponsiveState = Event.None; + override dispose(): void { + order.push(`dispose ${extensionHostId}`); + } + override representsRunningLocation(runningLocation: ExtensionRunningLocation): boolean { + return extensionHost.runningLocation.equals(runningLocation); + } + }; + } + protected _createExtensionHost(runningLocation: ExtensionRunningLocation, isInitialStart: boolean): IExtensionHost | null { + return new class extends mock<IExtensionHost>() { + override runningLocation = runningLocation; + }; + } + protected _scanAndHandleExtensions(): Promise<void> { + throw new Error('Method not implemented.'); + } + protected _scanSingleExtension(extension: IExtension): Promise<Readonly<IRelaxedExtensionDescription> | null> { + throw new Error('Method not implemented.'); + } + public _onExtensionHostExit(code: number): void { + throw new Error('Method not implemented.'); + } + } + + let disposables: DisposableStore; + let instantiationService: TestInstantiationService; + let extService: MyTestExtensionService; + + setup(() => { + disposables = new DisposableStore(); + instantiationService = createServices(disposables, [ + // custom + [IExtensionService, MyTestExtensionService], + // default + [ILifecycleService, TestLifecycleService], + [IWorkbenchExtensionManagementService, TestWorkbenchExtensionManagementService], + [INotificationService, TestNotificationService], + [IRemoteAgentService, TestRemoteAgentService], + [ILogService, NullLogService], + [IWebExtensionsScannerService, TestWebExtensionsScannerService], + [IExtensionManifestPropertiesService, ExtensionManifestPropertiesService], + [IConfigurationService, TestConfigurationService], + [IWorkspaceContextService, TestContextService], + [IProductService, { _serviceBrand: undefined, ...product }], + [IFileService, TestFileService], + [IWorkbenchExtensionEnablementService, TestWorkbenchExtensionEnablementService], + [ITelemetryService, NullTelemetryService], + [IEnvironmentService, TestEnvironmentService], + [IWorkspaceTrustEnablementService, WorkspaceTrustEnablementService] + ]); + extService = <MyTestExtensionService>instantiationService.get(IExtensionService); + }); + + teardown(() => { + disposables.dispose(); + }); + + test('issue #152204: Remote extension host not disposed after closing vscode client', async () => { + await extService.startExtensionHosts(); + extService.stopExtensionHosts(); + assert.deepStrictEqual(extService.order, (['create 1', 'create 2', 'create 3', 'dispose 3', 'dispose 2', 'dispose 1'])); + }); +}); diff --git a/src/vs/workbench/services/extensions/test/browser/extensionStorageMigration.test.ts b/src/vs/workbench/services/extensions/test/browser/extensionStorageMigration.test.ts index 47167afb1a8..bbee18ae82b 100644 --- a/src/vs/workbench/services/extensions/test/browser/extensionStorageMigration.test.ts +++ b/src/vs/workbench/services/extensions/test/browser/extensionStorageMigration.test.ts @@ -19,12 +19,15 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { TestWorkspace } from 'vs/platform/workspace/test/common/testWorkspace'; import { migrateExtensionStorage } from 'vs/workbench/services/extensions/common/extensionStorageMigration'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { IUserDataProfilesService, UserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { UserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfileService'; +import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; suite('ExtensionStorageMigration', () => { const disposables = new DisposableStore(); const ROOT = URI.file('tests').with({ scheme: 'vscode-tests' }); - const globalStorageHome = joinPath(ROOT, 'globalStorageHome'), workspaceStorageHome = joinPath(ROOT, 'workspaceStorageHome'); + const workspaceStorageHome = joinPath(ROOT, 'workspaceStorageHome'); let instantiationService: TestInstantiationService; @@ -34,7 +37,9 @@ suite('ExtensionStorageMigration', () => { const fileService = disposables.add(new FileService(new NullLogService())); fileService.registerProvider(ROOT.scheme, disposables.add(new InMemoryFileSystemProvider())); instantiationService.stub(IFileService, fileService); - instantiationService.stub(IEnvironmentService, <Partial<IEnvironmentService>>{ globalStorageHome, workspaceStorageHome }); + const environmentService = instantiationService.stub(IEnvironmentService, <Partial<IEnvironmentService>>{ userRoamingDataHome: ROOT, workspaceStorageHome }); + const userDataProfilesService = instantiationService.stub(IUserDataProfilesService, new UserDataProfilesService(environmentService, fileService, new NullLogService())); + instantiationService.stub(IUserDataProfileService, new UserDataProfileService(userDataProfilesService.defaultProfile)); instantiationService.stub(IExtensionStorageService, instantiationService.createInstance(ExtensionStorageService)); }); @@ -43,11 +48,11 @@ suite('ExtensionStorageMigration', () => { test('migrate extension storage', async () => { const fromExtensionId = 'pub.from', toExtensionId = 'pub.to', storageMigratedKey = `extensionStorage.migrate.${fromExtensionId}-${toExtensionId}`; - const extensionStorageService = instantiationService.get(IExtensionStorageService), fileService = instantiationService.get(IFileService), storageService = instantiationService.get(IStorageService); + const extensionStorageService = instantiationService.get(IExtensionStorageService), fileService = instantiationService.get(IFileService), storageService = instantiationService.get(IStorageService), userDataProfilesService = instantiationService.get(IUserDataProfilesService); extensionStorageService.setExtensionState(fromExtensionId, { globalKey: 'hello global state' }, true); extensionStorageService.setExtensionState(fromExtensionId, { workspaceKey: 'hello workspace state' }, false); - await fileService.writeFile(joinPath(globalStorageHome, fromExtensionId), VSBuffer.fromString('hello global storage')); + await fileService.writeFile(joinPath(userDataProfilesService.defaultProfile.globalStorageHome, fromExtensionId), VSBuffer.fromString('hello global storage')); await fileService.writeFile(joinPath(workspaceStorageHome, TestWorkspace.id, fromExtensionId), VSBuffer.fromString('hello workspace storage')); await migrateExtensionStorage(fromExtensionId, toExtensionId, true, instantiationService); @@ -55,37 +60,37 @@ suite('ExtensionStorageMigration', () => { assert.deepStrictEqual(extensionStorageService.getExtensionState(fromExtensionId, true), undefined); assert.deepStrictEqual(extensionStorageService.getExtensionState(fromExtensionId, false), undefined); - assert.deepStrictEqual((await fileService.exists(joinPath(globalStorageHome, fromExtensionId))), false); + assert.deepStrictEqual((await fileService.exists(joinPath(userDataProfilesService.defaultProfile.globalStorageHome, fromExtensionId))), false); assert.deepStrictEqual((await fileService.exists(joinPath(workspaceStorageHome, TestWorkspace.id, fromExtensionId))), false); assert.deepStrictEqual(extensionStorageService.getExtensionState(toExtensionId, true), { globalKey: 'hello global state' }); assert.deepStrictEqual(extensionStorageService.getExtensionState(toExtensionId, false), { workspaceKey: 'hello workspace state' }); - assert.deepStrictEqual((await fileService.readFile(joinPath(globalStorageHome, toExtensionId))).value.toString(), 'hello global storage'); + assert.deepStrictEqual((await fileService.readFile(joinPath(userDataProfilesService.defaultProfile.globalStorageHome, toExtensionId))).value.toString(), 'hello global storage'); assert.deepStrictEqual((await fileService.readFile(joinPath(workspaceStorageHome, TestWorkspace.id, toExtensionId))).value.toString(), 'hello workspace storage'); - assert.deepStrictEqual(storageService.get(storageMigratedKey, StorageScope.GLOBAL), 'true'); + assert.deepStrictEqual(storageService.get(storageMigratedKey, StorageScope.PROFILE), 'true'); assert.deepStrictEqual(storageService.get(storageMigratedKey, StorageScope.WORKSPACE), 'true'); }); test('migrate extension storage when does not exist', async () => { const fromExtensionId = 'pub.from', toExtensionId = 'pub.to', storageMigratedKey = `extensionStorage.migrate.${fromExtensionId}-${toExtensionId}`; - const extensionStorageService = instantiationService.get(IExtensionStorageService), fileService = instantiationService.get(IFileService), storageService = instantiationService.get(IStorageService); + const extensionStorageService = instantiationService.get(IExtensionStorageService), fileService = instantiationService.get(IFileService), storageService = instantiationService.get(IStorageService), userDataProfilesService = instantiationService.get(IUserDataProfilesService); await migrateExtensionStorage(fromExtensionId, toExtensionId, true, instantiationService); await migrateExtensionStorage(fromExtensionId, toExtensionId, false, instantiationService); assert.deepStrictEqual(extensionStorageService.getExtensionState(fromExtensionId, true), undefined); assert.deepStrictEqual(extensionStorageService.getExtensionState(fromExtensionId, false), undefined); - assert.deepStrictEqual((await fileService.exists(joinPath(globalStorageHome, fromExtensionId))), false); + assert.deepStrictEqual((await fileService.exists(joinPath(userDataProfilesService.defaultProfile.globalStorageHome, fromExtensionId))), false); assert.deepStrictEqual((await fileService.exists(joinPath(workspaceStorageHome, TestWorkspace.id, fromExtensionId))), false); assert.deepStrictEqual(extensionStorageService.getExtensionState(toExtensionId, true), undefined); assert.deepStrictEqual(extensionStorageService.getExtensionState(toExtensionId, false), undefined); - assert.deepStrictEqual((await fileService.exists(joinPath(globalStorageHome, toExtensionId))), false); + assert.deepStrictEqual((await fileService.exists(joinPath(userDataProfilesService.defaultProfile.globalStorageHome, toExtensionId))), false); assert.deepStrictEqual((await fileService.exists(joinPath(workspaceStorageHome, TestWorkspace.id, toExtensionId))), false); - assert.deepStrictEqual(storageService.get(storageMigratedKey, StorageScope.GLOBAL), 'true'); + assert.deepStrictEqual(storageService.get(storageMigratedKey, StorageScope.PROFILE), 'true'); assert.deepStrictEqual(storageService.get(storageMigratedKey, StorageScope.WORKSPACE), 'true'); }); diff --git a/src/vs/workbench/services/extensions/test/common/extensionDescriptionRegistry.test.ts b/src/vs/workbench/services/extensions/test/common/extensionDescriptionRegistry.test.ts new file mode 100644 index 00000000000..8249f569082 --- /dev/null +++ b/src/vs/workbench/services/extensions/test/common/extensionDescriptionRegistry.test.ts @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { URI } from 'vs/base/common/uri'; +import { ExtensionIdentifier, IExtensionDescription, TargetPlatform } from 'vs/platform/extensions/common/extensions'; +import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; + +suite('ExtensionDescriptionRegistry', () => { + test('allow removing and adding the same extension at a different version', () => { + const idA = new ExtensionIdentifier('a'); + const extensionA1 = desc(idA, '1.0.0'); + const extensionA2 = desc(idA, '2.0.0'); + + const registry = new ExtensionDescriptionRegistry([extensionA1]); + registry.deltaExtensions([extensionA2], [idA]); + + assert.deepStrictEqual(registry.getAllExtensionDescriptions(), [extensionA2]); + }); + + function desc(id: ExtensionIdentifier, version: string, activationEvents: string[] = ['*']): IExtensionDescription { + return { + name: id.value, + publisher: 'test', + version: '0.0.0', + engines: { vscode: '^1.0.0' }, + identifier: id, + extensionLocation: URI.parse(`nothing://nowhere`), + isBuiltin: false, + isUnderDevelopment: false, + isUserBuiltin: false, + activationEvents, + main: 'index.js', + targetPlatform: TargetPlatform.UNDEFINED, + extensionDependencies: [] + }; + } +}); diff --git a/src/vs/workbench/services/extensions/test/common/rpcProtocol.test.ts b/src/vs/workbench/services/extensions/test/common/rpcProtocol.test.ts index bc023d0e50e..7724cb3d3c3 100644 --- a/src/vs/workbench/services/extensions/test/common/rpcProtocol.test.ts +++ b/src/vs/workbench/services/extensions/test/common/rpcProtocol.test.ts @@ -39,13 +39,13 @@ suite('RPCProtocol', () => { } setup(() => { - let a_protocol = new MessagePassingProtocol(); - let b_protocol = new MessagePassingProtocol(); + const a_protocol = new MessagePassingProtocol(); + const b_protocol = new MessagePassingProtocol(); a_protocol.setPair(b_protocol); b_protocol.setPair(a_protocol); - let A = new RPCProtocol(a_protocol); - let B = new RPCProtocol(b_protocol); + const A = new RPCProtocol(a_protocol); + const B = new RPCProtocol(b_protocol); const bIdentifier = new ProxyIdentifier<BClass>('bb'); const bInstance = new BClass(); @@ -74,7 +74,7 @@ suite('RPCProtocol', () => { assert.ok(a1 instanceof VSBuffer); return a1.buffer[a2]; }; - let b = VSBuffer.alloc(4); + const b = VSBuffer.alloc(4); b.buffer[0] = 1; b.buffer[1] = 2; b.buffer[2] = 3; @@ -87,7 +87,7 @@ suite('RPCProtocol', () => { test('returning a buffer', function (done) { delegate = (a1: number, a2: number) => { - let b = VSBuffer.alloc(4); + const b = VSBuffer.alloc(4); b.buffer[0] = 1; b.buffer[1] = 2; b.buffer[2] = 3; @@ -106,7 +106,7 @@ suite('RPCProtocol', () => { test('cancelling a call via CancellationToken before', function (done) { delegate = (a1: number, a2: number) => a1 + a2; - let p = bProxy.$m(4, CancellationToken.Cancelled); + const p = bProxy.$m(4, CancellationToken.Cancelled); p.then((res: number) => { assert.fail('should not receive result'); }, (err) => { @@ -135,8 +135,8 @@ suite('RPCProtocol', () => { }); }); }; - let tokenSource = new CancellationTokenSource(); - let p = bProxy.$m(4, tokenSource.token); + const tokenSource = new CancellationTokenSource(); + const p = bProxy.$m(4, tokenSource.token); p.then((res: number) => { assert.strictEqual(res, 7); }, (err) => { @@ -169,7 +169,7 @@ suite('RPCProtocol', () => { test('issue #60450: Converting circular structure to JSON', function (done) { delegate = (a1: number, a2: number) => { - let circular = <any>{}; + const circular = <any>{}; circular.self = circular; return circular; }; @@ -204,7 +204,7 @@ suite('RPCProtocol', () => { }); test('issue #81424: SerializeRequest should throw if an argument can not be serialized', () => { - let badObject = {}; + const badObject = {}; (<any>badObject).loop = badObject; assert.throws(() => { diff --git a/src/vs/workbench/services/history/browser/historyService.ts b/src/vs/workbench/services/history/browser/historyService.ts index 6692e1c2dd1..e0fb71b15ed 100644 --- a/src/vs/workbench/services/history/browser/historyService.ts +++ b/src/vs/workbench/services/history/browser/historyService.ts @@ -1360,7 +1360,7 @@ export class EditorNavigationStack extends Disposable { return; } - let entryLabels: string[] = []; + const entryLabels: string[] = []; for (const entry of this.stack) { if (typeof entry.selection?.log === 'function') { entryLabels.push(`- group: ${entry.groupId}, editor: ${entry.editor.resource?.toString()}, selection: ${entry.selection.log()}`); @@ -1574,7 +1574,7 @@ ${entryLabels.join('\n')} const newStackEntry: IEditorNavigationStackEntry = { groupId, editor, selection }; // Replace at current position - let removedEntries: IEditorNavigationStackEntry[] = []; + const removedEntries: IEditorNavigationStackEntry[] = []; if (replace) { if (this.current) { removedEntries.push(this.current); diff --git a/src/vs/workbench/services/history/test/browser/historyService.test.ts b/src/vs/workbench/services/history/test/browser/historyService.test.ts index c86ebaf8e78..c2a7ef4def6 100644 --- a/src/vs/workbench/services/history/test/browser/historyService.test.ts +++ b/src/vs/workbench/services/history/test/browser/historyService.test.ts @@ -300,7 +300,7 @@ suite('HistoryService', function () { // [one.txt] [>two.html<] | <empty> - let editorChangePromise = Event.toPromise(editorService.onDidActiveEditorChange); + const editorChangePromise = Event.toPromise(editorService.onDidActiveEditorChange); pane1?.group?.moveEditor(pane1.input!, sideGroup); await editorChangePromise; diff --git a/src/vs/workbench/services/hover/browser/hoverService.ts b/src/vs/workbench/services/hover/browser/hoverService.ts index f0a3c2487f4..37cd88393a3 100644 --- a/src/vs/workbench/services/hover/browser/hoverService.ts +++ b/src/vs/workbench/services/hover/browser/hoverService.ts @@ -38,7 +38,11 @@ export class HoverService implements IHoverService { const hoverDisposables = new DisposableStore(); const hover = this._instantiationService.createInstance(HoverWidget, options); hover.onDispose(() => { - this._currentHoverOptions = undefined; + // Only clear the current options if it's the current hover, the current options help + // reduce flickering when the same hover is shown multiple times + if (this._currentHoverOptions === options) { + this._currentHoverOptions = undefined; + } hoverDisposables.dispose(); }); const provider = this._contextViewService as IContextViewProvider; diff --git a/src/vs/workbench/services/hover/browser/hoverWidget.ts b/src/vs/workbench/services/hover/browser/hoverWidget.ts index 24f4173e3f8..76d9654ef27 100644 --- a/src/vs/workbench/services/hover/browser/hoverWidget.ts +++ b/src/vs/workbench/services/hover/browser/hoverWidget.ts @@ -507,7 +507,7 @@ export class HoverWidget extends Widget { } class CompositeMouseTracker extends Widget { - private _isMouseIn: boolean = false; + private _isMouseIn: boolean = true; private _mouseTimeout: number | undefined; private readonly _onMouseOut = this._register(new Emitter<void>()); @@ -520,7 +520,7 @@ class CompositeMouseTracker extends Widget { ) { super(); this._elements.forEach(n => this.onmouseover(n, () => this._onTargetMouseOver())); - this._elements.forEach(n => this.onnonbubblingmouseout(n, () => this._onTargetMouseOut())); + this._elements.forEach(n => this.onmouseleave(n, () => this._onTargetMouseLeave())); } private _onTargetMouseOver(): void { @@ -528,7 +528,7 @@ class CompositeMouseTracker extends Widget { this._clearEvaluateMouseStateTimeout(); } - private _onTargetMouseOut(): void { + private _onTargetMouseLeave(): void { this._isMouseIn = false; this._evaluateMouseState(); } diff --git a/src/vs/workbench/services/integrity/electron-sandbox/integrityService.ts b/src/vs/workbench/services/integrity/electron-sandbox/integrityService.ts index 75e464e95f4..7125f82440e 100644 --- a/src/vs/workbench/services/integrity/electron-sandbox/integrityService.ts +++ b/src/vs/workbench/services/integrity/electron-sandbox/integrityService.ts @@ -33,7 +33,7 @@ class IntegrityStorage { } private _read(): IStorageData | null { - let jsonValue = this.storageService.get(IntegrityStorage.KEY, StorageScope.GLOBAL); + const jsonValue = this.storageService.get(IntegrityStorage.KEY, StorageScope.APPLICATION); if (!jsonValue) { return null; } @@ -50,7 +50,7 @@ class IntegrityStorage { set(data: IStorageData | null): void { this.value = data; - this.storageService.store(IntegrityStorage.KEY, JSON.stringify(this.value), StorageScope.GLOBAL, StorageTarget.MACHINE); + this.storageService.store(IntegrityStorage.KEY, JSON.stringify(this.value), StorageScope.APPLICATION, StorageTarget.MACHINE); } } diff --git a/src/vs/workbench/services/keybinding/browser/keybindingService.ts b/src/vs/workbench/services/keybinding/browser/keybindingService.ts index b55025bade5..ccd362686a0 100644 --- a/src/vs/workbench/services/keybinding/browser/keybindingService.ts +++ b/src/vs/workbench/services/keybinding/browser/keybindingService.ts @@ -17,10 +17,9 @@ import { ICommandService, CommandsRegistry } from 'vs/platform/commands/common/c import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ConfigurationScope, Extensions as ConfigExtensions, IConfigurationNode, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { ContextKeyExpr, IContextKeyService, ContextKeyExpression, IContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { Extensions, IJSONContributionRegistry } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import { AbstractKeybindingService } from 'vs/platform/keybinding/common/abstractKeybindingService'; -import { IKeyboardEvent, IUserFriendlyKeybinding, KeybindingSource, IKeybindingService, IKeybindingEvent, KeybindingsSchemaContribution } from 'vs/platform/keybinding/common/keybinding'; +import { IKeyboardEvent, IUserFriendlyKeybinding, IKeybindingService, KeybindingsSchemaContribution } from 'vs/platform/keybinding/common/keybinding'; import { KeybindingResolver } from 'vs/platform/keybinding/common/keybindingResolver'; import { IKeybindingItem, IExtensionKeybindingRule, KeybindingWeight, KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem'; @@ -35,9 +34,8 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { MenuRegistry } from 'vs/platform/actions/common/actions'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { commandsExtensionPoint } from 'vs/workbench/services/actions/common/menusExtensionPoint'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { RunOnceScheduler } from 'vs/base/common/async'; -import { URI } from 'vs/base/common/uri'; import { FileOperation, IFileService } from 'vs/platform/files/common/files'; import { parse } from 'vs/base/common/json'; import * as objects from 'vs/base/common/objects'; @@ -52,6 +50,7 @@ import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { dirname } from 'vs/base/common/resources'; import { getAllUnboundCommands } from 'vs/workbench/services/keybinding/browser/unboundCommands'; import { UserSettingsLabelProvider } from 'vs/base/common/keybindingLabels'; +import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; interface ContributedKeyBinding { command: string; @@ -191,7 +190,7 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService { @ICommandService commandService: ICommandService, @ITelemetryService telemetryService: ITelemetryService, @INotificationService notificationService: INotificationService, - @IEnvironmentService environmentService: IEnvironmentService, + @IUserDataProfileService userDataProfileService: IUserDataProfileService, @IConfigurationService configurationService: IConfigurationService, @IHostService private readonly hostService: IHostService, @IExtensionService extensionService: IExtensionService, @@ -213,29 +212,26 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService { dispatchConfig = newDispatchConfig; this._keyboardMapper = this.keyboardLayoutService.getKeyboardMapper(dispatchConfig); - this.updateResolver({ source: KeybindingSource.Default }); + this.updateResolver(); }); this._keyboardMapper = this.keyboardLayoutService.getKeyboardMapper(dispatchConfig); this.keyboardLayoutService.onDidChangeKeyboardLayout(() => { this._keyboardMapper = this.keyboardLayoutService.getKeyboardMapper(dispatchConfig); - this.updateResolver({ source: KeybindingSource.Default }); + this.updateResolver(); }); this._cachedResolver = null; - this.userKeybindings = this._register(new UserKeybindings(environmentService.keybindingsResource, fileService, logService)); + this.userKeybindings = this._register(new UserKeybindings(userDataProfileService, fileService, logService)); this.userKeybindings.initialize().then(() => { if (this.userKeybindings.keybindings.length) { - this.updateResolver({ source: KeybindingSource.User }); + this.updateResolver(); } }); this._register(this.userKeybindings.onDidChange(() => { logService.debug('User keybindings changed'); - this.updateResolver({ - source: KeybindingSource.User, - keybindings: this.userKeybindings.keybindings - }); + this.updateResolver(); })); keybindingsExtPoint.setHandler((extensions) => { @@ -246,7 +242,7 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService { } KeybindingsRegistry.setExtensionKeybindings(keybindings); - this.updateResolver({ source: KeybindingSource.Default }); + this.updateResolver(); }); this.updateSchema(); @@ -291,7 +287,7 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService { // update resolver which will bring back all unbound keyboard shortcuts this._cachedResolver = null; - this._onDidUpdateKeybindings.fire({ source: KeybindingSource.User }); + this._onDidUpdateKeybindings.fire(); })); } @@ -330,7 +326,7 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService { } const firstRowIndentation = firstRow.length; - let isFirst = true; + const isFirst = true; for (const resolvedKeybinding of resolvedKeybindings) { if (isFirst) { output.push(`${firstRow}${this._printResolvedKeybinding(resolvedKeybinding).padStart(padLength, ' ')}`); @@ -397,9 +393,9 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService { return this.userKeybindings.keybindings.length; } - private updateResolver(event: IKeybindingEvent): void { + private updateResolver(): void { this._cachedResolver = null; - this._onDidUpdateKeybindings.fire(event); + this._onDidUpdateKeybindings.fire(); } protected _getResolver(): KeybindingResolver { @@ -710,34 +706,49 @@ class UserKeybindings extends Disposable { private readonly reloadConfigurationScheduler: RunOnceScheduler; + private readonly watchDisposables = this._register(new DisposableStore()); + private readonly _onDidChange: Emitter<void> = this._register(new Emitter<void>()); readonly onDidChange: Event<void> = this._onDidChange.event; constructor( - private readonly keybindingsResource: URI, + private readonly userDataProfileService: IUserDataProfileService, private readonly fileService: IFileService, logService: ILogService, ) { super(); - this._register(fileService.watch(dirname(keybindingsResource))); - // Also listen to the resource incase the resource is a symlink - https://github.com/microsoft/vscode/issues/118134 - this._register(this.fileService.watch(this.keybindingsResource)); + this.watch(); + this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this.reload().then(changed => { if (changed) { this._onDidChange.fire(); } }), 50)); - this._register(Event.filter(this.fileService.onDidFilesChange, e => e.contains(this.keybindingsResource))(() => { + + this._register(Event.filter(this.fileService.onDidFilesChange, e => e.contains(this.userDataProfileService.currentProfile.keybindingsResource))(() => { logService.debug('Keybindings file changed'); this.reloadConfigurationScheduler.schedule(); })); + this._register(this.fileService.onDidRunOperation((e) => { - if (e.operation === FileOperation.WRITE && e.resource.toString() === this.keybindingsResource.toString()) { + if (e.operation === FileOperation.WRITE && e.resource.toString() === this.userDataProfileService.currentProfile.keybindingsResource.toString()) { logService.debug('Keybindings file written'); this.reloadConfigurationScheduler.schedule(); } })); + + this._register(userDataProfileService.onDidChangeCurrentProfile(e => { + this.watch(); + this.reloadConfigurationScheduler.schedule(); + })); + } + + private watch(): void { + this.watchDisposables.clear(); + this.watchDisposables.add(this.fileService.watch(dirname(this.userDataProfileService.currentProfile.keybindingsResource))); + // Also listen to the resource incase the resource is a symlink - https://github.com/microsoft/vscode/issues/118134 + this.watchDisposables.add(this.fileService.watch(this.userDataProfileService.currentProfile.keybindingsResource)); } async initialize(): Promise<void> { @@ -747,7 +758,7 @@ class UserKeybindings extends Disposable { private async reload(): Promise<boolean> { const existing = this._keybindings; try { - const content = await this.fileService.readFile(this.keybindingsResource); + const content = await this.fileService.readFile(this.userDataProfileService.currentProfile.keybindingsResource); const value = parse(content.value.toString()); this._keybindings = isArray(value) ? value : []; } catch (e) { diff --git a/src/vs/workbench/services/keybinding/browser/keyboardLayoutService.ts b/src/vs/workbench/services/keybinding/browser/keyboardLayoutService.ts index 9b013e05a2b..0e630dc4ce8 100644 --- a/src/vs/workbench/services/keybinding/browser/keyboardLayoutService.ts +++ b/src/vs/workbench/services/keybinding/browser/keyboardLayoutService.ts @@ -113,7 +113,7 @@ export class BrowserKeyboardMapperFactoryBase { return null; } - let usStandard = this.getUSStandardLayout(); + const usStandard = this.getUSStandardLayout(); if (usStandard) { let maxScore = usStandard.getScore(keyMapping); @@ -126,7 +126,7 @@ export class BrowserKeyboardMapperFactoryBase { let result = usStandard; for (let i = 0; i < this._mru.length; i++) { - let score = this._mru[i].getScore(keyMapping); + const score = this._mru[i].getScore(keyMapping); if (score > maxScore) { if (score === 0) { return { @@ -178,7 +178,7 @@ export class BrowserKeyboardMapperFactoryBase { setActiveKeyMapping(keymap: IKeyboardMapping | null) { let keymapUpdated = false; - let matchedKeyboardLayout = this.getMatchedKeymapInfo(keymap); + const matchedKeyboardLayout = this.getMatchedKeymapInfo(keymap); if (matchedKeyboardLayout) { // let score = matchedKeyboardLayout.score; @@ -186,7 +186,7 @@ export class BrowserKeyboardMapperFactoryBase { // we shoud avoid yielding the false error. // if (keymap && score < 0) { // const donotAskUpdateKey = 'missing.keyboardlayout.donotask'; - // if (this._storageService.getBoolean(donotAskUpdateKey, StorageScope.GLOBAL)) { + // if (this._storageService.getBoolean(donotAskUpdateKey, StorageScope.APPLICATION)) { // return; // } @@ -200,7 +200,7 @@ export class BrowserKeyboardMapperFactoryBase { // }, { // label: nls.localize('neverAgain', "Don't Show Again"), // isSecondary: true, - // run: () => this._storageService.store(donotAskUpdateKey, true, StorageScope.GLOBAL) + // run: () => this._storageService.store(donotAskUpdateKey, true, StorageScope.APPLICATION) // }] // ); @@ -286,7 +286,7 @@ export class BrowserKeyboardMapperFactoryBase { return; } - let isCurrentKeyboard = this._validateCurrentKeyboardMapping(keyboardEvent); + const isCurrentKeyboard = this._validateCurrentKeyboardMapping(keyboardEvent); if (isCurrentKeyboard) { return; @@ -296,7 +296,7 @@ export class BrowserKeyboardMapperFactoryBase { } public setKeyboardLayout(layoutName: string) { - let matchedLayouts: KeymapInfo[] = this.keymapInfos.filter(keymapInfo => getKeyboardLayoutId(keymapInfo.layout) === layoutName); + const matchedLayouts: KeymapInfo[] = this.keymapInfos.filter(keymapInfo => getKeyboardLayoutId(keymapInfo.layout) === layoutName); if (matchedLayouts.length > 0) { this.setActiveKeymapInfo(matchedLayouts[0]); @@ -311,7 +311,7 @@ export class BrowserKeyboardMapperFactoryBase { } private static _createKeyboardMapper(keymapInfo: KeymapInfo): IKeyboardMapper { - let rawMapping = keymapInfo.mapping; + const rawMapping = keymapInfo.mapping; const isUSStandard = !!keymapInfo.layout.isUSStandard; if (OS === OperatingSystem.Windows) { return new WindowsKeyboardMapper(isUSStandard, <IWindowsKeyboardMapping>rawMapping); @@ -387,8 +387,8 @@ export class BrowserKeyboardMapperFactoryBase { if ((navigator as any).keyboard) { try { return (navigator as any).keyboard.getLayoutMap().then((e: any) => { - let ret: IKeyboardMapping = {}; - for (let key of e) { + const ret: IKeyboardMapping = {}; + for (const key of e) { ret[key[0]] = { 'value': key[1], 'withShift': '', @@ -411,7 +411,7 @@ export class BrowserKeyboardMapperFactoryBase { // getLayoutMap can throw if invoked from a nested browsing context } } else if (keyboardEvent && !keyboardEvent.shiftKey && !keyboardEvent.altKey && !keyboardEvent.metaKey && !keyboardEvent.metaKey) { - let ret: IKeyboardMapping = {}; + const ret: IKeyboardMapping = {}; const standardKeyboardEvent = keyboardEvent as StandardKeyboardEvent; ret[standardKeyboardEvent.browserEvent.code] = { 'value': standardKeyboardEvent.browserEvent.key, @@ -443,7 +443,7 @@ export class BrowserKeyboardMapperFactory extends BrowserKeyboardMapperFactoryBa const platform = isWindows ? 'win' : isMacintosh ? 'darwin' : 'linux'; import('vs/workbench/services/keybinding/browser/keyboardLayouts/layout.contribution.' + platform).then((m) => { - let keymapInfos: IKeymapInfo[] = m.KeyboardLayoutContribution.INSTANCE.layoutInfos; + const keymapInfos: IKeymapInfo[] = m.KeyboardLayoutContribution.INSTANCE.layoutInfos; this._keymapInfos.push(...keymapInfos.map(info => (new KeymapInfo(info.layout, info.secondaryLayouts, info.mapping, info.isUserKeyboardLayout)))); this._mru = this._keymapInfos; this._initialized = true; @@ -561,7 +561,7 @@ export class BrowserKeyboardLayoutService extends Disposable implements IKeyboar }); this._register(this._userKeyboardLayout.onDidChange(() => { - let userKeyboardLayouts = this._factory.keymapInfos.filter(layout => layout.isUserKeyboardLayout); + const userKeyboardLayouts = this._factory.keymapInfos.filter(layout => layout.isUserKeyboardLayout); if (userKeyboardLayouts.length) { if (this._userKeyboardLayout.keyboardLayout) { diff --git a/src/vs/workbench/services/keybinding/common/keybindingEditing.ts b/src/vs/workbench/services/keybinding/common/keybindingEditing.ts index 047618c1d73..9cecbd01bb3 100644 --- a/src/vs/workbench/services/keybinding/common/keybindingEditing.ts +++ b/src/vs/workbench/services/keybinding/common/keybindingEditing.ts @@ -11,7 +11,6 @@ import { setProperty } from 'vs/base/common/jsonEdit'; import { Edit } from 'vs/base/common/jsonFormatter'; import { Disposable, IReference } from 'vs/base/common/lifecycle'; import { isArray } from 'vs/base/common/types'; -import { URI } from 'vs/base/common/uri'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; @@ -19,13 +18,13 @@ import { ITextModel } from 'vs/editor/common/model'; import { ITextModelService, IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IFileService } from 'vs/platform/files/common/files'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IUserFriendlyKeybinding } from 'vs/platform/keybinding/common/keybinding'; import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; export const IKeybindingEditingService = createDecorator<IKeybindingEditingService>('keybindingEditingService'); @@ -47,14 +46,12 @@ export class KeybindingsEditingService extends Disposable implements IKeybinding public _serviceBrand: undefined; private queue: Queue<void>; - private resource: URI = this.environmentService.keybindingsResource; - constructor( @ITextModelService private readonly textModelResolverService: ITextModelService, @ITextFileService private readonly textFileService: ITextFileService, @IFileService private readonly fileService: IFileService, @IConfigurationService private readonly configurationService: IConfigurationService, - @IEnvironmentService private readonly environmentService: IEnvironmentService + @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService ) { super(); this.queue = new Queue<void>(); @@ -122,7 +119,7 @@ export class KeybindingsEditingService extends Disposable implements IKeybinding } private save(): Promise<any> { - return this.textFileService.save(this.resource); + return this.textFileService.save(this.userDataProfileService.currentProfile.keybindingsResource); } private updateKeybinding(keybindingItem: ResolvedKeybindingItem, newKey: string, when: string | undefined, model: ITextModel, userKeybindingEntryIndex: number): void { @@ -238,24 +235,24 @@ export class KeybindingsEditingService extends Disposable implements IKeybinding const startPosition = model.getPositionAt(edit.offset); const endPosition = model.getPositionAt(edit.offset + edit.length); const range = new Range(startPosition.lineNumber, startPosition.column, endPosition.lineNumber, endPosition.column); - let currentText = model.getValueInRange(range); + const currentText = model.getValueInRange(range); const editOperation = currentText ? EditOperation.replace(range, edit.content) : EditOperation.insert(startPosition, edit.content); model.pushEditOperations([new Selection(startPosition.lineNumber, startPosition.column, startPosition.lineNumber, startPosition.column)], [editOperation], () => []); } private resolveModelReference(): Promise<IReference<IResolvedTextEditorModel>> { - return this.fileService.exists(this.resource) + return this.fileService.exists(this.userDataProfileService.currentProfile.keybindingsResource) .then(exists => { const EOL = this.configurationService.getValue<{ eol: string }>('files', { overrideIdentifier: 'json' })['eol']; - const result: Promise<any> = exists ? Promise.resolve(null) : this.textFileService.write(this.resource, this.getEmptyContent(EOL), { encoding: 'utf8' }); - return result.then(() => this.textModelResolverService.createModelReference(this.resource)); + const result: Promise<any> = exists ? Promise.resolve(null) : this.textFileService.write(this.userDataProfileService.currentProfile.keybindingsResource, this.getEmptyContent(EOL), { encoding: 'utf8' }); + return result.then(() => this.textModelResolverService.createModelReference(this.userDataProfileService.currentProfile.keybindingsResource)); }); } private resolveAndValidate(): Promise<IReference<IResolvedTextEditorModel>> { // Target cannot be dirty if not writing into buffer - if (this.textFileService.isDirty(this.resource)) { + if (this.textFileService.isDirty(this.userDataProfileService.currentProfile.keybindingsResource)) { return Promise.reject(new Error(localize('errorKeybindingsFileDirty', "Unable to write because the keybindings configuration file has unsaved changes. Please save it first and then try again."))); } diff --git a/src/vs/workbench/services/keybinding/common/keybindingIO.ts b/src/vs/workbench/services/keybinding/common/keybindingIO.ts index 0ddb5a9ba1e..b99f48336a4 100644 --- a/src/vs/workbench/services/keybinding/common/keybindingIO.ts +++ b/src/vs/workbench/services/keybinding/common/keybindingIO.ts @@ -22,11 +22,11 @@ export class KeybindingIO { if (!item.resolvedKeybinding) { return; } - let quotedSerializedKeybinding = JSON.stringify(item.resolvedKeybinding.getUserSettingsLabel()); + const quotedSerializedKeybinding = JSON.stringify(item.resolvedKeybinding.getUserSettingsLabel()); out.write(`{ "key": ${rightPaddedString(quotedSerializedKeybinding + ',', 25)} "command": `); - let quotedSerializedWhen = item.when ? JSON.stringify(item.when.serialize()) : ''; - let quotedSerializeCommand = JSON.stringify(item.command); + const quotedSerializedWhen = item.when ? JSON.stringify(item.when.serialize()) : ''; + const quotedSerializeCommand = JSON.stringify(item.command); if (quotedSerializedWhen.length > 0) { out.write(`${quotedSerializeCommand},`); out.writeLine(); diff --git a/src/vs/workbench/services/keybinding/common/keymapInfo.ts b/src/vs/workbench/services/keybinding/common/keymapInfo.ts index 700d16d4480..d5675fae02b 100644 --- a/src/vs/workbench/services/keybinding/common/keymapInfo.ts +++ b/src/vs/workbench/services/keybinding/common/keymapInfo.ts @@ -7,18 +7,18 @@ import { isWindows, isLinux } from 'vs/base/common/platform'; import { getKeyboardLayoutId, IKeyboardLayoutInfo } from 'vs/platform/keyboardLayout/common/keyboardLayout'; function deserializeMapping(serializedMapping: ISerializedMapping) { - let mapping = serializedMapping; + const mapping = serializedMapping; - let ret: { [key: string]: any } = {}; - for (let key in mapping) { - let result: (string | number)[] = mapping[key]; + const ret: { [key: string]: any } = {}; + for (const key in mapping) { + const result: (string | number)[] = mapping[key]; if (result.length) { - let value = result[0]; - let withShift = result[1]; - let withAltGr = result[2]; - let withShiftAltGr = result[3]; - let mask = Number(result[4]); - let vkey = result.length === 6 ? result[5] : undefined; + const value = result[0]; + const withShift = result[1]; + const withAltGr = result[2]; + const withShiftAltGr = result[3]; + const mask = Number(result[4]); + const vkey = result.length === 6 ? result[5] : undefined; ret[key] = { 'value': value, 'vkey': vkey, @@ -83,7 +83,7 @@ export class KeymapInfo { } static createKeyboardLayoutFromDebugInfo(layout: IKeyboardLayoutInfo, value: IRawMixedKeyboardMapping, isUserKeyboardLayout?: boolean): KeymapInfo { - let keyboardLayoutInfo = new KeymapInfo(layout, [], {}, true); + const keyboardLayoutInfo = new KeymapInfo(layout, [], {}, true); keyboardLayoutInfo.mapping = value; return keyboardLayoutInfo; } @@ -98,7 +98,7 @@ export class KeymapInfo { getScore(other: IRawMixedKeyboardMapping): number { let score = 0; - for (let key in other) { + for (const key in other) { if (isWindows && (key === 'Backslash' || key === 'KeyQ')) { // keymap from Chromium is probably wrong. continue; @@ -109,13 +109,13 @@ export class KeymapInfo { continue; } - let currentMapping = this.mapping[key]; + const currentMapping = this.mapping[key]; if (currentMapping === undefined) { score -= 1; } - let otherMapping = other[key]; + const otherMapping = other[key]; if (currentMapping && otherMapping && currentMapping.value !== otherMapping.value) { score -= 1; @@ -138,7 +138,7 @@ export class KeymapInfo { } fuzzyEqual(other: IRawMixedKeyboardMapping): boolean { - for (let key in other) { + for (const key in other) { if (isWindows && (key === 'Backslash' || key === 'KeyQ')) { // keymap from Chromium is probably wrong. continue; @@ -147,8 +147,8 @@ export class KeymapInfo { return false; } - let currentMapping = this.mapping[key]; - let otherMapping = other[key]; + const currentMapping = this.mapping[key]; + const otherMapping = other[key]; if (currentMapping.value !== otherMapping.value) { return false; diff --git a/src/vs/workbench/services/keybinding/common/macLinuxKeyboardMapper.ts b/src/vs/workbench/services/keybinding/common/macLinuxKeyboardMapper.ts index 424eccf2135..30a25ac9d0b 100644 --- a/src/vs/workbench/services/keybinding/common/macLinuxKeyboardMapper.ts +++ b/src/vs/workbench/services/keybinding/common/macLinuxKeyboardMapper.ts @@ -52,8 +52,8 @@ export class NativeResolvedKeybinding extends BaseResolvedKeybinding<ScanCodeBin if (IMMUTABLE_CODE_TO_KEY_CODE[binding.scanCode] !== KeyCode.DependsOnKbLayout) { return true; } - let a = this._mapper.getAriaLabelForScanCodeBinding(binding); - let b = this._mapper.getUserSettingsLabelForScanCodeBinding(binding); + const a = this._mapper.getAriaLabelForScanCodeBinding(binding); + const b = this._mapper.getUserSettingsLabelForScanCodeBinding(binding); if (!a && !b) { return true; @@ -261,7 +261,7 @@ class ScanCodeKeyCodeMapper { return []; } - let result: ScanCodeCombo[] = []; + const result: ScanCodeCombo[] = []; for (let i = 0, len = scanCodeCombosEncoded.length; i < len; i++) { const scanCodeComboEncoded = scanCodeCombosEncoded[i]; @@ -282,7 +282,7 @@ class ScanCodeKeyCodeMapper { return []; } - let result: KeyCodeCombo[] = []; + const result: KeyCodeCombo[] = []; for (let i = 0, len = keyCodeCombosEncoded.length; i < len; i++) { const keyCodeComboEncoded = keyCodeCombosEncoded[i]; @@ -436,8 +436,8 @@ export class MacLinuxKeyboardMapper implements IKeyboardMapper { const missingLatinLettersOverride: { [scanCode: string]: IMacLinuxKeyMapping } = {}; { - let producesLatinLetter: boolean[] = []; - for (let strScanCode in rawMappings) { + const producesLatinLetter: boolean[] = []; + for (const strScanCode in rawMappings) { if (rawMappings.hasOwnProperty(strScanCode)) { const scanCode = ScanCodeUtils.toEnum(strScanCode); if (scanCode === ScanCode.None) { @@ -497,8 +497,9 @@ export class MacLinuxKeyboardMapper implements IKeyboardMapper { _registerLetterIfMissing(CharCode.Z, ScanCode.KeyZ, 'z', 'Z'); } - let mappings: IScanCodeMapping[] = [], mappingsLen = 0; - for (let strScanCode in rawMappings) { + const mappings: IScanCodeMapping[] = []; + let mappingsLen = 0; + for (const strScanCode in rawMappings) { if (rawMappings.hasOwnProperty(strScanCode)) { const scanCode = ScanCodeUtils.toEnum(strScanCode); if (scanCode === ScanCode.None) { @@ -667,9 +668,9 @@ export class MacLinuxKeyboardMapper implements IKeyboardMapper { } public dumpDebugInfo(): string { - let result: string[] = []; + const result: string[] = []; - let immutableSamples = [ + const immutableSamples = [ ScanCode.ArrowUp, ScanCode.Numpad0 ]; @@ -779,7 +780,7 @@ export class MacLinuxKeyboardMapper implements IKeyboardMapper { new KeyCodeCombo(keybinding.ctrlKey, keybinding.shiftKey, keybinding.altKey, keybinding.keyCode) ); - let result: ScanCodeBinding[] = []; + const result: ScanCodeBinding[] = []; for (let i = 0, len = scanCodeCombos.length; i < len; i++) { const scanCodeCombo = scanCodeCombos[i]; result[i] = new ScanCodeBinding(scanCodeCombo.ctrlKey, scanCodeCombo.shiftKey, scanCodeCombo.altKey, keybinding.metaKey, scanCodeCombo.scanCode); @@ -857,10 +858,10 @@ export class MacLinuxKeyboardMapper implements IKeyboardMapper { } // Check if this scanCode always maps to the same keyCode and back - let constantKeyCode: KeyCode = this._scanCodeKeyCodeMapper.guessStableKeyCode(binding.scanCode); + const constantKeyCode: KeyCode = this._scanCodeKeyCodeMapper.guessStableKeyCode(binding.scanCode); if (constantKeyCode !== KeyCode.DependsOnKbLayout) { // Verify that this is a good key code that can be mapped back to the same scan code - let reverseBindings = this.simpleKeybindingToScanCodeBinding(new SimpleKeybinding(binding.ctrlKey, binding.shiftKey, binding.altKey, binding.metaKey, constantKeyCode)); + const reverseBindings = this.simpleKeybindingToScanCodeBinding(new SimpleKeybinding(binding.ctrlKey, binding.shiftKey, binding.altKey, binding.metaKey, constantKeyCode)); for (let i = 0, len = reverseBindings.length; i < len; i++) { const reverseBinding = reverseBindings[i]; if (reverseBinding.scanCode === binding.scanCode) { @@ -916,8 +917,8 @@ export class MacLinuxKeyboardMapper implements IKeyboardMapper { } public resolveKeybinding(keybinding: Keybinding): NativeResolvedKeybinding[] { - let chordParts: ScanCodeBinding[][] = []; - for (let part of keybinding.parts) { + const chordParts: ScanCodeBinding[][] = []; + for (const part of keybinding.parts) { chordParts.push(this.simpleKeybindingToScanCodeBinding(part)); } return this._toResolvedKeybinding(chordParts); @@ -927,7 +928,7 @@ export class MacLinuxKeyboardMapper implements IKeyboardMapper { if (chordParts.length === 0) { return []; } - let result: NativeResolvedKeybinding[] = []; + const result: NativeResolvedKeybinding[] = []; this._generateResolvedKeybindings(chordParts, 0, [], result); return result; } @@ -936,7 +937,7 @@ export class MacLinuxKeyboardMapper implements IKeyboardMapper { const chordPart = chordParts[currentIndex]; const isFinalIndex = currentIndex === chordParts.length - 1; for (let i = 0, len = chordPart.length; i < len; i++) { - let chords = [...previousParts, chordPart[i]]; + const chords = [...previousParts, chordPart[i]]; if (isFinalIndex) { result.push(new NativeResolvedKeybinding(this, this._OS, chords)); } else { diff --git a/src/vs/workbench/services/keybinding/common/windowsKeyboardMapper.ts b/src/vs/workbench/services/keybinding/common/windowsKeyboardMapper.ts index a9399028238..f3a3d80c9d0 100644 --- a/src/vs/workbench/services/keybinding/common/windowsKeyboardMapper.ts +++ b/src/vs/workbench/services/keybinding/common/windowsKeyboardMapper.ts @@ -183,11 +183,11 @@ export class WindowsKeyboardMapper implements IKeyboardMapper { } } - let producesLetter: boolean[] = []; + const producesLetter: boolean[] = []; let producesLetters = false; this._codeInfo = []; - for (let strCode in rawMappings) { + for (const strCode in rawMappings) { if (rawMappings.hasOwnProperty(strCode)) { const scanCode = ScanCodeUtils.toEnum(strCode); if (scanCode === ScanCode.None) { @@ -323,9 +323,9 @@ export class WindowsKeyboardMapper implements IKeyboardMapper { } public dumpDebugInfo(): string { - let result: string[] = []; + const result: string[] = []; - let immutableSamples = [ + const immutableSamples = [ ScanCode.ArrowUp, ScanCode.Numpad0 ]; diff --git a/src/vs/workbench/services/keybinding/test/browser/browserKeyboardMapper.test.ts b/src/vs/workbench/services/keybinding/test/browser/browserKeyboardMapper.test.ts index 6a74c51c00b..4787531f967 100644 --- a/src/vs/workbench/services/keybinding/test/browser/browserKeyboardMapper.test.ts +++ b/src/vs/workbench/services/keybinding/test/browser/browserKeyboardMapper.test.ts @@ -33,12 +33,12 @@ class TestKeyboardMapperFactory extends BrowserKeyboardMapperFactoryBase { } suite('keyboard layout loader', () => { - let instantiationService: TestInstantiationService = new TestInstantiationService(); - let notitifcationService = instantiationService.stub(INotificationService, new TestNotificationService()); - let storageService = instantiationService.stub(IStorageService, new TestStorageService()); + const instantiationService: TestInstantiationService = new TestInstantiationService(); + const notitifcationService = instantiationService.stub(INotificationService, new TestNotificationService()); + const storageService = instantiationService.stub(IStorageService, new TestStorageService()); - let commandService = instantiationService.stub(ICommandService, {}); - let instance = new TestKeyboardMapperFactory(notitifcationService, storageService, commandService); + const commandService = instantiationService.stub(ICommandService, {}); + const instance = new TestKeyboardMapperFactory(notitifcationService, storageService, commandService); test('load default US keyboard layout', () => { assert.notStrictEqual(instance.activeKeyboardLayout, null); diff --git a/src/vs/workbench/services/keybinding/test/browser/keybindingEditing.test.ts b/src/vs/workbench/services/keybinding/test/browser/keybindingEditing.test.ts index 9de89527a82..66b5e7fa8a8 100644 --- a/src/vs/workbench/services/keybinding/test/browser/keybindingEditing.test.ts +++ b/src/vs/workbench/services/keybinding/test/browser/keybindingEditing.test.ts @@ -28,6 +28,9 @@ import { joinPath } from 'vs/base/common/resources'; import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { VSBuffer } from 'vs/base/common/buffer'; +import { UserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { UserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfileService'; +import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; interface Modifiers { metaKey?: boolean; @@ -44,6 +47,7 @@ suite('KeybindingsEditing', () => { let instantiationService: TestInstantiationService; let fileService: IFileService; let environmentService: IEnvironmentService; + let userDataProfileService: IUserDataProfileService; let testObject: KeybindingsEditingService; setup(async () => { @@ -62,6 +66,8 @@ suite('KeybindingsEditing', () => { const configService = new TestConfigurationService(); configService.setUserConfiguration('files', { 'eol': '\n' }); + userDataProfileService = new UserDataProfileService(new UserDataProfilesService(environmentService, fileService, logService).defaultProfile); + instantiationService = workbenchInstantiationService({ fileService: () => fileService, configurationService: () => configService, @@ -74,7 +80,7 @@ suite('KeybindingsEditing', () => { teardown(() => disposables.clear()); test('errors cases - parse errors', async () => { - await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(',,,,,,,,,,,,,,')); + await fileService.writeFile(userDataProfileService.currentProfile.keybindingsResource, VSBuffer.fromString(',,,,,,,,,,,,,,')); try { await testObject.editKeybinding(aResolvedKeybindingItem({ firstPart: { keyCode: KeyCode.Escape } }), 'alt+c', undefined); assert.fail('Should fail with parse errors'); @@ -84,7 +90,7 @@ suite('KeybindingsEditing', () => { }); test('errors cases - parse errors 2', async () => { - await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString('[{"key": }]')); + await fileService.writeFile(userDataProfileService.currentProfile.keybindingsResource, VSBuffer.fromString('[{"key": }]')); try { await testObject.editKeybinding(aResolvedKeybindingItem({ firstPart: { keyCode: KeyCode.Escape } }), 'alt+c', undefined); assert.fail('Should fail with parse errors'); @@ -101,7 +107,7 @@ suite('KeybindingsEditing', () => { }); test('errors cases - did not find an array', async () => { - await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString('{"key": "alt+c", "command": "hello"}')); + await fileService.writeFile(userDataProfileService.currentProfile.keybindingsResource, VSBuffer.fromString('{"key": "alt+c", "command": "hello"}')); try { await testObject.editKeybinding(aResolvedKeybindingItem({ firstPart: { keyCode: KeyCode.Escape } }), 'alt+c', undefined); assert.fail('Should fail'); @@ -111,7 +117,7 @@ suite('KeybindingsEditing', () => { }); test('edit a default keybinding to an empty file', async () => { - await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString('')); + await fileService.writeFile(userDataProfileService.currentProfile.keybindingsResource, VSBuffer.fromString('')); const expected: IUserFriendlyKeybinding[] = [{ key: 'alt+c', command: 'a' }, { key: 'escape', command: '-a' }]; await testObject.editKeybinding(aResolvedKeybindingItem({ firstPart: { keyCode: KeyCode.Escape }, command: 'a' }), 'alt+c', undefined); assert.deepStrictEqual(await getUserKeybindings(), expected); @@ -241,11 +247,11 @@ suite('KeybindingsEditing', () => { }); async function writeToKeybindingsFile(...keybindings: IUserFriendlyKeybinding[]): Promise<void> { - await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify(keybindings || []))); + await fileService.writeFile(userDataProfileService.currentProfile.keybindingsResource, VSBuffer.fromString(JSON.stringify(keybindings || []))); } async function getUserKeybindings(): Promise<IUserFriendlyKeybinding[]> { - return json.parse((await fileService.readFile(environmentService.keybindingsResource)).value.toString()); + return json.parse((await fileService.readFile(userDataProfileService.currentProfile.keybindingsResource)).value.toString()); } function aResolvedKeybindingItem({ command, when, isDefault, firstPart, chordPart }: { command?: string; when?: string; isDefault?: boolean; firstPart?: { keyCode: KeyCode; modifiers?: Modifiers }; chordPart?: { keyCode: KeyCode; modifiers?: Modifiers } }): ResolvedKeybindingItem { @@ -253,7 +259,7 @@ suite('KeybindingsEditing', () => { const { ctrlKey, shiftKey, altKey, metaKey } = part.modifiers || { ctrlKey: false, shiftKey: false, altKey: false, metaKey: false }; return new SimpleKeybinding(ctrlKey!, shiftKey!, altKey!, metaKey!, part.keyCode); }; - let parts: SimpleKeybinding[] = []; + const parts: SimpleKeybinding[] = []; if (firstPart) { parts.push(aSimpleKeybinding(firstPart)); if (chordPart) { diff --git a/src/vs/workbench/services/keybinding/test/browser/keybindingIO.test.ts b/src/vs/workbench/services/keybinding/test/browser/keybindingIO.test.ts index aec901a94c1..e6657487202 100644 --- a/src/vs/workbench/services/keybinding/test/browser/keybindingIO.test.ts +++ b/src/vs/workbench/services/keybinding/test/browser/keybindingIO.test.ts @@ -16,8 +16,8 @@ suite('keybindingIO', () => { test('serialize/deserialize', () => { function testOneSerialization(keybinding: number, expected: string, msg: string, OS: OperatingSystem): void { - let usLayoutResolvedKeybinding = new USLayoutResolvedKeybinding(createKeybinding(keybinding, OS)!, OS); - let actualSerialized = usLayoutResolvedKeybinding.getUserSettingsLabel(); + const usLayoutResolvedKeybinding = new USLayoutResolvedKeybinding(createKeybinding(keybinding, OS)!, OS); + const actualSerialized = usLayoutResolvedKeybinding.getUserSettingsLabel(); assert.strictEqual(actualSerialized, expected, expected + ' - ' + msg); } function testSerialization(keybinding: number, expectedWin: string, expectedMac: string, expectedLinux: string): void { @@ -27,8 +27,8 @@ suite('keybindingIO', () => { } function testOneDeserialization(keybinding: string, _expected: number, msg: string, OS: OperatingSystem): void { - let actualDeserialized = KeybindingParser.parseKeybinding(keybinding, OS); - let expected = createKeybinding(_expected, OS); + const actualDeserialized = KeybindingParser.parseKeybinding(keybinding, OS); + const expected = createKeybinding(_expected, OS); assert.deepStrictEqual(actualDeserialized, expected, keybinding + ' - ' + msg); } function testDeserialization(inWin: string, inMac: string, inLinux: string, expected: number): void { @@ -124,37 +124,37 @@ suite('keybindingIO', () => { }); test('issue #10452 - invalid command', () => { - let strJSON = `[{ "key": "ctrl+k ctrl+f", "command": ["firstcommand", "seccondcommand"] }]`; - let userKeybinding = <IUserFriendlyKeybinding>JSON.parse(strJSON)[0]; - let keybindingItem = KeybindingIO.readUserKeybindingItem(userKeybinding); + const strJSON = `[{ "key": "ctrl+k ctrl+f", "command": ["firstcommand", "seccondcommand"] }]`; + const userKeybinding = <IUserFriendlyKeybinding>JSON.parse(strJSON)[0]; + const keybindingItem = KeybindingIO.readUserKeybindingItem(userKeybinding); assert.strictEqual(keybindingItem.command, null); }); test('issue #10452 - invalid when', () => { - let strJSON = `[{ "key": "ctrl+k ctrl+f", "command": "firstcommand", "when": [] }]`; - let userKeybinding = <IUserFriendlyKeybinding>JSON.parse(strJSON)[0]; - let keybindingItem = KeybindingIO.readUserKeybindingItem(userKeybinding); + const strJSON = `[{ "key": "ctrl+k ctrl+f", "command": "firstcommand", "when": [] }]`; + const userKeybinding = <IUserFriendlyKeybinding>JSON.parse(strJSON)[0]; + const keybindingItem = KeybindingIO.readUserKeybindingItem(userKeybinding); assert.strictEqual(keybindingItem.when, undefined); }); test('issue #10452 - invalid key', () => { - let strJSON = `[{ "key": [], "command": "firstcommand" }]`; - let userKeybinding = <IUserFriendlyKeybinding>JSON.parse(strJSON)[0]; - let keybindingItem = KeybindingIO.readUserKeybindingItem(userKeybinding); + const strJSON = `[{ "key": [], "command": "firstcommand" }]`; + const userKeybinding = <IUserFriendlyKeybinding>JSON.parse(strJSON)[0]; + const keybindingItem = KeybindingIO.readUserKeybindingItem(userKeybinding); assert.deepStrictEqual(keybindingItem.parts, []); }); test('issue #10452 - invalid key 2', () => { - let strJSON = `[{ "key": "", "command": "firstcommand" }]`; - let userKeybinding = <IUserFriendlyKeybinding>JSON.parse(strJSON)[0]; - let keybindingItem = KeybindingIO.readUserKeybindingItem(userKeybinding); + const strJSON = `[{ "key": "", "command": "firstcommand" }]`; + const userKeybinding = <IUserFriendlyKeybinding>JSON.parse(strJSON)[0]; + const keybindingItem = KeybindingIO.readUserKeybindingItem(userKeybinding); assert.deepStrictEqual(keybindingItem.parts, []); }); test('test commands args', () => { - let strJSON = `[{ "key": "ctrl+k ctrl+f", "command": "firstcommand", "when": [], "args": { "text": "theText" } }]`; - let userKeybinding = <IUserFriendlyKeybinding>JSON.parse(strJSON)[0]; - let keybindingItem = KeybindingIO.readUserKeybindingItem(userKeybinding); + const strJSON = `[{ "key": "ctrl+k ctrl+f", "command": "firstcommand", "when": [], "args": { "text": "theText" } }]`; + const userKeybinding = <IUserFriendlyKeybinding>JSON.parse(strJSON)[0]; + const keybindingItem = KeybindingIO.readUserKeybindingItem(userKeybinding); assert.strictEqual(keybindingItem.commandArgs.text, 'theText'); }); }); diff --git a/src/vs/workbench/services/keybinding/test/electron-browser/keyboardMapperTestUtils.ts b/src/vs/workbench/services/keybinding/test/electron-browser/keyboardMapperTestUtils.ts index 1e952e6de4e..5d3f52834c2 100644 --- a/src/vs/workbench/services/keybinding/test/electron-browser/keyboardMapperTestUtils.ts +++ b/src/vs/workbench/services/keybinding/test/electron-browser/keyboardMapperTestUtils.ts @@ -36,24 +36,24 @@ function toIResolvedKeybinding(kb: ResolvedKeybinding): IResolvedKeybinding { } export function assertResolveKeybinding(mapper: IKeyboardMapper, keybinding: Keybinding | null, expected: IResolvedKeybinding[]): void { - let actual: IResolvedKeybinding[] = mapper.resolveKeybinding(keybinding!).map(toIResolvedKeybinding); + const actual: IResolvedKeybinding[] = mapper.resolveKeybinding(keybinding!).map(toIResolvedKeybinding); assert.deepStrictEqual(actual, expected); } export function assertResolveKeyboardEvent(mapper: IKeyboardMapper, keyboardEvent: IKeyboardEvent, expected: IResolvedKeybinding): void { - let actual = toIResolvedKeybinding(mapper.resolveKeyboardEvent(keyboardEvent)); + const actual = toIResolvedKeybinding(mapper.resolveKeyboardEvent(keyboardEvent)); assert.deepStrictEqual(actual, expected); } export function assertResolveUserBinding(mapper: IKeyboardMapper, parts: (SimpleKeybinding | ScanCodeBinding)[], expected: IResolvedKeybinding[]): void { - let actual: IResolvedKeybinding[] = mapper.resolveUserBinding(parts).map(toIResolvedKeybinding); + const actual: IResolvedKeybinding[] = mapper.resolveUserBinding(parts).map(toIResolvedKeybinding); assert.deepStrictEqual(actual, expected); } export function readRawMapping<T>(file: string): Promise<T> { return Promises.readFile(getPathFromAmdModule(require, `vs/workbench/services/keybinding/test/electron-browser/${file}.js`)).then((buff) => { - let contents = buff.toString(); - let func = new Function('define', contents); + const contents = buff.toString(); + const func = new Function('define', contents); let rawMappings: T | null = null; func(function (value: T) { rawMappings = value; diff --git a/src/vs/workbench/services/keybinding/test/electron-browser/macLinuxFallbackKeyboardMapper.test.ts b/src/vs/workbench/services/keybinding/test/electron-browser/macLinuxFallbackKeyboardMapper.test.ts index 25dd4f57a33..b7db656ffd6 100644 --- a/src/vs/workbench/services/keybinding/test/electron-browser/macLinuxFallbackKeyboardMapper.test.ts +++ b/src/vs/workbench/services/keybinding/test/electron-browser/macLinuxFallbackKeyboardMapper.test.ts @@ -11,7 +11,7 @@ import { IResolvedKeybinding, assertResolveKeybinding, assertResolveKeyboardEven suite('keyboardMapper - MAC fallback', () => { - let mapper = new MacLinuxFallbackKeyboardMapper(OperatingSystem.Macintosh); + const mapper = new MacLinuxFallbackKeyboardMapper(OperatingSystem.Macintosh); function _assertResolveKeybinding(k: number, expected: IResolvedKeybinding[]): void { assertResolveKeybinding(mapper, createKeybinding(k, OperatingSystem.Macintosh)!, expected); @@ -225,7 +225,7 @@ suite('keyboardMapper - MAC fallback', () => { suite('keyboardMapper - LINUX fallback', () => { - let mapper = new MacLinuxFallbackKeyboardMapper(OperatingSystem.Linux); + const mapper = new MacLinuxFallbackKeyboardMapper(OperatingSystem.Linux); function _assertResolveKeybinding(k: number, expected: IResolvedKeybinding[]): void { assertResolveKeybinding(mapper, createKeybinding(k, OperatingSystem.Linux)!, expected); diff --git a/src/vs/workbench/services/keybinding/test/electron-browser/macLinuxKeyboardMapper.test.ts b/src/vs/workbench/services/keybinding/test/electron-browser/macLinuxKeyboardMapper.test.ts index a5905bc8704..a5984fc12e0 100644 --- a/src/vs/workbench/services/keybinding/test/electron-browser/macLinuxKeyboardMapper.test.ts +++ b/src/vs/workbench/services/keybinding/test/electron-browser/macLinuxKeyboardMapper.test.ts @@ -1455,7 +1455,7 @@ suite('keyboardMapper - LINUX en_us', () => { suite('keyboardMapper', () => { test('issue #23706: Linux UK layout: Ctrl + Apostrophe also toggles terminal', () => { - let mapper = new MacLinuxKeyboardMapper(false, { + const mapper = new MacLinuxKeyboardMapper(false, { 'Backquote': { 'value': '`', 'withShift': '¬', @@ -1489,7 +1489,7 @@ suite('keyboardMapper', () => { }); test('issue #24064: NumLock/NumPad keys stopped working in 1.11 on Linux', () => { - let mapper = new MacLinuxKeyboardMapper(false, {}, OperatingSystem.Linux); + const mapper = new MacLinuxKeyboardMapper(false, {}, OperatingSystem.Linux); function assertNumpadKeyboardEvent(keyCode: KeyCode, code: string, label: string, electronAccelerator: string | null, userSettingsLabel: string, dispatch: string): void { assertResolveKeyboardEvent( @@ -1530,7 +1530,7 @@ suite('keyboardMapper', () => { }); test('issue #24107: Delete, Insert, Home, End, PgUp, PgDn, and arrow keys no longer work editor in 1.11', () => { - let mapper = new MacLinuxKeyboardMapper(false, {}, OperatingSystem.Linux); + const mapper = new MacLinuxKeyboardMapper(false, {}, OperatingSystem.Linux); function assertKeyboardEvent(keyCode: KeyCode, code: string, label: string, electronAccelerator: string, userSettingsLabel: string, dispatch: string): void { assertResolveKeyboardEvent( diff --git a/src/vs/workbench/services/label/common/labelService.ts b/src/vs/workbench/services/label/common/labelService.ts index c16d82ee53a..4135a251e94 100644 --- a/src/vs/workbench/services/label/common/labelService.ts +++ b/src/vs/workbench/services/label/common/labelService.ts @@ -26,6 +26,7 @@ import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteA import { Schemas } from 'vs/base/common/network'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { Memento } from 'vs/workbench/common/memento'; +import { firstOrDefault } from 'vs/base/common/arrays'; const resourceLabelFormattersExtPoint = ExtensionsRegistry.registerExtensionPoint<ResourceLabelFormatter[]>({ extensionPoint: 'resourceLabelFormatters', @@ -142,8 +143,8 @@ export class LabelService extends Disposable implements ILabelService { this.os = OS; this.userHome = pathService.defaultUriScheme === Schemas.file ? this.pathService.userHome({ preferLocal: true }) : undefined; - const memento = this.storedFormattersMemento = new Memento('cachedResourceFormatters', storageService); - this.storedFormatters = memento.getMemento(StorageScope.GLOBAL, StorageTarget.USER); + const memento = this.storedFormattersMemento = new Memento('cachedResourceLabelFormatters', storageService); + this.storedFormatters = memento.getMemento(StorageScope.PROFILE, StorageTarget.MACHINE); this.formatters = this.storedFormatters?.formatters || []; // Remote environment is potentially long running @@ -223,8 +224,23 @@ export class LabelService extends Disposable implements ILabelService { } // Relative label - if (options.relative) { - const folder = this.contextService?.getWorkspaceFolder(resource); + if (options.relative && this.contextService) { + let folder = this.contextService.getWorkspaceFolder(resource); + if (!folder) { + + // It is possible that the resource we want to resolve the + // workspace folder for is not using the same scheme as + // the folders in the workspace, so we help by trying again + // to resolve a workspace folder by trying again with a + // scheme that is workspace contained. + + const workspace = this.contextService.getWorkspace(); + const firstFolder = firstOrDefault(workspace.folders); + if (firstFolder && resource.scheme !== firstFolder.uri.scheme && resource.path.startsWith(posix.sep)) { + folder = this.contextService.getWorkspaceFolder(firstFolder.uri.with({ path: resource.path })); + } + } + if (folder) { const folderLabel = this.formatUri(folder.uri, formatting, options.noPrefix); diff --git a/src/vs/workbench/services/label/test/browser/label.test.ts b/src/vs/workbench/services/label/test/browser/label.test.ts index 31252fd11ef..745055c930e 100644 --- a/src/vs/workbench/services/label/test/browser/label.test.ts +++ b/src/vs/workbench/services/label/test/browser/label.test.ts @@ -166,7 +166,7 @@ suite('URI Label', () => { test('label caching', () => { - const m = new Memento('cachedResourceFormatters', storageService).getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); + const m = new Memento('cachedResourceLabelFormatters', storageService).getMemento(StorageScope.PROFILE, StorageTarget.MACHINE); const makeFormatter = (scheme: string): ResourceLabelFormatter => ({ formatting: { label: `\${path} (${scheme})`, separator: '/' }, scheme }); assert.deepStrictEqual(m, {}); @@ -190,7 +190,7 @@ suite('URI Label', () => { for (let i = 0; i < 100; i++) { labelService.registerCachedFormatter(makeFormatter(`i${i}`)); } - let expected: ResourceLabelFormatter[] = []; + const expected: ResourceLabelFormatter[] = []; for (let i = 50; i < 100; i++) { expected.unshift(makeFormatter(`i${i}`)); } diff --git a/src/vs/workbench/services/label/test/common/mockLabelService.ts b/src/vs/workbench/services/label/test/common/mockLabelService.ts new file mode 100644 index 00000000000..0338dd144a2 --- /dev/null +++ b/src/vs/workbench/services/label/test/common/mockLabelService.ts @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { basename, normalize } from 'vs/base/common/path'; +import { URI } from 'vs/base/common/uri'; +import { IFormatterChangeEvent, ILabelService, ResourceLabelFormatter } from 'vs/platform/label/common/label'; +import { IWorkspace, IWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; + +export class MockLabelService implements ILabelService { + _serviceBrand: undefined; + + registerCachedFormatter(formatter: ResourceLabelFormatter): IDisposable { + throw new Error('Method not implemented.'); + } + getUriLabel(resource: URI, options?: { relative?: boolean | undefined; noPrefix?: boolean | undefined }): string { + return normalize(resource.fsPath); + } + getUriBasenameLabel(resource: URI): string { + return basename(resource.fsPath); + } + getWorkspaceLabel(workspace: URI | IWorkspaceIdentifier | IWorkspace, options?: { verbose: boolean }): string { + return ''; + } + getHostLabel(scheme: string, authority?: string): string { + return ''; + } + public getHostTooltip(): string | undefined { + return ''; + } + getSeparator(scheme: string, authority?: string): '/' | '\\' { + return '/'; + } + registerFormatter(formatter: ResourceLabelFormatter): IDisposable { + return Disposable.None; + } + onDidChangeFormatters: Event<IFormatterChangeEvent> = new Emitter<IFormatterChangeEvent>().event; +} diff --git a/src/vs/workbench/services/language/common/languageService.ts b/src/vs/workbench/services/language/common/languageService.ts index 3b60d31507a..43180b51534 100644 --- a/src/vs/workbench/services/language/common/languageService.ts +++ b/src/vs/workbench/services/language/common/languageService.ts @@ -122,10 +122,10 @@ export class WorkbenchLanguageService extends LanguageService { this._extensionService = extensionService; languagesExtPoint.setHandler((extensions: readonly IExtensionPointUser<IRawLanguageExtensionPoint[]>[]) => { - let allValidLanguages: ILanguageExtensionPoint[] = []; + const allValidLanguages: ILanguageExtensionPoint[] = []; for (let i = 0, len = extensions.length; i < len; i++) { - let extension = extensions[i]; + const extension = extensions[i]; if (!Array.isArray(extension.value)) { extension.collector.error(localize('invalid', "Invalid `contributes.{0}`. Expected an array.", languagesExtPoint.name)); @@ -133,7 +133,7 @@ export class WorkbenchLanguageService extends LanguageService { } for (let j = 0, lenJ = extension.value.length; j < lenJ; j++) { - let ext = extension.value[j]; + const ext = extension.value[j]; if (isValidLanguageExtensionPoint(ext, extension.description, extension.collector)) { let configuration: URI | undefined = undefined; if (ext.configuration) { diff --git a/src/vs/workbench/services/languageDetection/browser/languageDetectionWorkerServiceImpl.ts b/src/vs/workbench/services/languageDetection/browser/languageDetectionWorkerServiceImpl.ts index d3428409896..4eebf46840b 100644 --- a/src/vs/workbench/services/languageDetection/browser/languageDetectionWorkerServiceImpl.ts +++ b/src/vs/workbench/services/languageDetection/browser/languageDetectionWorkerServiceImpl.ts @@ -151,7 +151,7 @@ export class LanguageDetectionService extends Disposable implements ILanguageDet private initEditorOpenedListeners(storageService: IStorageService) { try { - const globalLangHistroyData = JSON.parse(storageService.get(LanguageDetectionService.globalOpenedLanguagesStorageKey, StorageScope.GLOBAL, '[]')); + const globalLangHistroyData = JSON.parse(storageService.get(LanguageDetectionService.globalOpenedLanguagesStorageKey, StorageScope.PROFILE, '[]')); this.historicalGlobalOpenedLanguageIds.fromJSON(globalLangHistroyData); } catch (e) { console.error(e); } @@ -166,7 +166,7 @@ export class LanguageDetectionService extends Disposable implements ILanguageDet this.sessionOpenedLanguageIds.add(activeLanguage); this.historicalGlobalOpenedLanguageIds.set(activeLanguage, true); this.historicalWorkspaceOpenedLanguageIds.set(activeLanguage, true); - storageService.store(LanguageDetectionService.globalOpenedLanguagesStorageKey, JSON.stringify(this.historicalGlobalOpenedLanguageIds.toJSON()), StorageScope.GLOBAL, StorageTarget.MACHINE); + storageService.store(LanguageDetectionService.globalOpenedLanguagesStorageKey, JSON.stringify(this.historicalGlobalOpenedLanguageIds.toJSON()), StorageScope.PROFILE, StorageTarget.MACHINE); storageService.store(LanguageDetectionService.workspaceOpenedLanguagesStorageKey, JSON.stringify(this.historicalWorkspaceOpenedLanguageIds.toJSON()), StorageScope.WORKSPACE, StorageTarget.MACHINE); this.dirtyBiases = true; } @@ -203,6 +203,7 @@ export class LanguageDetectionWorkerHost { async sendTelemetryEvent(languages: string[], confidences: number[], timeSpent: number): Promise<void> { type LanguageDetectionStats = { languages: string; confidences: string; timeSpent: number }; type LanguageDetectionStatsClassification = { + owner: 'TylerLeonhardt'; languages: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; confidences: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; timeSpent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; @@ -337,6 +338,7 @@ export class LanguageDetectionWorkerClient extends EditorWorkerClient { } type LanguageDetectionPerfClassification = { + owner: 'TylerLeonhardt'; timeSpent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; detection: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; }; diff --git a/src/vs/workbench/services/localizations/electron-sandbox/localizationsService.ts b/src/vs/workbench/services/localization/electron-sandbox/languagePackService.ts index 5b716004793..b71303b3a90 100644 --- a/src/vs/workbench/services/localizations/electron-sandbox/localizationsService.ts +++ b/src/vs/workbench/services/localization/electron-sandbox/languagePackService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ILocalizationsService } from 'vs/platform/localizations/common/localizations'; +import { ILanguagePackService } from 'vs/platform/languagePacks/common/languagePacks'; import { registerSharedProcessRemoteService } from 'vs/platform/ipc/electron-sandbox/services'; -registerSharedProcessRemoteService(ILocalizationsService, 'localizations', { supportsDelayedInstantiation: true }); +registerSharedProcessRemoteService(ILanguagePackService, 'languagePacks', { supportsDelayedInstantiation: true }); diff --git a/src/vs/workbench/services/notification/common/notificationService.ts b/src/vs/workbench/services/notification/common/notificationService.ts index b18c46c2f4f..1a768532b79 100644 --- a/src/vs/workbench/services/notification/common/notificationService.ts +++ b/src/vs/workbench/services/notification/common/notificationService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { INotificationService, INotification, INotificationHandle, Severity, NotificationMessage, INotificationActions, IPromptChoice, IPromptOptions, IStatusMessageOptions, NoOpNotification, NeverShowAgainScope, NotificationsFilter } from 'vs/platform/notification/common/notification'; +import { INotificationService, INotification, INotificationHandle, Severity, NotificationMessage, INotificationActions, IPromptChoice, IPromptOptions, IStatusMessageOptions, NoOpNotification, NeverShowAgainScope, NotificationsFilter, INeverShowAgainOptions } from 'vs/platform/notification/common/notification'; import { NotificationsModel, ChoiceAction, NotificationChangeType } from 'vs/workbench/common/notifications'; import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { Emitter, Event } from 'vs/base/common/event'; @@ -24,11 +24,15 @@ export class NotificationService extends Disposable implements INotificationServ private readonly _onDidRemoveNotification = this._register(new Emitter<INotification>()); readonly onDidRemoveNotification = this._onDidRemoveNotification.event; + private readonly _onDidChangeDoNotDisturbMode = this._register(new Emitter<void>()); + readonly onDidChangeDoNotDisturbMode = this._onDidChangeDoNotDisturbMode.event; + constructor( @IStorageService private readonly storageService: IStorageService ) { super(); + this.updateDoNotDisturbFilters(); this.registerListeners(); } @@ -58,10 +62,44 @@ export class NotificationService extends Disposable implements INotificationServ })); } - setFilter(filter: NotificationsFilter): void { + //#region Do not disturb mode + + static readonly DND_SETTINGS_KEY = 'notifications.doNotDisturbMode'; + + private _doNotDisturbMode = this.storageService.getBoolean(NotificationService.DND_SETTINGS_KEY, StorageScope.APPLICATION, false); + + get doNotDisturbMode() { + return this._doNotDisturbMode; + } + + set doNotDisturbMode(enabled: boolean) { + if (this._doNotDisturbMode === enabled) { + return; // no change + } + + this.storageService.store(NotificationService.DND_SETTINGS_KEY, enabled, StorageScope.APPLICATION, StorageTarget.MACHINE); + this._doNotDisturbMode = enabled; + + // Toggle via filter + this.updateDoNotDisturbFilters(); + + // Events + this._onDidChangeDoNotDisturbMode.fire(); + } + + private updateDoNotDisturbFilters(): void { + let filter: NotificationsFilter; + if (this._doNotDisturbMode) { + filter = NotificationsFilter.ERROR; + } else { + filter = NotificationsFilter.OFF; + } + this.model.setFilter(filter); } + //#endregion + info(message: NotificationMessage | NotificationMessage[]): void { if (Array.isArray(message)) { message.forEach(m => this.info(m)); @@ -96,9 +134,9 @@ export class NotificationService extends Disposable implements INotificationServ const toDispose = new DisposableStore(); // Handle neverShowAgain option accordingly - let handle: INotificationHandle; + if (notification.neverShowAgain) { - const scope = notification.neverShowAgain.scope === NeverShowAgainScope.WORKSPACE ? StorageScope.WORKSPACE : StorageScope.GLOBAL; + const scope = this.toStorageScope(notification.neverShowAgain); const id = notification.neverShowAgain.id; // If the user already picked to not show the notification @@ -134,7 +172,7 @@ export class NotificationService extends Disposable implements INotificationServ } // Show notification - handle = this.model.addNotification(notification); + const handle = this.model.addNotification(notification); // Cleanup when notification gets disposed Event.once(handle.onDidClose)(() => toDispose.dispose()); @@ -142,12 +180,25 @@ export class NotificationService extends Disposable implements INotificationServ return handle; } + private toStorageScope(options: INeverShowAgainOptions): StorageScope { + switch (options.scope) { + case NeverShowAgainScope.APPLICATION: + return StorageScope.APPLICATION; + case NeverShowAgainScope.PROFILE: + return StorageScope.PROFILE; + case NeverShowAgainScope.WORKSPACE: + return StorageScope.WORKSPACE; + default: + return StorageScope.APPLICATION; + } + } + prompt(severity: Severity, message: string, choices: IPromptChoice[], options?: IPromptOptions): INotificationHandle { const toDispose = new DisposableStore(); // Handle neverShowAgain option accordingly if (options?.neverShowAgain) { - const scope = options.neverShowAgain.scope === NeverShowAgainScope.WORKSPACE ? StorageScope.WORKSPACE : StorageScope.GLOBAL; + const scope = this.toStorageScope(options.neverShowAgain); const id = options.neverShowAgain.id; // If the user already picked to not show the notification @@ -171,7 +222,7 @@ export class NotificationService extends Disposable implements INotificationServ } let choiceClicked = false; - let handle: INotificationHandle; + // Convert choices into primary/secondary actions const primaryActions: IAction[] = []; @@ -199,7 +250,7 @@ export class NotificationService extends Disposable implements INotificationServ // Show notification with actions const actions: INotificationActions = { primary: primaryActions, secondary: secondaryActions }; - handle = this.notify({ severity, message, actions, sticky: options?.sticky, silent: options?.silent }); + const handle = this.notify({ severity, message, actions, sticky: options?.sticky, silent: options?.silent }); Event.once(handle.onDidClose)(() => { diff --git a/src/vs/workbench/services/outline/browser/outlineService.ts b/src/vs/workbench/services/outline/browser/outlineService.ts index 02f080fde6a..fac1d91dd08 100644 --- a/src/vs/workbench/services/outline/browser/outlineService.ts +++ b/src/vs/workbench/services/outline/browser/outlineService.ts @@ -21,7 +21,7 @@ class OutlineService implements IOutlineService { readonly onDidChange: Event<void> = this._onDidChange.event; canCreateOutline(pane: IEditorPane): boolean { - for (let factory of this._factories) { + for (const factory of this._factories) { if (factory.matches(pane)) { return true; } @@ -30,7 +30,7 @@ class OutlineService implements IOutlineService { } async createOutline(pane: IEditorPane, target: OutlineTarget, token: CancellationToken): Promise<IOutline<any> | undefined> { - for (let factory of this._factories) { + for (const factory of this._factories) { if (factory.matches(pane)) { return await factory.createOutline(pane, target, token); } diff --git a/src/vs/workbench/services/preferences/browser/preferencesService.ts b/src/vs/workbench/services/preferences/browser/preferencesService.ts index 555a3a7ee20..fa8078055ce 100644 --- a/src/vs/workbench/services/preferences/browser/preferencesService.ts +++ b/src/vs/workbench/services/preferences/browser/preferencesService.ts @@ -20,7 +20,6 @@ import * as nls from 'vs/nls'; import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { Extensions, getDefaultValue, IConfigurationRegistry, OVERRIDE_PROPERTY_REGEX } from 'vs/platform/configuration/common/configurationRegistry'; import { EditorResolution } from 'vs/platform/editor/common/editor'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -28,7 +27,6 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ILabelService } from 'vs/platform/label/common/label'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { Registry } from 'vs/platform/registry/common/platform'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IEditorPane } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; @@ -46,6 +44,7 @@ import { ITextEditorService } from 'vs/workbench/services/textfile/common/textEd import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { isArray, isObject } from 'vs/base/common/types'; import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestController'; +import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; const emptyEditableSettingsContent = '{\n}'; @@ -67,8 +66,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic @INotificationService private readonly notificationService: INotificationService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IInstantiationService private readonly instantiationService: IInstantiationService, - @IEnvironmentService private readonly environmentService: IEnvironmentService, - @ITelemetryService private readonly telemetryService: ITelemetryService, + @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, @ITextModelService private readonly textModelResolverService: ITextModelService, @IKeybindingService keybindingService: IKeybindingService, @IModelService private readonly modelService: IModelService, @@ -95,7 +93,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic private readonly defaultSettingsRawResource = URI.from({ scheme: network.Schemas.vscode, authority: 'defaultsettings', path: '/defaultSettings.json' }); get userSettingsResource(): URI { - return this.environmentService.settingsResource; + return this.userDataProfileService.currentProfile.settingsResource; } get workspaceSettingsResource(): URI | null { @@ -303,15 +301,10 @@ export class PreferencesService extends Disposable implements IPreferencesServic } async openGlobalKeybindingSettings(textual: boolean, options?: IKeybindingsEditorOptions): Promise<void> { - type OpenKeybindingsClassification = { - textual: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; - }; - this.telemetryService.publicLog2<{ textual: boolean }, OpenKeybindingsClassification>('openKeybindings', { textual }); - options = { pinned: true, revealIfOpened: true, ...options }; if (textual) { const emptyContents = '// ' + nls.localize('emptyKeybindingsHeader', "Place your key bindings in this file to override the defaults") + '\n[\n]'; - const editableKeybindings = this.environmentService.keybindingsResource; + const editableKeybindings = this.userDataProfileService.currentProfile.keybindingsResource; const openDefaultKeybindings = !!this.configurationService.getValue('workbench.settings.openDefaultKeybindings'); // Create as needed and open in editor diff --git a/src/vs/workbench/services/preferences/common/preferences.ts b/src/vs/workbench/services/preferences/common/preferences.ts index 5aaf1198fe4..06b43562999 100644 --- a/src/vs/workbench/services/preferences/common/preferences.ts +++ b/src/vs/workbench/services/preferences/common/preferences.ts @@ -87,7 +87,7 @@ export interface ISetting { enumItemLabels?: string[]; allKeysAreBoolean?: boolean; editPresentation?: EditPresentationTypes; - defaultValueSource?: string | IExtensionInfo; + nonLanguageSpecificDefaultValueSource?: string | IExtensionInfo; isLanguageTagSetting?: boolean; categoryOrder?: number; categoryLabel?: string; diff --git a/src/vs/workbench/services/preferences/common/preferencesModels.ts b/src/vs/workbench/services/preferences/common/preferencesModels.ts index d93923deb81..96d09e3a041 100644 --- a/src/vs/workbench/services/preferences/common/preferencesModels.ts +++ b/src/vs/workbench/services/preferences/common/preferencesModels.ts @@ -640,9 +640,7 @@ export class DefaultSettings extends Disposable { settingsGroup.sections[settingsGroup.sections.length - 1].settings = configurationSettings; } } - if (config.allOf) { - config.allOf.forEach(c => this.parseConfig(c, result, configurations, settingsGroup, seenSettings)); - } + config.allOf?.forEach(c => this.parseConfig(c, result, configurations, settingsGroup, seenSettings)); return result; } @@ -672,7 +670,7 @@ export class DefaultSettings extends Disposable { const prop = settingsObject[key]; if (this.matchesScope(prop)) { const value = prop.default; - let description = (prop.description || prop.markdownDescription || ''); + let description = (prop.markdownDescription || prop.description || ''); if (typeof description !== 'string') { description = ''; } @@ -692,12 +690,12 @@ export class DefaultSettings extends Disposable { const objectAdditionalProperties = prop.type === 'object' ? prop.additionalProperties : undefined; let enumToUse = prop.enum; - let enumDescriptions = prop.enumDescriptions ?? prop.markdownEnumDescriptions; - let enumDescriptionsAreMarkdown = !prop.enumDescriptions; + let enumDescriptions = prop.markdownEnumDescriptions ?? prop.enumDescriptions; + let enumDescriptionsAreMarkdown = !!prop.markdownEnumDescriptions; if (listItemType === 'enum' && !isArray(prop.items)) { enumToUse = prop.items!.enum; - enumDescriptions = prop.items!.enumDescriptions ?? prop.items!.markdownEnumDescriptions; - enumDescriptionsAreMarkdown = enumDescriptionsAreMarkdown && !prop.items!.enumDescriptions; + enumDescriptions = prop.items!.markdownEnumDescriptions ?? prop.items!.enumDescriptions; + enumDescriptionsAreMarkdown = !!prop.items!.markdownEnumDescriptions; } let allKeysAreBoolean = false; @@ -724,7 +722,7 @@ export class DefaultSettings extends Disposable { key, value, description: descriptionLines, - descriptionIsMarkdown: !prop.description, + descriptionIsMarkdown: !!prop.markdownDescription, range: nullRange, keyRange: nullRange, valueRange: nullRange, @@ -751,7 +749,7 @@ export class DefaultSettings extends Disposable { allKeysAreBoolean, editPresentation: prop.editPresentation, order: prop.order, - defaultValueSource, + nonLanguageSpecificDefaultValueSource: defaultValueSource, isLanguageTagSetting, categoryLabel, categoryOrder diff --git a/src/vs/workbench/services/preferences/test/browser/keybindingsEditorModel.test.ts b/src/vs/workbench/services/preferences/test/browser/keybindingsEditorModel.test.ts index 1e540920f00..341f95fd936 100644 --- a/src/vs/workbench/services/preferences/test/browser/keybindingsEditorModel.test.ts +++ b/src/vs/workbench/services/preferences/test/browser/keybindingsEditorModel.test.ts @@ -711,7 +711,7 @@ suite('KeybindingsEditorModel', () => { const { ctrlKey, shiftKey, altKey, metaKey } = part.modifiers || { ctrlKey: false, shiftKey: false, altKey: false, metaKey: false }; return new SimpleKeybinding(ctrlKey!, shiftKey!, altKey!, metaKey!, part.keyCode); }; - let parts: SimpleKeybinding[] = []; + const parts: SimpleKeybinding[] = []; if (firstPart) { parts.push(aSimpleKeybinding(firstPart)); if (chordPart) { diff --git a/src/vs/workbench/services/profiles/common/profile.ts b/src/vs/workbench/services/profiles/common/profile.ts deleted file mode 100644 index a57331a9c56..00000000000 --- a/src/vs/workbench/services/profiles/common/profile.ts +++ /dev/null @@ -1,44 +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 { isUndefined } from 'vs/base/common/types'; -import { localize } from 'vs/nls'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; - -export interface IProfile { - readonly name?: string; - readonly settings?: string; - readonly globalState?: string; - readonly extensions?: string; -} - -export function isProfile(thing: any): thing is IProfile { - const candidate = thing as IProfile | undefined; - - return !!(candidate && typeof candidate === 'object' - && (isUndefined(candidate.name) || typeof candidate.name === 'string') - && (isUndefined(candidate.settings) || typeof candidate.settings === 'string') - && (isUndefined(candidate.globalState) || typeof candidate.globalState === 'string') - && (isUndefined(candidate.extensions) || typeof candidate.extensions === 'string')); -} - -export type ProfileCreationOptions = { readonly skipComments: boolean }; - -export const IWorkbenchProfileService = createDecorator<IWorkbenchProfileService>('IWorkbenchProfileService'); -export interface IWorkbenchProfileService { - readonly _serviceBrand: undefined; - - createProfile(options?: ProfileCreationOptions): Promise<IProfile>; - setProfile(profile: IProfile): Promise<void>; -} - -export interface IResourceProfile { - getProfileContent(): Promise<string>; - applyProfile(content: string): Promise<void>; -} - -export const PROFILES_CATEGORY = localize('settings profiles', "Settings Profile"); -export const PROFILE_EXTENSION = 'code-profile'; -export const PROFILE_FILTER = [{ name: localize('profile', "Settings Profile"), extensions: [PROFILE_EXTENSION] }]; diff --git a/src/vs/workbench/services/profiles/common/profileService.ts b/src/vs/workbench/services/profiles/common/profileService.ts deleted file mode 100644 index 053b9e59bcf..00000000000 --- a/src/vs/workbench/services/profiles/common/profileService.ts +++ /dev/null @@ -1,65 +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 { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; -import { ExtensionsProfile } from 'vs/workbench/services/profiles/common/extensionsProfile'; -import { GlobalStateProfile } from 'vs/workbench/services/profiles/common/globalStateProfile'; -import { IProfile, IWorkbenchProfileService, PROFILES_CATEGORY } from 'vs/workbench/services/profiles/common/profile'; -import { SettingsProfile } from 'vs/workbench/services/profiles/common/settingsProfile'; - -export class WorkbenchProfileService implements IWorkbenchProfileService { - - readonly _serviceBrand: undefined; - - private readonly settingsProfile: SettingsProfile; - private readonly globalStateProfile: GlobalStateProfile; - private readonly extensionsProfile: ExtensionsProfile; - - constructor( - @IInstantiationService instantiationService: IInstantiationService, - @IProgressService private readonly progressService: IProgressService, - @INotificationService private readonly notificationService: INotificationService - ) { - this.settingsProfile = instantiationService.createInstance(SettingsProfile); - this.globalStateProfile = instantiationService.createInstance(GlobalStateProfile); - this.extensionsProfile = instantiationService.createInstance(ExtensionsProfile); - } - - async createProfile(options?: { skipComments: boolean }): Promise<IProfile> { - const settings = await this.settingsProfile.getProfileContent(options); - const globalState = await this.globalStateProfile.getProfileContent(); - const extensions = await this.extensionsProfile.getProfileContent(); - return { - settings, - globalState, - extensions - }; - } - - async setProfile(profile: IProfile): Promise<void> { - await this.progressService.withProgress({ - location: ProgressLocation.Notification, - title: localize('profiles.applying', "{0}: Applying...", PROFILES_CATEGORY), - }, async progress => { - if (profile.settings) { - await this.settingsProfile.applyProfile(profile.settings); - } - if (profile.globalState) { - await this.globalStateProfile.applyProfile(profile.globalState); - } - if (profile.extensions) { - await this.extensionsProfile.applyProfile(profile.extensions); - } - }); - this.notificationService.info(localize('applied profile', "{0}: Applied successfully.", PROFILES_CATEGORY)); - } - -} - -registerSingleton(IWorkbenchProfileService, WorkbenchProfileService); diff --git a/src/vs/workbench/services/progress/browser/progressService.ts b/src/vs/workbench/services/progress/browser/progressService.ts index 17395a2821d..8172f41fa24 100644 --- a/src/vs/workbench/services/progress/browser/progressService.ts +++ b/src/vs/workbench/services/progress/browser/progressService.ts @@ -70,7 +70,7 @@ export class ProgressService extends Disposable implements IProgressService { switch (location) { case ProgressLocation.Notification: - return this.withNotificationProgress({ ...options, location }, task, onDidCancel); + return this.withNotificationProgress({ ...options, location, silent: this.notificationService.doNotDisturbMode }, task, onDidCancel); case ProgressLocation.Window: if ((options as IProgressWindowOptions).command) { // Window progress with command get's shown in the status bar @@ -127,9 +127,9 @@ export class ProgressService extends Disposable implements IProgressService { if (idx < this.windowProgressStack.length) { const [options, progress] = this.windowProgressStack[idx]; - let progressTitle = options.title; - let progressMessage = progress.value && progress.value.message; - let progressCommand = (<IProgressWindowOptions>options).command; + const progressTitle = options.title; + const progressMessage = progress.value && progress.value.message; + const progressCommand = (<IProgressWindowOptions>options).command; let text: string; let title: string; const source = options.source && typeof options.source !== 'string' ? options.source.label : options.source; diff --git a/src/vs/workbench/services/progress/test/browser/progressIndicator.test.ts b/src/vs/workbench/services/progress/test/browser/progressIndicator.test.ts index c2090d6a4b8..82f59c20cdf 100644 --- a/src/vs/workbench/services/progress/test/browser/progressIndicator.test.ts +++ b/src/vs/workbench/services/progress/test/browser/progressIndicator.test.ts @@ -87,9 +87,9 @@ class TestProgressBar { suite('Progress Indicator', () => { test('CompositeScope', () => { - let paneCompositeService = new TestPaneCompositeService(); - let viewsService = new TestViewsService(); - let service = new CompositeProgressScope(paneCompositeService, viewsService, 'test.scopeId', false); + const paneCompositeService = new TestPaneCompositeService(); + const viewsService = new TestViewsService(); + const service = new CompositeProgressScope(paneCompositeService, viewsService, 'test.scopeId', false); const testViewlet = new TestViewlet('test.scopeId'); assert(!service.isActive); @@ -107,10 +107,10 @@ suite('Progress Indicator', () => { }); test('CompositeProgressIndicator', async () => { - let testProgressBar = new TestProgressBar(); - let paneCompositeService = new TestPaneCompositeService(); - let viewsService = new TestViewsService(); - let service = new CompositeProgressIndicator((<any>testProgressBar), 'test.scopeId', true, paneCompositeService, viewsService); + const testProgressBar = new TestProgressBar(); + const paneCompositeService = new TestPaneCompositeService(); + const viewsService = new TestViewsService(); + const service = new CompositeProgressIndicator((<any>testProgressBar), 'test.scopeId', true, paneCompositeService, viewsService); // Active: Show (Infinite) let fn = service.show(true); diff --git a/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts b/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts index 0eba0de6b00..d962e1a23c9 100644 --- a/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts +++ b/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts @@ -233,7 +233,7 @@ export class RemoteAgentConnection extends Disposable implements IRemoteAgentCon ipcLogger: false ? new IPCLogger(`Local \u2192 Remote`, `Remote \u2192 Local`) : null }; let connection: ManagementPersistentConnection; - let start = Date.now(); + const start = Date.now(); try { connection = this._register(await connectRemoteAgentManagement(options, this.remoteAuthority, `renderer`)); } finally { diff --git a/src/vs/workbench/services/remote/common/remoteAgentService.ts b/src/vs/workbench/services/remote/common/remoteAgentService.ts index 6a899e927be..0054090e83d 100644 --- a/src/vs/workbench/services/remote/common/remoteAgentService.ts +++ b/src/vs/workbench/services/remote/common/remoteAgentService.ts @@ -68,6 +68,7 @@ export interface IRemoteAgentConnection { readonly onReconnecting: Event<void>; readonly onDidStateChange: Event<PersistentConnectionEvent>; + dispose(): void; getChannel<T extends IChannel>(channelName: string): T; withChannel<T extends IChannel, R>(channelName: string, callback: (channel: T) => Promise<R>): Promise<R>; registerChannel<T extends IServerChannel<RemoteAgentConnectionContext>>(channelName: string, channel: T): void; diff --git a/src/vs/workbench/services/remote/common/remoteExplorerService.ts b/src/vs/workbench/services/remote/common/remoteExplorerService.ts index 474747c2282..ff29e0375ca 100644 --- a/src/vs/workbench/services/remote/common/remoteExplorerService.ts +++ b/src/vs/workbench/services/remote/common/remoteExplorerService.ts @@ -290,7 +290,7 @@ export class PortsAttributes extends Disposable { } const attributes: PortAttributes[] = []; - for (let attributesKey in settingValue) { + for (const attributesKey in settingValue) { if (attributesKey === undefined) { continue; } @@ -379,7 +379,7 @@ export class PortsAttributes extends Disposable { } public async addAttributes(port: number, attributes: Partial<Attributes>, target: ConfigurationTarget) { - let settingValue = this.configurationService.inspect(PortsAttributes.SETTING); + const settingValue = this.configurationService.inspect(PortsAttributes.SETTING); const remoteValue: any = settingValue.userRemoteValue; let newRemoteValue: any; if (!remoteValue || !isObject(remoteValue)) { @@ -532,7 +532,7 @@ export class TunnelModel extends Disposable { return deprecatedValue; } - return this.storageService.get(await this.getStorageKey(), StorageScope.GLOBAL); + return this.storageService.get(await this.getStorageKey(), StorageScope.PROFILE); } async restoreForwarded() { @@ -541,7 +541,7 @@ export class TunnelModel extends Disposable { if (tunnelRestoreValue && (tunnelRestoreValue !== this.knownPortsRestoreValue)) { const tunnels = <Tunnel[] | undefined>JSON.parse(tunnelRestoreValue) ?? []; this.logService.trace(`ForwardedPorts: (TunnelModel) restoring ports ${tunnels.map(tunnel => tunnel.remotePort).join(', ')}`); - for (let tunnel of tunnels) { + for (const tunnel of tunnels) { if (!mapHasAddressLocalhostOrAllInterfaces(this.detected, tunnel.remoteHost, tunnel.remotePort)) { await this.forward({ remote: { host: tunnel.remoteHost, port: tunnel.remotePort }, @@ -560,7 +560,7 @@ export class TunnelModel extends Disposable { const key = await this.getStorageKey(); this.restoreListener = this._register(this.storageService.onDidChangeValue(async (e) => { if (e.key === key) { - this.tunnelRestoreValue = Promise.resolve(this.storageService.get(await this.getStorageKey(), StorageScope.GLOBAL)); + this.tunnelRestoreValue = Promise.resolve(this.storageService.get(await this.getStorageKey(), StorageScope.PROFILE)); await this.restoreForwarded(); } })); @@ -572,7 +572,7 @@ export class TunnelModel extends Disposable { const valueToStore = JSON.stringify(Array.from(this.forwarded.values()).filter(value => value.source.source === TunnelSource.User)); if (valueToStore !== this.knownPortsRestoreValue) { this.knownPortsRestoreValue = valueToStore; - this.storageService.store(await this.getStorageKey(), this.knownPortsRestoreValue, StorageScope.GLOBAL, StorageTarget.USER); + this.storageService.store(await this.getStorageKey(), this.knownPortsRestoreValue, StorageScope.PROFILE, StorageTarget.USER); } } } @@ -939,7 +939,7 @@ class RemoteExplorerService implements IRemoteExplorerService { if (current !== newName) { this._targetType = name; this.storageService.store(REMOTE_EXPLORER_TYPE_KEY, this._targetType.toString(), StorageScope.WORKSPACE, StorageTarget.USER); - this.storageService.store(REMOTE_EXPLORER_TYPE_KEY, this._targetType.toString(), StorageScope.GLOBAL, StorageTarget.USER); + this.storageService.store(REMOTE_EXPLORER_TYPE_KEY, this._targetType.toString(), StorageScope.PROFILE, StorageTarget.USER); this._onDidChangeTargetType.fire(this._targetType); } } diff --git a/src/vs/workbench/services/search/common/ignoreFile.ts b/src/vs/workbench/services/search/common/ignoreFile.ts index a1a0b181a01..769c8aa8273 100644 --- a/src/vs/workbench/services/search/common/ignoreFile.ts +++ b/src/vs/workbench/services/search/common/ignoreFile.ts @@ -10,8 +10,25 @@ export class IgnoreFile { private isPathIgnored: (path: string, isDir: boolean, parent?: IgnoreFile) => boolean; - constructor(contents: string, location: string, parent?: IgnoreFile) { - this.isPathIgnored = this.parseIgnoreFile(contents, location, parent); + constructor( + contents: string, + private readonly location: string, + private readonly parent?: IgnoreFile) { + if (location[location.length - 1] === '\\') { + throw Error('Unexpected path format, do not use trailing backslashes'); + } + if (location[location.length - 1] !== '/') { + location += '/'; + } + this.isPathIgnored = this.parseIgnoreFile(contents, this.location, this.parent); + } + + /** + * Updates the contents of the ignorefile. Preservering the location and parent + * @param contents The new contents of the gitignore file + */ + updateContents(contents: string) { + this.isPathIgnored = this.parseIgnoreFile(contents, this.location, this.parent); } /** diff --git a/src/vs/workbench/services/search/common/searchExtTypes.ts b/src/vs/workbench/services/search/common/searchExtTypes.ts index 5b8449ffd83..6d037174bed 100644 --- a/src/vs/workbench/services/search/common/searchExtTypes.ts +++ b/src/vs/workbench/services/search/common/searchExtTypes.ts @@ -223,7 +223,7 @@ export interface TextSearchOptions extends SearchOptions { } /** - * Represents the severiry of a TextSearchComplete message. + * Represents the severity of a TextSearchComplete message. */ export enum TextSearchCompleteMessageType { Information = 1, diff --git a/src/vs/workbench/services/search/common/searchService.ts b/src/vs/workbench/services/search/common/searchService.ts index 282bbfbe40e..f5127405714 100644 --- a/src/vs/workbench/services/search/common/searchService.ts +++ b/src/vs/workbench/services/search/common/searchService.ts @@ -134,9 +134,7 @@ export class SearchService extends Disposable implements ISearchService { return; } - if (onProgress) { - onProgress(item); - } + onProgress?.(item); }; const exists = await Promise.all(query.folderQueries.map(query => this.fileService.exists(query.folder))); @@ -173,13 +171,9 @@ export class SearchService extends Disposable implements ISearchService { private getSchemesInQuery(query: ISearchQuery): Set<string> { const schemes = new Set<string>(); - if (query.folderQueries) { - query.folderQueries.forEach(fq => schemes.add(fq.folder.scheme)); - } + query.folderQueries?.forEach(fq => schemes.add(fq.folder.scheme)); - if (query.extraFileResources) { - query.extraFileResources.forEach(extraFile => schemes.add(extraFile.scheme)); - } + query.extraFileResources?.forEach(extraFile => schemes.add(extraFile.scheme)); return schemes; } @@ -288,6 +282,7 @@ export class SearchService extends Disposable implements ISearchService { const cacheStats: ICachedSearchStats = fileSearchStats.detailStats as ICachedSearchStats; type CachedSearchCompleteClassifcation = { + owner: 'roblourens'; reason?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; resultCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true }; workspaceFolderCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true }; @@ -330,6 +325,7 @@ export class SearchService extends Disposable implements ISearchService { const searchEngineStats: ISearchEngineStats = fileSearchStats.detailStats as ISearchEngineStats; type SearchCompleteClassification = { + owner: 'roblourens'; reason?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; resultCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true }; workspaceFolderCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true }; @@ -387,6 +383,7 @@ export class SearchService extends Disposable implements ISearchService { } type TextSearchCompleteClassification = { + owner: 'roblourens'; reason?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; workspaceFolderCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true }; endToEndTime: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true }; @@ -419,7 +416,7 @@ export class SearchService extends Disposable implements ISearchService { if (query.type === QueryType.Text) { const canonicalToOriginalResources = new ResourceMap<URI>(); - for (let editorInput of this.editorService.editors) { + for (const editorInput of this.editorService.editors) { const canonical = EditorResourceAccessor.getCanonicalUri(editorInput, { supportSideBySide: SideBySideEditor.PRIMARY }); const original = EditorResourceAccessor.getOriginalUri(editorInput, { supportSideBySide: SideBySideEditor.PRIMARY }); diff --git a/src/vs/workbench/services/search/node/fileSearch.ts b/src/vs/workbench/services/search/node/fileSearch.ts index e6b9bb1633c..a7afc5cae5b 100644 --- a/src/vs/workbench/services/search/node/fileSearch.ts +++ b/src/vs/workbench/services/search/node/fileSearch.ts @@ -184,7 +184,7 @@ export class FileWalker { private cmdTraversal(folderQuery: IFolderQuery, onResult: (result: IRawFileMatch) => void, onMessage: (message: IProgressMessage) => void, cb: (err?: Error) => void): void { const rootFolder = folderQuery.folder.fsPath; const isMac = platform.isMacintosh; - let cmd: childProcess.ChildProcess; + const killCmd = () => cmd && cmd.kill(); killCmds.add(killCmd); @@ -196,10 +196,9 @@ export class FileWalker { let leftover = ''; const tree = this.initDirectoryTree(); - let noSiblingsClauses: boolean; const ripgrep = spawnRipgrepCmd(this.config, folderQuery, this.config.includePattern, this.folderExcludePatterns.get(folderQuery.folder.fsPath)!.expression); - cmd = ripgrep.cmd; - noSiblingsClauses = !Object.keys(ripgrep.siblingClauses).length; + const cmd = ripgrep.cmd; + const noSiblingsClauses = !Object.keys(ripgrep.siblingClauses).length; const escapedArgs = ripgrep.rgArgs.args .map(arg => arg.match(/^-/) ? arg : `'${arg}'`) diff --git a/src/vs/workbench/services/search/node/rawSearchService.ts b/src/vs/workbench/services/search/node/rawSearchService.ts index 82e31e6f31f..6e9251f016f 100644 --- a/src/vs/workbench/services/search/node/rawSearchService.ts +++ b/src/vs/workbench/services/search/node/rawSearchService.ts @@ -334,9 +334,7 @@ export class SearchService implements IRawSearchService { private doSearch(engine: ISearchEngine<IRawFileMatch>, progressCallback: IFileProgressCallback, batchSize: number, token?: CancellationToken): Promise<ISearchEngineSuccess> { return new Promise<ISearchEngineSuccess>((c, e) => { let batch: IRawFileMatch[] = []; - if (token) { - token.onCancellationRequested(() => engine.cancel()); - } + token?.onCancellationRequested(() => engine.cancel()); engine.search((match) => { if (match) { diff --git a/src/vs/workbench/services/search/test/common/ignoreFile.test.ts b/src/vs/workbench/services/search/test/common/ignoreFile.test.ts index c05122f092b..bf65e4ffdb2 100644 --- a/src/vs/workbench/services/search/test/common/ignoreFile.test.ts +++ b/src/vs/workbench/services/search/test/common/ignoreFile.test.ts @@ -64,7 +64,7 @@ function assertNoIgnoreMatch(ignoreFile: string, ignoreFileLocation: string, inp suite('Parsing .gitignore files', () => { test('paths with trailing slashes do not match files', () => { - let i = 'node_modules/\n'; + const i = 'node_modules/\n'; assertNoIgnoreMatch(i, '/', '/node_modules'); assertIgnoreMatch(i, '/', '/node_modules/'); @@ -253,7 +253,7 @@ suite('Parsing .gitignore files', () => { }); test('real world example: vscode-js-debug', () => { - let i = `.cache/ + const i = `.cache/ .profile/ .cdp-profile/ .headless-profile/ @@ -273,7 +273,7 @@ suite('Parsing .gitignore files', () => { /testWorkspace/webview/win/true/ *.cpuprofile`; - let included = [ + const included = [ '/distro', '/inner/coverage', @@ -288,7 +288,7 @@ suite('Parsing .gitignore files', () => { '/best/b/c.actual', ]; - let excluded = [ + const excluded = [ '/.profile/', '/inner/.profile/', @@ -357,7 +357,7 @@ suite('Parsing .gitignore files', () => { vscode.db /.profile-oss`; - let included = [ + const included = [ '/inner/extensions/dist', '/inner/extensions/boop/dist/test', '/inner/extensions/boop/doop/dist', @@ -383,7 +383,7 @@ suite('Parsing .gitignore files', () => { '/extensions/boop/out', ]; - let excluded = [ + const excluded = [ '/extensions/dist/', '/extensions/boop/dist/test', '/extensions/boop/doop/dist/', diff --git a/src/vs/workbench/services/search/test/common/replace.test.ts b/src/vs/workbench/services/search/test/common/replace.test.ts index e5d13d8f413..a0376960de5 100644 --- a/src/vs/workbench/services/search/test/common/replace.test.ts +++ b/src/vs/workbench/services/search/test/common/replace.test.ts @@ -141,8 +141,8 @@ suite('Replace Pattern test', () => { }); test('case operations', () => { - let testObject = new ReplacePattern('a\\u$1l\\u\\l\\U$2M$3n', { pattern: 'a(l)l(good)m(e)n', isRegExp: true }); - let actual = testObject.getReplaceString('allgoodmen'); + const testObject = new ReplacePattern('a\\u$1l\\u\\l\\U$2M$3n', { pattern: 'a(l)l(good)m(e)n', isRegExp: true }); + const actual = testObject.getReplaceString('allgoodmen'); assert.strictEqual(actual, 'aLlGoODMen'); }); @@ -161,8 +161,8 @@ suite('Replace Pattern test', () => { }); test('case operations and newline', () => { // #140734 - let testObject = new ReplacePattern('$1\n\\U$2', { pattern: '(multi)(line)', isRegExp: true }); - let actual = testObject.getReplaceString('multiline'); + const testObject = new ReplacePattern('$1\n\\U$2', { pattern: '(multi)(line)', isRegExp: true }); + const actual = testObject.getReplaceString('multiline'); assert.strictEqual(actual, 'multi\nLINE'); }); diff --git a/src/vs/workbench/services/search/worker/localFileSearch.ts b/src/vs/workbench/services/search/worker/localFileSearch.ts index 108ffef411e..a49661f0285 100644 --- a/src/vs/workbench/services/search/worker/localFileSearch.ts +++ b/src/vs/workbench/services/search/worker/localFileSearch.ts @@ -122,7 +122,7 @@ export class LocalFileSearchSimpleWorker implements ILocalFileSearchSimpleWorker let fileCount = 0; let resultCount = 0; - let limitHit = false; + const limitHit = false; const processFile = async (file: FileNode) => { if (token.token.isCancellationRequested) { diff --git a/src/vs/workbench/services/sessionSync/browser/sessionSyncWorkbenchService.ts b/src/vs/workbench/services/sessionSync/browser/sessionSyncWorkbenchService.ts new file mode 100644 index 00000000000..2c0a8bb167e --- /dev/null +++ b/src/vs/workbench/services/sessionSync/browser/sessionSyncWorkbenchService.ts @@ -0,0 +1,358 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { URI } from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { ContextKeyExpr, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IFileService } from 'vs/platform/files/common/files'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; +import { IRequestService } from 'vs/platform/request/common/request'; +import { IStorageService, IStorageValueChangeEvent, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { IAuthenticationProvider } from 'vs/platform/userDataSync/common/userDataSync'; +import { UserDataSyncStoreClient } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; +import { AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { EDIT_SESSIONS_SIGNED_IN, EditSession, EDIT_SESSION_SYNC_TITLE, ISessionSyncWorkbenchService, EDIT_SESSIONS_SIGNED_IN_KEY } from 'vs/workbench/services/sessionSync/common/sessionSync'; + +type ExistingSession = IQuickPickItem & { session: AuthenticationSession & { providerId: string } }; +type AuthenticationProviderOption = IQuickPickItem & { provider: IAuthenticationProvider }; + +export class SessionSyncWorkbenchService extends Disposable implements ISessionSyncWorkbenchService { + + _serviceBrand = undefined; + + private serverConfiguration = this.productService['sessionSync.store']; + private storeClient: UserDataSyncStoreClient | undefined; + + #authenticationInfo: { sessionId: string; token: string; providerId: string } | undefined; + private static CACHED_SESSION_STORAGE_KEY = 'editSessionSyncAccountPreference'; + + private initialized = false; + private readonly signedInContext: IContextKey<boolean>; + + constructor( + @IFileService private readonly fileService: IFileService, + @IStorageService private readonly storageService: IStorageService, + @IQuickInputService private readonly quickInputService: IQuickInputService, + @IAuthenticationService private readonly authenticationService: IAuthenticationService, + @IExtensionService private readonly extensionService: IExtensionService, + @IEnvironmentService private readonly environmentService: IEnvironmentService, + @ILogService private readonly logService: ILogService, + @IProductService private readonly productService: IProductService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IRequestService private readonly requestService: IRequestService, + ) { + super(); + + // If the user signs out of the current session, reset our cached auth state in memory and on disk + this._register(this.authenticationService.onDidChangeSessions((e) => this.onDidChangeSessions(e.event))); + + // If another window changes the preferred session storage, reset our cached auth state in memory + this._register(this.storageService.onDidChangeValue(e => this.onDidChangeStorage(e))); + + this.registerResetAuthenticationAction(); + + this.signedInContext = EDIT_SESSIONS_SIGNED_IN.bindTo(this.contextKeyService); + this.signedInContext.set(this.existingSessionId !== undefined); + } + + /** + * + * @param editSession An object representing edit session state to be restored. + * @returns The ref of the stored edit session state. + */ + async write(editSession: EditSession): Promise<string> { + await this.initialize(); + if (!this.initialized) { + throw new Error('Please sign in to store your edit session.'); + } + + return this.storeClient!.write('editSessions', JSON.stringify(editSession), null); + } + + /** + * @param ref: A specific content ref to retrieve content for, if it exists. + * If undefined, this method will return the latest saved edit session, if any. + * + * @returns An object representing the requested or latest edit session state, if any. + */ + async read(ref: string | undefined): Promise<{ ref: string; editSession: EditSession } | undefined> { + await this.initialize(); + if (!this.initialized) { + throw new Error('Please sign in to apply your latest edit session.'); + } + + let content: string | undefined | null; + try { + if (ref !== undefined) { + content = await this.storeClient?.resolveContent('editSessions', ref); + } else { + const result = await this.storeClient?.read('editSessions', null); + content = result?.content; + ref = result?.ref; + } + } catch (ex) { + this.logService.error(ex); + } + + // TODO@joyceerhl Validate session data, check schema version + return (content !== undefined && content !== null && ref !== undefined) ? { ref: ref, editSession: JSON.parse(content) } : undefined; + } + + async delete(ref: string) { + await this.initialize(); + if (!this.initialized) { + throw new Error(`Unable to delete edit session with ref ${ref}.`); + } + + try { + await this.storeClient?.delete('editSessions', ref); + } catch (ex) { + this.logService.error(ex); + } + } + + private async initialize() { + if (this.initialized) { + return; + } + this.initialized = await this.doInitialize(); + this.signedInContext.set(this.initialized); + } + + /** + * + * Ensures that the store client is initialized, + * meaning that authentication is configured and it + * can be used to communicate with the remote storage service + */ + private async doInitialize(): Promise<boolean> { + // Wait for authentication extensions to be registered + await this.extensionService.whenInstalledExtensionsRegistered(); + + if (!this.serverConfiguration?.url) { + throw new Error('Unable to initialize sessions sync as session sync preference is not configured in product.json.'); + } + + if (!this.storeClient) { + this.storeClient = new UserDataSyncStoreClient(URI.parse(this.serverConfiguration.url), this.productService, this.requestService, this.logService, this.environmentService, this.fileService, this.storageService); + this._register(this.storeClient.onTokenFailed(() => { + this.logService.info('Edit Sessions: clearing edit sessions authentication preference because of successive token failures.'); + this.clearAuthenticationPreference(); + })); + } + + // If we already have an existing auth session in memory, use that + if (this.#authenticationInfo !== undefined) { + return true; + } + + // If the user signed in previously and the session is still available, reuse that without prompting the user again + const existingSessionId = this.existingSessionId; + if (existingSessionId) { + this.logService.trace(`Edit Sessions: Searching for existing authentication session with ID ${existingSessionId}`); + const existing = await this.getExistingSession(); + if (existing !== undefined) { + this.logService.trace(`Edit Sessions: Found existing authentication session with ID ${existingSessionId}`); + this.#authenticationInfo = { sessionId: existing.session.id, token: existing.session.accessToken, providerId: existing.session.providerId }; + this.storeClient.setAuthToken(this.#authenticationInfo.token, this.#authenticationInfo.providerId); + return true; + } + } + + // Ask the user to pick a preferred account + const session = await this.getAccountPreference(); + if (session !== undefined) { + this.#authenticationInfo = { sessionId: session.id, token: session.accessToken, providerId: session.providerId }; + this.storeClient.setAuthToken(this.#authenticationInfo.token, this.#authenticationInfo.providerId); + this.existingSessionId = session.id; + this.logService.trace(`Edit Sessions: Saving authentication session preference for ID ${session.id}.`); + return true; + } + + return false; + } + + /** + * + * Prompts the user to pick an authentication option for storing and getting edit sessions. + */ + private async getAccountPreference(): Promise<AuthenticationSession & { providerId: string } | undefined> { + const quickpick = this.quickInputService.createQuickPick<ExistingSession | AuthenticationProviderOption>(); + quickpick.title = localize('account preference', 'Sign In to Use Edit Sessions'); + quickpick.ok = false; + quickpick.placeholder = localize('choose account placeholder', "Select an account to sign in"); + quickpick.ignoreFocusOut = true; + quickpick.items = await this.createQuickpickItems(); + + return new Promise((resolve, reject) => { + quickpick.onDidHide((e) => { + resolve(undefined); + quickpick.dispose(); + }); + + quickpick.onDidAccept(async (e) => { + const selection = quickpick.selectedItems[0]; + const session = 'provider' in selection ? { ...await this.authenticationService.createSession(selection.provider.id, selection.provider.scopes), providerId: selection.provider.id } : selection.session; + resolve(session); + quickpick.hide(); + }); + + quickpick.show(); + }); + } + + private async createQuickpickItems(): Promise<(ExistingSession | AuthenticationProviderOption | IQuickPickSeparator)[]> { + const options: (ExistingSession | AuthenticationProviderOption | IQuickPickSeparator)[] = []; + + options.push({ type: 'separator', label: localize('signed in', "Signed In") }); + + const sessions = await this.getAllSessions(); + options.push(...sessions); + + options.push({ type: 'separator', label: localize('others', "Others") }); + + for (const authenticationProvider of (await this.getAuthenticationProviders())) { + const signedInForProvider = sessions.some(account => account.session.providerId === authenticationProvider.id); + if (!signedInForProvider || this.authenticationService.supportsMultipleAccounts(authenticationProvider.id)) { + const providerName = this.authenticationService.getLabel(authenticationProvider.id); + options.push({ label: localize('sign in using account', "Sign in with {0}", providerName), provider: authenticationProvider }); + } + } + + return options; + } + + /** + * + * Returns all authentication sessions available from {@link getAuthenticationProviders}. + */ + private async getAllSessions() { + const authenticationProviders = await this.getAuthenticationProviders(); + const accounts = new Map<string, ExistingSession>(); + let currentSession: ExistingSession | undefined; + + for (const provider of authenticationProviders) { + const sessions = await this.authenticationService.getSessions(provider.id, provider.scopes); + + for (const session of sessions) { + const item = { + label: session.account.label, + description: this.authenticationService.getLabel(provider.id), + session: { ...session, providerId: provider.id } + }; + accounts.set(item.session.account.id, item); + if (this.existingSessionId === session.id) { + currentSession = item; + } + } + } + + if (currentSession !== undefined) { + accounts.set(currentSession.session.account.id, currentSession); + } + + return [...accounts.values()]; + } + + /** + * + * Returns all authentication providers which can be used to authenticate + * to the remote storage service, based on product.json configuration + * and registered authentication providers. + */ + private async getAuthenticationProviders() { + if (!this.serverConfiguration) { + throw new Error('Unable to get configured authentication providers as session sync preference is not configured in product.json.'); + } + + // Get the list of authentication providers configured in product.json + const authenticationProviders = this.serverConfiguration.authenticationProviders; + const configuredAuthenticationProviders = Object.keys(authenticationProviders).reduce<IAuthenticationProvider[]>((result, id) => { + result.push({ id, scopes: authenticationProviders[id].scopes }); + return result; + }, []); + + // Filter out anything that isn't currently available through the authenticationService + const availableAuthenticationProviders = this.authenticationService.declaredProviders; + + return configuredAuthenticationProviders.filter(({ id }) => availableAuthenticationProviders.some(provider => provider.id === id)); + } + + private get existingSessionId() { + return this.storageService.get(SessionSyncWorkbenchService.CACHED_SESSION_STORAGE_KEY, StorageScope.APPLICATION); + } + + private set existingSessionId(sessionId: string | undefined) { + if (sessionId === undefined) { + this.storageService.remove(SessionSyncWorkbenchService.CACHED_SESSION_STORAGE_KEY, StorageScope.APPLICATION); + } else { + this.storageService.store(SessionSyncWorkbenchService.CACHED_SESSION_STORAGE_KEY, sessionId, StorageScope.APPLICATION, StorageTarget.MACHINE); + } + } + + private async getExistingSession() { + const accounts = await this.getAllSessions(); + return accounts.find((account) => account.session.id === this.existingSessionId); + } + + private async onDidChangeStorage(e: IStorageValueChangeEvent): Promise<void> { + if (e.key === SessionSyncWorkbenchService.CACHED_SESSION_STORAGE_KEY + && e.scope === StorageScope.APPLICATION + ) { + const newSessionId = this.existingSessionId; + const previousSessionId = this.#authenticationInfo?.sessionId; + + if (previousSessionId !== newSessionId) { + this.logService.trace(`Edit Sessions: resetting authentication state because authentication session ID preference changed from ${previousSessionId} to ${newSessionId}.`); + this.#authenticationInfo = undefined; + this.initialized = false; + } + } + } + + private clearAuthenticationPreference(): void { + this.#authenticationInfo = undefined; + this.initialized = false; + this.existingSessionId = undefined; + this.signedInContext.set(false); + } + + private onDidChangeSessions(e: AuthenticationSessionsChangeEvent): void { + if (this.#authenticationInfo?.sessionId && e.removed.find(session => session.id === this.#authenticationInfo?.sessionId)) { + this.clearAuthenticationPreference(); + } + } + + private registerResetAuthenticationAction() { + const that = this; + this._register(registerAction2(class ResetEditSessionAuthenticationAction extends Action2 { + constructor() { + super({ + id: 'workbench.sessionSync.actions.resetAuth', + title: localize('reset auth', '{0}: Sign Out', EDIT_SESSION_SYNC_TITLE), + precondition: ContextKeyExpr.equals(EDIT_SESSIONS_SIGNED_IN_KEY, true), + menu: [{ + id: MenuId.CommandPalette, + }, + { + id: MenuId.AccountsContext, + group: '2_editSessions', + when: ContextKeyExpr.equals(EDIT_SESSIONS_SIGNED_IN_KEY, true), + }] + }); + } + + run() { + that.clearAuthenticationPreference(); + } + })); + } +} diff --git a/src/vs/workbench/services/sessionSync/common/sessionSync.ts b/src/vs/workbench/services/sessionSync/common/sessionSync.ts new file mode 100644 index 00000000000..6eafdb65490 --- /dev/null +++ b/src/vs/workbench/services/sessionSync/common/sessionSync.ts @@ -0,0 +1,59 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; + +export const EDIT_SESSION_SYNC_TITLE = localize('session sync', 'Edit Sessions'); + +export const ISessionSyncWorkbenchService = createDecorator<ISessionSyncWorkbenchService>('ISessionSyncWorkbenchService'); +export interface ISessionSyncWorkbenchService { + _serviceBrand: undefined; + + read(ref: string | undefined): Promise<{ ref: string; editSession: EditSession } | undefined>; + write(editSession: EditSession): Promise<string>; + delete(ref: string): Promise<void>; +} + +export enum ChangeType { + Addition = 1, + Deletion = 2, +} + +export enum FileType { + File = 1, +} + +interface Addition { + relativeFilePath: string; + fileType: FileType.File; + contents: string; + type: ChangeType.Addition; +} + +interface Deletion { + relativeFilePath: string; + fileType: FileType.File; + contents: undefined; + type: ChangeType.Deletion; +} + +export type Change = Addition | Deletion; + +export interface Folder { + name: string; + workingChanges: Change[]; +} + +export const EditSessionSchemaVersion = 1; + +export interface EditSession { + version: number; + folders: Folder[]; +} + +export const EDIT_SESSIONS_SIGNED_IN_KEY = 'editSessionsSignedIn'; +export const EDIT_SESSIONS_SIGNED_IN = new RawContextKey<boolean>(EDIT_SESSIONS_SIGNED_IN_KEY, false); diff --git a/src/vs/workbench/services/storage/browser/storageService.ts b/src/vs/workbench/services/storage/browser/storageService.ts new file mode 100644 index 00000000000..694d8317a1f --- /dev/null +++ b/src/vs/workbench/services/storage/browser/storageService.ts @@ -0,0 +1,437 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { isSafari } from 'vs/base/browser/browser'; +import { IndexedDB } from 'vs/base/browser/indexedDB'; +import { DeferredPromise, Promises } from 'vs/base/common/async'; +import { toErrorMessage } from 'vs/base/common/errorMessage'; +import { Emitter } from 'vs/base/common/event'; +import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { assertIsDefined } from 'vs/base/common/types'; +import { InMemoryStorageDatabase, isStorageItemsChangeEvent, IStorage, IStorageDatabase, IStorageItemsChangeEvent, IUpdateRequest, Storage } from 'vs/base/parts/storage/common/storage'; +import { ILogService } from 'vs/platform/log/common/log'; +import { AbstractStorageService, isProfileUsingDefaultStorage, IS_NEW_KEY, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { IUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { IAnyWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; +import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; + +export class BrowserStorageService extends AbstractStorageService { + + private static BROWSER_DEFAULT_FLUSH_INTERVAL = 5 * 1000; // every 5s because async operations are not permitted on shutdown + + private applicationStorage: IStorage | undefined; + private applicationStorageDatabase: IIndexedDBStorageDatabase | undefined; + private readonly applicationStoragePromise = new DeferredPromise<{ indededDb: IIndexedDBStorageDatabase; storage: IStorage }>(); + + private profileStorage: IStorage | undefined; + private profileStorageDatabase: IIndexedDBStorageDatabase | undefined; + private profileStorageProfile = this.userDataProfileService.currentProfile; + private readonly profileStorageDisposables = this._register(new DisposableStore()); + + private workspaceStorage: IStorage | undefined; + private workspaceStorageDatabase: IIndexedDBStorageDatabase | undefined; + + get hasPendingUpdate(): boolean { + return Boolean( + this.applicationStorageDatabase?.hasPendingUpdate || + this.profileStorageDatabase?.hasPendingUpdate || + this.workspaceStorageDatabase?.hasPendingUpdate + ); + } + + constructor( + private readonly payload: IAnyWorkspaceIdentifier, + private readonly userDataProfileService: IUserDataProfileService, + @ILogService private readonly logService: ILogService, + ) { + super({ flushInterval: BrowserStorageService.BROWSER_DEFAULT_FLUSH_INTERVAL }); + + this.registerListeners(); + } + + private registerListeners(): void { + this._register(this.userDataProfileService.onDidChangeCurrentProfile(e => e.join(this.switchToProfile(e.profile, e.preserveData)))); + } + + private getId(scope: StorageScope): string { + switch (scope) { + case StorageScope.APPLICATION: + return 'global'; // use the default profile application DB for application scope + case StorageScope.PROFILE: + if (isProfileUsingDefaultStorage(this.profileStorageProfile)) { + return 'global'; // default profile DB has a fixed name for backwards compatibility + } else { + return `global-${this.profileStorageProfile.id}`; + } + case StorageScope.WORKSPACE: + return this.payload.id; + } + } + + protected async doInitialize(): Promise<void> { + + // Init storages + await Promises.settled([ + this.createApplicationStorage(), + this.createProfileStorage(this.profileStorageProfile), + this.createWorkspaceStorage() + ]); + } + + private async createApplicationStorage(): Promise<void> { + const applicationStorageIndexedDB = await IndexedDBStorageDatabase.create({ id: this.getId(StorageScope.APPLICATION), broadcastChanges: true }, this.logService); + + this.applicationStorageDatabase = this._register(applicationStorageIndexedDB); + this.applicationStorage = this._register(new Storage(this.applicationStorageDatabase)); + + this._register(this.applicationStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.APPLICATION, key))); + + await this.applicationStorage.init(); + + this.updateIsNew(this.applicationStorage); + + this.applicationStoragePromise.complete({ indededDb: applicationStorageIndexedDB, storage: this.applicationStorage }); + } + + private async createProfileStorage(profile: IUserDataProfile): Promise<void> { + + // First clear any previously associated disposables + this.profileStorageDisposables.clear(); + + // Remember profile associated to profile storage + this.profileStorageProfile = profile; + + if (isProfileUsingDefaultStorage(this.profileStorageProfile)) { + + // If we are using default profile storage, the profile storage is + // actually the same as application storage. As such we + // avoid creating the storage library a second time on + // the same DB. + + const { indededDb: applicationStorageIndexedDB, storage: applicationStorage } = await this.applicationStoragePromise.p; + + this.profileStorageDatabase = applicationStorageIndexedDB; + this.profileStorage = applicationStorage; + } else { + const profileStorageIndexedDB = await IndexedDBStorageDatabase.create({ id: this.getId(StorageScope.PROFILE), broadcastChanges: true }, this.logService); + + this.profileStorageDatabase = this.profileStorageDisposables.add(profileStorageIndexedDB); + this.profileStorage = this.profileStorageDisposables.add(new Storage(this.profileStorageDatabase)); + } + + this.profileStorageDisposables.add(this.profileStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.PROFILE, key))); + + await this.profileStorage.init(); + + this.updateIsNew(this.profileStorage); + } + + private async createWorkspaceStorage(): Promise<void> { + const workspaceStorageIndexedDB = await IndexedDBStorageDatabase.create({ id: this.getId(StorageScope.WORKSPACE) }, this.logService); + + this.workspaceStorageDatabase = this._register(workspaceStorageIndexedDB); + this.workspaceStorage = this._register(new Storage(this.workspaceStorageDatabase)); + + this._register(this.workspaceStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.WORKSPACE, key))); + + await this.workspaceStorage.init(); + + this.updateIsNew(this.workspaceStorage); + } + + private updateIsNew(storage: IStorage): void { + const firstOpen = storage.getBoolean(IS_NEW_KEY); + if (firstOpen === undefined) { + storage.set(IS_NEW_KEY, true); + } else if (firstOpen) { + storage.set(IS_NEW_KEY, false); + } + } + + protected getStorage(scope: StorageScope): IStorage | undefined { + switch (scope) { + case StorageScope.APPLICATION: + return this.applicationStorage; + case StorageScope.PROFILE: + return this.profileStorage; + default: + return this.workspaceStorage; + } + } + + protected getLogDetails(scope: StorageScope): string | undefined { + return this.getId(scope); + } + + protected async switchToProfile(toProfile: IUserDataProfile, preserveData: boolean): Promise<void> { + if (this.profileStorageProfile && !this.canSwitchProfile(this.profileStorageProfile, toProfile)) { + return; + } + + const oldProfileStorage = assertIsDefined(this.profileStorage); + const oldItems = oldProfileStorage.items; + + // Close old profile storage but only if this is + // different from application storage! + if (oldProfileStorage !== this.applicationStorage) { + await oldProfileStorage.close(); + } + + // Create new profile storage & init + await this.createProfileStorage(toProfile); + + // Handle data switch and eventing + this.switchData(oldItems, assertIsDefined(this.profileStorage), StorageScope.PROFILE, preserveData); + } + + protected async switchToWorkspace(toWorkspace: IAnyWorkspaceIdentifier, preserveData: boolean): Promise<void> { + throw new Error('Migrating storage is currently unsupported in Web'); + } + + protected override shouldFlushWhenIdle(): boolean { + // this flush() will potentially cause new state to be stored + // since new state will only be created while the document + // has focus, one optimization is to not run this when the + // document has no focus, assuming that state has not changed + // + // another optimization is to not collect more state if we + // have a pending update already running which indicates + // that the connection is either slow or disconnected and + // thus unhealthy. + return document.hasFocus() && !this.hasPendingUpdate; + } + + close(): void { + + // Safari: there is an issue where the page can hang on load when + // a previous session has kept IndexedDB transactions running. + // The only fix seems to be to cancel any pending transactions + // (https://github.com/microsoft/vscode/issues/136295) + // + // On all other browsers, we keep the databases opened because + // we expect data to be written when the unload happens. + if (isSafari) { + this.applicationStorage?.close(); + this.profileStorageDatabase?.close(); + this.workspaceStorageDatabase?.close(); + } + + // Always dispose to ensure that no timeouts or callbacks + // get triggered in this phase. + this.dispose(); + } + + async clear(): Promise<void> { + + // Clear key/values + for (const scope of [StorageScope.APPLICATION, StorageScope.PROFILE, StorageScope.WORKSPACE]) { + for (const target of [StorageTarget.USER, StorageTarget.MACHINE]) { + for (const key of this.keys(scope, target)) { + this.remove(key, scope); + } + } + + await this.getStorage(scope)?.whenFlushed(); + } + + // Clear databases + await Promises.settled([ + this.applicationStorageDatabase?.clear() ?? Promise.resolve(), + this.profileStorageDatabase?.clear() ?? Promise.resolve(), + this.workspaceStorageDatabase?.clear() ?? Promise.resolve() + ]); + } +} + +interface IIndexedDBStorageDatabase extends IStorageDatabase, IDisposable { + + /** + * Whether an update in the DB is currently pending + * (either update or delete operation). + */ + readonly hasPendingUpdate: boolean; + + /** + * For testing only. + */ + clear(): Promise<void>; +} + +class InMemoryIndexedDBStorageDatabase extends InMemoryStorageDatabase implements IIndexedDBStorageDatabase { + + readonly hasPendingUpdate = false; + + async clear(): Promise<void> { + (await this.getItems()).clear(); + } + + dispose(): void { + // No-op + } +} + +interface IndexedDBStorageDatabaseOptions { + id: string; + broadcastChanges?: boolean; +} + +export class IndexedDBStorageDatabase extends Disposable implements IIndexedDBStorageDatabase { + + static async create(options: IndexedDBStorageDatabaseOptions, logService: ILogService): Promise<IIndexedDBStorageDatabase> { + try { + const database = new IndexedDBStorageDatabase(options, logService); + await database.whenConnected; + + return database; + } catch (error) { + logService.error(`[IndexedDB Storage ${options.id}] create(): ${toErrorMessage(error, true)}`); + + return new InMemoryIndexedDBStorageDatabase(); + } + } + + private static readonly STORAGE_DATABASE_PREFIX = 'vscode-web-state-db-'; + private static readonly STORAGE_OBJECT_STORE = 'ItemTable'; + + private static readonly STORAGE_BROADCAST_CHANNEL = 'vscode.web.state.changes'; + + private readonly _onDidChangeItemsExternal = this._register(new Emitter<IStorageItemsChangeEvent>()); + readonly onDidChangeItemsExternal = this._onDidChangeItemsExternal.event; + + private broadcastChannel: BroadcastChannel | undefined; + + private pendingUpdate: Promise<boolean> | undefined = undefined; + get hasPendingUpdate(): boolean { return !!this.pendingUpdate; } + + private readonly name: string; + private readonly whenConnected: Promise<IndexedDB>; + + private constructor( + options: IndexedDBStorageDatabaseOptions, + private readonly logService: ILogService + ) { + super(); + + this.name = `${IndexedDBStorageDatabase.STORAGE_DATABASE_PREFIX}${options.id}`; + this.broadcastChannel = options.broadcastChanges && ('BroadcastChannel' in window) ? new BroadcastChannel(IndexedDBStorageDatabase.STORAGE_BROADCAST_CHANNEL) : undefined; + + this.whenConnected = this.connect(); + + this.registerListeners(); + } + + private registerListeners(): void { + + // Check for storage change events from other + // windows/tabs via `BroadcastChannel` mechanisms. + if (this.broadcastChannel) { + const listener = (event: MessageEvent) => { + if (isStorageItemsChangeEvent(event.data)) { + this._onDidChangeItemsExternal.fire(event.data); + } + }; + + this.broadcastChannel.addEventListener('message', listener); + this._register(toDisposable(() => { + this.broadcastChannel?.removeEventListener('message', listener); + this.broadcastChannel?.close(); + })); + } + } + + private async connect(): Promise<IndexedDB> { + try { + return await IndexedDB.create(this.name, undefined, [IndexedDBStorageDatabase.STORAGE_OBJECT_STORE]); + } catch (error) { + this.logService.error(`[IndexedDB Storage ${this.name}] connect() error: ${toErrorMessage(error)}`); + + throw error; + } + } + + async getItems(): Promise<Map<string, string>> { + const db = await this.whenConnected; + + function isValid(value: unknown): value is string { + return typeof value === 'string'; + } + + return db.getKeyValues<string>(IndexedDBStorageDatabase.STORAGE_OBJECT_STORE, isValid); + } + + async updateItems(request: IUpdateRequest): Promise<void> { + + // Run the update + let didUpdate = false; + this.pendingUpdate = this.doUpdateItems(request); + try { + didUpdate = await this.pendingUpdate; + } finally { + this.pendingUpdate = undefined; + } + + // Broadcast changes to other windows/tabs if enabled + // and only if we actually did update storage items. + if (this.broadcastChannel && didUpdate) { + const event: IStorageItemsChangeEvent = { + changed: request.insert, + deleted: request.delete + }; + + this.broadcastChannel.postMessage(event); + } + } + + private async doUpdateItems(request: IUpdateRequest): Promise<boolean> { + + // Return early if the request is empty + const toInsert = request.insert; + const toDelete = request.delete; + if ((!toInsert && !toDelete) || (toInsert?.size === 0 && toDelete?.size === 0)) { + return false; + } + + const db = await this.whenConnected; + + // Update `ItemTable` with inserts and/or deletes + await db.runInTransaction(IndexedDBStorageDatabase.STORAGE_OBJECT_STORE, 'readwrite', objectStore => { + const requests: IDBRequest[] = []; + + // Inserts + if (toInsert) { + for (const [key, value] of toInsert) { + requests.push(objectStore.put(value, key)); + } + } + + // Deletes + if (toDelete) { + for (const key of toDelete) { + requests.push(objectStore.delete(key)); + } + } + + return requests; + }); + + return true; + } + + async close(): Promise<void> { + const db = await this.whenConnected; + + // Wait for pending updates to having finished + await this.pendingUpdate; + + // Finally, close IndexedDB + return db.close(); + } + + async clear(): Promise<void> { + const db = await this.whenConnected; + + await db.runInTransaction(IndexedDBStorageDatabase.STORAGE_OBJECT_STORE, 'readwrite', objectStore => objectStore.clear()); + } +} diff --git a/src/vs/workbench/services/storage/electron-sandbox/storageService.ts b/src/vs/workbench/services/storage/electron-sandbox/storageService.ts new file mode 100644 index 00000000000..6c69b79301a --- /dev/null +++ b/src/vs/workbench/services/storage/electron-sandbox/storageService.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/services'; +import { NativeStorageService } from 'vs/platform/storage/electron-sandbox/storageService'; +import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { IEmptyWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; +import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; + +export class NativeWorkbenchStorageService extends NativeStorageService { + + constructor( + workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier | undefined, + private readonly userDataProfileService: IUserDataProfileService, + userDataProfilesService: IUserDataProfilesService, + mainProcessService: IMainProcessService, + environmentService: IEnvironmentService + ) { + super(workspace, { currentProfile: userDataProfileService.currentProfile, defaultProfile: userDataProfilesService.defaultProfile }, mainProcessService, environmentService); + + this.registerListeners(); + } + + private registerListeners(): void { + this._register(this.userDataProfileService.onDidChangeCurrentProfile(e => e.join(this.switchToProfile(e.profile, e.preserveData)))); + } +} diff --git a/src/vs/workbench/services/storage/test/browser/storageService.test.ts b/src/vs/workbench/services/storage/test/browser/storageService.test.ts new file mode 100644 index 00000000000..58d1c19d3cb --- /dev/null +++ b/src/vs/workbench/services/storage/test/browser/storageService.test.ts @@ -0,0 +1,268 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { strictEqual } from 'assert'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { Schemas } from 'vs/base/common/network'; +import { joinPath } from 'vs/base/common/resources'; +import { URI } from 'vs/base/common/uri'; +import { Storage } from 'vs/base/parts/storage/common/storage'; +import { flakySuite } from 'vs/base/test/common/testUtils'; +import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; +import { FileService } from 'vs/platform/files/common/fileService'; +import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; +import { NullLogService } from 'vs/platform/log/common/log'; +import { StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { createSuite } from 'vs/platform/storage/test/common/storageService.test'; +import { IUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { BrowserStorageService, IndexedDBStorageDatabase } from 'vs/workbench/services/storage/browser/storageService'; +import { UserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfileService'; + +async function createStorageService(): Promise<[DisposableStore, BrowserStorageService]> { + const disposables = new DisposableStore(); + const logService = new NullLogService(); + + const fileService = disposables.add(new FileService(logService)); + + const userDataProvider = disposables.add(new InMemoryFileSystemProvider()); + disposables.add(fileService.registerProvider(Schemas.vscodeUserData, userDataProvider)); + + const profilesRoot = URI.file('/profiles').with({ scheme: Schemas.inMemory }); + + const inMemoryExtraProfileRoot = joinPath(profilesRoot, 'extra'); + const inMemoryExtraProfile: IUserDataProfile = { + id: 'id', + name: 'inMemory', + isDefault: false, + location: inMemoryExtraProfileRoot, + globalStorageHome: joinPath(inMemoryExtraProfileRoot, 'globalStorageHome'), + settingsResource: joinPath(inMemoryExtraProfileRoot, 'settingsResource'), + keybindingsResource: joinPath(inMemoryExtraProfileRoot, 'keybindingsResource'), + tasksResource: joinPath(inMemoryExtraProfileRoot, 'tasksResource'), + snippetsHome: joinPath(inMemoryExtraProfileRoot, 'snippetsHome'), + extensionsResource: joinPath(inMemoryExtraProfileRoot, 'extensionsResource') + }; + + const storageService = disposables.add(new BrowserStorageService({ id: 'workspace-storage-test' }, new UserDataProfileService(inMemoryExtraProfile), logService)); + + await storageService.initialize(); + + return [disposables, storageService]; +} + +flakySuite('StorageService (browser)', function () { + const disposables = new DisposableStore(); + let storageService: BrowserStorageService; + + createSuite<BrowserStorageService>({ + setup: async () => { + const res = await createStorageService(); + disposables.add(res[0]); + storageService = res[1]; + + return storageService; + }, + teardown: async () => { + await storageService.clear(); + disposables.clear(); + } + }); +}); + +flakySuite('StorageService (browser specific)', () => { + const disposables = new DisposableStore(); + let storageService: BrowserStorageService; + + setup(async () => { + const res = await createStorageService(); + disposables.add(res[0]); + + storageService = res[1]; + }); + + teardown(async () => { + await storageService.clear(); + disposables.clear(); + }); + + test('clear', () => { + return runWithFakedTimers({ useFakeTimers: true }, async () => { + storageService.store('bar', 'foo', StorageScope.APPLICATION, StorageTarget.MACHINE); + storageService.store('bar', 3, StorageScope.APPLICATION, StorageTarget.USER); + storageService.store('bar', 'foo', StorageScope.PROFILE, StorageTarget.MACHINE); + storageService.store('bar', 3, StorageScope.PROFILE, StorageTarget.USER); + storageService.store('bar', 'foo', StorageScope.WORKSPACE, StorageTarget.MACHINE); + storageService.store('bar', 3, StorageScope.WORKSPACE, StorageTarget.USER); + + await storageService.clear(); + + for (const scope of [StorageScope.APPLICATION, StorageScope.PROFILE, StorageScope.WORKSPACE]) { + for (const target of [StorageTarget.USER, StorageTarget.MACHINE]) { + strictEqual(storageService.get('bar', scope), undefined); + strictEqual(storageService.keys(scope, target).length, 0); + } + } + }); + }); +}); + +flakySuite('IndexDBStorageDatabase (browser)', () => { + + const id = 'workspace-storage-db-test'; + const logService = new NullLogService(); + + teardown(async () => { + const storage = await IndexedDBStorageDatabase.create({ id }, logService); + await storage.clear(); + }); + + test('Basics', async () => { + let storage = new Storage(await IndexedDBStorageDatabase.create({ id }, logService)); + + await storage.init(); + + // Insert initial data + storage.set('bar', 'foo'); + storage.set('barNumber', 55); + storage.set('barBoolean', true); + storage.set('barUndefined', undefined); + storage.set('barNull', null); + + strictEqual(storage.get('bar'), 'foo'); + strictEqual(storage.get('barNumber'), '55'); + strictEqual(storage.get('barBoolean'), 'true'); + strictEqual(storage.get('barUndefined'), undefined); + strictEqual(storage.get('barNull'), undefined); + + strictEqual(storage.size, 3); + strictEqual(storage.items.size, 3); + + await storage.close(); + + storage = new Storage(await IndexedDBStorageDatabase.create({ id }, logService)); + + await storage.init(); + + // Check initial data still there + strictEqual(storage.get('bar'), 'foo'); + strictEqual(storage.get('barNumber'), '55'); + strictEqual(storage.get('barBoolean'), 'true'); + strictEqual(storage.get('barUndefined'), undefined); + strictEqual(storage.get('barNull'), undefined); + + strictEqual(storage.size, 3); + strictEqual(storage.items.size, 3); + + // Update data + storage.set('bar', 'foo2'); + storage.set('barNumber', 552); + + strictEqual(storage.get('bar'), 'foo2'); + strictEqual(storage.get('barNumber'), '552'); + + await storage.close(); + + storage = new Storage(await IndexedDBStorageDatabase.create({ id }, logService)); + + await storage.init(); + + // Check initial data still there + strictEqual(storage.get('bar'), 'foo2'); + strictEqual(storage.get('barNumber'), '552'); + strictEqual(storage.get('barBoolean'), 'true'); + strictEqual(storage.get('barUndefined'), undefined); + strictEqual(storage.get('barNull'), undefined); + + strictEqual(storage.size, 3); + strictEqual(storage.items.size, 3); + + // Delete data + storage.delete('bar'); + storage.delete('barNumber'); + storage.delete('barBoolean'); + + strictEqual(storage.get('bar', 'undefined'), 'undefined'); + strictEqual(storage.get('barNumber', 'undefinedNumber'), 'undefinedNumber'); + strictEqual(storage.get('barBoolean', 'undefinedBoolean'), 'undefinedBoolean'); + + strictEqual(storage.size, 0); + strictEqual(storage.items.size, 0); + + await storage.close(); + + storage = new Storage(await IndexedDBStorageDatabase.create({ id }, logService)); + + await storage.init(); + + strictEqual(storage.get('bar', 'undefined'), 'undefined'); + strictEqual(storage.get('barNumber', 'undefinedNumber'), 'undefinedNumber'); + strictEqual(storage.get('barBoolean', 'undefinedBoolean'), 'undefinedBoolean'); + + strictEqual(storage.size, 0); + strictEqual(storage.items.size, 0); + }); + + test('Clear', async () => { + let storage = new Storage(await IndexedDBStorageDatabase.create({ id }, logService)); + + await storage.init(); + + storage.set('bar', 'foo'); + storage.set('barNumber', 55); + storage.set('barBoolean', true); + + await storage.close(); + + const db = await IndexedDBStorageDatabase.create({ id }, logService); + storage = new Storage(db); + + await storage.init(); + await db.clear(); + + storage = new Storage(await IndexedDBStorageDatabase.create({ id }, logService)); + + await storage.init(); + + strictEqual(storage.get('bar'), undefined); + strictEqual(storage.get('barNumber'), undefined); + strictEqual(storage.get('barBoolean'), undefined); + + strictEqual(storage.size, 0); + strictEqual(storage.items.size, 0); + }); + + test('Inserts and Deletes at the same time', async () => { + let storage = new Storage(await IndexedDBStorageDatabase.create({ id }, logService)); + + await storage.init(); + + storage.set('bar', 'foo'); + storage.set('barNumber', 55); + storage.set('barBoolean', true); + + await storage.close(); + + storage = new Storage(await IndexedDBStorageDatabase.create({ id }, logService)); + + await storage.init(); + + storage.set('bar', 'foobar'); + const largeItem = JSON.stringify({ largeItem: 'Hello World'.repeat(1000) }); + storage.set('largeItem', largeItem); + storage.delete('barNumber'); + storage.delete('barBoolean'); + + await storage.close(); + + storage = new Storage(await IndexedDBStorageDatabase.create({ id }, logService)); + + await storage.init(); + + strictEqual(storage.get('bar'), 'foobar'); + strictEqual(storage.get('largeItem'), largeItem); + strictEqual(storage.get('barNumber'), undefined); + strictEqual(storage.get('barBoolean'), undefined); + }); +}); diff --git a/src/vs/workbench/services/telemetry/browser/telemetryService.ts b/src/vs/workbench/services/telemetry/browser/telemetryService.ts index 4b4d5ead47c..892f318d293 100644 --- a/src/vs/workbench/services/telemetry/browser/telemetryService.ts +++ b/src/vs/workbench/services/telemetry/browser/telemetryService.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { ApplicationInsights } from '@microsoft/applicationinsights-web'; import { Disposable } from 'vs/base/common/lifecycle'; import { IObservableValue } from 'vs/base/common/observableValue'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -11,98 +10,17 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ILoggerService } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; import { IStorageService } from 'vs/platform/storage/common/storage'; +import { OneDataSystemWebAppender } from 'vs/platform/telemetry/browser/1dsAppender'; +import { WebAppInsightsAppender } from 'vs/platform/telemetry/browser/appInsightsAppender'; import { ClassifiedEvent, GDPRClassification, StrictPropertyCheck } from 'vs/platform/telemetry/common/gdprTypings'; import { ITelemetryData, ITelemetryInfo, ITelemetryService, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; import { TelemetryLogAppender } from 'vs/platform/telemetry/common/telemetryLogAppender'; import { ITelemetryServiceConfig, TelemetryService as BaseTelemetryService } from 'vs/platform/telemetry/common/telemetryService'; -import { ITelemetryAppender, NullTelemetryService, supportsTelemetry, validateTelemetryData } from 'vs/platform/telemetry/common/telemetryUtils'; +import { ITelemetryAppender, NullTelemetryService, supportsTelemetry } from 'vs/platform/telemetry/common/telemetryUtils'; import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { resolveWorkbenchCommonProperties } from 'vs/workbench/services/telemetry/browser/workbenchCommonProperties'; -class WebAppInsightsAppender implements ITelemetryAppender { - private _aiClient: ApplicationInsights | undefined; - private _aiClientLoaded = false; - private _telemetryCache: { eventName: string; data: any }[] = []; - - constructor(private _eventPrefix: string, aiKey: string) { - const endpointUrl = 'https://mobile.events.data.microsoft.com/collect/v1'; - import('@microsoft/applicationinsights-web').then(aiLibrary => { - this._aiClient = new aiLibrary.ApplicationInsights({ - config: { - instrumentationKey: aiKey, - endpointUrl, - disableAjaxTracking: true, - disableExceptionTracking: true, - disableFetchTracking: true, - disableCorrelationHeaders: true, - disableCookiesUsage: true, - autoTrackPageVisitTime: false, - emitLineDelimitedJson: true, - }, - }); - this._aiClient.loadAppInsights(); - // Client is loaded we can now flush the cached events - this._aiClientLoaded = true; - this._telemetryCache.forEach(cacheEntry => this.log(cacheEntry.eventName, cacheEntry.data)); - this._telemetryCache = []; - - // If we cannot access the endpoint this most likely means it's being blocked - // and we should not attempt to send any telemetry. - fetch(endpointUrl).catch(() => (this._aiClient = undefined)); - }).catch(err => { - console.error(err); - }); - } - - /** - * Logs a telemetry event with eventName and data - * @param eventName The event name - * @param data The data associated with the events - */ - public log(eventName: string, data: any): void { - if (!this._aiClient && this._aiClientLoaded) { - return; - } else if (!this._aiClient && !this._aiClientLoaded) { - this._telemetryCache.push({ eventName, data }); - return; - } - - data = validateTelemetryData(data); - - // Web does not expect properties and measurements so we must - // spread them out. This is different from desktop which expects them - data = { ...data.properties, ...data.measurements }; - - // undefined assertion is ok since above two if statements cover both cases - this._aiClient!.trackEvent({ name: this._eventPrefix + '/' + eventName }, data); - } - - /** - * Flushes all the telemetry data still in the buffer - */ - public flush(): Promise<any> { - if (this._aiClient) { - this._aiClient.flush(); - this._aiClient = undefined; - } - return Promise.resolve(undefined); - } -} - -class WebTelemetryAppender implements ITelemetryAppender { - - constructor(private _appender: ITelemetryAppender) { } - - log(eventName: string, data: any): void { - this._appender.log(eventName, data); - } - - flush(): Promise<void> { - return this._appender.flush(); - } -} - export class TelemetryService extends Disposable implements ITelemetryService { declare readonly _serviceBrand: undefined; @@ -120,11 +38,20 @@ export class TelemetryService extends Disposable implements ITelemetryService { ) { super(); - if (supportsTelemetry(productService, environmentService) && productService.aiConfig?.asimovKey) { + if (supportsTelemetry(productService, environmentService) && productService.aiConfig?.asimovKey && productService.aiConfig?.ariaKey) { // If remote server is present send telemetry through that, else use the client side appender - const telemetryProvider: ITelemetryAppender = remoteAgentService.getConnection() !== null ? { log: remoteAgentService.logTelemetry.bind(remoteAgentService), flush: remoteAgentService.flushTelemetry.bind(remoteAgentService) } : new WebAppInsightsAppender('monacoworkbench', productService.aiConfig?.asimovKey); + const internalTesting = configurationService.getValue<boolean>('telemetry.internalTesting'); + const appenders = []; + if (internalTesting || productService.aiConfig?.preferAria) { + const telemetryProvider: ITelemetryAppender = remoteAgentService.getConnection() !== null ? { log: remoteAgentService.logTelemetry.bind(remoteAgentService), flush: remoteAgentService.flushTelemetry.bind(remoteAgentService) } : new OneDataSystemWebAppender('monacoworkbench', null, productService.aiConfig?.ariaKey); + appenders.push(telemetryProvider); + } else { + const telemetryProvider: ITelemetryAppender = remoteAgentService.getConnection() !== null ? { log: remoteAgentService.logTelemetry.bind(remoteAgentService), flush: remoteAgentService.flushTelemetry.bind(remoteAgentService) } : new WebAppInsightsAppender('monacoworkbench', productService.aiConfig?.asimovKey); + appenders.push(telemetryProvider); + } + appenders.push(new TelemetryLogAppender(loggerService, environmentService)); const config: ITelemetryServiceConfig = { - appenders: [new WebTelemetryAppender(telemetryProvider), new TelemetryLogAppender(loggerService, environmentService)], + appenders, commonProperties: resolveWorkbenchCommonProperties(storageService, productService.commit, productService.version, environmentService.remoteAuthority, productService.embedderIdentifier, productService.removeTelemetryMachineId, environmentService.options && environmentService.options.resolveCommonTelemetryProperties), sendErrorTelemetry: this.sendErrorTelemetry, }; diff --git a/src/vs/workbench/services/telemetry/browser/workbenchCommonProperties.ts b/src/vs/workbench/services/telemetry/browser/workbenchCommonProperties.ts index 69116d9a458..16bce794a23 100644 --- a/src/vs/workbench/services/telemetry/browser/workbenchCommonProperties.ts +++ b/src/vs/workbench/services/telemetry/browser/workbenchCommonProperties.ts @@ -30,15 +30,15 @@ export async function resolveWorkbenchCommonProperties( resolveAdditionalProperties?: () => { [key: string]: any } ): Promise<{ [name: string]: string | undefined }> { const result: { [name: string]: string | undefined } = Object.create(null); - const firstSessionDate = storageService.get(firstSessionDateStorageKey, StorageScope.GLOBAL)!; - const lastSessionDate = storageService.get(lastSessionDateStorageKey, StorageScope.GLOBAL)!; + const firstSessionDate = storageService.get(firstSessionDateStorageKey, StorageScope.APPLICATION)!; + const lastSessionDate = storageService.get(lastSessionDateStorageKey, StorageScope.APPLICATION)!; let machineId: string | undefined; if (!removeMachineId) { - machineId = storageService.get(machineIdKey, StorageScope.GLOBAL); + machineId = storageService.get(machineIdKey, StorageScope.APPLICATION); if (!machineId) { machineId = uuid.generateUuid(); - storageService.store(machineIdKey, machineId, StorageScope.GLOBAL, StorageTarget.MACHINE); + storageService.store(machineIdKey, machineId, StorageScope.APPLICATION, StorageTarget.MACHINE); } } else { machineId = `Redacted-${productIdentifier ?? 'web'}`; diff --git a/src/vs/workbench/services/telemetry/electron-sandbox/workbenchCommonProperties.ts b/src/vs/workbench/services/telemetry/electron-sandbox/workbenchCommonProperties.ts index ff36a5231e6..9d5b4282308 100644 --- a/src/vs/workbench/services/telemetry/electron-sandbox/workbenchCommonProperties.ts +++ b/src/vs/workbench/services/telemetry/electron-sandbox/workbenchCommonProperties.ts @@ -23,8 +23,8 @@ export async function resolveWorkbenchCommonProperties( remoteAuthority?: string ): Promise<{ [name: string]: string | boolean | undefined }> { const result = await resolveCommonProperties(fileService, release, hostname, process.arch, commit, version, machineId, msftInternalDomains, installSourcePath); - const firstSessionDate = storageService.get(firstSessionDateStorageKey, StorageScope.GLOBAL)!; - const lastSessionDate = storageService.get(lastSessionDateStorageKey, StorageScope.GLOBAL)!; + const firstSessionDate = storageService.get(firstSessionDateStorageKey, StorageScope.APPLICATION)!; + const lastSessionDate = storageService.get(lastSessionDateStorageKey, StorageScope.APPLICATION)!; // __GDPR__COMMON__ "common.version.shell" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } result['common.version.shell'] = process.versions['electron']; diff --git a/src/vs/workbench/services/telemetry/test/electron-browser/commonProperties.test.ts b/src/vs/workbench/services/telemetry/test/electron-browser/commonProperties.test.ts index ac596534940..8ae51addd3e 100644 --- a/src/vs/workbench/services/telemetry/test/electron-browser/commonProperties.test.ts +++ b/src/vs/workbench/services/telemetry/test/electron-browser/commonProperties.test.ts @@ -72,7 +72,7 @@ suite('Telemetry - common properties', function () { test('lastSessionDate when available', async function () { - testStorageService.store('telemetry.lastSessionDate', new Date().toUTCString(), StorageScope.GLOBAL, StorageTarget.MACHINE); + testStorageService.store('telemetry.lastSessionDate', new Date().toUTCString(), StorageScope.APPLICATION, StorageTarget.MACHINE); const props = await resolveWorkbenchCommonProperties(testStorageService, testFileService, release(), hostname(), commit, version, 'someMachineId', undefined, installSource); assert.ok('common.lastSessionDate' in props); // conditional, see below diff --git a/src/vs/workbench/services/textMate/browser/abstractTextMateService.ts b/src/vs/workbench/services/textMate/browser/abstractTextMateService.ts index 0df6157e1bd..8a59203a4df 100644 --- a/src/vs/workbench/services/textMate/browser/abstractTextMateService.ts +++ b/src/vs/workbench/services/textMate/browser/abstractTextMateService.ts @@ -12,7 +12,8 @@ import * as resources from 'vs/base/common/resources'; import * as types from 'vs/base/common/types'; import { equals as equalArray } from 'vs/base/common/arrays'; import { URI } from 'vs/base/common/uri'; -import { IState, ITokenizationSupport, LanguageId, TokenizationRegistry, StandardTokenType, ITokenizationSupportFactory, TokenizationResult, EncodedTokenizationResult } from 'vs/editor/common/languages'; +import { IState, ITokenizationSupport, TokenizationRegistry, ITokenizationSupportFactory, TokenizationResult, EncodedTokenizationResult } from 'vs/editor/common/languages'; +import { LanguageId, StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; import { nullTokenizeEncoded } from 'vs/editor/common/languages/nullTokenize'; import { generateTokensCSSForColorMap } from 'vs/editor/common/languages/supports/tokenization'; import { ILanguageService } from 'vs/editor/common/languages/language'; @@ -95,10 +96,10 @@ export abstract class AbstractTextMateService extends Disposable implements ITex const embeddedLanguages: IValidEmbeddedLanguagesMap = Object.create(null); if (grammar.embeddedLanguages) { - let scopes = Object.keys(grammar.embeddedLanguages); + const scopes = Object.keys(grammar.embeddedLanguages); for (let i = 0, len = scopes.length; i < len; i++) { - let scope = scopes[i]; - let language = grammar.embeddedLanguages[scope]; + const scope = scopes[i]; + const language = grammar.embeddedLanguages[scope]; if (typeof language !== 'string') { // never hurts to be too careful continue; @@ -290,7 +291,7 @@ export abstract class AbstractTextMateService extends Disposable implements ITex } private static _toColorMap(colorMap: string[]): Color[] { - let result: Color[] = [null!]; + const result: Color[] = [null!]; for (let i = 1, len = colorMap.length; i < len; i++) { result[i] = Color.fromHex(colorMap[i]); } @@ -308,8 +309,8 @@ export abstract class AbstractTextMateService extends Disposable implements ITex protected _doUpdateTheme(grammarFactory: TMGrammarFactory | null, theme: IRawTheme, tokenColorMap: string[]): void { grammarFactory?.setTheme(theme, tokenColorMap); - let colorMap = AbstractTextMateService._toColorMap(tokenColorMap); - let cssRules = generateTokensCSSForColorMap(colorMap); + const colorMap = AbstractTextMateService._toColorMap(tokenColorMap); + const cssRules = generateTokensCSSForColorMap(colorMap); this._styleElement.textContent = cssRules; TokenizationRegistry.setColorMap(colorMap); } @@ -319,13 +320,13 @@ export abstract class AbstractTextMateService extends Disposable implements ITex return false; } for (let i = b.length - 1; i >= 0; i--) { - let r1 = b[i]; - let r2 = a[i]; + const r1 = b[i]; + const r2 = a[i]; if (r1.scope !== r2.scope) { return false; } - let s1 = r1.settings; - let s2 = r2.settings; + const s1 = r1.settings; + const s2 = r2.settings; if (s1 && s2) { if (s1.fontStyle !== s2.fontStyle || s1.foreground !== s2.foreground || s1.background !== s2.background) { return false; diff --git a/src/vs/workbench/services/textMate/browser/nativeTextMateService.ts b/src/vs/workbench/services/textMate/browser/nativeTextMateService.ts index bbf4fddc3e0..fa3ff793511 100644 --- a/src/vs/workbench/services/textMate/browser/nativeTextMateService.ts +++ b/src/vs/workbench/services/textMate/browser/nativeTextMateService.ts @@ -238,7 +238,7 @@ export class TextMateService extends AbstractTextMateService { } private _killWorker(): void { - for (let key of Object.keys(this._tokenizers)) { + for (const key of Object.keys(this._tokenizers)) { this._tokenizers[key].dispose(); } this._tokenizers = Object.create(null); diff --git a/src/vs/workbench/services/textMate/browser/textMateWorker.ts b/src/vs/workbench/services/textMate/browser/textMateWorker.ts index 6b848d33c24..9c4339175eb 100644 --- a/src/vs/workbench/services/textMate/browser/textMateWorker.ts +++ b/src/vs/workbench/services/textMate/browser/textMateWorker.ts @@ -5,7 +5,7 @@ import { IWorkerContext } from 'vs/editor/common/services/editorSimpleWorker'; import { UriComponents, URI } from 'vs/base/common/uri'; -import { LanguageId } from 'vs/editor/common/languages'; +import { LanguageId } from 'vs/editor/common/encodedTokenAttributes'; import { IValidEmbeddedLanguagesMap, IValidTokenTypeMap, IValidGrammarDefinition } from 'vs/workbench/services/textMate/common/TMScopeRegistry'; import { TMGrammarFactory, ICreateGrammarResult } from 'vs/workbench/services/textMate/common/TMGrammarFactory'; import { IModelChangedEvent, MirrorTextModel } from 'vs/editor/common/model/mirrorTextModel'; @@ -214,9 +214,7 @@ export class TextMateWorker { public async acceptTheme(theme: IRawTheme, colorMap: string[]): Promise<void> { const grammarFactory = await this._grammarFactory; - if (grammarFactory) { - grammarFactory.setTheme(theme, colorMap); - } + grammarFactory?.setTheme(theme, colorMap); } public _setTokens(resource: URI, versionId: number, tokens: Uint8Array): void { diff --git a/src/vs/workbench/services/textMate/common/TMGrammarFactory.ts b/src/vs/workbench/services/textMate/common/TMGrammarFactory.ts index fc43c1b81da..1e9480e81a3 100644 --- a/src/vs/workbench/services/textMate/common/TMGrammarFactory.ts +++ b/src/vs/workbench/services/textMate/common/TMGrammarFactory.ts @@ -73,7 +73,7 @@ export class TMGrammarFactory extends Disposable { this._scopeRegistry.register(validGrammar); if (validGrammar.injectTo) { - for (let injectScope of validGrammar.injectTo) { + for (const injectScope of validGrammar.injectTo) { let injections = this._injections[injectScope]; if (!injections) { this._injections[injectScope] = injections = []; @@ -82,7 +82,7 @@ export class TMGrammarFactory extends Disposable { } if (validGrammar.embeddedLanguages) { - for (let injectScope of validGrammar.injectTo) { + for (const injectScope of validGrammar.injectTo) { let injectedEmbeddedLanguages = this._injectedEmbeddedLanguages[injectScope]; if (!injectedEmbeddedLanguages) { this._injectedEmbeddedLanguages[injectScope] = injectedEmbeddedLanguages = []; @@ -123,7 +123,7 @@ export class TMGrammarFactory extends Disposable { throw new Error(missingTMGrammarErrorMessage); } - let embeddedLanguages = grammarDefinition.embeddedLanguages; + const embeddedLanguages = grammarDefinition.embeddedLanguages; if (this._injectedEmbeddedLanguages[scopeName]) { const injectedEmbeddedLanguages = this._injectedEmbeddedLanguages[scopeName]; for (const injected of injectedEmbeddedLanguages) { diff --git a/src/vs/workbench/services/textMate/common/TMHelper.ts b/src/vs/workbench/services/textMate/common/TMHelper.ts index 32d73fa14b7..a773c8ec63f 100644 --- a/src/vs/workbench/services/textMate/common/TMHelper.ts +++ b/src/vs/workbench/services/textMate/common/TMHelper.ts @@ -21,9 +21,9 @@ export interface ITokenColorizationSetting { export function findMatchingThemeRule(theme: IColorTheme, scopes: string[], onlyColorRules: boolean = true): ThemeRule | null { for (let i = scopes.length - 1; i >= 0; i--) { - let parentScopes = scopes.slice(0, i); - let scope = scopes[i]; - let r = findMatchingThemeRule2(theme, scope, parentScopes, onlyColorRules); + const parentScopes = scopes.slice(0, i); + const scope = scopes[i]; + const r = findMatchingThemeRule2(theme, scope, parentScopes, onlyColorRules); if (r) { return r; } @@ -36,7 +36,7 @@ function findMatchingThemeRule2(theme: IColorTheme, scope: string, parentScopes: // Loop backwards, to ensure the last most specific rule wins for (let i = theme.tokenColors.length - 1; i >= 0; i--) { - let rule = theme.tokenColors[i]; + const rule = theme.tokenColors[i]; if (onlyColorRules && !rule.settings.foreground) { continue; } @@ -51,9 +51,9 @@ function findMatchingThemeRule2(theme: IColorTheme, scope: string, parentScopes: } for (let j = 0, lenJ = selectors.length; j < lenJ; j++) { - let rawSelector = selectors[j]; + const rawSelector = selectors[j]; - let themeRule = new ThemeRule(rawSelector, rule.settings); + const themeRule = new ThemeRule(rawSelector, rule.settings); if (themeRule.matches(scope, parentScopes)) { if (themeRule.isMoreSpecific(result)) { result = themeRule; @@ -74,7 +74,7 @@ export class ThemeRule { constructor(rawSelector: string, settings: ITokenColorizationSetting) { this.rawSelector = rawSelector; this.settings = settings; - let rawSelectorPieces = this.rawSelector.split(/ /); + const rawSelectorPieces = this.rawSelector.split(/ /); this.scope = rawSelectorPieces[rawSelectorPieces.length - 1]; this.parentScopes = rawSelectorPieces.slice(0, rawSelectorPieces.length - 1); } @@ -120,7 +120,7 @@ export class ThemeRule { } private static _matchesOne(selectorScope: string, scope: string): boolean { - let selectorPrefix = selectorScope + '.'; + const selectorPrefix = selectorScope + '.'; if (selectorScope === scope || scope.substring(0, selectorPrefix.length) === selectorPrefix) { return true; } diff --git a/src/vs/workbench/services/textMate/common/TMScopeRegistry.ts b/src/vs/workbench/services/textMate/common/TMScopeRegistry.ts index 83b041e7823..5d88a28f4f4 100644 --- a/src/vs/workbench/services/textMate/common/TMScopeRegistry.ts +++ b/src/vs/workbench/services/textMate/common/TMScopeRegistry.ts @@ -6,7 +6,7 @@ import * as resources from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { Disposable } from 'vs/base/common/lifecycle'; -import { StandardTokenType, LanguageId } from 'vs/editor/common/languages'; +import { LanguageId, StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; export interface IValidGrammarDefinition { location: URI; diff --git a/src/vs/workbench/services/textMate/common/TMTokenization.ts b/src/vs/workbench/services/textMate/common/TMTokenization.ts index 64816e5d912..d5265f8181c 100644 --- a/src/vs/workbench/services/textMate/common/TMTokenization.ts +++ b/src/vs/workbench/services/textMate/common/TMTokenization.ts @@ -4,7 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from 'vs/base/common/event'; -import { IState, ITokenizationSupport, LanguageId, TokenMetadata, TokenizationResult, EncodedTokenizationResult } from 'vs/editor/common/languages'; +import { IState, ITokenizationSupport, TokenizationResult, EncodedTokenizationResult } from 'vs/editor/common/languages'; +import { LanguageId, TokenMetadata } from 'vs/editor/common/encodedTokenAttributes'; import type { IGrammar, StackElement } from 'vscode-textmate'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -44,13 +45,13 @@ export class TMTokenization extends Disposable implements ITokenizationSupport { } if (this._containsEmbeddedLanguages) { - let seenLanguages = this._seenLanguages; - let tokens = textMateResult.tokens; + const seenLanguages = this._seenLanguages; + const tokens = textMateResult.tokens; // Must check if any of the embedded languages was hit for (let i = 0, len = (tokens.length >>> 1); i < len; i++) { - let metadata = tokens[(i << 1) + 1]; - let languageId = TokenMetadata.getLanguageId(metadata); + const metadata = tokens[(i << 1) + 1]; + const languageId = TokenMetadata.getLanguageId(metadata); if (!seenLanguages[languageId]) { seenLanguages[languageId] = true; diff --git a/src/vs/workbench/services/textfile/common/textEditorService.ts b/src/vs/workbench/services/textfile/common/textEditorService.ts index 9e40eb88f5a..c8c9050875b 100644 --- a/src/vs/workbench/services/textfile/common/textEditorService.ts +++ b/src/vs/workbench/services/textfile/common/textEditorService.ts @@ -227,9 +227,7 @@ export class TextEditorService extends Disposable implements ITextEditorService // Return early if already cached let input = this.editorInputCache.get(resource); if (input) { - if (cachedFn) { - cachedFn(input); - } + cachedFn?.(input); return input; } diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index 4b5f8ac1838..6672a7126d3 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -370,7 +370,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } // Abort if someone else managed to resolve the model by now - let isNewModel = !this.isResolved(); + const isNewModel = !this.isResolved(); if (!isNewModel) { this.trace('resolveFromBackup() - exit - without resolving because previously new model got created meanwhile'); diff --git a/src/vs/workbench/services/textfile/test/browser/browserTextFileService.io.test.ts b/src/vs/workbench/services/textfile/test/browser/browserTextFileService.io.test.ts index 4f157c3539e..bdb8ab83c92 100644 --- a/src/vs/workbench/services/textfile/test/browser/browserTextFileService.io.test.ts +++ b/src/vs/workbench/services/textfile/test/browser/browserTextFileService.io.test.ts @@ -53,7 +53,7 @@ if (isWeb) { service = instantiationService.createChild(collection).createInstance(TestBrowserTextFileServiceWithEncodingOverrides); await fileProvider.mkdir(URI.file(testDir)); - for (let fileName in files) { + for (const fileName in files) { await fileProvider.writeFile( URI.file(join(testDir, fileName)), files[fileName], diff --git a/src/vs/workbench/services/textfile/test/browser/textEditorService.test.ts b/src/vs/workbench/services/textfile/test/browser/textEditorService.test.ts index f29fd72d650..9c051e10cbc 100644 --- a/src/vs/workbench/services/textfile/test/browser/textEditorService.test.ts +++ b/src/vs/workbench/services/textfile/test/browser/textEditorService.test.ts @@ -66,7 +66,7 @@ suite('TextEditorService', () => { // Untyped Input (file casing) input = service.createTextEditor({ resource: toResource.call(this, '/index.html') }); - let inputDifferentCase = service.createTextEditor({ resource: toResource.call(this, '/INDEX.html') }); + const inputDifferentCase = service.createTextEditor({ resource: toResource.call(this, '/INDEX.html') }); if (!isLinux) { assert.strictEqual(input, inputDifferentCase); diff --git a/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts b/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts index 9f6d6e6fd0a..0fc86c74e4f 100644 --- a/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts +++ b/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts @@ -605,7 +605,7 @@ suite('Files - TextFileEditorModel', () => { }); test('File not modified error is handled gracefully', async function () { - let model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); await model.resolve(); @@ -620,7 +620,7 @@ suite('Files - TextFileEditorModel', () => { }); test('Resolve error is handled gracefully if model already exists', async function () { - let model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); await model.resolve(); accessor.textFileService.setReadStreamErrorOnce(new FileOperationError('error', FileOperationResult.FILE_NOT_FOUND)); @@ -771,7 +771,7 @@ suite('Files - TextFileEditorModel', () => { test('Save Participant, participant cancelled when saved again', async function () { const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); - let participations: boolean[] = []; + const participations: boolean[] = []; const participant = accessor.textFileService.files.addSaveParticipant({ participate: async (model, context, progress, token) => { @@ -821,7 +821,7 @@ suite('Files - TextFileEditorModel', () => { }); async function testSaveFromSaveParticipant(model: TextFileEditorModel, async: boolean): Promise<void> { - let savePromise: Promise<boolean>; + let breakLoop = false; const participant = accessor.textFileService.files.addSaveParticipant({ @@ -845,7 +845,7 @@ suite('Files - TextFileEditorModel', () => { await model.resolve(); model.updateTextEditorModel(createTextBufferFactory('foo')); - savePromise = model.save(); + const savePromise = model.save(); await savePromise; participant.dispose(); diff --git a/src/vs/workbench/services/textfile/test/browser/textFileEditorModelManager.test.ts b/src/vs/workbench/services/textfile/test/browser/textFileEditorModelManager.test.ts index a742cab97a6..3370269542d 100644 --- a/src/vs/workbench/services/textfile/test/browser/textFileEditorModelManager.test.ts +++ b/src/vs/workbench/services/textfile/test/browser/textFileEditorModelManager.test.ts @@ -136,7 +136,7 @@ suite('Files - TextFileEditorModelManager', () => { await manager.resolve(resource); let didResolve = false; - let onDidResolve = new Promise<void>(resolve => { + const onDidResolve = new Promise<void>(resolve => { manager.onDidResolve(({ model }) => { if (model.resource.toString() === resource.toString()) { didResolve = true; @@ -386,7 +386,7 @@ suite('Files - TextFileEditorModelManager', () => { const model = await manager.resolve(resource, { encoding: 'utf8' }); model.updateTextEditorModel(createTextBufferFactory('make dirty')); - let canDisposePromise = manager.canDispose(model as TextFileEditorModel); + const canDisposePromise = manager.canDispose(model as TextFileEditorModel); assert.ok(canDisposePromise instanceof Promise); let canDispose = false; @@ -401,7 +401,7 @@ suite('Files - TextFileEditorModelManager', () => { assert.strictEqual(canDispose, true); - let canDispose2 = manager.canDispose(model as TextFileEditorModel); + const canDispose2 = manager.canDispose(model as TextFileEditorModel); assert.strictEqual(canDispose2, true); manager.dispose(); @@ -436,7 +436,7 @@ suite('Files - TextFileEditorModelManager', () => { await manager.resolve(resource); let didResolve = false; - let onDidResolve = new Promise<void>(resolve => { + const onDidResolve = new Promise<void>(resolve => { manager.onDidResolve(({ model }) => { if (model.resource.toString() === resource.toString()) { didResolve = true; @@ -459,7 +459,7 @@ suite('Files - TextFileEditorModelManager', () => { let didResolve = false; let resolvedCounter = 0; - let onDidResolve = new Promise<void>(resolve => { + const onDidResolve = new Promise<void>(resolve => { manager.onDidResolve(({ model }) => { if (model.resource.toString() === resource.toString()) { resolvedCounter++; diff --git a/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts b/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts index 6bd19ad2707..70e05c4f3b8 100644 --- a/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts +++ b/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts @@ -143,7 +143,7 @@ suite('Files - TextFileService', () => { extensions: ['.one', '.two'] }); - let suggested = accessor.textFileService.suggestFilename('shleem', 'Untitled-1'); + const suggested = accessor.textFileService.suggestFilename('shleem', 'Untitled-1'); assert.strictEqual(suggested, 'Untitled-1'); registration.dispose(); }); @@ -155,7 +155,7 @@ suite('Files - TextFileService', () => { filenames: ['plumbus'] }); - let suggested = accessor.textFileService.suggestFilename('plumbus1', 'Untitled-1'); + const suggested = accessor.textFileService.suggestFilename('plumbus1', 'Untitled-1'); assert.strictEqual(suggested, 'Untitled-1.shleem'); registration.dispose(); }); @@ -166,7 +166,7 @@ suite('Files - TextFileService', () => { filenames: ['plumbus', 'shleem', 'gazorpazorp'] }); - let suggested = accessor.textFileService.suggestFilename('plumbus2', 'Untitled-1'); + const suggested = accessor.textFileService.suggestFilename('plumbus2', 'Untitled-1'); assert.strictEqual(suggested, 'plumbus'); registration.dispose(); }); diff --git a/src/vs/workbench/services/textfile/test/common/textFileService.io.test.ts b/src/vs/workbench/services/textfile/test/common/textFileService.io.test.ts index 584ad739372..3bc8637e4aa 100644 --- a/src/vs/workbench/services/textfile/test/common/textFileService.io.test.ts +++ b/src/vs/workbench/services/textfile/test/common/textFileService.io.test.ts @@ -380,7 +380,7 @@ export default function createSuite(params: Params) { await service.write(resource, '', { encoding: UTF8_with_bom }); - let detectedEncoding = await detectEncodingByBOM(resource.fsPath); + const detectedEncoding = await detectEncodingByBOM(resource.fsPath); assert.strictEqual(detectedEncoding, UTF8_with_bom); }); @@ -389,7 +389,7 @@ export default function createSuite(params: Params) { await service.write(resource, createTextModelSnapshot(''), { encoding: UTF8_with_bom }); - let detectedEncoding = await detectEncodingByBOM(resource.fsPath); + const detectedEncoding = await detectEncodingByBOM(resource.fsPath); assert.strictEqual(detectedEncoding, UTF8_with_bom); }); diff --git a/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts b/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts index 8f84c7dd937..ad472a25211 100644 --- a/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts +++ b/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts @@ -11,7 +11,7 @@ import { IModelService } from 'vs/editor/common/services/model'; import { TextResourceEditorModel } from 'vs/workbench/common/editor/textResourceEditorModel'; import { ITextFileService, TextFileResolveReason } from 'vs/workbench/services/textfile/common/textfiles'; import { Schemas } from 'vs/base/common/network'; -import { ITextModelService, ITextModelContentProvider, ITextEditorModel, IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService'; +import { ITextModelService, ITextModelContentProvider, ITextEditorModel, IResolvedTextEditorModel, isResolvedTextEditorModel } from 'vs/editor/common/services/resolverService'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { IFileService } from 'vs/platform/files/common/files'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; @@ -19,7 +19,7 @@ import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { ModelUndoRedoParticipant } from 'vs/editor/common/services/modelUndoRedoParticipant'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; -class ResourceModelCollection extends ReferenceCollection<Promise<ITextEditorModel>> { +class ResourceModelCollection extends ReferenceCollection<Promise<IResolvedTextEditorModel>> { private readonly providers = new Map<string, ITextModelContentProvider[]>(); private readonly modelsToDispose = new Set<string>(); @@ -33,11 +33,11 @@ class ResourceModelCollection extends ReferenceCollection<Promise<ITextEditorMod super(); } - createReferencedObject(key: string): Promise<ITextEditorModel> { + createReferencedObject(key: string): Promise<IResolvedTextEditorModel> { return this.doCreateReferencedObject(key); } - private async doCreateReferencedObject(key: string, skipActivateProvider?: boolean): Promise<ITextEditorModel> { + private async doCreateReferencedObject(key: string, skipActivateProvider?: boolean): Promise<IResolvedTextEditorModel> { // Untrack as being disposed this.modelsToDispose.delete(key); @@ -50,24 +50,36 @@ class ResourceModelCollection extends ReferenceCollection<Promise<ITextEditorMod throw new Error(`Unable to resolve inMemory resource ${key}`); } - return this.instantiationService.createInstance(TextResourceEditorModel, resource); + const model = this.instantiationService.createInstance(TextResourceEditorModel, resource); + if (this.ensureResolvedModel(model, key)) { + return model; + } } // Untitled Schema: go through untitled text service if (resource.scheme === Schemas.untitled) { - return this.textFileService.untitled.resolve({ untitledResource: resource }); + const model = await this.textFileService.untitled.resolve({ untitledResource: resource }); + if (this.ensureResolvedModel(model, key)) { + return model; + } } // File or remote file: go through text file service if (this.fileService.hasProvider(resource)) { - return this.textFileService.files.resolve(resource, { reason: TextFileResolveReason.REFERENCE }); + const model = await this.textFileService.files.resolve(resource, { reason: TextFileResolveReason.REFERENCE }); + if (this.ensureResolvedModel(model, key)) { + return model; + } } // Virtual documents if (this.providers.has(resource.scheme)) { await this.resolveTextModelContent(key); - return this.instantiationService.createInstance(TextResourceEditorModel, resource); + const model = this.instantiationService.createInstance(TextResourceEditorModel, resource); + if (this.ensureResolvedModel(model, key)) { + return model; + } } // Either unknown schema, or not yet registered, try to activate @@ -80,6 +92,14 @@ class ResourceModelCollection extends ReferenceCollection<Promise<ITextEditorMod throw new Error(`Unable to resolve resource ${key}`); } + private ensureResolvedModel(model: ITextEditorModel, key: string): model is IResolvedTextEditorModel { + if (isResolvedTextEditorModel(model)) { + return true; + } + + throw new Error(`Unable to resolve resource ${key}`); + } + destroyReferencedObject(key: string, modelPromise: Promise<ITextEditorModel>): void { // untitled and inMemory are bound to a different lifecycle @@ -173,7 +193,7 @@ export class TextModelResolverService extends Disposable implements ITextModelSe declare readonly _serviceBrand: undefined; - private readonly resourceModelCollection = this.instantiationService.createInstance(ResourceModelCollection); + private readonly resourceModelCollection: ResourceModelCollection & ReferenceCollection<Promise<IResolvedTextEditorModel>> /* TS Fail */ = this.instantiationService.createInstance(ResourceModelCollection); private readonly asyncModelCollection = new AsyncReferenceCollection(this.resourceModelCollection); constructor( @@ -195,8 +215,7 @@ export class TextModelResolverService extends Disposable implements ITextModelSe // with different resource forms (e.g. path casing on Windows) resource = this.uriIdentityService.asCanonicalUri(resource); - const result = await this.asyncModelCollection.acquire(resource.toString()); - return result as IReference<IResolvedTextEditorModel>; // TODO@Ben: why is this cast here? + return await this.asyncModelCollection.acquire(resource.toString()); } registerTextModelContentProvider(scheme: string, provider: ITextModelContentProvider): IDisposable { diff --git a/src/vs/workbench/services/textmodelResolver/test/browser/textModelResolverService.test.ts b/src/vs/workbench/services/textmodelResolver/test/browser/textModelResolverService.test.ts index 2130229b2ed..f394ba99eae 100644 --- a/src/vs/workbench/services/textmodelResolver/test/browser/textModelResolverService.test.ts +++ b/src/vs/workbench/services/textmodelResolver/test/browser/textModelResolverService.test.ts @@ -43,8 +43,8 @@ suite('Workbench - TextModelResolverService', () => { const disposable = accessor.textModelResolverService.registerTextModelContentProvider('test', { provideTextContent: async function (resource: URI): Promise<ITextModel | null> { if (resource.scheme === 'test') { - let modelContent = 'Hello Test'; - let languageSelection = accessor.languageService.createById('json'); + const modelContent = 'Hello Test'; + const languageSelection = accessor.languageService.createById('json'); return accessor.modelService.createModel(modelContent, languageSelection, resource); } @@ -53,14 +53,14 @@ suite('Workbench - TextModelResolverService', () => { } }); - let resource = URI.from({ scheme: 'test', authority: null!, path: 'thePath' }); - let input = instantiationService.createInstance(TextResourceEditorInput, resource, 'The Name', 'The Description', undefined, undefined); + const resource = URI.from({ scheme: 'test', authority: null!, path: 'thePath' }); + const input = instantiationService.createInstance(TextResourceEditorInput, resource, 'The Name', 'The Description', undefined, undefined); const model = await input.resolve(); assert.ok(model); assert.strictEqual(snapshotToString(((model as TextResourceEditorModel).createSnapshot()!)), 'Hello Test'); let disposed = false; - let disposedPromise = new Promise<void>(resolve => { + const disposedPromise = new Promise<void>(resolve => { Event.once(model.onWillDispose)(() => { disposed = true; resolve(); @@ -172,14 +172,14 @@ suite('Workbench - TextModelResolverService', () => { test('even loading documents should be refcounted', async () => { let resolveModel!: Function; - let waitForIt = new Promise(resolve => resolveModel = resolve); + const waitForIt = new Promise(resolve => resolveModel = resolve); const disposable = accessor.textModelResolverService.registerTextModelContentProvider('test', { provideTextContent: async (resource: URI): Promise<ITextModel> => { await waitForIt; - let modelContent = 'Hello Test'; - let languageSelection = accessor.languageService.createById('json'); + const modelContent = 'Hello Test'; + const languageSelection = accessor.languageService.createById('json'); return accessor.modelService.createModel(modelContent, languageSelection, resource); } }); @@ -203,7 +203,7 @@ suite('Workbench - TextModelResolverService', () => { modelRef1.dispose(); assert(!textModel.isDisposed(), 'the text model should still not be disposed'); - let p1 = new Promise<void>(resolve => textModel.onWillDispose(resolve)); + const p1 = new Promise<void>(resolve => textModel.onWillDispose(resolve)); modelRef2.dispose(); await p1; diff --git a/src/vs/workbench/services/themes/browser/fileIconThemeData.ts b/src/vs/workbench/services/themes/browser/fileIconThemeData.ts index 702c14464c3..c570a913f61 100644 --- a/src/vs/workbench/services/themes/browser/fileIconThemeData.ts +++ b/src/vs/workbench/services/themes/browser/fileIconThemeData.ts @@ -99,7 +99,7 @@ export class FileIconThemeData implements IWorkbenchFileIconTheme { static fromStorageData(storageService: IStorageService): FileIconThemeData | undefined { - const input = storageService.get(FileIconThemeData.STORAGE_KEY, StorageScope.GLOBAL); + const input = storageService.get(FileIconThemeData.STORAGE_KEY, StorageScope.PROFILE); if (!input) { return undefined; } @@ -146,7 +146,7 @@ export class FileIconThemeData implements IWorkbenchFileIconTheme { extensionData: ExtensionData.toJSONObject(this.extensionData), watch: this.watch }); - storageService.store(FileIconThemeData.STORAGE_KEY, data, StorageScope.GLOBAL, StorageTarget.MACHINE); + storageService.store(FileIconThemeData.STORAGE_KEY, data, StorageScope.PROFILE, StorageTarget.MACHINE); } } diff --git a/src/vs/workbench/services/themes/browser/productIconThemeData.ts b/src/vs/workbench/services/themes/browser/productIconThemeData.ts index 9e991bc6971..80a9e6d973b 100644 --- a/src/vs/workbench/services/themes/browser/productIconThemeData.ts +++ b/src/vs/workbench/services/themes/browser/productIconThemeData.ts @@ -107,14 +107,14 @@ export class ProductIconThemeData implements IWorkbenchProductIconTheme { } static fromStorageData(storageService: IStorageService): ProductIconThemeData | undefined { - const input = storageService.get(ProductIconThemeData.STORAGE_KEY, StorageScope.GLOBAL); + const input = storageService.get(ProductIconThemeData.STORAGE_KEY, StorageScope.PROFILE); if (!input) { return undefined; } try { - let data = JSON.parse(input); + const data = JSON.parse(input); const theme = new ProductIconThemeData('', '', ''); - for (let key in data) { + for (const key in data) { switch (key) { case 'id': case 'label': @@ -148,7 +148,7 @@ export class ProductIconThemeData implements IWorkbenchProductIconTheme { watch: this.watch, extensionData: ExtensionData.toJSONObject(this.extensionData), }); - storageService.store(ProductIconThemeData.STORAGE_KEY, data, StorageScope.GLOBAL, StorageTarget.MACHINE); + storageService.store(ProductIconThemeData.STORAGE_KEY, data, StorageScope.PROFILE, StorageTarget.MACHINE); } } @@ -159,7 +159,7 @@ interface ProductIconThemeDocument { function _loadProductIconThemeDocument(fileService: IExtensionResourceLoaderService, location: URI, warnings: string[]): Promise<ProductIconThemeDocument> { return fileService.readExtensionResource(location).then((content) => { const parseErrors: Json.ParseError[] = []; - let contentValue = Json.parse(content, parseErrors); + const contentValue = Json.parse(content, parseErrors); if (parseErrors.length > 0) { return Promise.reject(new Error(nls.localize('error.cannotparseicontheme', "Problems parsing product icons file: {0}", parseErrors.map(e => getParseErrorMessage(e.error)).join(', ')))); } else if (Json.getNodeType(contentValue) !== 'object') { diff --git a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts index 901f92d9f3e..3800c0fbd23 100644 --- a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts @@ -48,6 +48,7 @@ const DEFAULT_COLOR_THEME_ID = 'vs-dark vscode-theme-defaults-themes-dark_plus-j const DEFAULT_LIGHT_COLOR_THEME_ID = 'vs vscode-theme-defaults-themes-light_plus-json'; const PERSISTED_OS_COLOR_SCHEME = 'osColorScheme'; +const PERSISTED_OS_COLOR_SCHEME_SCOPE = StorageScope.APPLICATION; // the OS scheme depends on settings in the OS const defaultThemeExtensionId = 'vscode-theme-defaults'; @@ -151,7 +152,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { // the preferred color scheme (high contrast, light, dark) has changed since the last start const preferredColorScheme = this.getPreferredColorScheme(); - if (preferredColorScheme && themeData?.type !== preferredColorScheme && this.storageService.get(PERSISTED_OS_COLOR_SCHEME, StorageScope.GLOBAL) !== preferredColorScheme) { + if (preferredColorScheme && themeData?.type !== preferredColorScheme && this.storageService.get(PERSISTED_OS_COLOR_SCHEME, PERSISTED_OS_COLOR_SCHEME_SCOPE) !== preferredColorScheme) { themeData = ColorThemeData.createUnloadedThemeForThemeType(preferredColorScheme); } if (!themeData) { @@ -209,9 +210,9 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { const theme = this.colorThemeRegistry.findThemeBySettingsId(this.settings.colorTheme, fallbackTheme); const preferredColorScheme = this.getPreferredColorScheme(); - const prevScheme = this.storageService.get(PERSISTED_OS_COLOR_SCHEME, StorageScope.GLOBAL); + const prevScheme = this.storageService.get(PERSISTED_OS_COLOR_SCHEME, PERSISTED_OS_COLOR_SCHEME_SCOPE); if (preferredColorScheme !== prevScheme) { - this.storageService.store(PERSISTED_OS_COLOR_SCHEME, preferredColorScheme, StorageScope.GLOBAL, StorageTarget.USER); + this.storageService.store(PERSISTED_OS_COLOR_SCHEME, preferredColorScheme, PERSISTED_OS_COLOR_SCHEME_SCOPE, StorageTarget.USER); if (preferredColorScheme && theme?.type !== preferredColorScheme) { return this.applyPreferredColorTheme(preferredColorScheme); } @@ -372,9 +373,9 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { private async handlePreferredSchemeUpdated() { const scheme = this.getPreferredColorScheme(); - const prevScheme = this.storageService.get(PERSISTED_OS_COLOR_SCHEME, StorageScope.GLOBAL); + const prevScheme = this.storageService.get(PERSISTED_OS_COLOR_SCHEME, PERSISTED_OS_COLOR_SCHEME_SCOPE); if (scheme !== prevScheme) { - this.storageService.store(PERSISTED_OS_COLOR_SCHEME, scheme, StorageScope.GLOBAL, StorageTarget.MACHINE); + this.storageService.store(PERSISTED_OS_COLOR_SCHEME, scheme, PERSISTED_OS_COLOR_SCHEME_SCOPE, StorageTarget.MACHINE); if (scheme) { if (!prevScheme) { // remember the theme before scheme switching @@ -579,6 +580,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { const key = themeType + themeData.extensionId; if (!this.themeExtensionsActivated.get(key)) { type ActivatePluginClassification = { + owner: 'aeschli'; id: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight' }; name: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight' }; isBuiltin: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; diff --git a/src/vs/workbench/services/themes/common/colorExtensionPoint.ts b/src/vs/workbench/services/themes/common/colorExtensionPoint.ts index 5cd4f739140..bda39a526a2 100644 --- a/src/vs/workbench/services/themes/common/colorExtensionPoint.ts +++ b/src/vs/workbench/services/themes/common/colorExtensionPoint.ts @@ -93,7 +93,7 @@ export class ColorExtensionPoint { collector.error(nls.localize('invalid.colorConfiguration', "'configuration.colors' must be a array")); return; } - let parseColorValue = (s: string, name: string) => { + const parseColorValue = (s: string, name: string) => { if (s.length > 0) { if (s[0] === '#') { return Color.Format.CSS.parseHex(s); @@ -118,7 +118,7 @@ export class ColorExtensionPoint { collector.error(nls.localize('invalid.description', "'configuration.colors.description' must be defined and can not be empty")); return; } - let defaults = colorContribution.defaults; + const defaults = colorContribution.defaults; if (!defaults || typeof defaults !== 'object' || typeof defaults.light !== 'string' || typeof defaults.dark !== 'string') { collector.error(nls.localize('invalid.defaults', "'configuration.colors.defaults' must be defined and must contain 'light' and 'dark'")); return; diff --git a/src/vs/workbench/services/themes/common/colorThemeData.ts b/src/vs/workbench/services/themes/common/colorThemeData.ts index 9af7e58c941..5013b0db476 100644 --- a/src/vs/workbench/services/themes/common/colorThemeData.ts +++ b/src/vs/workbench/services/themes/common/colorThemeData.ts @@ -25,9 +25,9 @@ import { StorageScope, IStorageService, StorageTarget } from 'vs/platform/storag import { ThemeConfiguration } from 'vs/workbench/services/themes/common/themeConfiguration'; import { ColorScheme } from 'vs/platform/theme/common/theme'; -let colorRegistry = Registry.as<IColorRegistry>(ColorRegistryExtensions.ColorContribution); +const colorRegistry = Registry.as<IColorRegistry>(ColorRegistryExtensions.ColorContribution); -let tokenClassificationRegistry = getTokenClassificationRegistry(); +const tokenClassificationRegistry = getTokenClassificationRegistry(); const tokenGroupToScopesMap = { comments: ['comment', 'punctuation.definition.comment'], @@ -144,14 +144,14 @@ export class ColorThemeData implements IWorkbenchColorTheme { } private getTokenStyle(type: string, modifiers: string[], language: string, useDefault = true, definitions: TokenStyleDefinitions = {}): TokenStyle | undefined { - let result: any = { + const result: any = { foreground: undefined, bold: undefined, underline: undefined, strikethrough: undefined, italic: undefined }; - let score = { + const score = { foreground: -1, bold: -1, underline: -1, @@ -165,7 +165,7 @@ export class ColorThemeData implements IWorkbenchColorTheme { result.foreground = style.foreground; definitions.foreground = definition; } - for (let p of ['bold', 'underline', 'strikethrough', 'italic']) { + for (const p of ['bold', 'underline', 'strikethrough', 'italic']) { const property = p as keyof TokenStyle; const info = style[property]; if (info !== undefined) { @@ -188,7 +188,7 @@ export class ColorThemeData implements IWorkbenchColorTheme { this.customSemanticTokenRules.forEach(_processSemanticTokenRule); let hasUndefinedStyleProperty = false; - for (let k in score) { + for (const k in score) { const key = k as keyof TokenStyle; if (score[key] === -1) { hasUndefinedStyleProperty = true; @@ -265,7 +265,7 @@ export class ColorThemeData implements IWorkbenchColorTheme { public getTokenStyleMetadata(typeWithLanguage: string, modifiers: string[], defaultLanguage: string, useDefault = true, definitions: TokenStyleDefinitions = {}): ITokenStyle | undefined { const { type, language } = parseClassifierString(typeWithLanguage, defaultLanguage); - let style = this.getTokenStyle(type, modifiers, language, useDefault, definitions); + const style = this.getTokenStyle(type, modifiers, language, useDefault, definitions); if (!style) { return undefined; } @@ -303,7 +303,7 @@ export class ColorThemeData implements IWorkbenchColorTheme { this.customTokenScopeMatchers = this.customTokenColors.map(getScopeMatcher); } - for (let scope of scopes) { + for (const scope of scopes) { let foreground: string | undefined = undefined; let fontStyle: string | undefined = undefined; let foregroundScore = -1; @@ -370,8 +370,8 @@ export class ColorThemeData implements IWorkbenchColorTheme { } private overwriteCustomColors(colors: IColorCustomizations) { - for (let id in colors) { - let colorVal = colors[id]; + for (const id in colors) { + const colorVal = colors[id]; if (typeof colorVal === 'string') { this.customColorMap[id] = Color.fromHex(colorVal); } @@ -438,18 +438,18 @@ export class ColorThemeData implements IWorkbenchColorTheme { public getThemeSpecificColors(colors: IThemeScopableCustomizations): IThemeScopedCustomizations | undefined { let themeSpecificColors; - for (let key in colors) { + for (const key in colors) { const scopedColors = colors[key]; if (this.isThemeScope(key) && scopedColors instanceof Object && !types.isArray(scopedColors)) { const themeScopeList = key.match(themeScopeRegex) || []; - for (let themeScope of themeScopeList) { + for (const themeScope of themeScopeList) { const themeId = themeScope.substring(1, themeScope.length - 1); if (this.isThemeScopeMatch(themeId)) { if (!themeSpecificColors) { themeSpecificColors = {} as IThemeScopedCustomizations; } const scopedThemeSpecificColors = scopedColors as IThemeScopedCustomizations; - for (let subkey in scopedThemeSpecificColors) { + for (const subkey in scopedThemeSpecificColors) { const originalColors = themeSpecificColors[subkey]; const overrideColors = scopedThemeSpecificColors[subkey]; if (types.isArray(originalColors) && types.isArray(overrideColors)) { @@ -466,7 +466,7 @@ export class ColorThemeData implements IWorkbenchColorTheme { } private readSemanticTokenRules(tokenStylingRuleSection: ISemanticTokenRules) { - for (let key in tokenStylingRuleSection) { + for (const key in tokenStylingRuleSection) { if (!this.isThemeScope(key)) { // still do this test until experimental settings are gone try { const rule = readSemanticTokenRule(key, tokenStylingRuleSection[key]); @@ -483,13 +483,13 @@ export class ColorThemeData implements IWorkbenchColorTheme { private addCustomTokenColors(customTokenColors: ITokenColorCustomizations) { // Put the general customizations such as comments, strings, etc. first so that // they can be overridden by specific customizations like "string.interpolated" - for (let tokenGroup in tokenGroupToScopesMap) { + for (const tokenGroup in tokenGroupToScopesMap) { const group = <keyof typeof tokenGroupToScopesMap>tokenGroup; // TS doesn't type 'tokenGroup' properly - let value = customTokenColors[group]; + const value = customTokenColors[group]; if (value) { - let settings = typeof value === 'string' ? { foreground: value } : value; - let scopes = tokenGroupToScopesMap[group]; - for (let scope of scopes) { + const settings = typeof value === 'string' ? { foreground: value } : value; + const scopes = tokenGroupToScopesMap[group]; + for (const scope of scopes) { this.customTokenColors.push({ scope, settings }); } } @@ -497,7 +497,7 @@ export class ColorThemeData implements IWorkbenchColorTheme { // specific customizations if (Array.isArray(customTokenColors.textMateRules)) { - for (let rule of customTokenColors.textMateRules) { + for (const rule of customTokenColors.textMateRules) { if (rule.scope && rule.settings) { this.customTokenColors.push(rule); } @@ -546,8 +546,8 @@ export class ColorThemeData implements IWorkbenchColorTheme { } toStorage(storageService: IStorageService) { - let colorMapData: { [key: string]: string } = {}; - for (let key in this.colorMap) { + const colorMapData: { [key: string]: string } = {}; + for (const key in this.colorMap) { colorMapData[key] = Color.Format.CSS.formatHexA(this.colorMap[key], true); } // no need to persist custom colors, they will be taken from the settings @@ -564,7 +564,7 @@ export class ColorThemeData implements IWorkbenchColorTheme { }); // roam persisted color theme colors. Don't enable for icons as they contain references to fonts and images. - storageService.store(ColorThemeData.STORAGE_KEY, value, StorageScope.GLOBAL, StorageTarget.USER); + storageService.store(ColorThemeData.STORAGE_KEY, value, StorageScope.PROFILE, StorageTarget.USER); } get baseTheme(): string { @@ -591,12 +591,12 @@ export class ColorThemeData implements IWorkbenchColorTheme { } static createUnloadedTheme(id: string, colorMap?: { [id: string]: string }): ColorThemeData { - let themeData = new ColorThemeData(id, '', '__' + id); + const themeData = new ColorThemeData(id, '', '__' + id); themeData.isLoaded = false; themeData.themeTokenColors = []; themeData.watch = false; if (colorMap) { - for (let id in colorMap) { + for (const id in colorMap) { themeData.colorMap[id] = Color.fromHex(colorMap[id]); } } @@ -604,7 +604,7 @@ export class ColorThemeData implements IWorkbenchColorTheme { } static createLoadedEmptyTheme(id: string, settingsId: string): ColorThemeData { - let themeData = new ColorThemeData(id, '', settingsId); + const themeData = new ColorThemeData(id, '', settingsId); themeData.isLoaded = true; themeData.themeTokenColors = []; themeData.watch = false; @@ -612,18 +612,18 @@ export class ColorThemeData implements IWorkbenchColorTheme { } static fromStorageData(storageService: IStorageService): ColorThemeData | undefined { - const input = storageService.get(ColorThemeData.STORAGE_KEY, StorageScope.GLOBAL); + const input = storageService.get(ColorThemeData.STORAGE_KEY, StorageScope.PROFILE); if (!input) { return undefined; } try { - let data = JSON.parse(input); - let theme = new ColorThemeData('', '', ''); - for (let key in data) { + const data = JSON.parse(input); + const theme = new ColorThemeData('', '', ''); + for (const key in data) { switch (key) { case 'colorMap': { - let colorMapData = data[key]; - for (let id in colorMapData) { + const colorMapData = data[key]; + for (const id in colorMapData) { theme.colorMap[id] = Color.fromHex(colorMapData[id]); } break; @@ -635,7 +635,7 @@ export class ColorThemeData implements IWorkbenchColorTheme { case 'semanticTokenRules': { const rulesData = data[key]; if (Array.isArray(rulesData)) { - for (let d of rulesData) { + for (const d of rulesData) { const rule = SemanticTokenRule.fromJSONObject(tokenClassificationRegistry, d); if (rule) { theme.semanticTokenRules.push(rule); @@ -694,8 +694,8 @@ function toCSSSelector(extensionId: string, path: string) { async function _loadColorTheme(extensionResourceLoaderService: IExtensionResourceLoaderService, themeLocation: URI, result: { textMateRules: ITextMateThemingRule[]; colors: IColorMap; semanticTokenRules: SemanticTokenRule[]; semanticHighlighting: boolean }): Promise<any> { if (resources.extname(themeLocation) === '.json') { const content = await extensionResourceLoaderService.readExtensionResource(themeLocation); - let errors: Json.ParseError[] = []; - let contentValue = Json.parse(content, errors); + const errors: Json.ParseError[] = []; + const contentValue = Json.parse(content, errors); if (errors.length > 0) { return Promise.reject(new Error(nls.localize('error.cannotparsejson', "Problems parsing JSON theme file: {0}", errors.map(e => getParseErrorMessage(e.error)).join(', ')))); } else if (Json.getNodeType(contentValue) !== 'object') { @@ -709,20 +709,20 @@ async function _loadColorTheme(extensionResourceLoaderService: IExtensionResourc return null; } result.semanticHighlighting = result.semanticHighlighting || contentValue.semanticHighlighting; - let colors = contentValue.colors; + const colors = contentValue.colors; if (colors) { if (typeof colors !== 'object') { return Promise.reject(new Error(nls.localize({ key: 'error.invalidformat.colors', comment: ['{0} will be replaced by a path. Values in quotes should not be translated.'] }, "Problem parsing color theme file: {0}. Property 'colors' is not of type 'object'.", themeLocation.toString()))); } // new JSON color themes format - for (let colorId in colors) { - let colorHex = colors[colorId]; + for (const colorId in colors) { + const colorHex = colors[colorId]; if (typeof colorHex === 'string') { // ignore colors tht are null result.colors[colorId] = Color.fromHex(colors[colorId]); } } } - let tokenColors = contentValue.tokenColors; + const tokenColors = contentValue.tokenColors; if (tokenColors) { if (Array.isArray(tokenColors)) { result.textMateRules.push(...tokenColors); @@ -732,9 +732,9 @@ async function _loadColorTheme(extensionResourceLoaderService: IExtensionResourc return Promise.reject(new Error(nls.localize({ key: 'error.invalidformat.tokenColors', comment: ['{0} will be replaced by a path. Values in quotes should not be translated.'] }, "Problem parsing color theme file: {0}. Property 'tokenColors' should be either an array specifying colors or a path to a TextMate theme file", themeLocation.toString()))); } } - let semanticTokenColors = contentValue.semanticTokenColors; + const semanticTokenColors = contentValue.semanticTokenColors; if (semanticTokenColors && typeof semanticTokenColors === 'object') { - for (let key in semanticTokenColors) { + for (const key in semanticTokenColors) { try { const rule = readSemanticTokenRule(key, semanticTokenColors[key]); if (rule) { @@ -753,8 +753,8 @@ async function _loadColorTheme(extensionResourceLoaderService: IExtensionResourc function _loadSyntaxTokens(extensionResourceLoaderService: IExtensionResourceLoaderService, themeLocation: URI, result: { textMateRules: ITextMateThemingRule[]; colors: IColorMap }): Promise<any> { return extensionResourceLoaderService.readExtensionResource(themeLocation).then(content => { try { - let contentValue = parsePList(content); - let settings: ITextMateThemingRule[] = contentValue.settings; + const contentValue = parsePList(content); + const settings: ITextMateThemingRule[] = contentValue.settings; if (!Array.isArray(settings)) { return Promise.reject(new Error(nls.localize('error.plist.invalidformat', "Problem parsing tmTheme file: {0}. 'settings' is not array."))); } @@ -768,7 +768,7 @@ function _loadSyntaxTokens(extensionResourceLoaderService: IExtensionResourceLoa }); } -let defaultThemeColors: { [baseTheme: string]: ITextMateThemingRule[] } = { +const defaultThemeColors: { [baseTheme: string]: ITextMateThemingRule[] } = { 'light': [ { scope: 'token.info-token', settings: { foreground: '#316bcd' } }, { scope: 'token.warn-token', settings: { foreground: '#cd9731' } }, @@ -843,7 +843,7 @@ function getScopeMatcher(rule: ITextMateThemingRule): Matcher<ProbeScope> { } const matchers: MatcherWithPriority<ProbeScope>[] = []; if (Array.isArray(ruleScope)) { - for (let rs of ruleScope) { + for (const rs of ruleScope) { createMatchers(rs, nameMatcher, matchers); } } else { @@ -915,7 +915,7 @@ class TokenColorIndex { if (color === undefined) { return 0; } - let value = this._color2id[color]; + const value = this._color2id[color]; if (value) { return value; } @@ -940,7 +940,7 @@ function normalizeColor(color: string | Color | undefined | null): string | unde if (color.charCodeAt(0) !== CharCode.Hash || (len !== 4 && len !== 5 && len !== 7 && len !== 9)) { return undefined; } - let result = [CharCode.Hash]; + const result = [CharCode.Hash]; for (let i = 1; i < len; i++) { const upper = hexUpper(color.charCodeAt(i)); diff --git a/src/vs/workbench/services/themes/common/colorThemeSchema.ts b/src/vs/workbench/services/themes/common/colorThemeSchema.ts index ca201c73c95..142c65e0814 100644 --- a/src/vs/workbench/services/themes/common/colorThemeSchema.ts +++ b/src/vs/workbench/services/themes/common/colorThemeSchema.ts @@ -11,7 +11,7 @@ import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { workbenchColorsSchemaId } from 'vs/platform/theme/common/colorRegistry'; import { tokenStylingSchemaId } from 'vs/platform/theme/common/tokenClassificationRegistry'; -let textMateScopes = [ +const textMateScopes = [ 'comment', 'comment.block', 'comment.block.documentation', @@ -212,7 +212,7 @@ const textmateColorSchema: IJSONSchema = { } }, required: [ - 'settings', 'scope' + 'settings' ], additionalProperties: false } @@ -256,7 +256,7 @@ const colorThemeSchema: IJSONSchema = { export function registerColorThemeSchemas() { - let schemaRegistry = Registry.as<IJSONContributionRegistry>(JSONExtensions.JSONContribution); + const schemaRegistry = Registry.as<IJSONContributionRegistry>(JSONExtensions.JSONContribution); schemaRegistry.registerSchema(colorThemeSchemaId, colorThemeSchema); schemaRegistry.registerSchema(textmateColorsSchemaId, textmateColorSchema); } diff --git a/src/vs/workbench/services/themes/common/fileIconThemeSchema.ts b/src/vs/workbench/services/themes/common/fileIconThemeSchema.ts index dfd6630917b..1e4182151aa 100644 --- a/src/vs/workbench/services/themes/common/fileIconThemeSchema.ts +++ b/src/vs/workbench/services/themes/common/fileIconThemeSchema.ts @@ -235,6 +235,6 @@ const schema: IJSONSchema = { }; export function registerFileIconThemeSchemas() { - let schemaRegistry = Registry.as<IJSONContributionRegistry>(JSONExtensions.JSONContribution); + const schemaRegistry = Registry.as<IJSONContributionRegistry>(JSONExtensions.JSONContribution); schemaRegistry.registerSchema(schemaId, schema); } diff --git a/src/vs/workbench/services/themes/common/iconExtensionPoint.ts b/src/vs/workbench/services/themes/common/iconExtensionPoint.ts index 3c333cfeda5..a9b4bacc26f 100644 --- a/src/vs/workbench/services/themes/common/iconExtensionPoint.ts +++ b/src/vs/workbench/services/themes/common/iconExtensionPoint.ts @@ -93,7 +93,7 @@ export class IconExtensionPoint { collector.error(nls.localize('invalid.icons.description', "'configuration.icons.description' must be defined and can not be empty")); return; } - let defaultIcon = iconContribution.default; + const defaultIcon = iconContribution.default; if (typeof defaultIcon === 'string') { iconRegistry.registerIcon(id, { id: defaultIcon }, iconContribution.description); } else if (typeof defaultIcon === 'object' && typeof defaultIcon.fontPath === 'string' && typeof defaultIcon.fontCharacter === 'string') { diff --git a/src/vs/workbench/services/themes/common/plistParser.ts b/src/vs/workbench/services/themes/common/plistParser.ts index 6825c65147b..bf78b296089 100644 --- a/src/vs/workbench/services/themes/common/plistParser.ts +++ b/src/vs/workbench/services/themes/common/plistParser.ts @@ -52,7 +52,7 @@ function _parse(content: string, filename: string | null, locationKeyName: strin pos = pos + by; } else { while (by > 0) { - let chCode = content.charCodeAt(pos); + const chCode = content.charCodeAt(pos); if (chCode === ChCode.LINE_FEED) { pos++; line++; char = 0; } else { @@ -72,7 +72,7 @@ function _parse(content: string, filename: string | null, locationKeyName: strin function skipWhitespace(): void { while (pos < len) { - let chCode = content.charCodeAt(pos); + const chCode = content.charCodeAt(pos); if (chCode !== ChCode.SPACE && chCode !== ChCode.TAB && chCode !== ChCode.CARRIAGE_RETURN && chCode !== ChCode.LINE_FEED) { break; } @@ -89,7 +89,7 @@ function _parse(content: string, filename: string | null, locationKeyName: strin } function advanceUntil(str: string): void { - let nextOccurence = content.indexOf(str, pos); + const nextOccurence = content.indexOf(str, pos); if (nextOccurence !== -1) { advancePosTo(nextOccurence + str.length); } else { @@ -99,14 +99,14 @@ function _parse(content: string, filename: string | null, locationKeyName: strin } function captureUntil(str: string): string { - let nextOccurence = content.indexOf(str, pos); + const nextOccurence = content.indexOf(str, pos); if (nextOccurence !== -1) { - let r = content.substring(pos, nextOccurence); + const r = content.substring(pos, nextOccurence); advancePosTo(nextOccurence + str.length); return r; } else { // EOF - let r = content.substr(pos); + const r = content.substr(pos); advancePosTo(len); return r; } @@ -115,8 +115,8 @@ function _parse(content: string, filename: string | null, locationKeyName: strin let state = State.ROOT_STATE; let cur: any = null; - let stateStack: State[] = []; - let objStack: any[] = []; + const stateStack: State[] = []; + const objStack: any[] = []; let curKey: string | null = null; function pushState(newState: State, newCur: any): void { @@ -143,7 +143,7 @@ function _parse(content: string, filename: string | null, locationKeyName: strin if (curKey === null) { return fail('missing <key>'); } - let newDict: { [key: string]: any } = {}; + const newDict: { [key: string]: any } = {}; if (locationKeyName !== null) { newDict[locationKeyName] = { filename: filename, @@ -159,7 +159,7 @@ function _parse(content: string, filename: string | null, locationKeyName: strin if (curKey === null) { return fail('missing <key>'); } - let newArr: any[] = []; + const newArr: any[] = []; cur[curKey] = newArr; curKey = null; pushState(State.ARR_STATE, newArr); @@ -168,7 +168,7 @@ function _parse(content: string, filename: string | null, locationKeyName: strin const arrState = { enterDict: function () { - let newDict: { [key: string]: any } = {}; + const newDict: { [key: string]: any } = {}; if (locationKeyName !== null) { newDict[locationKeyName] = { filename: filename, @@ -180,7 +180,7 @@ function _parse(content: string, filename: string | null, locationKeyName: strin pushState(State.DICT_STATE, newDict); }, enterArray: function () { - let newArr: any[] = []; + const newArr: any[] = []; cur.push(newArr); pushState(State.ARR_STATE, newArr); } @@ -369,7 +369,7 @@ function _parse(content: string, filename: string | null, locationKeyName: strin if (tag.isClosed) { return ''; } - let val = captureUntil('</'); + const val = captureUntil('</'); advanceUntil('>'); return escapeVal(val); } @@ -434,7 +434,7 @@ function _parse(content: string, filename: string | null, locationKeyName: strin return fail('unexpected closed tag'); } - let tag = parseOpenTag(); + const tag = parseOpenTag(); switch (tag.name) { case 'dict': diff --git a/src/vs/workbench/services/themes/common/productIconThemeSchema.ts b/src/vs/workbench/services/themes/common/productIconThemeSchema.ts index 40516264920..f401690b666 100644 --- a/src/vs/workbench/services/themes/common/productIconThemeSchema.ts +++ b/src/vs/workbench/services/themes/common/productIconThemeSchema.ts @@ -79,13 +79,12 @@ const schema: IJSONSchema = { }, iconDefinitions: { description: nls.localize('schema.iconDefinitions', 'Association of icon name to a font character.'), - $ref: iconsSchemaId, - additionalProperties: false + $ref: iconsSchemaId } } }; export function registerProductIconThemeSchemas() { - let schemaRegistry = Registry.as<IJSONContributionRegistry>(JSONExtensions.JSONContribution); + const schemaRegistry = Registry.as<IJSONContributionRegistry>(JSONExtensions.JSONContribution); schemaRegistry.registerSchema(schemaId, schema); } diff --git a/src/vs/workbench/services/themes/common/textMateScopeMatcher.ts b/src/vs/workbench/services/themes/common/textMateScopeMatcher.ts index 4d1c66adf6f..2bbe90caad4 100644 --- a/src/vs/workbench/services/themes/common/textMateScopeMatcher.ts +++ b/src/vs/workbench/services/themes/common/textMateScopeMatcher.ts @@ -28,7 +28,7 @@ export function createMatchers<T>(selector: string, matchesName: (names: string[ } token = tokenizer.next(); } - let matcher = parseConjunction(); + const matcher = parseConjunction(); if (matcher) { results.push({ matcher, priority }); } @@ -119,7 +119,7 @@ function isIdentifier(token: string | null): token is string { } function newTokenizer(input: string): { next: () => string | null } { - let regex = /([LR]:|[\w\.:][\w\.:\-]*|[\,\|\-\(\)])/g; + const regex = /([LR]:|[\w\.:][\w\.:\-]*|[\,\|\-\(\)])/g; let match = regex.exec(input); return { next: () => { diff --git a/src/vs/workbench/services/themes/common/themeCompatibility.ts b/src/vs/workbench/services/themes/common/themeCompatibility.ts index 0490bf80509..503d6fa50dd 100644 --- a/src/vs/workbench/services/themes/common/themeCompatibility.ts +++ b/src/vs/workbench/services/themes/common/themeCompatibility.ts @@ -19,21 +19,21 @@ function addSettingMapping(settingId: string, colorId: string) { } export function convertSettings(oldSettings: ITextMateThemingRule[], result: { textMateRules: ITextMateThemingRule[]; colors: IColorMap }): void { - for (let rule of oldSettings) { + for (const rule of oldSettings) { result.textMateRules.push(rule); if (!rule.scope) { - let settings = rule.settings; + const settings = rule.settings; if (!settings) { rule.settings = {}; } else { for (const settingKey in settings) { const key = <keyof typeof settings>settingKey; - let mappings = settingToColorIdMapping[key]; + const mappings = settingToColorIdMapping[key]; if (mappings) { - let colorHex = settings[key]; + const colorHex = settings[key]; if (typeof colorHex === 'string') { - let color = Color.fromHex(colorHex); - for (let colorId of mappings) { + const color = Color.fromHex(colorHex); + for (const colorId of mappings) { result.colors[colorId] = color; } } diff --git a/src/vs/workbench/services/themes/common/themeConfiguration.ts b/src/vs/workbench/services/themes/common/themeConfiguration.ts index 45c758ce8d1..05b0408d8c7 100644 --- a/src/vs/workbench/services/themes/common/themeConfiguration.ts +++ b/src/vs/workbench/services/themes/common/themeConfiguration.ts @@ -233,7 +233,7 @@ export function updateColorThemeConfigurationSchemas(themes: IWorkbenchColorThem const workbenchColors = { $ref: workbenchColorsSchemaId, additionalProperties: false }; const tokenColors = { properties: tokenColorSchema.properties, additionalProperties: false }; - for (let t of themes) { + for (const t of themes) { // add theme specific color customization ("[Abyss]":{ ... }) const themeId = `[${t.settingsId}]`; themeSpecificWorkbenchColors.properties![themeId] = workbenchColors; @@ -312,12 +312,12 @@ export class ThemeConfiguration { } public isDefaultColorTheme(): boolean { - let settings = this.configurationService.inspect(ThemeSettings.COLOR_THEME); + const settings = this.configurationService.inspect(ThemeSettings.COLOR_THEME); return settings && settings.default?.value === settings.value; } public findAutoConfigurationTarget(key: string) { - let settings = this.configurationService.inspect(key); + const settings = this.configurationService.inspect(key); if (!types.isUndefined(settings.workspaceFolderValue)) { return ConfigurationTarget.WORKSPACE_FOLDER; } else if (!types.isUndefined(settings.workspaceValue)) { @@ -333,7 +333,7 @@ export class ThemeConfiguration { return; } - let settings = this.configurationService.inspect(key); + const settings = this.configurationService.inspect(key); if (settingsTarget === 'auto') { return this.configurationService.updateValue(key, value); } diff --git a/src/vs/workbench/services/themes/common/themeExtensionPoints.ts b/src/vs/workbench/services/themes/common/themeExtensionPoints.ts index b7281cd238e..741e79f8f85 100644 --- a/src/vs/workbench/services/themes/common/themeExtensionPoints.ts +++ b/src/vs/workbench/services/themes/common/themeExtensionPoints.ts @@ -191,7 +191,7 @@ export class ThemeRegistry<T extends IThemeData> { log?.warn(nls.localize('invalid.path.1', "Expected `contributes.{0}.path` ({1}) to be included inside extension's folder ({2}). This might make the extension non-portable.", this.themesExtPoint.name, themeLocation.path, extensionLocation.path)); } - let themeData = this.create(theme, themeLocation, extensionData); + const themeData = this.create(theme, themeLocation, extensionData); resultingThemes.push(themeData); }); return resultingThemes; @@ -203,7 +203,7 @@ export class ThemeRegistry<T extends IThemeData> { } const allThemes = this.getThemes(); let defaultTheme: T | undefined = undefined; - for (let t of allThemes) { + for (const t of allThemes) { if (t.id === themeId) { return t; } @@ -220,7 +220,7 @@ export class ThemeRegistry<T extends IThemeData> { } const allThemes = this.getThemes(); let defaultTheme: T | undefined = undefined; - for (let t of allThemes) { + for (const t of allThemes) { if (t.settingsId === settingsId) { return t; } diff --git a/src/vs/workbench/services/themes/common/tokenClassificationExtensionPoint.ts b/src/vs/workbench/services/themes/common/tokenClassificationExtensionPoint.ts index 6ea7f40b026..80f5ee077cb 100644 --- a/src/vs/workbench/services/themes/common/tokenClassificationExtensionPoint.ts +++ b/src/vs/workbench/services/themes/common/tokenClassificationExtensionPoint.ts @@ -190,7 +190,7 @@ export class TokenClassificationExtensionPoints { collector.error(nls.localize('invalid.semanticTokenScopes.scopes', "'configuration.semanticTokenScopes.scopes' must be defined as an object")); continue; } - for (let selectorString in contribution.scopes) { + for (const selectorString in contribution.scopes) { const tmScopes = contribution.scopes[selectorString]; if (!Array.isArray(tmScopes) || tmScopes.some(l => typeof l !== 'string')) { collector.error(nls.localize('invalid.semanticTokenScopes.scopes.value', "'configuration.semanticTokenScopes.scopes' values must be an array of strings")); @@ -209,7 +209,7 @@ export class TokenClassificationExtensionPoints { for (const extension of delta.removed) { const extensionValue = <ITokenStyleDefaultExtensionPoint[]>extension.value; for (const contribution of extensionValue) { - for (let selectorString in contribution.scopes) { + for (const selectorString in contribution.scopes) { const tmScopes = contribution.scopes[selectorString]; try { const selector = tokenClassificationRegistry.parseTokenSelector(selectorString, contribution.language); diff --git a/src/vs/workbench/services/themes/electron-sandbox/nativeHostColorSchemeService.ts b/src/vs/workbench/services/themes/electron-sandbox/nativeHostColorSchemeService.ts index c44c4d37f91..f9578f6805a 100644 --- a/src/vs/workbench/services/themes/electron-sandbox/nativeHostColorSchemeService.ts +++ b/src/vs/workbench/services/themes/electron-sandbox/nativeHostColorSchemeService.ts @@ -44,7 +44,7 @@ export class NativeHostColorSchemeService extends Disposable implements IHostCol } private getStoredValue(): IColorScheme | undefined { - const stored = this.storageService.get(NativeHostColorSchemeService.STORAGE_KEY, StorageScope.GLOBAL); + const stored = this.storageService.get(NativeHostColorSchemeService.STORAGE_KEY, StorageScope.APPLICATION); if (stored) { try { const scheme = JSON.parse(stored); @@ -63,7 +63,7 @@ export class NativeHostColorSchemeService extends Disposable implements IHostCol this.dark = dark; this.highContrast = highContrast; - this.storageService.store(NativeHostColorSchemeService.STORAGE_KEY, JSON.stringify({ highContrast, dark }), StorageScope.GLOBAL, StorageTarget.MACHINE); + this.storageService.store(NativeHostColorSchemeService.STORAGE_KEY, JSON.stringify({ highContrast, dark }), StorageScope.APPLICATION, StorageTarget.MACHINE); this._onDidChangeColorScheme.fire(); } } diff --git a/src/vs/workbench/services/themes/test/electron-browser/tokenStyleResolving.test.ts b/src/vs/workbench/services/themes/test/electron-browser/tokenStyleResolving.test.ts index be43a275771..7067dd39095 100644 --- a/src/vs/workbench/services/themes/test/electron-browser/tokenStyleResolving.test.ts +++ b/src/vs/workbench/services/themes/test/electron-browser/tokenStyleResolving.test.ts @@ -71,7 +71,7 @@ function assertTokenStyleMetaData(colorIndex: string[], actual: ITokenStyle | un function assertTokenStyles(themeData: ColorThemeData, expected: { [qualifiedClassifier: string]: TokenStyle }, language = 'typescript') { const colorIndex = themeData.tokenColorMap; - for (let qualifiedClassifier in expected) { + for (const qualifiedClassifier in expected) { const [type, ...modifiers] = qualifiedClassifier.split('.'); const expectedTokenStyle = expected[qualifiedClassifier]; @@ -160,7 +160,7 @@ suite('Themes - TokenStyleResolving', () => { themeData.setCustomTokenColors(customTokenColors); let tokenStyle; - let defaultTokenStyle = undefined; + const defaultTokenStyle = undefined; tokenStyle = themeData.resolveScopes([['variable']]); assertTokenStyle(tokenStyle, ts('#F8F8F2', unsetStyle), 'variable'); diff --git a/src/vs/workbench/services/timer/browser/timerService.ts b/src/vs/workbench/services/timer/browser/timerService.ts index be619fd1188..c4fd56aa771 100644 --- a/src/vs/workbench/services/timer/browser/timerService.ts +++ b/src/vs/workbench/services/timer/browser/timerService.ts @@ -439,7 +439,7 @@ class PerfMarks { } private _findEntry(name: string): perf.PerformanceMark | void { - for (let [, marks] of this._entries) { + for (const [, marks] of this._entries) { for (let i = marks.length - 1; i >= 0; i--) { if (marks[i].name === name) { return marks[i]; @@ -514,6 +514,7 @@ export abstract class AbstractTimerService implements ITimerService { // report IStartupMetrics as telemetry /* __GDPR__ "startupTimeVaried" : { + "owner": "jrieken", "${include}": [ "${IStartupMetrics}" ] @@ -538,7 +539,7 @@ export abstract class AbstractTimerService implements ITimerService { let lastMark: perf.PerformanceMark = marks[0]; for (const mark of marks) { - let delta = mark.startTime - lastMark.startTime; + const delta = mark.startTime - lastMark.startTime; this._telemetryService.publicLog2<Mark, MarkClassification>('startup.timer.mark', { source, name: mark.name, diff --git a/src/vs/workbench/services/timer/electron-sandbox/timerService.ts b/src/vs/workbench/services/timer/electron-sandbox/timerService.ts index 19347cb694a..e0a1a9f09cf 100644 --- a/src/vs/workbench/services/timer/electron-sandbox/timerService.ts +++ b/src/vs/workbench/services/timer/electron-sandbox/timerService.ts @@ -99,10 +99,10 @@ export function didUseCachedData(productService: IProductService, storageService if (typeof _didUseCachedData !== 'boolean') { if (!environmentService.window.isCodeCaching || !productService.commit) { _didUseCachedData = false; // we only produce cached data whith commit and code cache path - } else if (storageService.get(lastRunningCommitStorageKey, StorageScope.GLOBAL) === productService.commit) { + } else if (storageService.get(lastRunningCommitStorageKey, StorageScope.APPLICATION) === productService.commit) { _didUseCachedData = true; // subsequent start on same commit, assume cached data is there } else { - storageService.store(lastRunningCommitStorageKey, productService.commit, StorageScope.GLOBAL, StorageTarget.MACHINE); + storageService.store(lastRunningCommitStorageKey, productService.commit, StorageScope.APPLICATION, StorageTarget.MACHINE); _didUseCachedData = false; // first time start on commit, assume cached data is not yet there } } diff --git a/src/vs/workbench/services/title/common/titleService.ts b/src/vs/workbench/services/title/common/titleService.ts index a89bb2f096a..57b7ae7430e 100644 --- a/src/vs/workbench/services/title/common/titleService.ts +++ b/src/vs/workbench/services/title/common/titleService.ts @@ -26,12 +26,12 @@ export interface ITitleService { /** * Title menu is visible */ - readonly titleMenuVisible: boolean; + readonly isCommandCenterVisible: boolean; /** * An event when the title menu is enabled/disabled */ - readonly onDidChangeTitleMenuVisibility: Event<void>; + readonly onDidChangeCommandCenterVisibility: Event<void>; /** * Update some environmental title properties. diff --git a/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts b/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts index 02eb92cc47c..5d3e956cadb 100644 --- a/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts +++ b/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts @@ -191,7 +191,7 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt //#region Language override setLanguageId(languageId: string): void { - let actualLanguage: string | undefined = languageId === UntitledTextEditorModel.ACTIVE_EDITOR_LANGUAGE_ID + const actualLanguage: string | undefined = languageId === UntitledTextEditorModel.ACTIVE_EDITOR_LANGUAGE_ID ? this.editorService.activeTextEditorLanguageId : languageId; this.preferredLanguageId = actualLanguage; diff --git a/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts b/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts index da053c29e11..2ab69b9299f 100644 --- a/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts +++ b/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts @@ -495,7 +495,7 @@ suite('Untitled text editors', () => { assert.strictEqual(counter, 7); function createSingleEditOp(text: string, positionLineNumber: number, positionColumn: number, selectionLineNumber: number = positionLineNumber, selectionColumn: number = positionColumn): ISingleEditOperation { - let range = new Range( + const range = new Range( selectionLineNumber, selectionColumn, positionLineNumber, diff --git a/src/vs/workbench/services/userData/browser/userDataInit.ts b/src/vs/workbench/services/userData/browser/userDataInit.ts index b2a85b9e458..1d7504a2656 100644 --- a/src/vs/workbench/services/userData/browser/userDataInit.ts +++ b/src/vs/workbench/services/userData/browser/userDataInit.ts @@ -37,6 +37,7 @@ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity' import { IExtensionStorageService } from 'vs/platform/extensionManagement/common/extensionStorage'; import { ICredentialsService } from 'vs/platform/credentials/common/credentials'; import { TasksInitializer } from 'vs/platform/userDataSync/common/tasksSync'; +import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; export const IUserDataInitializationService = createDecorator<IUserDataInitializationService>('IUserDataInitializationService'); export interface IUserDataInitializationService { @@ -62,6 +63,7 @@ export class UserDataInitializationService implements IUserDataInitializationSer @ICredentialsService private readonly credentialsService: ICredentialsService, @IUserDataSyncStoreManagementService private readonly userDataSyncStoreManagementService: IUserDataSyncStoreManagementService, @IFileService private readonly fileService: IFileService, + @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, @IStorageService private readonly storageService: IStorageService, @IProductService private readonly productService: IProductService, @IRequestService private readonly requestService: IRequestService, @@ -85,7 +87,7 @@ export class UserDataInitializationService implements IUserDataInitializationSer return; } - if (!this.storageService.isNew(StorageScope.GLOBAL)) { + if (!this.storageService.isNew(StorageScope.APPLICATION)) { this.logService.trace(`Skipping initializing user data as application was opened before`); return; } @@ -270,11 +272,11 @@ export class UserDataInitializationService implements IUserDataInitializationSer private createSyncResourceInitializer(syncResource: SyncResource): IUserDataInitializer { switch (syncResource) { - case SyncResource.Settings: return new SettingsInitializer(this.fileService, this.environmentService, this.logService, this.uriIdentityService); - case SyncResource.Keybindings: return new KeybindingsInitializer(this.fileService, this.environmentService, this.logService, this.uriIdentityService); - case SyncResource.Tasks: return new TasksInitializer(this.fileService, this.environmentService, this.logService, this.uriIdentityService); - case SyncResource.Snippets: return new SnippetsInitializer(this.fileService, this.environmentService, this.logService, this.uriIdentityService); - case SyncResource.GlobalState: return new GlobalStateInitializer(this.storageService, this.fileService, this.environmentService, this.logService, this.uriIdentityService); + case SyncResource.Settings: return new SettingsInitializer(this.fileService, this.userDataProfilesService, this.environmentService, this.logService, this.uriIdentityService); + case SyncResource.Keybindings: return new KeybindingsInitializer(this.fileService, this.userDataProfilesService, this.environmentService, this.logService, this.uriIdentityService); + case SyncResource.Tasks: return new TasksInitializer(this.fileService, this.userDataProfilesService, this.environmentService, this.logService, this.uriIdentityService); + case SyncResource.Snippets: return new SnippetsInitializer(this.fileService, this.userDataProfilesService, this.environmentService, this.logService, this.uriIdentityService); + case SyncResource.GlobalState: return new GlobalStateInitializer(this.storageService, this.fileService, this.userDataProfilesService, this.environmentService, this.logService, this.uriIdentityService); } throw new Error(`Cannot create initializer for ${syncResource}`); } @@ -291,11 +293,12 @@ class ExtensionsPreviewInitializer extends AbstractExtensionsInitializer { @IExtensionManagementService extensionManagementService: IExtensionManagementService, @IIgnoredExtensionsManagementService ignoredExtensionsManagementService: IIgnoredExtensionsManagementService, @IFileService fileService: IFileService, + @IUserDataProfilesService userDataProfilesService: IUserDataProfilesService, @IEnvironmentService environmentService: IEnvironmentService, @IUserDataSyncLogService logService: IUserDataSyncLogService, @IUriIdentityService uriIdentityService: IUriIdentityService, ) { - super(extensionManagementService, ignoredExtensionsManagementService, fileService, environmentService, logService, uriIdentityService); + super(extensionManagementService, ignoredExtensionsManagementService, fileService, userDataProfilesService, environmentService, logService, uriIdentityService); } getPreview(): Promise<IExtensionsInitializerPreviewResult | null> { diff --git a/src/vs/workbench/services/userDataProfile/browser/userDataProfileManagement.ts b/src/vs/workbench/services/userDataProfile/browser/userDataProfileManagement.ts new file mode 100644 index 00000000000..2488577017b --- /dev/null +++ b/src/vs/workbench/services/userDataProfile/browser/userDataProfileManagement.ts @@ -0,0 +1,155 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { joinPath } from 'vs/base/common/resources'; +import { URI } from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { ILocalExtension, Metadata } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionsProfileScannerService } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService'; +import { ExtensionType } from 'vs/platform/extensions/common/extensions'; +import { IFileService } from 'vs/platform/files/common/files'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { DidChangeProfilesEvent, EXTENSIONS_RESOURCE_NAME, IUserDataProfile, IUserDataProfilesService, UseDefaultProfileFlags, WorkspaceIdentifier } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IExtensionManagementServerService, IWorkbenchExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IHostService } from 'vs/workbench/services/host/browser/host'; +import { IUserDataProfileManagementService, IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; + +export class UserDataProfileManagementService extends Disposable implements IUserDataProfileManagementService { + readonly _serviceBrand: undefined; + + constructor( + @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, + @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, + @IFileService private readonly fileService: IFileService, + @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, + @IWorkbenchExtensionManagementService private readonly extensionManagementService: IWorkbenchExtensionManagementService, + @IExtensionsProfileScannerService private readonly extensionsProfileScannerService: IExtensionsProfileScannerService, + @IHostService private readonly hostService: IHostService, + @IDialogService private readonly dialogService: IDialogService, + @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, + @IExtensionService private readonly extensionService: IExtensionService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + ) { + super(); + this._register(userDataProfilesService.onDidChangeProfiles(e => this.onDidChangeProfiles(e))); + } + + private async checkAndCreateExtensionsProfileResource(): Promise<URI> { + if (this.userDataProfileService.currentProfile.extensionsResource) { + return this.userDataProfileService.currentProfile.extensionsResource; + } + if (!this.userDataProfilesService.defaultProfile.extensionsResource) { + // Extensions profile is not yet created for default profile, create it now + return this.createDefaultExtensionsProfile(joinPath(this.userDataProfilesService.defaultProfile.location, EXTENSIONS_RESOURCE_NAME)); + } + throw new Error('Invalid Profile'); + } + + private onDidChangeProfiles(e: DidChangeProfilesEvent): void { + if (e.removed.some(profile => profile.id === this.userDataProfileService.currentProfile.id)) { + this.enterProfile(this.userDataProfilesService.defaultProfile, false, localize('reload message when removed', "The current profile has been removed. Please reload to switch back to default profile")); + return; + } + if (this.userDataProfileService.currentProfile.isDefault) { + this.userDataProfileService.updateCurrentProfile(this.userDataProfilesService.defaultProfile, false); + return; + } + } + + async createAndEnterProfile(name: string, useDefaultFlags?: UseDefaultProfileFlags, fromExisting?: boolean): Promise<IUserDataProfile> { + const workspaceIdentifier = this.getWorkspaceIdentifier(); + const promises: Promise<any>[] = []; + const newProfile = this.userDataProfilesService.newProfile(name, useDefaultFlags); + await this.fileService.createFolder(newProfile.location); + const extensionsProfileResourcePromise = this.checkAndCreateExtensionsProfileResource(); + promises.push(extensionsProfileResourcePromise); + if (fromExisting) { + // Storage copy is handled by storage service while entering profile + promises.push(this.fileService.copy(this.userDataProfileService.currentProfile.settingsResource, newProfile.settingsResource)); + promises.push((async () => this.fileService.copy(await extensionsProfileResourcePromise, newProfile.extensionsResource))()); + promises.push(this.fileService.copy(this.userDataProfileService.currentProfile.keybindingsResource, newProfile.keybindingsResource)); + promises.push(this.fileService.copy(this.userDataProfileService.currentProfile.tasksResource, newProfile.tasksResource)); + promises.push(this.fileService.copy(this.userDataProfileService.currentProfile.snippetsHome, newProfile.snippetsHome)); + } + await Promise.allSettled(promises); + const createdProfile = await this.userDataProfilesService.createProfile(newProfile, workspaceIdentifier); + await this.enterProfile(createdProfile, !!fromExisting); + return createdProfile; + } + + async removeProfile(profile: IUserDataProfile): Promise<void> { + if (!this.userDataProfilesService.profiles.some(p => p.id === profile.id)) { + throw new Error(`Profile ${profile.name} does not exist`); + } + if (profile.isDefault) { + throw new Error(localize('cannotDeleteDefaultProfile', "Cannot delete the default profile")); + } + if (profile.id === this.userDataProfileService.currentProfile.id) { + throw new Error(localize('cannotDeleteCurrentProfile', "Cannot delete the current profile")); + } + const defaultExtensionsResourceToDelete = this.userDataProfilesService.profiles.length === 2 ? this.userDataProfilesService.defaultProfile.extensionsResource : undefined; + await this.userDataProfilesService.removeProfile(profile); + if (defaultExtensionsResourceToDelete) { + try { await this.fileService.del(defaultExtensionsResourceToDelete); } catch (error) { /* ignore */ } + } + } + + async switchProfile(profile: IUserDataProfile): Promise<void> { + const workspaceIdentifier = this.getWorkspaceIdentifier(); + if (!this.userDataProfilesService.profiles.some(p => p.id === profile.id)) { + throw new Error(`Profile ${profile.name} does not exist`); + } + if (this.userDataProfileService.currentProfile.id === profile.id) { + return; + } + await this.userDataProfilesService.setProfileForWorkspace(profile, workspaceIdentifier); + await this.enterProfile(profile, false); + } + + private getWorkspaceIdentifier(): WorkspaceIdentifier { + const workspace = this.workspaceContextService.getWorkspace(); + switch (this.workspaceContextService.getWorkbenchState()) { + case WorkbenchState.FOLDER: + return { uri: workspace.folders[0].uri, id: workspace.id }; + case WorkbenchState.WORKSPACE: + return { configPath: workspace.configuration!, id: workspace.id }; + } + return 'empty-window'; + } + + private async enterProfile(profile: IUserDataProfile, preserveData: boolean, reloadMessage?: string): Promise<void> { + if (this.environmentService.remoteAuthority) { + const result = await this.dialogService.confirm({ + type: 'info', + message: reloadMessage ?? localize('reload message', "Switching a profile requires reloading VS Code."), + primaryButton: localize('reload button', "&&Reload"), + }); + if (result.confirmed) { + await this.hostService.reload(); + } + return; + } + + this.extensionService.stopExtensionHosts(); + await this.userDataProfileService.updateCurrentProfile(profile, preserveData); + await this.extensionService.startExtensionHosts(); + } + + private async createDefaultExtensionsProfile(extensionsProfileResource: URI): Promise<URI> { + try { await this.fileService.del(extensionsProfileResource); } catch (error) { /* ignore */ } + const extensionManagementService = this.extensionManagementServerService.localExtensionManagementServer?.extensionManagementService ?? this.extensionManagementService; + const userExtensions = await extensionManagementService.getInstalled(ExtensionType.User); + const extensions: [ILocalExtension, Metadata | undefined][] = await Promise.all(userExtensions.map(async e => ([e, await this.extensionManagementService.getMetadata(e)]))); + await this.extensionsProfileScannerService.addExtensionsToProfile(extensions, extensionsProfileResource); + return extensionsProfileResource; + } +} + +registerSingleton(IUserDataProfileManagementService, UserDataProfileManagementService); diff --git a/src/vs/workbench/services/profiles/common/extensionsProfile.ts b/src/vs/workbench/services/userDataProfile/common/extensionsProfile.ts index 3e10dd0f9f5..254e8d6bbdd 100644 --- a/src/vs/workbench/services/profiles/common/extensionsProfile.ts +++ b/src/vs/workbench/services/userDataProfile/common/extensionsProfile.ts @@ -9,7 +9,7 @@ import { areSameExtensions } from 'vs/platform/extensionManagement/common/extens import { ExtensionType } from 'vs/platform/extensions/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; import { EnablementState, IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; -import { IResourceProfile } from 'vs/workbench/services/profiles/common/profile'; +import { IResourceProfile } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; interface IProfileExtension { identifier: IExtensionIdentifier; diff --git a/src/vs/workbench/services/profiles/common/globalStateProfile.ts b/src/vs/workbench/services/userDataProfile/common/globalStateProfile.ts index 7016a4c6136..5f0fd58cadb 100644 --- a/src/vs/workbench/services/profiles/common/globalStateProfile.ts +++ b/src/vs/workbench/services/userDataProfile/common/globalStateProfile.ts @@ -7,8 +7,8 @@ import { IStringDictionary } from 'vs/base/common/collections'; import { ILogService } from 'vs/platform/log/common/log'; import { Registry } from 'vs/platform/registry/common/platform'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { IResourceProfile } from 'vs/workbench/services/profiles/common/profile'; -import { Extensions, IProfileStorageRegistry } from 'vs/workbench/services/profiles/common/profileStorageRegistry'; +import { IResourceProfile } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { Extensions, IProfileStorageRegistry } from 'vs/workbench/services/userDataProfile/common/userDataProfileStorageRegistry'; interface IGlobalState { storage: IStringDictionary<string>; @@ -35,7 +35,7 @@ export class GlobalStateProfile implements IResourceProfile { private async getLocalGlobalState(): Promise<IGlobalState> { const storage: IStringDictionary<string> = {}; for (const { key } of Registry.as<IProfileStorageRegistry>(Extensions.ProfileStorageRegistry).all) { - const value = this.storageService.get(key, StorageScope.GLOBAL); + const value = this.storageService.get(key, StorageScope.PROFILE); if (value) { storage[key] = value; } @@ -56,7 +56,7 @@ export class GlobalStateProfile implements IResourceProfile { if (updatedStorageKeys.length) { this.logService.trace(`Profile: Updating global state...`); for (const key of updatedStorageKeys) { - this.storageService.store(key, globalState.storage[key], StorageScope.GLOBAL, StorageTarget.USER); + this.storageService.store(key, globalState.storage[key], StorageScope.PROFILE, StorageTarget.USER); } this.logService.info(`Profile: Updated global state`, updatedStorageKeys); } diff --git a/src/vs/workbench/services/profiles/common/settingsProfile.ts b/src/vs/workbench/services/userDataProfile/common/settingsProfile.ts index 906e0891449..6cc9a6d284f 100644 --- a/src/vs/workbench/services/profiles/common/settingsProfile.ts +++ b/src/vs/workbench/services/userDataProfile/common/settingsProfile.ts @@ -5,13 +5,12 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { ConfigurationScope, Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IFileService } from 'vs/platform/files/common/files'; import { ILogService } from 'vs/platform/log/common/log'; import { Registry } from 'vs/platform/registry/common/platform'; +import { IUserDataProfileService, IResourceProfile, ProfileCreationOptions } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; import { removeComments, updateIgnoredSettings } from 'vs/platform/userDataSync/common/settingsMerge'; import { IUserDataSyncUtilService } from 'vs/platform/userDataSync/common/userDataSync'; -import { IResourceProfile, ProfileCreationOptions } from 'vs/workbench/services/profiles/common/profile'; interface ISettingsContent { settings: string; @@ -21,7 +20,7 @@ export class SettingsProfile implements IResourceProfile { constructor( @IFileService private readonly fileService: IFileService, - @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, @IUserDataSyncUtilService private readonly userDataSyncUtilService: IUserDataSyncUtilService, @ILogService private readonly logService: ILogService, ) { @@ -29,7 +28,7 @@ export class SettingsProfile implements IResourceProfile { async getProfileContent(options?: ProfileCreationOptions): Promise<string> { const ignoredSettings = this.getIgnoredSettings(); - const formattingOptions = await this.userDataSyncUtilService.resolveFormattingOptions(this.environmentService.settingsResource); + const formattingOptions = await this.userDataSyncUtilService.resolveFormattingOptions(this.userDataProfileService.currentProfile.settingsResource); const localContent = await this.getLocalFileContent(); let settingsProfileContent = updateIgnoredSettings(localContent || '{}', '{}', ignoredSettings, formattingOptions); if (options?.skipComments) { @@ -45,9 +44,9 @@ export class SettingsProfile implements IResourceProfile { const settingsContent: ISettingsContent = JSON.parse(content); this.logService.trace(`Profile: Applying settings...`); const localSettingsContent = await this.getLocalFileContent(); - const formattingOptions = await this.userDataSyncUtilService.resolveFormattingOptions(this.environmentService.settingsResource); + const formattingOptions = await this.userDataSyncUtilService.resolveFormattingOptions(this.userDataProfileService.currentProfile.settingsResource); const contentToUpdate = updateIgnoredSettings(settingsContent.settings, localSettingsContent || '{}', this.getIgnoredSettings(), formattingOptions); - await this.fileService.writeFile(this.environmentService.settingsResource, VSBuffer.fromString(contentToUpdate)); + await this.fileService.writeFile(this.userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString(contentToUpdate)); this.logService.info(`Profile: Applied settings`); } @@ -59,7 +58,7 @@ export class SettingsProfile implements IResourceProfile { private async getLocalFileContent(): Promise<string | null> { try { - const content = await this.fileService.readFile(this.environmentService.settingsResource); + const content = await this.fileService.readFile(this.userDataProfileService.currentProfile.settingsResource); return content.value.toString(); } catch (error) { return null; diff --git a/src/vs/workbench/services/userDataProfile/common/userDataProfile.ts b/src/vs/workbench/services/userDataProfile/common/userDataProfile.ts new file mode 100644 index 00000000000..a293ec712b1 --- /dev/null +++ b/src/vs/workbench/services/userDataProfile/common/userDataProfile.ts @@ -0,0 +1,75 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { isUndefined } from 'vs/base/common/types'; +import { Event } from 'vs/base/common/event'; +import { localize } from 'vs/nls'; +import { MenuId } from 'vs/platform/actions/common/actions'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IUserDataProfile, PROFILES_ENABLEMENT_CONFIG, UseDefaultProfileFlags } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { ContextKeyDefinedExpr, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { IsWebContext, ProductQualityContext } from 'vs/platform/contextkey/common/contextkeys'; + +export interface DidChangeUserDataProfileEvent { + readonly preserveData: boolean; + readonly previous: IUserDataProfile; + readonly profile: IUserDataProfile; + join(promise: Promise<void>): void; +} + +export const IUserDataProfileService = createDecorator<IUserDataProfileService>('IUserDataProfileService'); +export interface IUserDataProfileService { + readonly _serviceBrand: undefined; + readonly onDidChangeCurrentProfile: Event<DidChangeUserDataProfileEvent>; + readonly currentProfile: IUserDataProfile; + updateCurrentProfile(currentProfile: IUserDataProfile, preserveData: boolean): Promise<void>; +} + +export const IUserDataProfileManagementService = createDecorator<IUserDataProfileManagementService>('IUserDataProfileManagementService'); +export interface IUserDataProfileManagementService { + readonly _serviceBrand: undefined; + + createAndEnterProfile(name: string, useDefaultFlags?: UseDefaultProfileFlags, fromExisting?: boolean): Promise<IUserDataProfile>; + removeProfile(profile: IUserDataProfile): Promise<void>; + switchProfile(profile: IUserDataProfile): Promise<void>; + +} + +export interface IUserDataProfileTemplate { + readonly settings?: string; + readonly globalState?: string; + readonly extensions?: string; +} + +export function isUserDataProfileTemplate(thing: unknown): thing is IUserDataProfileTemplate { + const candidate = thing as IUserDataProfileTemplate | undefined; + + return !!(candidate && typeof candidate === 'object' + && (isUndefined(candidate.settings) || typeof candidate.settings === 'string') + && (isUndefined(candidate.globalState) || typeof candidate.globalState === 'string') + && (isUndefined(candidate.extensions) || typeof candidate.extensions === 'string')); +} + +export type ProfileCreationOptions = { readonly skipComments: boolean }; + +export const IUserDataProfileImportExportService = createDecorator<IUserDataProfileImportExportService>('IUserDataProfileImportExportService'); +export interface IUserDataProfileImportExportService { + readonly _serviceBrand: undefined; + + exportProfile(options?: ProfileCreationOptions): Promise<IUserDataProfileTemplate>; + importProfile(profile: IUserDataProfileTemplate): Promise<void>; +} + +export interface IResourceProfile { + getProfileContent(): Promise<string>; + applyProfile(content: string): Promise<void>; +} + +export const ManageProfilesSubMenu = new MenuId('SettingsProfiles'); +export const PROFILES_TTILE = { value: localize('settings profiles', "Settings Profiles"), original: 'Settings Profiles' }; +export const PROFILES_CATEGORY = PROFILES_TTILE.value; +export const PROFILE_EXTENSION = 'code-profile'; +export const PROFILE_FILTER = [{ name: localize('profile', "Settings Profile"), extensions: [PROFILE_EXTENSION] }]; +export const PROFILES_ENABLEMENT_CONTEXT = ContextKeyExpr.and(ProductQualityContext.notEqualsTo('stable'), IsWebContext.negate(), ContextKeyDefinedExpr.create(`config.${PROFILES_ENABLEMENT_CONFIG}`)); diff --git a/src/vs/workbench/services/userDataProfile/common/userDataProfileImportExportService.ts b/src/vs/workbench/services/userDataProfile/common/userDataProfileImportExportService.ts new file mode 100644 index 00000000000..7403079c1ef --- /dev/null +++ b/src/vs/workbench/services/userDataProfile/common/userDataProfileImportExportService.ts @@ -0,0 +1,78 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; +import { ExtensionsProfile } from 'vs/workbench/services/userDataProfile/common/extensionsProfile'; +import { GlobalStateProfile } from 'vs/workbench/services/userDataProfile/common/globalStateProfile'; +import { IUserDataProfileTemplate, IUserDataProfileImportExportService, PROFILES_CATEGORY, IUserDataProfileManagementService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { SettingsProfile } from 'vs/workbench/services/userDataProfile/common/settingsProfile'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; + +export class UserDataProfileImportExportService implements IUserDataProfileImportExportService { + + readonly _serviceBrand: undefined; + + private readonly settingsProfile: SettingsProfile; + private readonly globalStateProfile: GlobalStateProfile; + private readonly extensionsProfile: ExtensionsProfile; + + constructor( + @IInstantiationService instantiationService: IInstantiationService, + @IProgressService private readonly progressService: IProgressService, + @INotificationService private readonly notificationService: INotificationService, + @IUserDataProfileManagementService private readonly userDataProfileManagementService: IUserDataProfileManagementService, + @IQuickInputService private readonly quickInputService: IQuickInputService, + ) { + this.settingsProfile = instantiationService.createInstance(SettingsProfile); + this.globalStateProfile = instantiationService.createInstance(GlobalStateProfile); + this.extensionsProfile = instantiationService.createInstance(ExtensionsProfile); + } + + async exportProfile(options?: { skipComments: boolean }): Promise<IUserDataProfileTemplate> { + const settings = await this.settingsProfile.getProfileContent(options); + const globalState = await this.globalStateProfile.getProfileContent(); + const extensions = await this.extensionsProfile.getProfileContent(); + return { + settings, + globalState, + extensions + }; + } + + async importProfile(profileTemplate: IUserDataProfileTemplate): Promise<void> { + const name = await this.quickInputService.input({ + placeHolder: localize('name', "Profile name"), + title: localize('save profile as', "Create from Current Profile..."), + }); + if (!name) { + return undefined; + } + + await this.progressService.withProgress({ + location: ProgressLocation.Notification, + title: localize('profiles.applying', "{0}: Importing...", PROFILES_CATEGORY), + }, async progress => { + await this.userDataProfileManagementService.createAndEnterProfile(name); + if (profileTemplate.settings) { + await this.settingsProfile.applyProfile(profileTemplate.settings); + } + if (profileTemplate.globalState) { + await this.globalStateProfile.applyProfile(profileTemplate.globalState); + } + if (profileTemplate.extensions) { + await this.extensionsProfile.applyProfile(profileTemplate.extensions); + } + }); + + this.notificationService.info(localize('applied profile', "{0}: Imported successfully.", PROFILES_CATEGORY)); + } + +} + +registerSingleton(IUserDataProfileImportExportService, UserDataProfileImportExportService); diff --git a/src/vs/workbench/services/userDataProfile/common/userDataProfileService.ts b/src/vs/workbench/services/userDataProfile/common/userDataProfileService.ts new file mode 100644 index 00000000000..96fa879239f --- /dev/null +++ b/src/vs/workbench/services/userDataProfile/common/userDataProfileService.ts @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Promises } from 'vs/base/common/async'; +import { Emitter } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { DidChangeUserDataProfileEvent, IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; + +export class UserDataProfileService extends Disposable implements IUserDataProfileService { + + readonly _serviceBrand: undefined; + + private readonly _onDidChangeCurrentProfile = this._register(new Emitter<DidChangeUserDataProfileEvent>()); + readonly onDidChangeCurrentProfile = this._onDidChangeCurrentProfile.event; + + private _currentProfile: IUserDataProfile; + get currentProfile(): IUserDataProfile { return this._currentProfile; } + + constructor(currentProfile: IUserDataProfile) { + super(); + this._currentProfile = currentProfile; + } + + async updateCurrentProfile(userDataProfile: IUserDataProfile, preserveData: boolean): Promise<void> { + const previous = this._currentProfile; + this._currentProfile = userDataProfile; + if (this._currentProfile.id === previous.id) { + return; + } + const joiners: Promise<void>[] = []; + this._onDidChangeCurrentProfile.fire({ + preserveData, + previous, + profile: userDataProfile, + join(promise) { + joiners.push(promise); + } + }); + await Promises.settled(joiners); + } +} diff --git a/src/vs/workbench/services/profiles/common/profileStorageRegistry.ts b/src/vs/workbench/services/userDataProfile/common/userDataProfileStorageRegistry.ts index 807b20e259b..807b20e259b 100644 --- a/src/vs/workbench/services/profiles/common/profileStorageRegistry.ts +++ b/src/vs/workbench/services/userDataProfile/common/userDataProfileStorageRegistry.ts diff --git a/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts b/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts index 482be496d9b..8d28e06a802 100644 --- a/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts +++ b/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts @@ -36,18 +36,10 @@ import { UserDataSyncStoreTypeSynchronizer } from 'vs/platform/userDataSync/comm import { ICredentialsService } from 'vs/platform/credentials/common/credentials'; import { CancellationError } from 'vs/base/common/errors'; -type UserAccountClassification = { - id: { classification: 'EndUserPseudonymizedInformation'; purpose: 'BusinessInsight' }; - providerId: { classification: 'EndUserPseudonymizedInformation'; purpose: 'BusinessInsight' }; -}; - type FirstTimeSyncClassification = { - action: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; -}; - -type UserAccountEvent = { - id: string; - providerId: string; + owner: 'sandy081'; + comment: 'Action taken when there are merges while turning on settins sync'; + action: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'action taken turning on sync. Eg: merge, pull, manual or cancel' }; }; type FirstTimeSyncAction = 'pull' | 'push' | 'merge' | 'manual'; @@ -213,13 +205,13 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat } private async getAccounts(authenticationProviderId: string, scopes: string[]): Promise<UserDataSyncAccount[]> { - let accounts: Map<string, UserDataSyncAccount> = new Map<string, UserDataSyncAccount>(); + const accounts: Map<string, UserDataSyncAccount> = new Map<string, UserDataSyncAccount>(); let currentAccount: UserDataSyncAccount | null = null; const sessions = await this.authenticationService.getSessions(authenticationProviderId, scopes) || []; for (const session of sessions) { const account: UserDataSyncAccount = new UserDataSyncAccount(authenticationProviderId, session); - accounts.set(account.accountName, account); + accounts.set(account.accountId, account); if (this.isCurrentAccount(account)) { currentAccount = account; } @@ -227,7 +219,7 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat if (currentAccount) { // Always use current account if available - accounts.set(currentAccount.accountName, currentAccount); + accounts.set(currentAccount.accountId, currentAccount); } return [...accounts.values()]; @@ -612,12 +604,11 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat // accounts are switched while sync is enabled. } this.currentSessionId = sessionId; - this.telemetryService.publicLog2<UserAccountEvent, UserAccountClassification>('sync.userAccount', { id: accountId, providerId: authenticationProviderId }); await this.update(); } private async onDidSuccessiveAuthFailures(): Promise<void> { - this.telemetryService.publicLog2('sync/successiveAuthFailures'); + this.telemetryService.publicLog2<{}, { owner: 'sandy081'; comment: 'Report when there are successive auth failures during settings sync' }>('sync/successiveAuthFailures'); this.currentSessionId = undefined; await this.update(); @@ -640,7 +631,7 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat } private onDidChangeStorage(e: IStorageValueChangeEvent): void { - if (e.key === UserDataSyncWorkbenchService.CACHED_SESSION_STORAGE_KEY && e.scope === StorageScope.GLOBAL + if (e.key === UserDataSyncWorkbenchService.CACHED_SESSION_STORAGE_KEY && e.scope === StorageScope.APPLICATION && this.currentSessionId !== this.getStoredCachedSessionId() /* This checks if current window changed the value or not */) { this._cachedCurrentSessionId = null; this.update(); @@ -660,24 +651,24 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat this._cachedCurrentSessionId = cachedSessionId; if (cachedSessionId === undefined) { this.logService.info('Settings Sync: Reset current session'); - this.storageService.remove(UserDataSyncWorkbenchService.CACHED_SESSION_STORAGE_KEY, StorageScope.GLOBAL); + this.storageService.remove(UserDataSyncWorkbenchService.CACHED_SESSION_STORAGE_KEY, StorageScope.APPLICATION); } else { this.logService.info('Settings Sync: Updated current session', cachedSessionId); - this.storageService.store(UserDataSyncWorkbenchService.CACHED_SESSION_STORAGE_KEY, cachedSessionId, StorageScope.GLOBAL, StorageTarget.MACHINE); + this.storageService.store(UserDataSyncWorkbenchService.CACHED_SESSION_STORAGE_KEY, cachedSessionId, StorageScope.APPLICATION, StorageTarget.MACHINE); } } } private getStoredCachedSessionId(): string | undefined { - return this.storageService.get(UserDataSyncWorkbenchService.CACHED_SESSION_STORAGE_KEY, StorageScope.GLOBAL); + return this.storageService.get(UserDataSyncWorkbenchService.CACHED_SESSION_STORAGE_KEY, StorageScope.APPLICATION); } private get useWorkbenchSessionId(): boolean { - return !this.storageService.getBoolean(UserDataSyncWorkbenchService.DONOT_USE_WORKBENCH_SESSION_STORAGE_KEY, StorageScope.GLOBAL, false); + return !this.storageService.getBoolean(UserDataSyncWorkbenchService.DONOT_USE_WORKBENCH_SESSION_STORAGE_KEY, StorageScope.APPLICATION, false); } private set useWorkbenchSessionId(useWorkbenchSession: boolean) { - this.storageService.store(UserDataSyncWorkbenchService.DONOT_USE_WORKBENCH_SESSION_STORAGE_KEY, !useWorkbenchSession, StorageScope.GLOBAL, StorageTarget.MACHINE); + this.storageService.store(UserDataSyncWorkbenchService.DONOT_USE_WORKBENCH_SESSION_STORAGE_KEY, !useWorkbenchSession, StorageScope.APPLICATION, StorageTarget.MACHINE); } } diff --git a/src/vs/workbench/services/views/browser/treeViewsService.ts b/src/vs/workbench/services/views/browser/treeViewsService.ts index 93d426deed5..052cc97c7e0 100644 --- a/src/vs/workbench/services/views/browser/treeViewsService.ts +++ b/src/vs/workbench/services/views/browser/treeViewsService.ts @@ -5,10 +5,10 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IDataTransfer } from 'vs/base/common/dataTransfer'; +import { VSDataTransfer } from 'vs/base/common/dataTransfer'; import { ITreeItem } from 'vs/workbench/common/views'; import { ITreeViewsService as ITreeViewsServiceCommon, TreeviewsService } from 'vs/workbench/services/views/common/treeViewsService'; -export interface ITreeViewsService extends ITreeViewsServiceCommon<IDataTransfer, ITreeItem, HTMLElement> { } +export interface ITreeViewsService extends ITreeViewsServiceCommon<VSDataTransfer, ITreeItem, HTMLElement> { } export const ITreeViewsService = createDecorator<ITreeViewsService>('treeViewsService'); registerSingleton(ITreeViewsService, TreeviewsService); diff --git a/src/vs/workbench/services/views/browser/viewDescriptorService.ts b/src/vs/workbench/services/views/browser/viewDescriptorService.ts index 4e84c1e5796..4059d3cbb81 100644 --- a/src/vs/workbench/services/views/browser/viewDescriptorService.ts +++ b/src/vs/workbench/services/views/browser/viewDescriptorService.ts @@ -19,7 +19,7 @@ import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiati import { getViewsStateStorageId, ViewContainerModel } from 'vs/workbench/services/views/common/viewContainerModel'; import { registerAction2, Action2, MenuId } from 'vs/platform/actions/common/actions'; import { localize } from 'vs/nls'; -import { Extensions, IProfileStorageRegistry } from 'vs/workbench/services/profiles/common/profileStorageRegistry'; +import { Extensions, IProfileStorageRegistry } from 'vs/workbench/services/userDataProfile/common/userDataProfileStorageRegistry'; interface ICachedViewContainerInfo { containerId: string; @@ -346,7 +346,7 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor moveViewToLocation(view: IViewDescriptor, location: ViewContainerLocation): void { - let container = this.registerGeneratedViewContainer(location); + const container = this.registerGeneratedViewContainer(location); this.moveViewsToContainer([view], container); } @@ -476,7 +476,7 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor // Clean up caches of container this.cachedViewContainerInfo.delete(viewContainerId); this.cachedViewContainerLocationsValue = JSON.stringify([...this.cachedViewContainerInfo]); - this.storageService.remove(getViewsStateStorageId(viewContainer?.storageId || getViewContainerStorageId(viewContainerId)), StorageScope.GLOBAL); + this.storageService.remove(getViewsStateStorageId(viewContainer?.storageId || getViewContainerStorageId(viewContainerId)), StorageScope.PROFILE); } private registerGeneratedViewContainer(location: ViewContainerLocation, existingId?: string): ViewContainer { @@ -528,12 +528,12 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor } private onDidStorageChange(e: IStorageValueChangeEvent): void { - if (e.key === ViewDescriptorService.CACHED_VIEW_POSITIONS && e.scope === StorageScope.GLOBAL + if (e.key === ViewDescriptorService.CACHED_VIEW_POSITIONS && e.scope === StorageScope.PROFILE && this.cachedViewPositionsValue !== this.getStoredCachedViewPositionsValue() /* This checks if current window changed the value or not */) { this.onDidCachedViewPositionsStorageChange(); } - if (e.key === ViewDescriptorService.CACHED_VIEW_CONTAINER_LOCATIONS && e.scope === StorageScope.GLOBAL + if (e.key === ViewDescriptorService.CACHED_VIEW_CONTAINER_LOCATIONS && e.scope === StorageScope.PROFILE && this.cachedViewContainerLocationsValue !== this.getStoredCachedViewContainerLocationsValue() /* This checks if current window changed the value or not */) { this.onDidCachedViewContainerLocationsStorageChange(); } @@ -545,7 +545,7 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor const newCachedPositions = this.getCachedViewPositions(); const viewsToMove: { views: IViewDescriptor[]; from: ViewContainer; to: ViewContainer }[] = []; - for (let viewId of newCachedPositions.keys()) { + for (const viewId of newCachedPositions.keys()) { const viewDescriptor = this.getViewDescriptorById(viewId); if (!viewDescriptor) { continue; @@ -633,19 +633,19 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor } private getStoredCachedViewPositionsValue(): string { - return this.storageService.get(ViewDescriptorService.CACHED_VIEW_POSITIONS, StorageScope.GLOBAL, '[]'); + return this.storageService.get(ViewDescriptorService.CACHED_VIEW_POSITIONS, StorageScope.PROFILE, '[]'); } private setStoredCachedViewPositionsValue(value: string): void { - this.storageService.store(ViewDescriptorService.CACHED_VIEW_POSITIONS, value, StorageScope.GLOBAL, StorageTarget.USER); + this.storageService.store(ViewDescriptorService.CACHED_VIEW_POSITIONS, value, StorageScope.PROFILE, StorageTarget.USER); } private getStoredCachedViewContainerLocationsValue(): string { - return this.storageService.get(ViewDescriptorService.CACHED_VIEW_CONTAINER_LOCATIONS, StorageScope.GLOBAL, '[]'); + return this.storageService.get(ViewDescriptorService.CACHED_VIEW_CONTAINER_LOCATIONS, StorageScope.PROFILE, '[]'); } private setStoredCachedViewContainerLocationsValue(value: string): void { - this.storageService.store(ViewDescriptorService.CACHED_VIEW_CONTAINER_LOCATIONS, value, StorageScope.GLOBAL, StorageTarget.USER); + this.storageService.store(ViewDescriptorService.CACHED_VIEW_CONTAINER_LOCATIONS, value, StorageScope.PROFILE, StorageTarget.USER); } private saveViewPositionsToCache(): void { diff --git a/src/vs/workbench/services/views/common/viewContainerModel.ts b/src/vs/workbench/services/views/common/viewContainerModel.ts index 5cb12926dc9..3df7d12368f 100644 --- a/src/vs/workbench/services/views/common/viewContainerModel.ts +++ b/src/vs/workbench/services/views/common/viewContainerModel.ts @@ -16,7 +16,7 @@ import { isUndefined, isUndefinedOrNull } from 'vs/base/common/types'; import { isEqual } from 'vs/base/common/resources'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { IStringDictionary } from 'vs/base/common/collections'; -import { Extensions, IProfileStorageRegistry } from 'vs/workbench/services/profiles/common/profileStorageRegistry'; +import { Extensions, IProfileStorageRegistry } from 'vs/workbench/services/userDataProfile/common/userDataProfileStorageRegistry'; import { localize } from 'vs/nls'; export function getViewsStateStorageId(viewContainerStorageId: string): string { return `${viewContainerStorageId}.hidden`; } @@ -152,7 +152,7 @@ class ViewDescriptorsState extends Disposable { } private onDidStorageChange(e: IStorageValueChangeEvent): void { - if (e.key === this.globalViewsStateStorageId && e.scope === StorageScope.GLOBAL + if (e.key === this.globalViewsStateStorageId && e.scope === StorageScope.PROFILE && this.globalViewsStatesValue !== this.getStoredGlobalViewsStatesValue() /* This checks if current window changed the value or not */) { this._globalViewsStatesValue = undefined; const storedViewsVisibilityStates = this.getStoredGlobalState(); @@ -202,7 +202,7 @@ class ViewDescriptorsState extends Disposable { const { state: workspaceVisibilityStates } = this.parseStoredGlobalState(value); if (workspaceVisibilityStates.size > 0) { for (const { id, isHidden } of workspaceVisibilityStates.values()) { - let viewState = viewStates.get(id); + const viewState = viewStates.get(id); // Not migrated to `viewletStateStorageId` if (viewState) { if (isUndefined(viewState.visibleWorkspace)) { @@ -225,7 +225,7 @@ class ViewDescriptorsState extends Disposable { this.setStoredGlobalState(state); } for (const { id, isHidden, order } of state.values()) { - let viewState = viewStates.get(id); + const viewState = viewStates.get(id); if (viewState) { viewState.visibleGlobal = !isHidden; if (!isUndefined(order)) { @@ -289,11 +289,11 @@ class ViewDescriptorsState extends Disposable { } private getStoredGlobalViewsStatesValue(): string { - return this.storageService.get(this.globalViewsStateStorageId, StorageScope.GLOBAL, '[]'); + return this.storageService.get(this.globalViewsStateStorageId, StorageScope.PROFILE, '[]'); } private setStoredGlobalViewsStatesValue(value: string): void { - this.storageService.store(this.globalViewsStateStorageId, value, StorageScope.GLOBAL, StorageTarget.USER); + this.storageService.store(this.globalViewsStateStorageId, value, StorageScope.PROFILE, StorageTarget.USER); } } diff --git a/src/vs/workbench/services/views/test/browser/viewContainerModel.test.ts b/src/vs/workbench/services/views/test/browser/viewContainerModel.test.ts index 097be5f2aaa..06e375acf9d 100644 --- a/src/vs/workbench/services/views/test/browser/viewContainerModel.test.ts +++ b/src/vs/workbench/services/views/test/browser/viewContainerModel.test.ts @@ -260,7 +260,7 @@ suite('ViewContainerModel', () => { }); test('view states', async function () { - storageService.store(`${container.id}.state.hidden`, JSON.stringify([{ id: 'view1', isHidden: true }]), StorageScope.GLOBAL, StorageTarget.MACHINE); + storageService.store(`${container.id}.state.hidden`, JSON.stringify([{ id: 'view1', isHidden: true }]), StorageScope.PROFILE, StorageTarget.MACHINE); container = ViewContainerRegistry.registerViewContainer({ id: 'test', title: 'test', ctorDescriptor: new SyncDescriptor(<any>{}) }, ViewContainerLocation.Sidebar); const testObject = viewDescriptorService.getViewContainerModel(container); const target = disposableStore.add(new ViewDescriptorSequence(testObject)); @@ -280,7 +280,7 @@ suite('ViewContainerModel', () => { }); test('view states and when contexts', async function () { - storageService.store(`${container.id}.state.hidden`, JSON.stringify([{ id: 'view1', isHidden: true }]), StorageScope.GLOBAL, StorageTarget.MACHINE); + storageService.store(`${container.id}.state.hidden`, JSON.stringify([{ id: 'view1', isHidden: true }]), StorageScope.PROFILE, StorageTarget.MACHINE); container = ViewContainerRegistry.registerViewContainer({ id: 'test', title: 'test', ctorDescriptor: new SyncDescriptor(<any>{}) }, ViewContainerLocation.Sidebar); const testObject = viewDescriptorService.getViewContainerModel(container); const target = disposableStore.add(new ViewDescriptorSequence(testObject)); @@ -310,7 +310,7 @@ suite('ViewContainerModel', () => { }); test('view states and when contexts multiple views', async function () { - storageService.store(`${container.id}.state.hidden`, JSON.stringify([{ id: 'view1', isHidden: true }]), StorageScope.GLOBAL, StorageTarget.MACHINE); + storageService.store(`${container.id}.state.hidden`, JSON.stringify([{ id: 'view1', isHidden: true }]), StorageScope.PROFILE, StorageTarget.MACHINE); container = ViewContainerRegistry.registerViewContainer({ id: 'test', title: 'test', ctorDescriptor: new SyncDescriptor(<any>{}) }, ViewContainerLocation.Sidebar); const testObject = viewDescriptorService.getViewContainerModel(container); const target = disposableStore.add(new ViewDescriptorSequence(testObject)); @@ -595,7 +595,7 @@ suite('ViewContainerModel', () => { id: viewDescriptor.id, isHidden: true, order: undefined - }]), StorageScope.GLOBAL, StorageTarget.USER); + }]), StorageScope.PROFILE, StorageTarget.USER); ViewsRegistry.registerViews([viewDescriptor], container); @@ -647,7 +647,7 @@ suite('ViewContainerModel', () => { id: viewDescriptor3.id, isHidden: true, order: undefined - }]), StorageScope.GLOBAL, StorageTarget.USER); + }]), StorageScope.PROFILE, StorageTarget.USER); assert.ok(!addEvent.called, 'add event should not be called'); assert.ok(remomveEvent.calledOnce, 'remove event should be called'); @@ -707,7 +707,7 @@ suite('ViewContainerModel', () => { id: viewDescriptor3.id, isHidden: false, order: undefined - }]), StorageScope.GLOBAL, StorageTarget.USER); + }]), StorageScope.PROFILE, StorageTarget.USER); assert.ok(!removeEvent.called, 'remove event should not be called'); @@ -784,7 +784,7 @@ suite('ViewContainerModel', () => { id: viewDescriptor4.id, isHidden: true, order: undefined - }]), StorageScope.GLOBAL, StorageTarget.USER); + }]), StorageScope.PROFILE, StorageTarget.USER); assert.ok(removeEvent.calledOnce, 'remove event should be called once'); assert.deepStrictEqual(removeEvent.args[0][0], [{ diff --git a/src/vs/workbench/services/views/test/browser/viewDescriptorService.test.ts b/src/vs/workbench/services/views/test/browser/viewDescriptorService.test.ts index 8e97ef286b4..23eee4b47bd 100644 --- a/src/vs/workbench/services/views/test/browser/viewDescriptorService.test.ts +++ b/src/vs/workbench/services/views/test/browser/viewDescriptorService.test.ts @@ -116,8 +116,8 @@ suite('ViewDescriptorService', () => { viewDescriptorService.moveViewsToContainer(viewDescriptors.slice(2), sidebarContainer); viewDescriptorService.moveViewsToContainer(viewDescriptors.slice(0, 2), panelContainer); - let sidebarViews = viewDescriptorService.getViewContainerModel(sidebarContainer); - let panelViews = viewDescriptorService.getViewContainerModel(panelContainer); + const sidebarViews = viewDescriptorService.getViewContainerModel(sidebarContainer); + const panelViews = viewDescriptorService.getViewContainerModel(panelContainer); assert.strictEqual(sidebarViews.activeViewDescriptors.length, 1, 'Sidebar should have 2 views'); assert.strictEqual(panelViews.activeViewDescriptors.length, 2, 'Panel should have 1 view'); diff --git a/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts index 4534377d02e..5cc96b59be1 100644 --- a/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts @@ -73,7 +73,7 @@ export abstract class BaseFileWorkingCopyManager<M extends IFileWorkingCopyModel // Add to our working copy map this.mapResourceToWorkingCopy.set(resource, workingCopy); - // Update our dipsose listener to remove it on dispose + // Update our dispose listener to remove it on dispose this.mapResourceToDisposeListener.get(resource)?.dispose(); this.mapResourceToDisposeListener.set(resource, workingCopy.onWillDispose(() => this.remove(resource))); diff --git a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts index 55387453019..3d2df342bae 100644 --- a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts @@ -126,7 +126,7 @@ export interface IFileWorkingCopySaveAsOptions extends ISaveOptions { /** * Optional target resource to suggest to the user in case - * no taget resource is provided to save to. + * no target resource is provided to save to. */ suggestedTarget?: URI; } diff --git a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts index ff9b4a7c408..82771996b1a 100644 --- a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts @@ -494,7 +494,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend const backup = await this.workingCopyBackupService.resolve<IStoredFileWorkingCopyBackupMetaData>(this); // Abort if someone else managed to resolve the working copy by now - let isNew = !this.isResolved(); + const isNew = !this.isResolved(); if (!isNew) { this.trace('resolveFromBackup() - exit - withoutresolving because previously new file working copy got created meanwhile'); diff --git a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts index d7c4f83dcc0..d0bc02b3e64 100644 --- a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts @@ -625,7 +625,7 @@ export class StoredFileWorkingCopyManager<M extends IStoredFileWorkingCopyModel> protected override remove(resource: URI): boolean { const removed = super.remove(resource); - // Dispose any exsting working copy listeners + // Dispose any existing working copy listeners const workingCopyListener = this.mapResourceToWorkingCopyListeners.get(resource); if (workingCopyListener) { dispose(workingCopyListener); diff --git a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts index 7640a135058..f985afb7dc9 100644 --- a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts @@ -236,7 +236,7 @@ export class UntitledFileWorkingCopyManager<M extends IUntitledFileWorkingCopyMo protected override remove(resource: URI): boolean { const removed = super.remove(resource); - // Dispose any exsting working copy listeners + // Dispose any existing working copy listeners const workingCopyListener = this.mapResourceToWorkingCopyListeners.get(resource); if (workingCopyListener) { dispose(workingCopyListener); diff --git a/src/vs/workbench/services/workingCopy/test/browser/resourceWorkingCopy.test.ts b/src/vs/workbench/services/workingCopy/test/browser/resourceWorkingCopy.test.ts index aa09a909665..758c611b6a5 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/resourceWorkingCopy.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/resourceWorkingCopy.test.ts @@ -32,7 +32,7 @@ suite('ResourceWorkingCopy', function () { } let disposables: DisposableStore; - let resource = URI.file('test/resource'); + const resource = URI.file('test/resource'); let instantiationService: IInstantiationService; let accessor: TestServiceAccessor; let workingCopy: TestResourceWorkingCopy; diff --git a/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test.ts b/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test.ts index d8fccfa5189..0fce73b2ce7 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test.ts @@ -93,7 +93,7 @@ suite('StoredFileWorkingCopy', function () { const factory = new TestStoredFileWorkingCopyModelFactory(); let disposables: DisposableStore; - let resource = URI.file('test/resource'); + const resource = URI.file('test/resource'); let instantiationService: IInstantiationService; let accessor: TestServiceAccessor; let workingCopy: StoredFileWorkingCopy<TestStoredFileWorkingCopyModel>; @@ -423,7 +423,7 @@ suite('StoredFileWorkingCopy', function () { assert.strictEqual(backupContents, 'hello backup'); }); - test('save (no errors)', async () => { + test('save (no errors) - simple', async () => { let savedCounter = 0; let lastSaveEvent: IStoredFileWorkingCopySaveEvent | undefined = undefined; workingCopy.onDidSave(e => { @@ -453,21 +453,49 @@ suite('StoredFileWorkingCopy', function () { assert.ok(lastSaveEvent!.stat); assert.ok(isStoredFileWorkingCopySaveEvent(lastSaveEvent!)); assert.strictEqual(workingCopy.model?.pushedStackElement, true); + }); + + test('save (no errors) - save reason', async () => { + let savedCounter = 0; + let lastSaveEvent: IStoredFileWorkingCopySaveEvent | undefined = undefined; + workingCopy.onDidSave(e => { + savedCounter++; + lastSaveEvent = e; + }); + + let saveErrorCounter = 0; + workingCopy.onDidSaveError(() => { + saveErrorCounter++; + }); // save reason + await workingCopy.resolve(); workingCopy.model?.updateContents('hello save'); const source = SaveSourceRegistry.registerSource('testSource', 'Hello Save'); await workingCopy.save({ reason: SaveReason.AUTO, source }); - assert.strictEqual(savedCounter, 2); + assert.strictEqual(savedCounter, 1); assert.strictEqual(saveErrorCounter, 0); assert.strictEqual(workingCopy.isDirty(), false); - assert.strictEqual((lastSaveEvent as IStoredFileWorkingCopySaveEvent).reason, SaveReason.AUTO); - assert.strictEqual((lastSaveEvent as IStoredFileWorkingCopySaveEvent).source, source); + assert.strictEqual((lastSaveEvent! as IStoredFileWorkingCopySaveEvent).reason, SaveReason.AUTO); + assert.strictEqual((lastSaveEvent! as IStoredFileWorkingCopySaveEvent).source, source); + }); + + test('save (no errors) - multiple', async () => { + let savedCounter = 0; + workingCopy.onDidSave(e => { + savedCounter++; + }); + + let saveErrorCounter = 0; + workingCopy.onDidSaveError(() => { + saveErrorCounter++; + }); // multiple saves in parallel are fine and result // in a single save when content does not change + await workingCopy.resolve(); workingCopy.model?.updateContents('hello save'); await Promises.settled([ workingCopy.save({ reason: SaveReason.AUTO }), @@ -475,34 +503,87 @@ suite('StoredFileWorkingCopy', function () { workingCopy.save({ reason: SaveReason.WINDOW_CHANGE }) ]); - assert.strictEqual(savedCounter, 3); + assert.strictEqual(savedCounter, 1); assert.strictEqual(saveErrorCounter, 0); assert.strictEqual(workingCopy.isDirty(), false); + }); + + test('save (no errors) - multiple, cancellation', async () => { + let savedCounter = 0; + workingCopy.onDidSave(e => { + savedCounter++; + }); + + let saveErrorCounter = 0; + workingCopy.onDidSaveError(() => { + saveErrorCounter++; + }); // multiple saves in parallel are fine and result // in just one save operation (the second one // cancels the first) + await workingCopy.resolve(); workingCopy.model?.updateContents('hello save'); const firstSave = workingCopy.save(); workingCopy.model?.updateContents('hello save more'); const secondSave = workingCopy.save(); await Promises.settled([firstSave, secondSave]); - assert.strictEqual(savedCounter, 4); + assert.strictEqual(savedCounter, 1); assert.strictEqual(saveErrorCounter, 0); assert.strictEqual(workingCopy.isDirty(), false); + }); + + test('save (no errors) - not forced but not dirty', async () => { + let savedCounter = 0; + workingCopy.onDidSave(e => { + savedCounter++; + }); + + let saveErrorCounter = 0; + workingCopy.onDidSaveError(() => { + saveErrorCounter++; + }); // no save when not forced and not dirty + await workingCopy.resolve(); await workingCopy.save(); - assert.strictEqual(savedCounter, 4); + assert.strictEqual(savedCounter, 0); assert.strictEqual(saveErrorCounter, 0); assert.strictEqual(workingCopy.isDirty(), false); + }); + + test('save (no errors) - forced but not dirty', async () => { + let savedCounter = 0; + workingCopy.onDidSave(e => { + savedCounter++; + }); + + let saveErrorCounter = 0; + workingCopy.onDidSaveError(() => { + saveErrorCounter++; + }); // save when forced even when not dirty + await workingCopy.resolve(); await workingCopy.save({ force: true }); - assert.strictEqual(savedCounter, 5); + assert.strictEqual(savedCounter, 1); assert.strictEqual(saveErrorCounter, 0); assert.strictEqual(workingCopy.isDirty(), false); + }); + + test('save (no errors) - save clears orphaned', async () => { + let savedCounter = 0; + workingCopy.onDidSave(e => { + savedCounter++; + }); + + let saveErrorCounter = 0; + workingCopy.onDidSaveError(() => { + saveErrorCounter++; + }); + + await workingCopy.resolve(); // save clears orphaned const orphanedPromise = Event.toPromise(workingCopy.onDidChangeOrphaned); @@ -514,7 +595,7 @@ suite('StoredFileWorkingCopy', function () { assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.ORPHAN), true); await workingCopy.save({ force: true }); - assert.strictEqual(savedCounter, 6); + assert.strictEqual(savedCounter, 1); assert.strictEqual(saveErrorCounter, 0); assert.strictEqual(workingCopy.isDirty(), false); assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.ORPHAN), false); diff --git a/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopyManager.test.ts b/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopyManager.test.ts index 95ab3cb327c..7b157378feb 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopyManager.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopyManager.test.ts @@ -598,7 +598,7 @@ suite('StoredFileWorkingCopyManager', () => { const workingCopy = await manager.resolve(resource); workingCopy.model?.updateContents('make dirty'); - let canDisposePromise = manager.canDispose(workingCopy); + const canDisposePromise = manager.canDispose(workingCopy); assert.ok(canDisposePromise instanceof Promise); let canDispose = false; @@ -613,7 +613,7 @@ suite('StoredFileWorkingCopyManager', () => { assert.strictEqual(canDispose, true); - let canDispose2 = manager.canDispose(workingCopy); + const canDispose2 = manager.canDispose(workingCopy); assert.strictEqual(canDispose2, true); }); diff --git a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts index 39d4865d360..6d4bbc6e1d2 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts @@ -91,7 +91,7 @@ suite('UntitledFileWorkingCopy', () => { const factory = new TestUntitledFileWorkingCopyModelFactory(); let disposables: DisposableStore; - let resource = URI.from({ scheme: Schemas.untitled, path: 'Untitled-1' }); + const resource = URI.from({ scheme: Schemas.untitled, path: 'Untitled-1' }); let instantiationService: IInstantiationService; let accessor: TestServiceAccessor; let workingCopy: UntitledFileWorkingCopy<TestUntitledFileWorkingCopyModel>; diff --git a/src/vs/workbench/services/workingCopy/test/browser/workingCopyBackupTracker.test.ts b/src/vs/workbench/services/workingCopy/test/browser/workingCopyBackupTracker.test.ts index 59c2c3e7188..5d51f1f23da 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/workingCopyBackupTracker.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/workingCopyBackupTracker.test.ts @@ -35,7 +35,7 @@ import { EditorResolution } from 'vs/platform/editor/common/editor'; suite('WorkingCopyBackupTracker (browser)', function () { let accessor: TestServiceAccessor; - let disposables = new DisposableStore(); + const disposables = new DisposableStore(); setup(() => { disposables.add(registerTestResourceEditor()); diff --git a/src/vs/workbench/services/workingCopy/test/browser/workingCopyEditorService.test.ts b/src/vs/workbench/services/workingCopy/test/browser/workingCopyEditorService.test.ts index 4107ce539f9..2a418ed49e1 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/workingCopyEditorService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/workingCopyEditorService.test.ts @@ -18,7 +18,7 @@ import { TestWorkingCopy } from 'vs/workbench/test/common/workbenchTestServices' suite('WorkingCopyEditorService', () => { - let disposables = new DisposableStore(); + const disposables = new DisposableStore(); setup(() => { disposables.add(registerTestResourceEditor()); diff --git a/src/vs/workbench/services/workingCopy/test/browser/workingCopyFileService.test.ts b/src/vs/workbench/services/workingCopy/test/browser/workingCopyFileService.test.ts index 2ae43c821eb..f13eab0e671 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/workingCopyFileService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/workingCopyFileService.test.ts @@ -56,7 +56,7 @@ suite('WorkingCopyFileService', () => { }); test('move - source identical to target', async function () { - let sourceModel: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined); + const sourceModel: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined); (<TestTextFileEditorModelManager>accessor.textFileService.files).add(sourceModel.resource, sourceModel); const eventCounter = await testEventsMoveOrCopy([{ file: { source: sourceModel.resource, target: sourceModel.resource }, overwrite: true }], true); @@ -66,9 +66,9 @@ suite('WorkingCopyFileService', () => { }); test('move - one source == target and another source != target', async function () { - let sourceModel1: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file1.txt'), 'utf8', undefined); - let sourceModel2: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file2.txt'), 'utf8', undefined); - let targetModel2: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file_target2.txt'), 'utf8', undefined); + const sourceModel1: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file1.txt'), 'utf8', undefined); + const sourceModel2: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file2.txt'), 'utf8', undefined); + const targetModel2: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file_target2.txt'), 'utf8', undefined); (<TestTextFileEditorModelManager>accessor.textFileService.files).add(sourceModel1.resource, sourceModel1); (<TestTextFileEditorModelManager>accessor.textFileService.files).add(sourceModel2.resource, sourceModel2); (<TestTextFileEditorModelManager>accessor.textFileService.files).add(targetModel2.resource, targetModel2); @@ -100,7 +100,7 @@ suite('WorkingCopyFileService', () => { }); test('copy - source identical to target', async function () { - let sourceModel: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined); + const sourceModel: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined); (<TestTextFileEditorModelManager>accessor.textFileService.files).add(sourceModel.resource, sourceModel); const eventCounter = await testEventsMoveOrCopy([{ file: { source: sourceModel.resource, target: sourceModel.resource }, overwrite: true }]); @@ -110,9 +110,9 @@ suite('WorkingCopyFileService', () => { }); test('copy - one source == target and another source != target', async function () { - let sourceModel1: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file1.txt'), 'utf8', undefined); - let sourceModel2: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file2.txt'), 'utf8', undefined); - let targetModel2: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file_target2.txt'), 'utf8', undefined); + const sourceModel1: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file1.txt'), 'utf8', undefined); + const sourceModel2: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file2.txt'), 'utf8', undefined); + const targetModel2: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file_target2.txt'), 'utf8', undefined); (<TestTextFileEditorModelManager>accessor.textFileService.files).add(sourceModel1.resource, sourceModel1); (<TestTextFileEditorModelManager>accessor.textFileService.files).add(sourceModel2.resource, sourceModel2); (<TestTextFileEditorModelManager>accessor.textFileService.files).add(targetModel2.resource, targetModel2); @@ -326,8 +326,8 @@ suite('WorkingCopyFileService', () => { let eventCounter = 0; const models = await Promise.all(files.map(async ({ source, target }, i) => { - let sourceModel: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, source, 'utf8', undefined); - let targetModel: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, target, 'utf8', undefined); + const sourceModel: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, source, 'utf8', undefined); + const targetModel: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, target, 'utf8', undefined); (<TestTextFileEditorModelManager>accessor.textFileService.files).add(sourceModel.resource, sourceModel); (<TestTextFileEditorModelManager>accessor.textFileService.files).add(targetModel.resource, targetModel); diff --git a/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyBackupService.test.ts b/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyBackupService.test.ts index a0c22c68010..f71466c1fe8 100644 --- a/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyBackupService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyBackupService.test.ts @@ -135,13 +135,13 @@ flakySuite('WorkingCopyBackupService', () => { let service: NodeTestWorkingCopyBackupService; - let workspaceResource = URI.file(isWindows ? 'c:\\workspace' : '/workspace'); - let fooFile = URI.file(isWindows ? 'c:\\Foo' : '/Foo'); - let customFile = URI.parse('customScheme://some/path'); - let customFileWithFragment = URI.parse('customScheme2://some/path#fragment'); - let barFile = URI.file(isWindows ? 'c:\\Bar' : '/Bar'); - let fooBarFile = URI.file(isWindows ? 'c:\\Foo Bar' : '/Foo Bar'); - let untitledFile = URI.from({ scheme: Schemas.untitled, path: 'Untitled-1' }); + const workspaceResource = URI.file(isWindows ? 'c:\\workspace' : '/workspace'); + const fooFile = URI.file(isWindows ? 'c:\\Foo' : '/Foo'); + const customFile = URI.parse('customScheme://some/path'); + const customFileWithFragment = URI.parse('customScheme2://some/path#fragment'); + const barFile = URI.file(isWindows ? 'c:\\Bar' : '/Bar'); + const fooBarFile = URI.file(isWindows ? 'c:\\Foo Bar' : '/Foo Bar'); + const untitledFile = URI.from({ scheme: Schemas.untitled, path: 'Untitled-1' }); setup(async () => { testDir = getRandomTestPath(tmpdir(), 'vsctests', 'workingcopybackupservice'); @@ -401,7 +401,7 @@ flakySuite('WorkingCopyBackupService', () => { }); test('text file with whitespace in name and type (with meta)', async () => { - let fileWithSpace = URI.file(isWindows ? 'c:\\Foo \n Bar' : '/Foo \n Bar'); + const fileWithSpace = URI.file(isWindows ? 'c:\\Foo \n Bar' : '/Foo \n Bar'); const identifier = toTypedWorkingCopyId(fileWithSpace, ' test id \n'); const backupPath = join(workspaceBackupPath, identifier.resource.scheme, hashIdentifier(identifier)); const meta = { etag: '678 \n k', orphaned: true }; @@ -414,7 +414,7 @@ flakySuite('WorkingCopyBackupService', () => { }); test('text file with unicode character in name and type (with meta)', async () => { - let fileWithUnicode = URI.file(isWindows ? 'c:\\so𒀅meࠄ' : '/so𒀅meࠄ'); + const fileWithUnicode = URI.file(isWindows ? 'c:\\so𒀅meࠄ' : '/so𒀅meࠄ'); const identifier = toTypedWorkingCopyId(fileWithUnicode, ' test so𒀅meࠄ id \n'); const backupPath = join(workspaceBackupPath, identifier.resource.scheme, hashIdentifier(identifier)); const meta = { etag: '678so𒀅meࠄ', orphaned: true }; @@ -540,9 +540,11 @@ flakySuite('WorkingCopyBackupService', () => { const backupId2 = toTypedWorkingCopyId(fooFile, 'type1'); const backupId3 = toTypedWorkingCopyId(fooFile, 'type2'); - await service.backup(backupId1); - await service.backup(backupId2); - await service.backup(backupId3); + await Promise.all([ + service.backup(backupId1), + service.backup(backupId2), + service.backup(backupId3) + ]); assert.strictEqual(readdirSync(join(workspaceBackupPath, 'file')).length, 3); @@ -609,9 +611,11 @@ flakySuite('WorkingCopyBackupService', () => { const backupId2 = toTypedWorkingCopyId(fooFile, 'type1'); const backupId3 = toTypedWorkingCopyId(fooFile, 'type2'); - await service.backup(backupId1); - await service.backup(backupId2); - await service.backup(backupId3); + await Promise.all([ + service.backup(backupId1), + service.backup(backupId2), + service.backup(backupId3) + ]); assert.strictEqual(readdirSync(join(workspaceBackupPath, 'file')).length, 3); @@ -716,9 +720,11 @@ flakySuite('WorkingCopyBackupService', () => { suite('getBackups', () => { test('text file', async () => { - await service.backup(toUntypedWorkingCopyId(fooFile), bufferToReadable(VSBuffer.fromString('test'))); - await service.backup(toTypedWorkingCopyId(fooFile, 'type1'), bufferToReadable(VSBuffer.fromString('test'))); - await service.backup(toTypedWorkingCopyId(fooFile, 'type2'), bufferToReadable(VSBuffer.fromString('test'))); + await Promise.all([ + service.backup(toUntypedWorkingCopyId(fooFile), bufferToReadable(VSBuffer.fromString('test'))), + service.backup(toTypedWorkingCopyId(fooFile, 'type1'), bufferToReadable(VSBuffer.fromString('test'))), + service.backup(toTypedWorkingCopyId(fooFile, 'type2'), bufferToReadable(VSBuffer.fromString('test'))) + ]); let backups = await service.getBackups(); assert.strictEqual(backups.length, 3); @@ -742,9 +748,11 @@ flakySuite('WorkingCopyBackupService', () => { }); test('untitled file', async () => { - await service.backup(toUntypedWorkingCopyId(untitledFile), bufferToReadable(VSBuffer.fromString('test'))); - await service.backup(toTypedWorkingCopyId(untitledFile, 'type1'), bufferToReadable(VSBuffer.fromString('test'))); - await service.backup(toTypedWorkingCopyId(untitledFile, 'type2'), bufferToReadable(VSBuffer.fromString('test'))); + await Promise.all([ + service.backup(toUntypedWorkingCopyId(untitledFile), bufferToReadable(VSBuffer.fromString('test'))), + service.backup(toTypedWorkingCopyId(untitledFile, 'type1'), bufferToReadable(VSBuffer.fromString('test'))), + service.backup(toTypedWorkingCopyId(untitledFile, 'type2'), bufferToReadable(VSBuffer.fromString('test'))) + ]); const backups = await service.getBackups(); assert.strictEqual(backups.length, 3); diff --git a/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyHistoryService.test.ts b/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyHistoryService.test.ts index 5068eccd657..6bf694104dd 100644 --- a/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyHistoryService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyHistoryService.test.ts @@ -135,7 +135,7 @@ flakySuite('WorkingCopyHistoryService', () => { }); test('addEntry', async () => { - let addEvents: IWorkingCopyHistoryEvent[] = []; + const addEvents: IWorkingCopyHistoryEvent[] = []; service.onDidAddEntry(e => addEvents.push(e)); const workingCopy1 = new TestWorkingCopy(URI.file(testFile1Path)); @@ -185,7 +185,7 @@ flakySuite('WorkingCopyHistoryService', () => { }); test('renameEntry', async () => { - let changeEvents: IWorkingCopyHistoryEvent[] = []; + const changeEvents: IWorkingCopyHistoryEvent[] = []; service.onDidChangeEntry(e => changeEvents.push(e)); const workingCopy1 = new TestWorkingCopy(URI.file(testFile1Path)); @@ -221,7 +221,7 @@ flakySuite('WorkingCopyHistoryService', () => { }); test('removeEntry', async () => { - let removeEvents: IWorkingCopyHistoryEvent[] = []; + const removeEvents: IWorkingCopyHistoryEvent[] = []; service.onDidRemoveEntry(e => removeEvents.push(e)); const workingCopy1 = new TestWorkingCopy(URI.file(testFile1Path)); @@ -414,7 +414,7 @@ flakySuite('WorkingCopyHistoryService', () => { assert.ok(existsSync(metaFile)); unlinkSync(metaFile); - let entries = await service.getEntries(workingCopy1.resource, CancellationToken.None); + const entries = await service.getEntries(workingCopy1.resource, CancellationToken.None); assert.strictEqual(entries.length, 1); assertEntryEqual(entries[0], entry1, false /* skip timestamp that is unreliable when entries.json is gone */); }); @@ -437,7 +437,7 @@ flakySuite('WorkingCopyHistoryService', () => { unlinkSync(entry1.location.fsPath); - let entries = await service.getEntries(workingCopy1.resource, CancellationToken.None); + const entries = await service.getEntries(workingCopy1.resource, CancellationToken.None); assert.strictEqual(entries.length, 1); assertEntryEqual(entries[0], entry2); }); @@ -461,7 +461,7 @@ flakySuite('WorkingCopyHistoryService', () => { const entry3 = await addEntry({ resource: workingCopy1.resource, source: 'test-source' }, CancellationToken.None); const entry4 = await addEntry({ resource: workingCopy1.resource, source: 'other-source' }, CancellationToken.None); - let entries = await service.getEntries(workingCopy1.resource, CancellationToken.None); + const entries = await service.getEntries(workingCopy1.resource, CancellationToken.None); assert.strictEqual(entries.length, 4); assertEntryEqual(entries[0], entry1); assertEntryEqual(entries[1], entry2); diff --git a/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts b/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts index ebe492e36ed..a7ab9d54798 100644 --- a/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts +++ b/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts @@ -20,13 +20,15 @@ import { IFileService } from 'vs/platform/files/common/files'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IFileDialogService, IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { Schemas } from 'vs/base/common/network'; import { SaveReason } from 'vs/workbench/common/editor'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; +import { IWorkbenchConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; +import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; export abstract class AbstractWorkspaceEditingService implements IWorkspaceEditingService { @@ -35,7 +37,7 @@ export abstract class AbstractWorkspaceEditingService implements IWorkspaceEditi constructor( @IJSONEditingService private readonly jsonEditingService: IJSONEditingService, @IWorkspaceContextService protected readonly contextService: WorkspaceService, - @IConfigurationService private readonly configurationService: IConfigurationService, + @IWorkbenchConfigurationService private readonly configurationService: IWorkbenchConfigurationService, @INotificationService private readonly notificationService: INotificationService, @ICommandService private readonly commandService: ICommandService, @IFileService private readonly fileService: IFileService, @@ -46,7 +48,9 @@ export abstract class AbstractWorkspaceEditingService implements IWorkspaceEditi @IDialogService protected readonly dialogService: IDialogService, @IHostService protected readonly hostService: IHostService, @IUriIdentityService protected readonly uriIdentityService: IUriIdentityService, - @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService + @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, + @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, + @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, ) { } async pickNewWorkspacePath(): Promise<URI | undefined> { @@ -76,23 +80,20 @@ export abstract class AbstractWorkspaceEditingService implements IWorkspaceEditi } private getNewWorkspaceName(): string { - switch (this.contextService.getWorkbenchState()) { - case WorkbenchState.FOLDER: { - const folder = firstOrDefault(this.contextService.getWorkspace().folders); - if (folder) { - return `${basename(folder.uri)}.${WORKSPACE_EXTENSION}`; - } - break; - } - case WorkbenchState.WORKSPACE: { - const configPathURI = this.getCurrentWorkspaceIdentifier()?.configPath; - if (configPathURI && isSavedWorkspace(configPathURI, this.environmentService)) { - return basename(configPathURI); - } - break; - } + + // First try with existing workspace name + const configPathURI = this.getCurrentWorkspaceIdentifier()?.configPath; + if (configPathURI && isSavedWorkspace(configPathURI, this.environmentService)) { + return basename(configPathURI); + } + + // Then fallback to first folder if any + const folder = firstOrDefault(this.contextService.getWorkspace().folders); + if (folder) { + return `${basename(folder.uri)}.${WORKSPACE_EXTENSION}`; } + // Finally pick a good default return `workspace.${WORKSPACE_EXTENSION}`; } @@ -244,6 +245,9 @@ export abstract class AbstractWorkspaceEditingService implements IWorkspaceEditi } } else { path = untitledWorkspace.configPath; + if (!this.userDataProfileService.currentProfile.isDefault) { + await this.userDataProfilesService.setProfileForWorkspace(this.userDataProfileService.currentProfile, untitledWorkspace); + } } return this.enterWorkspace(path); @@ -278,6 +282,12 @@ export abstract class AbstractWorkspaceEditingService implements IWorkspaceEditi protected async saveWorkspaceAs(workspace: IWorkspaceIdentifier, targetConfigPathURI: URI): Promise<void> { const configPathURI = workspace.configPath; + const isNotUntitledWorkspace = !isUntitledWorkspace(targetConfigPathURI, this.environmentService); + if (isNotUntitledWorkspace && !this.userDataProfileService.currentProfile.isDefault) { + const newWorkspace = await this.workspacesService.getWorkspaceIdentifier(targetConfigPathURI); + await this.userDataProfilesService.setProfileForWorkspace(this.userDataProfileService.currentProfile, newWorkspace); + } + // Return early if target is same as source if (this.uriIdentityService.extUri.isEqual(configPathURI, targetConfigPathURI)) { return; @@ -362,8 +372,7 @@ export abstract class AbstractWorkspaceEditingService implements IWorkspaceEditi await this.migrateWorkspaceSettings(workspace); } - const workspaceImpl = this.contextService as WorkspaceService; - await workspaceImpl.initialize(workspace); + await this.configurationService.initialize(workspace); return this.workspacesService.enterWorkspace(workspaceUri); } diff --git a/src/vs/workbench/services/workspaces/browser/workspaceEditingService.ts b/src/vs/workbench/services/workspaces/browser/workspaceEditingService.ts index b5230e4d641..637bf9d0a62 100644 --- a/src/vs/workbench/services/workspaces/browser/workspaceEditingService.ts +++ b/src/vs/workbench/services/workspaces/browser/workspaceEditingService.ts @@ -12,7 +12,6 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { IFileService } from 'vs/platform/files/common/files'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IFileDialogService, IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { AbstractWorkspaceEditingService } from 'vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService'; @@ -21,13 +20,16 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { URI } from 'vs/base/common/uri'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; +import { IWorkbenchConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; +import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; export class BrowserWorkspaceEditingService extends AbstractWorkspaceEditingService { constructor( @IJSONEditingService jsonEditingService: IJSONEditingService, @IWorkspaceContextService contextService: WorkspaceService, - @IConfigurationService configurationService: IConfigurationService, + @IWorkbenchConfigurationService configurationService: IWorkbenchConfigurationService, @INotificationService notificationService: INotificationService, @ICommandService commandService: ICommandService, @IFileService fileService: IFileService, @@ -38,9 +40,11 @@ export class BrowserWorkspaceEditingService extends AbstractWorkspaceEditingServ @IDialogService dialogService: IDialogService, @IHostService hostService: IHostService, @IUriIdentityService uriIdentityService: IUriIdentityService, - @IWorkspaceTrustManagementService workspaceTrustManagementService: IWorkspaceTrustManagementService + @IWorkspaceTrustManagementService workspaceTrustManagementService: IWorkspaceTrustManagementService, + @IUserDataProfilesService userDataProfilesService: IUserDataProfilesService, + @IUserDataProfileService userDataProfileService: IUserDataProfileService, ) { - super(jsonEditingService, contextService, configurationService, notificationService, commandService, fileService, textFileService, workspacesService, environmentService, fileDialogService, dialogService, hostService, uriIdentityService, workspaceTrustManagementService); + super(jsonEditingService, contextService, configurationService, notificationService, commandService, fileService, textFileService, workspacesService, environmentService, fileDialogService, dialogService, hostService, uriIdentityService, workspaceTrustManagementService, userDataProfilesService, userDataProfileService); } async enterWorkspace(workspaceUri: URI): Promise<void> { diff --git a/src/vs/workbench/services/workspaces/browser/workspacesService.ts b/src/vs/workbench/services/workspaces/browser/workspacesService.ts index 3723350dcd4..dda2b389113 100644 --- a/src/vs/workbench/services/workspaces/browser/workspacesService.ts +++ b/src/vs/workbench/services/workspaces/browser/workspacesService.ts @@ -57,7 +57,7 @@ export class BrowserWorkspacesService extends Disposable implements IWorkspacesS } private onDidChangeStorage(e: IStorageValueChangeEvent): void { - if (e.key === BrowserWorkspacesService.RECENTLY_OPENED_KEY && e.scope === StorageScope.GLOBAL) { + if (e.key === BrowserWorkspacesService.RECENTLY_OPENED_KEY && e.scope === StorageScope.APPLICATION) { this._onRecentlyOpenedChange.fire(); } } @@ -91,7 +91,7 @@ export class BrowserWorkspacesService extends Disposable implements IWorkspacesS //#region Workspaces History async getRecentlyOpened(): Promise<IRecentlyOpened> { - const recentlyOpenedRaw = this.storageService.get(BrowserWorkspacesService.RECENTLY_OPENED_KEY, StorageScope.GLOBAL); + const recentlyOpenedRaw = this.storageService.get(BrowserWorkspacesService.RECENTLY_OPENED_KEY, StorageScope.APPLICATION); if (recentlyOpenedRaw) { const recentlyOpened = restoreRecentlyOpened(JSON.parse(recentlyOpenedRaw), this.logService); recentlyOpened.workspaces = recentlyOpened.workspaces.filter(recent => { @@ -156,11 +156,11 @@ export class BrowserWorkspacesService extends Disposable implements IWorkspacesS } private async saveRecentlyOpened(data: IRecentlyOpened): Promise<void> { - return this.storageService.store(BrowserWorkspacesService.RECENTLY_OPENED_KEY, JSON.stringify(toStoreData(data)), StorageScope.GLOBAL, StorageTarget.USER); + return this.storageService.store(BrowserWorkspacesService.RECENTLY_OPENED_KEY, JSON.stringify(toStoreData(data)), StorageScope.APPLICATION, StorageTarget.USER); } async clearRecentlyOpened(): Promise<void> { - this.storageService.remove(BrowserWorkspacesService.RECENTLY_OPENED_KEY, StorageScope.GLOBAL); + this.storageService.remove(BrowserWorkspacesService.RECENTLY_OPENED_KEY, StorageScope.APPLICATION); } //#endregion diff --git a/src/vs/workbench/services/workspaces/common/workspaceEditing.ts b/src/vs/workbench/services/workspaces/common/workspaceEditing.ts index 6ae82b2fa00..8ca4f01f6d4 100644 --- a/src/vs/workbench/services/workspaces/common/workspaceEditing.ts +++ b/src/vs/workbench/services/workspaces/common/workspaceEditing.ts @@ -33,28 +33,28 @@ export interface IWorkspaceEditingService { updateFolders(index: number, deleteCount?: number, foldersToAdd?: IWorkspaceFolderCreationData[], donotNotifyError?: boolean): Promise<void>; /** - * enters the workspace with the provided path. + * Enters the workspace with the provided path. */ enterWorkspace(path: URI): Promise<void>; /** - * creates a new workspace with the provided folders and opens it. if path is provided + * Creates a new workspace with the provided folders and opens it. if path is provided * the workspace will be saved into that location. */ createAndEnterWorkspace(folders: IWorkspaceFolderCreationData[], path?: URI): Promise<void>; /** - * saves the current workspace to the provided path and opens it. requires a workspace to be opened. + * Saves the current workspace to the provided path and opens it. requires a workspace to be opened. */ saveAndEnterWorkspace(path: URI): Promise<void>; /** - * copies current workspace settings to the target workspace. + * Copies current workspace settings to the target workspace. */ copyWorkspaceSettings(toWorkspace: IWorkspaceIdentifier): Promise<void>; /** - * picks a new workspace path + * Picks a new workspace path */ pickNewWorkspacePath(): Promise<URI | undefined>; } diff --git a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts index e5039e9bc04..e464de637ee 100644 --- a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts +++ b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts @@ -244,7 +244,7 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork } private loadTrustInfo(): IWorkspaceTrustInfo { - const infoAsString = this.storageService.get(this.storageKey, StorageScope.GLOBAL); + const infoAsString = this.storageService.get(this.storageKey, StorageScope.APPLICATION); let result: IWorkspaceTrustInfo | undefined; try { @@ -270,7 +270,7 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork } private async saveTrustInfo(): Promise<void> { - this.storageService.store(this.storageKey, JSON.stringify(this._trustStateInfo), StorageScope.GLOBAL, StorageTarget.MACHINE); + this.storageService.store(this.storageKey, JSON.stringify(this._trustStateInfo), StorageScope.APPLICATION, StorageTarget.MACHINE); this._onDidChangeTrustedFolders.fire(); await this.updateWorkspaceTrust(); diff --git a/src/vs/workbench/services/workspaces/electron-sandbox/workspaceEditingService.ts b/src/vs/workbench/services/workspaces/electron-sandbox/workspaceEditingService.ts index e63f50d9d35..0cf6f3892c2 100644 --- a/src/vs/workbench/services/workspaces/electron-sandbox/workspaceEditingService.ts +++ b/src/vs/workbench/services/workspaces/electron-sandbox/workspaceEditingService.ts @@ -20,7 +20,6 @@ import { IFileService } from 'vs/platform/files/common/files'; import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; import { ILifecycleService, ShutdownReason } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IFileDialogService, IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ILabelService } from 'vs/platform/label/common/label'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; @@ -32,6 +31,9 @@ import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { WorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackupService'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; +import { IWorkbenchConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; +import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; export class NativeWorkspaceEditingService extends AbstractWorkspaceEditingService { @@ -39,7 +41,7 @@ export class NativeWorkspaceEditingService extends AbstractWorkspaceEditingServi @IJSONEditingService jsonEditingService: IJSONEditingService, @IWorkspaceContextService contextService: WorkspaceService, @INativeHostService private nativeHostService: INativeHostService, - @IConfigurationService configurationService: IConfigurationService, + @IWorkbenchConfigurationService configurationService: IWorkbenchConfigurationService, @IStorageService private storageService: IStorageService, @IExtensionService private extensionService: IExtensionService, @IWorkingCopyBackupService private workingCopyBackupService: IWorkingCopyBackupService, @@ -55,9 +57,11 @@ export class NativeWorkspaceEditingService extends AbstractWorkspaceEditingServi @ILabelService private readonly labelService: ILabelService, @IHostService hostService: IHostService, @IUriIdentityService uriIdentityService: IUriIdentityService, - @IWorkspaceTrustManagementService workspaceTrustManagementService: IWorkspaceTrustManagementService + @IWorkspaceTrustManagementService workspaceTrustManagementService: IWorkspaceTrustManagementService, + @IUserDataProfilesService userDataProfilesService: IUserDataProfilesService, + @IUserDataProfileService userDataProfileService: IUserDataProfileService, ) { - super(jsonEditingService, contextService, configurationService, notificationService, commandService, fileService, textFileService, workspacesService, environmentService, fileDialogService, dialogService, hostService, uriIdentityService, workspaceTrustManagementService); + super(jsonEditingService, contextService, configurationService, notificationService, commandService, fileService, textFileService, workspacesService, environmentService, fileDialogService, dialogService, hostService, uriIdentityService, workspaceTrustManagementService, userDataProfilesService, userDataProfileService); this.registerListeners(); } @@ -164,7 +168,7 @@ export class NativeWorkspaceEditingService extends AbstractWorkspaceEditingServi if (result) { // Migrate storage to new workspace - await this.storageService.migrate(result.workspace); + await this.storageService.switch(result.workspace, true /* preserve data */); // Reinitialize backup service if (this.workingCopyBackupService instanceof WorkingCopyBackupService) { diff --git a/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts b/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts index cb8808efc50..ab6f9d78a7d 100644 --- a/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts +++ b/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts @@ -109,7 +109,7 @@ suite('Workspace Trust', () => { test('empty workspace - trusted, open trusted file', async () => { await configurationService.setUserConfiguration('security', getUserSettings(true, true)); const trustInfo: IWorkspaceTrustInfo = { uriTrustInfo: [{ uri: URI.parse('file:///Folder'), trusted: true }] }; - storageService.store(WORKSPACE_TRUST_STORAGE_KEY, JSON.stringify(trustInfo), StorageScope.GLOBAL, StorageTarget.MACHINE); + storageService.store(WORKSPACE_TRUST_STORAGE_KEY, JSON.stringify(trustInfo), StorageScope.APPLICATION, StorageTarget.MACHINE); (environmentService as any).filesToOpenOrCreate = [{ fileUri: URI.parse('file:///Folder/file.txt') }]; instantiationService.stub(IWorkbenchEnvironmentService, { ...environmentService }); diff --git a/src/vs/workbench/test/browser/codeeditor.test.ts b/src/vs/workbench/test/browser/codeeditor.test.ts index c0be9631aec..75f27d69dca 100644 --- a/src/vs/workbench/test/browser/codeeditor.test.ts +++ b/src/vs/workbench/test/browser/codeeditor.test.ts @@ -34,7 +34,7 @@ suite('Editor - Range decorations', () => { let model: TextModel; let text: string; let testObject: RangeHighlightDecorations; - let modelsToDispose: TextModel[] = []; + const modelsToDispose: TextModel[] = []; setup(() => { disposables = new DisposableStore(); @@ -137,21 +137,21 @@ suite('Editor - Range decorations', () => { }); function prepareActiveEditor(resource: string): TextModel { - let model = aModel(URI.file(resource)); + const model = aModel(URI.file(resource)); codeEditor.setModel(model); return model; } function aModel(resource: URI, content: string = text): TextModel { - let model = createTextModel(content, undefined, undefined, resource); + const model = createTextModel(content, undefined, undefined, resource); modelsToDispose.push(model); return model; } function rangeHighlightDecorations(m: TextModel): IRange[] { - let rangeHighlights: IRange[] = []; + const rangeHighlights: IRange[] = []; - for (let dec of m.getAllDecorations()) { + for (const dec of m.getAllDecorations()) { if (dec.options.className === 'rangeHighlight') { rangeHighlights.push(dec.range); } diff --git a/src/vs/workbench/test/browser/part.test.ts b/src/vs/workbench/test/browser/part.test.ts index 60925fea20e..d89a50ddd4e 100644 --- a/src/vs/workbench/test/browser/part.test.ts +++ b/src/vs/workbench/test/browser/part.test.ts @@ -101,7 +101,7 @@ suite('Workbench parts', () => { } let fixture: HTMLElement; - let fixtureId = 'workbench-part-fixture'; + const fixtureId = 'workbench-part-fixture'; setup(() => { fixture = document.createElement('div'); @@ -114,7 +114,7 @@ suite('Workbench parts', () => { }); test('Creation', () => { - let b = document.createElement('div'); + const b = document.createElement('div'); document.getElementById(fixtureId)!.appendChild(b); hide(b); @@ -124,7 +124,7 @@ suite('Workbench parts', () => { assert.strictEqual(part.getId(), 'myPart'); // Memento - let memento = part.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE) as any; + let memento = part.getMemento(StorageScope.PROFILE, StorageTarget.MACHINE) as any; assert(memento); memento.foo = 'bar'; memento.bar = [1, 2, 3]; @@ -134,7 +134,7 @@ suite('Workbench parts', () => { // Re-Create to assert memento contents part = new MyPart(b); - memento = part.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); + memento = part.getMemento(StorageScope.PROFILE, StorageTarget.MACHINE); assert(memento); assert.strictEqual(memento.foo, 'bar'); assert.strictEqual(memento.bar.length, 3); @@ -145,17 +145,17 @@ suite('Workbench parts', () => { part.saveState(); part = new MyPart(b); - memento = part.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); + memento = part.getMemento(StorageScope.PROFILE, StorageTarget.MACHINE); assert(memento); assert.strictEqual(isEmptyObject(memento), true); }); test('Part Layout with Title and Content', function () { - let b = document.createElement('div'); + const b = document.createElement('div'); document.getElementById(fixtureId)!.appendChild(b); hide(b); - let part = new MyPart2(); + const part = new MyPart2(); part.create(b); assert(document.getElementById('myPart.title')); @@ -163,11 +163,11 @@ suite('Workbench parts', () => { }); test('Part Layout with Content only', function () { - let b = document.createElement('div'); + const b = document.createElement('div'); document.getElementById(fixtureId)!.appendChild(b); hide(b); - let part = new MyPart3(); + const part = new MyPart3(); part.create(b); assert(!document.getElementById('myPart.title')); diff --git a/src/vs/workbench/test/browser/parts/editor/breadcrumbModel.test.ts b/src/vs/workbench/test/browser/parts/editor/breadcrumbModel.test.ts index 8e20b9b710c..fdda3787190 100644 --- a/src/vs/workbench/test/browser/parts/editor/breadcrumbModel.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/breadcrumbModel.test.ts @@ -34,11 +34,11 @@ suite('Breadcrumb Model', function () { test('only uri, inside workspace', function () { - let model = new BreadcrumbsModel(URI.parse('foo:/bar/baz/ws/some/path/file.ts'), undefined, configService, workspaceService, new class extends mock<IOutlineService>() { }); - let elements = model.getElements(); + const model = new BreadcrumbsModel(URI.parse('foo:/bar/baz/ws/some/path/file.ts'), undefined, configService, workspaceService, new class extends mock<IOutlineService>() { }); + const elements = model.getElements(); assert.strictEqual(elements.length, 3); - let [one, two, three] = elements as FileElement[]; + const [one, two, three] = elements as FileElement[]; assert.strictEqual(one.kind, FileKind.FOLDER); assert.strictEqual(two.kind, FileKind.FOLDER); assert.strictEqual(three.kind, FileKind.FILE); @@ -49,11 +49,11 @@ suite('Breadcrumb Model', function () { test('display uri matters for FileElement', function () { - let model = new BreadcrumbsModel(URI.parse('foo:/bar/baz/ws/some/PATH/file.ts'), undefined, configService, workspaceService, new class extends mock<IOutlineService>() { }); - let elements = model.getElements(); + const model = new BreadcrumbsModel(URI.parse('foo:/bar/baz/ws/some/PATH/file.ts'), undefined, configService, workspaceService, new class extends mock<IOutlineService>() { }); + const elements = model.getElements(); assert.strictEqual(elements.length, 3); - let [one, two, three] = elements as FileElement[]; + const [one, two, three] = elements as FileElement[]; assert.strictEqual(one.kind, FileKind.FOLDER); assert.strictEqual(two.kind, FileKind.FOLDER); assert.strictEqual(three.kind, FileKind.FILE); @@ -64,11 +64,11 @@ suite('Breadcrumb Model', function () { test('only uri, outside workspace', function () { - let model = new BreadcrumbsModel(URI.parse('foo:/outside/file.ts'), undefined, configService, workspaceService, new class extends mock<IOutlineService>() { }); - let elements = model.getElements(); + const model = new BreadcrumbsModel(URI.parse('foo:/outside/file.ts'), undefined, configService, workspaceService, new class extends mock<IOutlineService>() { }); + const elements = model.getElements(); assert.strictEqual(elements.length, 2); - let [one, two] = elements as FileElement[]; + const [one, two] = elements as FileElement[]; assert.strictEqual(one.kind, FileKind.FOLDER); assert.strictEqual(two.kind, FileKind.FILE); assert.strictEqual(one.uri.toString(), 'foo:/outside'); diff --git a/src/vs/workbench/test/browser/parts/editor/diffEditorInput.test.ts b/src/vs/workbench/test/browser/parts/editor/diffEditorInput.test.ts index 8f2c0895bce..fbc271d79e1 100644 --- a/src/vs/workbench/test/browser/parts/editor/diffEditorInput.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/diffEditorInput.test.ts @@ -98,7 +98,7 @@ suite('Diff editor input', () => { let input = new MyEditorInput(); let otherInput = new MyEditorInput(); - let diffInput = instantiationService.createInstance(DiffEditorInput, 'name', 'description', input, otherInput, undefined); + const diffInput = instantiationService.createInstance(DiffEditorInput, 'name', 'description', input, otherInput, undefined); diffInput.onWillDispose(() => { counter++; assert(true); @@ -109,7 +109,7 @@ suite('Diff editor input', () => { input = new MyEditorInput(); otherInput = new MyEditorInput(); - let diffInput2 = instantiationService.createInstance(DiffEditorInput, 'name', 'description', input, otherInput, undefined); + const diffInput2 = instantiationService.createInstance(DiffEditorInput, 'name', 'description', input, otherInput, undefined); diffInput2.onWillDispose(() => { counter++; assert(true); diff --git a/src/vs/workbench/test/browser/parts/editor/editorDiffModel.test.ts b/src/vs/workbench/test/browser/parts/editor/editorDiffModel.test.ts index aa113a0807c..6187bda979f 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorDiffModel.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorDiffModel.test.ts @@ -33,8 +33,8 @@ suite('TextDiffEditorModel', () => { const dispose = accessor.textModelResolverService.registerTextModelContentProvider('test', { provideTextContent: async function (resource: URI): Promise<ITextModel | null> { if (resource.scheme === 'test') { - let modelContent = 'Hello Test'; - let languageSelection = accessor.languageService.createById('json'); + const modelContent = 'Hello Test'; + const languageSelection = accessor.languageService.createById('json'); return accessor.modelService.createModel(modelContent, languageSelection, resource); } @@ -43,16 +43,16 @@ suite('TextDiffEditorModel', () => { } }); - let input = instantiationService.createInstance(TextResourceEditorInput, URI.from({ scheme: 'test', authority: null!, path: 'thePath' }), 'name', 'description', undefined, undefined); - let otherInput = instantiationService.createInstance(TextResourceEditorInput, URI.from({ scheme: 'test', authority: null!, path: 'thePath' }), 'name2', 'description', undefined, undefined); - let diffInput = instantiationService.createInstance(DiffEditorInput, 'name', 'description', input, otherInput, undefined); + const input = instantiationService.createInstance(TextResourceEditorInput, URI.from({ scheme: 'test', authority: null!, path: 'thePath' }), 'name', 'description', undefined, undefined); + const otherInput = instantiationService.createInstance(TextResourceEditorInput, URI.from({ scheme: 'test', authority: null!, path: 'thePath' }), 'name2', 'description', undefined, undefined); + const diffInput = instantiationService.createInstance(DiffEditorInput, 'name', 'description', input, otherInput, undefined); let model = await diffInput.resolve() as TextDiffEditorModel; assert(model); assert(model instanceof TextDiffEditorModel); - let diffEditorModel = model.textDiffEditorModel!; + const diffEditorModel = model.textDiffEditorModel!; assert(diffEditorModel.original); assert(diffEditorModel.modified); diff --git a/src/vs/workbench/test/browser/parts/editor/editorGroupModel.test.ts b/src/vs/workbench/test/browser/parts/editor/editorGroupModel.test.ts index 3f4659894a3..33e0061620d 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorGroupModel.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorGroupModel.test.ts @@ -29,7 +29,7 @@ import { isEqual } from 'vs/base/common/resources'; suite('EditorGroupModel', () => { function inst(): IInstantiationService { - let inst = new TestInstantiationService(); + const inst = new TestInstantiationService(); inst.stub(IStorageService, new TestStorageService()); inst.stub(ILifecycleService, new TestLifecycleService()); inst.stub(IWorkspaceContextService, new TestContextService()); @@ -265,8 +265,8 @@ suite('EditorGroupModel', () => { return undefined; } - let testEditorInput = <TestEditorInput>editorInput; - let testInput: ISerializedTestInput = { + const testEditorInput = <TestEditorInput>editorInput; + const testInput: ISerializedTestInput = { id: testEditorInput.id }; @@ -278,7 +278,7 @@ suite('EditorGroupModel', () => { return undefined; } - let testInput: ISerializedTestInput = JSON.parse(serializedEditorInput); + const testInput: ISerializedTestInput = JSON.parse(serializedEditorInput); return new TestEditorInput(testInput.id); } @@ -826,7 +826,7 @@ suite('EditorGroupModel', () => { assert.strictEqual(events.activated[0].editor, input1); assert.strictEqual(events.activated[0].editorIndex, 0); - let index = group.indexOf(input1); + const index = group.indexOf(input1); let event = group.closeEditor(input1, EditorCloseContext.UNPIN); assert.strictEqual(event?.editor, input1); assert.strictEqual(event?.editorIndex, index); @@ -1031,7 +1031,7 @@ suite('EditorGroupModel', () => { }); test('Multiple Editors - Pinned and Active (DEFAULT_OPEN_EDITOR_DIRECTION = Direction.LEFT)', function () { - let inst = new TestInstantiationService(); + const inst = new TestInstantiationService(); inst.stub(IStorageService, new TestStorageService()); inst.stub(ILifecycleService, new TestLifecycleService()); inst.stub(IWorkspaceContextService, new TestContextService()); @@ -1263,7 +1263,7 @@ suite('EditorGroupModel', () => { }); test('Multiple Editors - closing picks next to the right', function () { - let inst = new TestInstantiationService(); + const inst = new TestInstantiationService(); inst.stub(IStorageService, new TestStorageService()); inst.stub(ILifecycleService, new TestLifecycleService()); inst.stub(IWorkspaceContextService, new TestContextService()); @@ -1640,7 +1640,7 @@ suite('EditorGroupModel', () => { }); test('Single Group, Single Editor - persist', function () { - let inst = new TestInstantiationService(); + const inst = new TestInstantiationService(); inst.stub(IStorageService, new TestStorageService()); inst.stub(IWorkspaceContextService, new TestContextService()); @@ -1674,7 +1674,7 @@ suite('EditorGroupModel', () => { }); test('Multiple Groups, Multiple editors - persist', function () { - let inst = new TestInstantiationService(); + const inst = new TestInstantiationService(); inst.stub(IStorageService, new TestStorageService()); inst.stub(IWorkspaceContextService, new TestContextService()); @@ -1744,7 +1744,7 @@ suite('EditorGroupModel', () => { }); test('Single group, multiple editors - persist (some not persistable)', function () { - let inst = new TestInstantiationService(); + const inst = new TestInstantiationService(); inst.stub(IStorageService, new TestStorageService()); inst.stub(IWorkspaceContextService, new TestContextService()); @@ -1788,7 +1788,7 @@ suite('EditorGroupModel', () => { }); test('Single group, multiple editors - persist (some not persistable, sticky editors)', function () { - let inst = new TestInstantiationService(); + const inst = new TestInstantiationService(); inst.stub(IStorageService, new TestStorageService()); inst.stub(IWorkspaceContextService, new TestContextService()); @@ -1823,7 +1823,7 @@ suite('EditorGroupModel', () => { }); test('Multiple groups, multiple editors - persist (some not persistable, causes empty group)', function () { - let inst = new TestInstantiationService(); + const inst = new TestInstantiationService(); inst.stub(IStorageService, new TestStorageService()); inst.stub(IWorkspaceContextService, new TestContextService()); diff --git a/src/vs/workbench/test/browser/parts/editor/editorInput.test.ts b/src/vs/workbench/test/browser/parts/editor/editorInput.test.ts index 426eecaf514..9045a383ab0 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorInput.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorInput.test.ts @@ -20,8 +20,8 @@ suite('EditorInput', () => { test('basics', () => { let counter = 0; - let input = new MyEditorInput(); - let otherInput = new MyEditorInput(); + const input = new MyEditorInput(); + const otherInput = new MyEditorInput(); assert.ok(isEditorInput(input)); assert.ok(!isEditorInput(undefined)); diff --git a/src/vs/workbench/test/browser/parts/editor/editorModel.test.ts b/src/vs/workbench/test/browser/parts/editor/editorModel.test.ts index c8c83c777c5..c4aad8cbb06 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorModel.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorModel.test.ts @@ -94,7 +94,7 @@ suite('EditorModel', () => { }); test('BaseTextEditorModel', async () => { - let modelService = stubModelService(instantiationService); + const modelService = stubModelService(instantiationService); const model = new MyTextEditorModel(modelService, languageService, instantiationService.createInstance(LanguageDetectionService), instantiationService.createInstance(TestAccessibilityService)); await model.resolve(); diff --git a/src/vs/workbench/test/browser/parts/editor/sideBySideEditorInput.test.ts b/src/vs/workbench/test/browser/parts/editor/sideBySideEditorInput.test.ts index fe772e2fa0c..9ace2cbba4c 100644 --- a/src/vs/workbench/test/browser/parts/editor/sideBySideEditorInput.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/sideBySideEditorInput.test.ts @@ -99,8 +99,8 @@ suite('SideBySideEditorInput', () => { test('events dispatching', () => { const instantiationService = workbenchInstantiationService(undefined, disposables); - let input = new MyEditorInput(); - let otherInput = new MyEditorInput(); + const input = new MyEditorInput(); + const otherInput = new MyEditorInput(); const sideBySideInut = instantiationService.createInstance(SideBySideEditorInput, 'name', 'description', otherInput, input); diff --git a/src/vs/workbench/test/browser/viewlet.test.ts b/src/vs/workbench/test/browser/viewlet.test.ts index 97a69c2afef..d77bf350282 100644 --- a/src/vs/workbench/test/browser/viewlet.test.ts +++ b/src/vs/workbench/test/browser/viewlet.test.ts @@ -24,7 +24,7 @@ suite('Viewlets', () => { } test('ViewletDescriptor API', function () { - let d = PaneCompositeDescriptor.create(TestViewlet, 'id', 'name', 'class', 5); + const d = PaneCompositeDescriptor.create(TestViewlet, 'id', 'name', 'class', 5); assert.strictEqual(d.id, 'id'); assert.strictEqual(d.name, 'name'); assert.strictEqual(d.cssClass, 'class'); @@ -46,8 +46,8 @@ suite('Viewlets', () => { assert(isFunction(Registry.as<PaneCompositeRegistry>(Extensions.Viewlets).getPaneComposite)); assert(isFunction(Registry.as<PaneCompositeRegistry>(Extensions.Viewlets).getPaneComposites)); - let oldCount = Registry.as<PaneCompositeRegistry>(Extensions.Viewlets).getPaneComposites().length; - let d = PaneCompositeDescriptor.create(TestViewlet, 'reg-test-id', 'name'); + const oldCount = Registry.as<PaneCompositeRegistry>(Extensions.Viewlets).getPaneComposites().length; + const d = PaneCompositeDescriptor.create(TestViewlet, 'reg-test-id', 'name'); Registry.as<PaneCompositeRegistry>(Extensions.Viewlets).registerPaneComposite(d); assert(d === Registry.as<PaneCompositeRegistry>(Extensions.Viewlets).getPaneComposite('reg-test-id')); diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 5bd962101c9..c45f26e708a 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -93,7 +93,7 @@ import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogSer import { CodeEditorService } from 'vs/workbench/services/editor/browser/codeEditorService'; import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { IDiffEditor, IEditor } from 'vs/editor/common/editorCommon'; +import { IDiffEditor } from 'vs/editor/common/editorCommon'; import { IInputBox, IInputOptions, IPickOptions, IQuickInputButton, IQuickInputService, IQuickNavigateConfiguration, IQuickPick, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; import { QuickInputService } from 'vs/workbench/services/quickinput/browser/quickInputService'; import { IListService } from 'vs/platform/list/browser/listService'; @@ -156,10 +156,15 @@ import { TestEditorWorkerService } from 'vs/editor/test/common/services/testEdit import { IExtensionHostExitInfo, IRemoteAgentConnection, IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { ILanguageDetectionService } from 'vs/workbench/services/languageDetection/common/languageDetectionWorkerService'; import { IDiagnosticInfoOptions, IDiagnosticInfo } from 'vs/platform/diagnostics/common/diagnostics'; -import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, ExtensionType, IExtension, IExtensionDescription, IRelaxedExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions'; import { ISocketFactory } from 'vs/platform/remote/common/remoteAgentConnection'; import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment'; import { ILayoutOffsetInfo } from 'vs/platform/layout/browser/layoutService'; +import { IUserDataProfilesService, UserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { UserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfileService'; +import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { EnablementState, IExtensionManagementServer, IScannedExtension, IWebExtensionsScannerService, IWorkbenchExtensionEnablementService, IWorkbenchExtensionManagementService, ScanOptions } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { InstallVSIXOptions, ILocalExtension, IGalleryExtension, InstallOptions, IExtensionIdentifier, UninstallOptions, IExtensionsControlManifest, IGalleryMetadata, IExtensionManagementParticipant } from 'vs/platform/extensionManagement/common/extensionManagement'; export function createFileEditorInput(instantiationService: IInstantiationService, resource: URI): FileEditorInput { return instantiationService.createInstance(FileEditorInput, resource, undefined, undefined, undefined, undefined, undefined, undefined); @@ -180,15 +185,15 @@ Registry.as<IEditorFactoryRegistry>(EditorExtensions.EditorFactory).registerFile export class TestTextResourceEditor extends TextResourceEditor { - protected override createEditorControl(parent: HTMLElement, configuration: any): IEditor { - return this.instantiationService.createInstance(TestCodeEditor, parent, configuration, {}); + protected override createEditorControl(parent: HTMLElement, configuration: any): void { + this.editorControl = this.instantiationService.createInstance(TestCodeEditor, parent, configuration, {}); } } export class TestTextFileEditor extends TextFileEditor { - protected override createEditorControl(parent: HTMLElement, configuration: any): IEditor { - return this.instantiationService.createInstance(TestCodeEditor, parent, configuration, {}); + protected override createEditorControl(parent: HTMLElement, configuration: any): void { + this.editorControl = this.instantiationService.createInstance(TestCodeEditor, parent, configuration, {}); } setSelection(selection: Selection | undefined, reason: EditorPaneSelectionChangeReason): void { @@ -280,6 +285,8 @@ export function workbenchInstantiationService( instantiationService.stub(IModelService, disposables.add(instantiationService.createInstance(ModelService))); const fileService = overrides?.fileService ? overrides.fileService(instantiationService) : new TestFileService(); instantiationService.stub(IFileService, fileService); + const userDataProfilesService = instantiationService.stub(IUserDataProfilesService, new UserDataProfilesService(environmentService, fileService, new NullLogService())); + instantiationService.stub(IUserDataProfileService, new UserDataProfileService(userDataProfilesService.defaultProfile)); instantiationService.stub(IUriIdentityService, new UriIdentityService(fileService)); instantiationService.stub(IWorkingCopyBackupService, new TestWorkingCopyBackupService()); instantiationService.stub(ITelemetryService, NullTelemetryService); @@ -630,7 +637,7 @@ export class TestLayoutService implements IWorkbenchLayoutService { focus() { } } -let activeViewlet: PaneComposite = {} as any; +const activeViewlet: PaneComposite = {} as any; export class TestPaneCompositeService extends Disposable implements IPaneCompositePartService { declare readonly _serviceBrand: undefined; @@ -1480,8 +1487,8 @@ export function registerTestEditor(id: string, inputs: SyncDescriptor<EditorInpu } serialize(editorInput: EditorInput): string { - let testEditorInput = <TestFileEditorInput>editorInput; - let testInput: ISerializedTestInput = { + const testEditorInput = <TestFileEditorInput>editorInput; + const testInput: ISerializedTestInput = { resource: testEditorInput.resource.toString() }; @@ -1489,7 +1496,7 @@ export function registerTestEditor(id: string, inputs: SyncDescriptor<EditorInpu } deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): EditorInput { - let testInput: ISerializedTestInput = JSON.parse(serializedEditorInput); + const testInput: ISerializedTestInput = JSON.parse(serializedEditorInput); return new TestFileEditorInput(URI.parse(testInput.resource), serializerInputId!); } @@ -1657,9 +1664,9 @@ export class TestEditorPart extends EditorPart { delete workspaceMemento[key]; } - const globalMemento = this.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); - for (const key of Object.keys(globalMemento)) { - delete globalMemento[key]; + const profileMemento = this.getMemento(StorageScope.PROFILE, StorageTarget.MACHINE); + for (const key of Object.keys(profileMemento)) { + delete profileMemento[key]; } } } @@ -1830,6 +1837,7 @@ export class TestTerminalGroupService implements ITerminalGroupService { getFindState(): FindReplaceState { throw new Error('Method not implemented.'); } findNext(): void { throw new Error('Method not implemented.'); } findPrevious(): void { throw new Error('Method not implemented.'); } + updateVisibility(): void { throw new Error('Method not implemented.'); } } export class TestTerminalProfileService implements ITerminalProfileService { @@ -1922,3 +1930,97 @@ export class TestRemoteAgentService implements IRemoteAgentService { async flushTelemetry(): Promise<void> { } async getRoundTripTime(): Promise<number | undefined> { return undefined; } } + +export class TestWorkbenchExtensionEnablementService implements IWorkbenchExtensionEnablementService { + _serviceBrand: undefined; + onEnablementChanged = Event.None; + getEnablementState(extension: IExtension): EnablementState { return EnablementState.EnabledGlobally; } + getEnablementStates(extensions: IExtension[], workspaceTypeOverrides?: { trusted?: boolean | undefined } | undefined): EnablementState[] { return []; } + getDependenciesEnablementStates(extension: IExtension): [IExtension, EnablementState][] { return []; } + canChangeEnablement(extension: IExtension): boolean { return true; } + canChangeWorkspaceEnablement(extension: IExtension): boolean { return true; } + isEnabled(extension: IExtension): boolean { return true; } + isEnabledEnablementState(enablementState: EnablementState): boolean { return true; } + isDisabledGlobally(extension: IExtension): boolean { return false; } + async setEnablement(extensions: IExtension[], state: EnablementState): Promise<boolean[]> { return []; } + async updateExtensionsEnablementsWhenWorkspaceTrustChanges(): Promise<void> { } +} + +export class TestWorkbenchExtensionManagementService implements IWorkbenchExtensionManagementService { + _serviceBrand: undefined; + onInstallExtension = Event.None; + onDidInstallExtensions = Event.None; + onUninstallExtension = Event.None; + onDidUninstallExtension = Event.None; + onDidChangeProfileExtensions = Event.None; + installVSIX(location: URI, manifest: Readonly<IRelaxedExtensionManifest>, installOptions?: InstallVSIXOptions | undefined): Promise<ILocalExtension> { + throw new Error('Method not implemented.'); + } + installWebExtension(location: URI): Promise<ILocalExtension> { + throw new Error('Method not implemented.'); + } + installExtensions(extensions: IGalleryExtension[], installOptions?: InstallOptions | undefined): Promise<ILocalExtension[]> { + throw new Error('Method not implemented.'); + } + async updateFromGallery(gallery: IGalleryExtension, extension: ILocalExtension, installOptions?: InstallOptions | undefined): Promise<ILocalExtension> { return extension; } + getExtensionManagementServerToInstall(manifest: Readonly<IRelaxedExtensionManifest>): IExtensionManagementServer | null { + throw new Error('Method not implemented.'); + } + zip(extension: ILocalExtension): Promise<URI> { + throw new Error('Method not implemented.'); + } + unzip(zipLocation: URI): Promise<IExtensionIdentifier> { + throw new Error('Method not implemented.'); + } + getManifest(vsix: URI): Promise<Readonly<IRelaxedExtensionManifest>> { + throw new Error('Method not implemented.'); + } + install(vsix: URI, options?: InstallVSIXOptions | undefined): Promise<ILocalExtension> { + throw new Error('Method not implemented.'); + } + async canInstall(extension: IGalleryExtension): Promise<boolean> { return false; } + installFromGallery(extension: IGalleryExtension, options?: InstallOptions | undefined): Promise<ILocalExtension> { + throw new Error('Method not implemented.'); + } + uninstall(extension: ILocalExtension, options?: UninstallOptions | undefined): Promise<void> { + throw new Error('Method not implemented.'); + } + async reinstallFromGallery(extension: ILocalExtension): Promise<void> { + } + async getInstalled(type?: ExtensionType | undefined): Promise<ILocalExtension[]> { return []; } + getExtensionsControlManifest(): Promise<IExtensionsControlManifest> { + throw new Error('Method not implemented.'); + } + getMetadata(extension: ILocalExtension): Promise<Partial<IGalleryMetadata & { isApplicationScoped: boolean; isMachineScoped: boolean; isBuiltin: boolean; isSystem: boolean; updated: boolean; preRelease: boolean; installedTimestamp: number }> | undefined> { + throw new Error('Method not implemented.'); + } + async updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata): Promise<ILocalExtension> { return local; } + async updateExtensionScope(local: ILocalExtension, isMachineScoped: boolean): Promise<ILocalExtension> { return local; } + registerParticipant(pariticipant: IExtensionManagementParticipant): void { } + async getTargetPlatform(): Promise<TargetPlatform> { return TargetPlatform.UNDEFINED; } +} + +export class TestWebExtensionsScannerService implements IWebExtensionsScannerService { + _serviceBrand: undefined; + async scanSystemExtensions(): Promise<IExtension[]> { return []; } + async scanUserExtensions(options?: ScanOptions | undefined): Promise<IScannedExtension[]> { return []; } + async scanExtensionsUnderDevelopment(): Promise<IExtension[]> { return []; } + scanExistingExtension(extensionLocation: URI, extensionType: ExtensionType): Promise<IScannedExtension | null> { + throw new Error('Method not implemented.'); + } + addExtension(location: URI, metadata?: Partial<IGalleryMetadata & { isApplicationScoped: boolean; isMachineScoped: boolean; isBuiltin: boolean; isSystem: boolean; updated: boolean; preRelease: boolean; installedTimestamp: number }> | undefined): Promise<IExtension> { + throw new Error('Method not implemented.'); + } + addExtensionFromGallery(galleryExtension: IGalleryExtension, metadata?: Partial<IGalleryMetadata & { isApplicationScoped: boolean; isMachineScoped: boolean; isBuiltin: boolean; isSystem: boolean; updated: boolean; preRelease: boolean; installedTimestamp: number }> | undefined): Promise<IExtension> { + throw new Error('Method not implemented.'); + } + removeExtension(identifier: IExtensionIdentifier, version?: string | undefined): Promise<void> { + throw new Error('Method not implemented.'); + } + scanMetadata(extensionLocation: URI): Promise<Partial<IGalleryMetadata & { isApplicationScoped: boolean; isMachineScoped: boolean; isBuiltin: boolean; isSystem: boolean; updated: boolean; preRelease: boolean; installedTimestamp: number }> | undefined> { + throw new Error('Method not implemented.'); + } + scanExtensionManifest(extensionLocation: URI): Promise<Readonly<IRelaxedExtensionManifest> | null> { + throw new Error('Method not implemented.'); + } +} diff --git a/src/vs/workbench/test/common/memento.test.ts b/src/vs/workbench/test/common/memento.test.ts index 46c694b2475..e523402f4bd 100644 --- a/src/vs/workbench/test/common/memento.test.ts +++ b/src/vs/workbench/test/common/memento.test.ts @@ -9,23 +9,29 @@ import { Memento } from 'vs/workbench/common/memento'; import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; suite('Memento', () => { - let context: StorageScope | undefined = undefined; let storage: IStorageService; setup(() => { storage = new TestStorageService(); - Memento.clear(StorageScope.GLOBAL); + Memento.clear(StorageScope.APPLICATION); + Memento.clear(StorageScope.PROFILE); Memento.clear(StorageScope.WORKSPACE); }); test('Loading and Saving Memento with Scopes', () => { - let myMemento = new Memento('memento.test', storage); + const myMemento = new Memento('memento.test', storage); - // Global - let memento = myMemento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); + // Application + let memento = myMemento.getMemento(StorageScope.APPLICATION, StorageTarget.MACHINE); memento.foo = [1, 2, 3]; - let globalMemento = myMemento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); - assert.deepStrictEqual(globalMemento, memento); + let applicationMemento = myMemento.getMemento(StorageScope.APPLICATION, StorageTarget.MACHINE); + assert.deepStrictEqual(applicationMemento, memento); + + // Profile + memento = myMemento.getMemento(StorageScope.PROFILE, StorageTarget.MACHINE); + memento.foo = [4, 5, 6]; + let profileMemento = myMemento.getMemento(StorageScope.PROFILE, StorageTarget.MACHINE); + assert.deepStrictEqual(profileMemento, memento); // Workspace memento = myMemento.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE); @@ -34,23 +40,33 @@ suite('Memento', () => { myMemento.saveMemento(); - // Global - memento = myMemento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); + // Application + memento = myMemento.getMemento(StorageScope.APPLICATION, StorageTarget.MACHINE); assert.deepStrictEqual(memento, { foo: [1, 2, 3] }); - globalMemento = myMemento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); - assert.deepStrictEqual(globalMemento, memento); + applicationMemento = myMemento.getMemento(StorageScope.APPLICATION, StorageTarget.MACHINE); + assert.deepStrictEqual(applicationMemento, memento); + + // Profile + memento = myMemento.getMemento(StorageScope.PROFILE, StorageTarget.MACHINE); + assert.deepStrictEqual(memento, { foo: [4, 5, 6] }); + profileMemento = myMemento.getMemento(StorageScope.PROFILE, StorageTarget.MACHINE); + assert.deepStrictEqual(profileMemento, memento); // Workspace memento = myMemento.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE); assert.deepStrictEqual(memento, { foo: 'Hello World' }); // Assert the Mementos are stored properly in storage - assert.deepStrictEqual(JSON.parse(storage.get('memento/memento.test', StorageScope.GLOBAL)!), { foo: [1, 2, 3] }); - + assert.deepStrictEqual(JSON.parse(storage.get('memento/memento.test', StorageScope.APPLICATION)!), { foo: [1, 2, 3] }); + assert.deepStrictEqual(JSON.parse(storage.get('memento/memento.test', StorageScope.PROFILE)!), { foo: [4, 5, 6] }); assert.deepStrictEqual(JSON.parse(storage.get('memento/memento.test', StorageScope.WORKSPACE)!), { foo: 'Hello World' }); - // Delete Global - memento = myMemento.getMemento(context!, StorageTarget.MACHINE); + // Delete Application + memento = myMemento.getMemento(StorageScope.APPLICATION, StorageTarget.MACHINE); + delete memento.foo; + + // Delete Profile + memento = myMemento.getMemento(StorageScope.PROFILE, StorageTarget.MACHINE); delete memento.foo; // Delete Workspace @@ -59,8 +75,12 @@ suite('Memento', () => { myMemento.saveMemento(); - // Global - memento = myMemento.getMemento(context!, StorageTarget.MACHINE); + // Application + memento = myMemento.getMemento(StorageScope.APPLICATION, StorageTarget.MACHINE); + assert.deepStrictEqual(memento, {}); + + // Profile + memento = myMemento.getMemento(StorageScope.PROFILE, StorageTarget.MACHINE); assert.deepStrictEqual(memento, {}); // Workspace @@ -68,16 +88,16 @@ suite('Memento', () => { assert.deepStrictEqual(memento, {}); // Assert the Mementos are also removed from storage - assert.strictEqual(storage.get('memento/memento.test', StorageScope.GLOBAL, null!), null); - + assert.strictEqual(storage.get('memento/memento.test', StorageScope.APPLICATION, null!), null); + assert.strictEqual(storage.get('memento/memento.test', StorageScope.PROFILE, null!), null); assert.strictEqual(storage.get('memento/memento.test', StorageScope.WORKSPACE, null!), null); }); test('Save and Load', () => { - let myMemento = new Memento('memento.test', storage); + const myMemento = new Memento('memento.test', storage); - // Global - let memento = myMemento.getMemento(context!, StorageTarget.MACHINE); + // Profile + let memento = myMemento.getMemento(StorageScope.PROFILE, StorageTarget.MACHINE); memento.foo = [1, 2, 3]; // Workspace @@ -87,18 +107,18 @@ suite('Memento', () => { myMemento.saveMemento(); - // Global - memento = myMemento.getMemento(context!, StorageTarget.MACHINE); + // Profile + memento = myMemento.getMemento(StorageScope.PROFILE, StorageTarget.MACHINE); assert.deepStrictEqual(memento, { foo: [1, 2, 3] }); - let globalMemento = myMemento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); - assert.deepStrictEqual(globalMemento, memento); + let profileMemento = myMemento.getMemento(StorageScope.PROFILE, StorageTarget.MACHINE); + assert.deepStrictEqual(profileMemento, memento); // Workspace memento = myMemento.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE); assert.deepStrictEqual(memento, { foo: 'Hello World' }); - // Global - memento = myMemento.getMemento(context!, StorageTarget.MACHINE); + // Profile + memento = myMemento.getMemento(StorageScope.PROFILE, StorageTarget.MACHINE); memento.foo = [4, 5, 6]; // Workspace @@ -108,18 +128,18 @@ suite('Memento', () => { myMemento.saveMemento(); - // Global - memento = myMemento.getMemento(context!, StorageTarget.MACHINE); + // Profile + memento = myMemento.getMemento(StorageScope.PROFILE, StorageTarget.MACHINE); assert.deepStrictEqual(memento, { foo: [4, 5, 6] }); - globalMemento = myMemento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); - assert.deepStrictEqual(globalMemento, memento); + profileMemento = myMemento.getMemento(StorageScope.PROFILE, StorageTarget.MACHINE); + assert.deepStrictEqual(profileMemento, memento); // Workspace memento = myMemento.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE); assert.deepStrictEqual(memento, { foo: 'World Hello' }); - // Delete Global - memento = myMemento.getMemento(context!, StorageTarget.MACHINE); + // Delete Profile + memento = myMemento.getMemento(StorageScope.PROFILE, StorageTarget.MACHINE); delete memento.foo; // Delete Workspace @@ -128,8 +148,8 @@ suite('Memento', () => { myMemento.saveMemento(); - // Global - memento = myMemento.getMemento(context!, StorageTarget.MACHINE); + // Profile + memento = myMemento.getMemento(StorageScope.PROFILE, StorageTarget.MACHINE); assert.deepStrictEqual(memento, {}); // Workspace @@ -138,14 +158,14 @@ suite('Memento', () => { }); test('Save and Load - 2 Components with same id', () => { - let myMemento = new Memento('memento.test', storage); - let myMemento2 = new Memento('memento.test', storage); + const myMemento = new Memento('memento.test', storage); + const myMemento2 = new Memento('memento.test', storage); - // Global - let memento = myMemento.getMemento(context!, StorageTarget.MACHINE); + // Profile + let memento = myMemento.getMemento(StorageScope.PROFILE, StorageTarget.MACHINE); memento.foo = [1, 2, 3]; - memento = myMemento2.getMemento(context!, StorageTarget.MACHINE); + memento = myMemento2.getMemento(StorageScope.PROFILE, StorageTarget.MACHINE); memento.bar = [1, 2, 3]; // Workspace @@ -160,16 +180,16 @@ suite('Memento', () => { myMemento.saveMemento(); myMemento2.saveMemento(); - // Global - memento = myMemento.getMemento(context!, StorageTarget.MACHINE); + // Profile + memento = myMemento.getMemento(StorageScope.PROFILE, StorageTarget.MACHINE); assert.deepStrictEqual(memento, { foo: [1, 2, 3], bar: [1, 2, 3] }); - let globalMemento = myMemento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); - assert.deepStrictEqual(globalMemento, memento); + let profileMemento = myMemento.getMemento(StorageScope.PROFILE, StorageTarget.MACHINE); + assert.deepStrictEqual(profileMemento, memento); - memento = myMemento2.getMemento(context!, StorageTarget.MACHINE); + memento = myMemento2.getMemento(StorageScope.PROFILE, StorageTarget.MACHINE); assert.deepStrictEqual(memento, { foo: [1, 2, 3], bar: [1, 2, 3] }); - globalMemento = myMemento2.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); - assert.deepStrictEqual(globalMemento, memento); + profileMemento = myMemento2.getMemento(StorageScope.PROFILE, StorageTarget.MACHINE); + assert.deepStrictEqual(profileMemento, memento); // Workspace memento = myMemento.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE); @@ -182,9 +202,9 @@ suite('Memento', () => { test('Clear Memento', () => { let myMemento = new Memento('memento.test', storage); - // Global - let globalMemento = myMemento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); - globalMemento.foo = 'Hello World'; + // Profile + let profileMemento = myMemento.getMemento(StorageScope.PROFILE, StorageTarget.MACHINE); + profileMemento.foo = 'Hello World'; // Workspace let workspaceMemento = myMemento.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE); @@ -194,14 +214,14 @@ suite('Memento', () => { // Clear storage = new TestStorageService(); - Memento.clear(StorageScope.GLOBAL); + Memento.clear(StorageScope.PROFILE); Memento.clear(StorageScope.WORKSPACE); myMemento = new Memento('memento.test', storage); - globalMemento = myMemento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); + profileMemento = myMemento.getMemento(StorageScope.PROFILE, StorageTarget.MACHINE); workspaceMemento = myMemento.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE); - assert.deepStrictEqual(globalMemento, {}); + assert.deepStrictEqual(profileMemento, {}); assert.deepStrictEqual(workspaceMemento, {}); }); }); diff --git a/src/vs/workbench/test/common/notifications.test.ts b/src/vs/workbench/test/common/notifications.test.ts index 1bdef91760a..126f512975b 100644 --- a/src/vs/workbench/test/common/notifications.test.ts +++ b/src/vs/workbench/test/common/notifications.test.ts @@ -21,12 +21,12 @@ suite('Notifications', () => { assert.ok(!NotificationViewItem.create({ severity: Severity.Error, message: null! })); // Duplicates - let item1 = NotificationViewItem.create({ severity: Severity.Error, message: 'Error Message' })!; - let item2 = NotificationViewItem.create({ severity: Severity.Error, message: 'Error Message' })!; - let item3 = NotificationViewItem.create({ severity: Severity.Info, message: 'Info Message' })!; - let item4 = NotificationViewItem.create({ severity: Severity.Error, message: 'Error Message', source: 'Source' })!; - let item5 = NotificationViewItem.create({ severity: Severity.Error, message: 'Error Message', actions: { primary: [new Action('id', 'label')] } })!; - let item6 = NotificationViewItem.create({ severity: Severity.Error, message: 'Error Message', actions: { primary: [new Action('id', 'label')] }, progress: { infinite: true } })!; + const item1 = NotificationViewItem.create({ severity: Severity.Error, message: 'Error Message' })!; + const item2 = NotificationViewItem.create({ severity: Severity.Error, message: 'Error Message' })!; + const item3 = NotificationViewItem.create({ severity: Severity.Info, message: 'Info Message' })!; + const item4 = NotificationViewItem.create({ severity: Severity.Error, message: 'Error Message', source: 'Source' })!; + const item5 = NotificationViewItem.create({ severity: Severity.Error, message: 'Error Message', actions: { primary: [new Action('id', 'label')] } })!; + const item6 = NotificationViewItem.create({ severity: Severity.Error, message: 'Error Message', actions: { primary: [new Action('id', 'label')] }, progress: { infinite: true } })!; assert.strictEqual(item1.equals(item1), true); assert.strictEqual(item2.equals(item2), true); @@ -39,8 +39,8 @@ suite('Notifications', () => { assert.strictEqual(item1.equals(item4), false); assert.strictEqual(item1.equals(item5), false); - let itemId1 = NotificationViewItem.create({ id: 'same', message: 'Info Message', severity: Severity.Info })!; - let itemId2 = NotificationViewItem.create({ id: 'same', message: 'Error Message', severity: Severity.Error })!; + const itemId1 = NotificationViewItem.create({ id: 'same', message: 'Info Message', severity: Severity.Info })!; + const itemId2 = NotificationViewItem.create({ id: 'same', message: 'Error Message', severity: Severity.Error })!; assert.strictEqual(itemId1.equals(itemId2), true); assert.strictEqual(itemId1.equals(item3), false); @@ -127,20 +127,20 @@ suite('Notifications', () => { assert.strictEqual(called, 1); // Error with Action - let item7 = NotificationViewItem.create({ severity: Severity.Error, message: createErrorWithActions('Hello Error', [new Action('id', 'label')]) })!; + const item7 = NotificationViewItem.create({ severity: Severity.Error, message: createErrorWithActions('Hello Error', [new Action('id', 'label')]) })!; assert.strictEqual(item7.actions!.primary!.length, 1); // Filter - let item8 = NotificationViewItem.create({ severity: Severity.Error, message: 'Error Message' }, NotificationsFilter.SILENT)!; + const item8 = NotificationViewItem.create({ severity: Severity.Error, message: 'Error Message' }, NotificationsFilter.SILENT)!; assert.strictEqual(item8.silent, true); - let item9 = NotificationViewItem.create({ severity: Severity.Error, message: 'Error Message' }, NotificationsFilter.OFF)!; + const item9 = NotificationViewItem.create({ severity: Severity.Error, message: 'Error Message' }, NotificationsFilter.OFF)!; assert.strictEqual(item9.silent, false); - let item10 = NotificationViewItem.create({ severity: Severity.Error, message: 'Error Message' }, NotificationsFilter.ERROR)!; + const item10 = NotificationViewItem.create({ severity: Severity.Error, message: 'Error Message' }, NotificationsFilter.ERROR)!; assert.strictEqual(item10.silent, false); - let item11 = NotificationViewItem.create({ severity: Severity.Warning, message: 'Error Message' }, NotificationsFilter.ERROR)!; + const item11 = NotificationViewItem.create({ severity: Severity.Warning, message: 'Error Message' }, NotificationsFilter.ERROR)!; assert.strictEqual(item11.silent, true); }); @@ -174,12 +174,12 @@ suite('Notifications', () => { lastStatusMessageEvent = e; }); - let item1: INotification = { severity: Severity.Error, message: 'Error Message', actions: { primary: [new Action('id', 'label')] } }; - let item2: INotification = { severity: Severity.Warning, message: 'Warning Message', source: 'Some Source' }; - let item2Duplicate: INotification = { severity: Severity.Warning, message: 'Warning Message', source: 'Some Source' }; - let item3: INotification = { severity: Severity.Info, message: 'Info Message' }; + const item1: INotification = { severity: Severity.Error, message: 'Error Message', actions: { primary: [new Action('id', 'label')] } }; + const item2: INotification = { severity: Severity.Warning, message: 'Warning Message', source: 'Some Source' }; + const item2Duplicate: INotification = { severity: Severity.Warning, message: 'Warning Message', source: 'Some Source' }; + const item3: INotification = { severity: Severity.Info, message: 'Info Message' }; - let item1Handle = model.addNotification(item1); + const item1Handle = model.addNotification(item1); assert.strictEqual(lastNotificationEvent.item.severity, item1.severity); assert.strictEqual(lastNotificationEvent.item.message.linkedText.toString(), item1.message); assert.strictEqual(lastNotificationEvent.index, 0); @@ -201,7 +201,7 @@ suite('Notifications', () => { assert.strictEqual(lastNotificationEvent.kind, NotificationChangeType.CHANGE); assert.strictEqual(lastNotificationEvent.detail, NotificationViewItemContentChangeKind.PROGRESS); - let item2Handle = model.addNotification(item2); + const item2Handle = model.addNotification(item2); assert.strictEqual(lastNotificationEvent.item.severity, item2.severity); assert.strictEqual(lastNotificationEvent.item.message.linkedText.toString(), item2.message); assert.strictEqual(lastNotificationEvent.index, 0); @@ -256,7 +256,7 @@ suite('Notifications', () => { assert.ok(!model.statusMessage); assert.strictEqual(lastStatusMessageEvent.kind, StatusMessageChangeType.REMOVE); - let disposable2 = model.showStatusMessage('Hello World 2'); + const disposable2 = model.showStatusMessage('Hello World 2'); const disposable3 = model.showStatusMessage('Hello World 3'); assert.strictEqual(model.statusMessage!.message, 'Hello World 3'); diff --git a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts index 9b1228a4c69..ded96d4e010 100644 --- a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts @@ -25,7 +25,7 @@ import { IReadTextFileOptions, ITextFileStreamContent, ITextFileService } from ' import { createTextBufferFactoryFromStream } from 'vs/editor/common/model/textModel'; import { IOpenEmptyWindowOptions, IWindowOpenable, IOpenWindowOptions, IOpenedWindow, IColorScheme, INativeWindowConfiguration } from 'vs/platform/window/common/window'; import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv'; -import { LogLevel, ILogService } from 'vs/platform/log/common/log'; +import { LogLevel, ILogService, NullLogService } from 'vs/platform/log/common/log'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; @@ -48,9 +48,28 @@ import { IElevatedFileService } from 'vs/workbench/services/files/common/elevate import { IDecorationsService } from 'vs/workbench/services/decorations/common/decorations'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { IPartsSplash } from 'vs/platform/theme/common/themeService'; +import { IUserDataProfilesService, UserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { FileService } from 'vs/platform/files/common/fileService'; +import { joinPath } from 'vs/base/common/resources'; +import { UserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfileService'; +import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; const args = parseArgs(process.argv, OPTIONS); +const homeDir = homedir(); +const NULL_PROFILE = { + name: '', + id: '', + isDefault: false, + location: URI.file(homeDir), + settingsResource: joinPath(URI.file(homeDir), 'settings.json'), + globalStorageHome: joinPath(URI.file(homeDir), 'globalStorage'), + keybindingsResource: joinPath(URI.file(homeDir), 'keybindings.json'), + tasksResource: joinPath(URI.file(homeDir), 'tasks.json'), + snippetsHome: joinPath(URI.file(homeDir), 'snippets'), + extensionsResource: undefined +}; + export const TestNativeWindowConfiguration: INativeWindowConfiguration = { windowId: 0, machineId: 'testMachineId', @@ -63,9 +82,10 @@ export const TestNativeWindowConfiguration: INativeWindowConfiguration = { colorScheme: { dark: true, highContrast: false }, os: { release: release(), hostname: hostname() }, product, - homeDir: homedir(), + homeDir: homeDir, tmpDir: tmpdir(), userDataDir: getUserDataPath(args), + profiles: { current: NULL_PROFILE, all: [NULL_PROFILE] }, ...args }; @@ -203,7 +223,7 @@ export class TestNativeHostService implements INativeHostService { async maximizeWindow(): Promise<void> { } async unmaximizeWindow(): Promise<void> { } async minimizeWindow(): Promise<void> { } - async updateTitleBarOverlay(backgroundColor: string, foregroundColor: string): Promise<void> { } + async updateTitleBarOverlay(options: { height?: number; backgroundColor?: string; foregroundColor?: string }): Promise<void> { } async setMinimumSize(width: number | undefined, height: number | undefined): Promise<void> { } async saveWindowSplash(value: IPartsSplash): Promise<void> { } async focusWindow(options?: { windowId?: number | undefined } | undefined): Promise<void> { } @@ -269,6 +289,8 @@ export function workbenchInstantiationService(disposables = new DisposableStore( instantiationService.stub(INativeEnvironmentService, TestEnvironmentService); instantiationService.stub(IWorkbenchEnvironmentService, TestEnvironmentService); instantiationService.stub(INativeWorkbenchEnvironmentService, TestEnvironmentService); + const userDataProfilesService = instantiationService.stub(IUserDataProfilesService, new UserDataProfilesService(TestEnvironmentService, new FileService(new NullLogService()), new NullLogService())); + instantiationService.stub(IUserDataProfileService, new UserDataProfileService(userDataProfilesService.defaultProfile)); return instantiationService; } diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 992aec9c53f..3319bbe6143 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -83,7 +83,8 @@ import 'vs/workbench/services/extensionRecommendations/common/extensionIgnoredRe import 'vs/workbench/services/extensionRecommendations/common/workspaceExtensionsConfig'; import 'vs/workbench/services/notification/common/notificationService'; import 'vs/workbench/services/userDataSync/common/userDataSyncUtil'; -import 'vs/workbench/services/profiles/common/profileService'; +import 'vs/workbench/services/userDataProfile/common/userDataProfileImportExportService'; +import 'vs/workbench/services/userDataProfile/browser/userDataProfileManagement'; import 'vs/workbench/services/remote/common/remoteExplorerService'; import 'vs/workbench/services/workingCopy/common/workingCopyService'; import 'vs/workbench/services/workingCopy/common/workingCopyFileService'; @@ -126,6 +127,7 @@ import { IgnoredExtensionsManagementService, IIgnoredExtensionsManagementService import { ExtensionStorageService, IExtensionStorageService } from 'vs/platform/extensionManagement/common/extensionStorage'; import { IUserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSync'; import { UserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSyncLog'; +import { IExtensionsProfileScannerService, ExtensionsProfileScannerService } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService'; registerSingleton(IUserDataSyncLogService, UserDataSyncLogService); registerSingleton(IIgnoredExtensionsManagementService, IgnoredExtensionsManagementService); @@ -142,6 +144,7 @@ registerSingleton(ITextResourceConfigurationService, TextResourceConfigurationSe registerSingleton(IMenuService, MenuService, true); registerSingleton(IDownloadService, DownloadService, true); registerSingleton(IOpenerService, OpenerService, true); +registerSingleton(IExtensionsProfileScannerService, ExtensionsProfileScannerService); //#endregion @@ -210,6 +213,9 @@ import 'vs/workbench/contrib/debug/browser/debugViewlet'; // Markers import 'vs/workbench/contrib/markers/browser/markers.contribution'; +// Merge Editor +import 'vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution'; + // Comments import 'vs/workbench/contrib/comments/browser/comments.contribution'; @@ -319,8 +325,11 @@ import 'vs/workbench/contrib/feedback/browser/feedback.contribution'; // User Data Sync import 'vs/workbench/contrib/userDataSync/browser/userDataSync.contribution'; -// Profiles -import 'vs/workbench/contrib/profiles/common/profiles.contribution'; +// User Data Profiles +import 'vs/workbench/contrib/userDataProfile/browser/userDataProfile.contribution'; + +// Continue Edit Session +import 'vs/workbench/contrib/sessionSync/browser/sessionSync.contribution'; // Code Actions import 'vs/workbench/contrib/codeActions/browser/codeActions.contribution'; diff --git a/src/vs/workbench/workbench.desktop.main.ts b/src/vs/workbench/workbench.desktop.main.ts index 75b3b27affa..6b7b44d4155 100644 --- a/src/vs/workbench/workbench.desktop.main.ts +++ b/src/vs/workbench/workbench.desktop.main.ts @@ -37,7 +37,7 @@ import 'vs/workbench/workbench.sandbox.main'; // // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -import 'vs/workbench/services/extensions/electron-browser/extensionService'; +import 'vs/workbench/services/extensions/electron-browser/nativeExtensionService'; // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // diff --git a/src/vs/workbench/workbench.desktop.sandbox.main.ts b/src/vs/workbench/workbench.desktop.sandbox.main.ts index 79258547199..894e6785212 100644 --- a/src/vs/workbench/workbench.desktop.sandbox.main.ts +++ b/src/vs/workbench/workbench.desktop.sandbox.main.ts @@ -27,12 +27,6 @@ import 'vs/workbench/electron-sandbox/desktop.main'; //#region --- workbench services -import { IExtensionService, NullExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; - -// TODO@bpasero sandbox: remove me when extension host is present -class SimpleExtensionService extends NullExtensionService { } - -registerSingleton(IExtensionService, SimpleExtensionService); +import 'vs/workbench/services/extensions/electron-sandbox/sandboxExtensionService'; //#endregion diff --git a/src/vs/workbench/workbench.sandbox.main.ts b/src/vs/workbench/workbench.sandbox.main.ts index e0708193c34..dd9ddd715b1 100644 --- a/src/vs/workbench/workbench.sandbox.main.ts +++ b/src/vs/workbench/workbench.sandbox.main.ts @@ -58,7 +58,7 @@ import 'vs/workbench/services/extensionManagement/electron-sandbox/extensionMana import 'vs/workbench/services/extensionManagement/electron-sandbox/extensionUrlTrustService'; import 'vs/workbench/services/credentials/electron-sandbox/credentialsService'; import 'vs/workbench/services/encryption/electron-sandbox/encryptionService'; -import 'vs/workbench/services/localizations/electron-sandbox/localizationsService'; +import 'vs/workbench/services/localization/electron-sandbox/languagePackService'; import 'vs/workbench/services/telemetry/electron-sandbox/telemetryService'; import 'vs/workbench/services/extensions/electron-sandbox/extensionHostStarter'; import 'vs/platform/extensionManagement/electron-sandbox/extensionsScannerService'; @@ -98,7 +98,7 @@ registerSingleton(IUserDataInitializationService, UserDataInitializationService) import 'vs/workbench/contrib/logs/electron-sandbox/logs.contribution'; // Localizations -import 'vs/workbench/contrib/localizations/browser/localizations.contribution'; +import 'vs/workbench/contrib/localization/electron-sandbox/localization.contribution'; // Explorer import 'vs/workbench/contrib/files/electron-sandbox/files.contribution'; diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts index 7cc89fe419b..f0833043d64 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -87,6 +87,8 @@ import { ITitleService } from 'vs/workbench/services/title/common/titleService'; import { TitlebarPart } from 'vs/workbench/browser/parts/titlebar/titlebarPart'; import { ITimerService, TimerService } from 'vs/workbench/services/timer/browser/timerService'; import { IDiagnosticsService, NullDiagnosticsService } from 'vs/platform/diagnostics/common/diagnostics'; +import { ILanguagePackService } from 'vs/platform/languagePacks/common/languagePacks'; +import { WebLanguagePacksService } from 'vs/platform/languagePacks/browser/languagePacks'; registerSingleton(IWorkbenchExtensionManagementService, ExtensionManagementService); registerSingleton(IAccessibilityService, AccessibilityService, true); @@ -103,6 +105,7 @@ registerSingleton(IExtensionTipsService, ExtensionTipsService); registerSingleton(ITimerService, TimerService); registerSingleton(ICustomEndpointTelemetryService, NullEndpointTelemetryService, true); registerSingleton(IDiagnosticsService, NullDiagnosticsService, true); +registerSingleton(ILanguagePackService, WebLanguagePacksService, true); //#endregion @@ -118,6 +121,9 @@ import 'vs/workbench/contrib/logs/browser/logs.contribution'; // Explorer import 'vs/workbench/contrib/files/browser/files.web.contribution'; +// Localization +import 'vs/workbench/contrib/localization/browser/localization.contribution'; + // Performance import 'vs/workbench/contrib/performance/browser/performance.web.contribution'; |