diff options
Diffstat (limited to 'src/vs/workbench')
459 files changed, 9881 insertions, 6518 deletions
diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts index 37afee52e07..2fa247a959b 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -64,6 +64,7 @@ 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 77c903cdf0c..7321fff0350 100644 --- a/src/vs/workbench/api/browser/mainThreadAuthentication.ts +++ b/src/vs/workbench/api/browser/mainThreadAuthentication.ts @@ -65,7 +65,7 @@ export class MainThreadAuthenticationProvider extends Disposable implements IAut quickPick.items = items; quickPick.selectedItems = items.filter(item => item.extension.allowed === undefined || item.extension.allowed); quickPick.title = nls.localize('manageTrustedExtensions', "Manage Trusted Extensions"); - quickPick.placeholder = nls.localize('manageExensions', "Choose which extensions can access this account"); + quickPick.placeholder = nls.localize('manageExtensions', "Choose which extensions can access this account"); quickPick.onDidAccept(() => { const updatedAllowedList = quickPick.items @@ -102,7 +102,7 @@ export class MainThreadAuthenticationProvider extends Disposable implements IAut const result = await this.dialogService.show( Severity.Info, accountUsages.length - ? nls.localize('signOutMessagve', "The account '{0}' has been used by: \n\n{1}\n\n Sign out from these extensions?", accountName, accountUsages.map(usage => usage.extensionName).join('\n')) + ? nls.localize('signOutMessage', "The account '{0}' has been used by: \n\n{1}\n\n Sign out from these extensions?", accountName, accountUsages.map(usage => usage.extensionName).join('\n')) : nls.localize('signOutMessageSimple', "Sign out of '{0}'?", accountName), [ nls.localize('signOut', "Sign Out"), diff --git a/src/vs/workbench/api/browser/mainThreadComments.ts b/src/vs/workbench/api/browser/mainThreadComments.ts index 6dbcd7fdc33..7d28c674abf 100644 --- a/src/vs/workbench/api/browser/mainThreadComments.ts +++ b/src/vs/workbench/api/browser/mainThreadComments.ts @@ -300,6 +300,7 @@ export class MainThreadCommentController { deleteCommentThread(commentThreadHandle: number) { let thread = this.getKnownThread(commentThreadHandle); this._threads.delete(commentThreadHandle); + thread.dispose(); if (thread.isDocumentCommentThread()) { this._commentService.updateComments(this._uniqueId, { @@ -314,8 +315,6 @@ export class MainThreadCommentController { changed: [] }); } - - thread.dispose(); } deleteCommentThreadMain(commentThreadId: string) { diff --git a/src/vs/workbench/api/browser/mainThreadEditorTabs.ts b/src/vs/workbench/api/browser/mainThreadEditorTabs.ts index f3677aec085..10b7782247b 100644 --- a/src/vs/workbench/api/browser/mainThreadEditorTabs.ts +++ b/src/vs/workbench/api/browser/mainThreadEditorTabs.ts @@ -21,7 +21,7 @@ import { TerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/termi import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { isEqual } from 'vs/base/common/resources'; - +import { isGroupEditorMoveEvent } from 'vs/workbench/common/editor/editorGroupModel'; interface TabInfo { tab: IEditorTabDto; @@ -386,6 +386,32 @@ export class MainThreadEditorTabs implements MainThreadEditorTabsShape { }); } + private _onDidTabMove(groupId: number, editorIndex: number, oldEditorIndex: number, editor: EditorInput) { + const tabs = this._groupLookup.get(groupId)?.tabs; + // Something wrong with the model state so we rebuild + if (!tabs) { + console.error('Invalid model for move change, rebuilding'); + this._createTabsModel(); + return; + } + + // Move tab from old index to new index + const removedTab = tabs.splice(oldEditorIndex, 1); + if (removedTab.length === 0) { + return; + } + tabs.splice(editorIndex, 0, removedTab[0]); + + // Notify exthost of move + this._proxy.$acceptTabOperation({ + kind: TabModelOperationKind.TAB_MOVE, + groupId, + tabDto: removedTab[0], + index: editorIndex, + oldIndex: oldEditorIndex + }); + } + /** * Builds the model from scratch based on the current state of the editor service. */ @@ -421,7 +447,7 @@ export class MainThreadEditorTabs implements MainThreadEditorTabsShape { } // TODOD @lramos15 Remove this after done finishing the tab model code - // private _eventToString(event: IEditorsChangeEvent): string { + // private _eventToString(event: IEditorsChangeEvent | IEditorsMoveEvent): string { // let eventString = ''; // switch (event.kind) { // case GroupModelChangeKind.GROUP_INDEX: eventString += 'GROUP_INDEX'; break; @@ -444,10 +470,12 @@ export class MainThreadEditorTabs implements MainThreadEditorTabsShape { * The main handler for the tab events * @param events The list of events to process */ - private _updateTabsModel(event: IEditorsChangeEvent): void { + private _updateTabsModel(changeEvent: IEditorsChangeEvent): void { + const event = changeEvent.event; + const groupId = changeEvent.groupId; switch (event.kind) { case GroupModelChangeKind.GROUP_ACTIVE: - if (event.groupId === this._editorGroupsService.activeGroup.id) { + if (groupId === this._editorGroupsService.activeGroup.id) { this._onDidGroupActivate(); break; } else { @@ -455,37 +483,42 @@ export class MainThreadEditorTabs implements MainThreadEditorTabsShape { } case GroupModelChangeKind.EDITOR_LABEL: if (event.editor !== undefined && event.editorIndex !== undefined) { - this._onDidTabLabelChange(event.groupId, event.editor, event.editorIndex); + this._onDidTabLabelChange(groupId, event.editor, event.editorIndex); break; } case GroupModelChangeKind.EDITOR_OPEN: if (event.editor !== undefined && event.editorIndex !== undefined) { - this._onDidTabOpen(event.groupId, event.editor, event.editorIndex); + this._onDidTabOpen(groupId, event.editor, event.editorIndex); break; } case GroupModelChangeKind.EDITOR_CLOSE: if (event.editorIndex !== undefined) { - this._onDidTabClose(event.groupId, event.editorIndex); + this._onDidTabClose(groupId, event.editorIndex); break; } case GroupModelChangeKind.EDITOR_ACTIVE: if (event.editorIndex !== undefined) { - this._onDidTabActiveChange(event.groupId, event.editorIndex); + this._onDidTabActiveChange(groupId, event.editorIndex); break; } case GroupModelChangeKind.EDITOR_DIRTY: if (event.editorIndex !== undefined && event.editor !== undefined) { - this._onDidTabDirty(event.groupId, event.editorIndex, event.editor); + this._onDidTabDirty(groupId, event.editorIndex, event.editor); break; } case GroupModelChangeKind.EDITOR_STICKY: if (event.editorIndex !== undefined && event.editor !== undefined) { - this._onDidTabPinChange(event.groupId, event.editorIndex, event.editor); + this._onDidTabPinChange(groupId, event.editorIndex, event.editor); break; } case GroupModelChangeKind.EDITOR_PIN: if (event.editorIndex !== undefined && event.editor !== undefined) { - this._onDidTabPreviewChange(event.groupId, event.editorIndex, event.editor); + this._onDidTabPreviewChange(groupId, event.editorIndex, event.editor); + break; + } + case GroupModelChangeKind.EDITOR_MOVE: + if (isGroupEditorMoveEvent(event) && event.editor && event.editorIndex !== undefined && event.oldEditorIndex !== undefined) { + this._onDidTabMove(groupId, event.editorIndex, event.oldEditorIndex, event.editor); break; } default: @@ -561,5 +594,20 @@ export class MainThreadEditorTabs implements MainThreadEditorTabsShape { // TODO @jrieken This isn't quite right how can we say true for some but not others? return results.every(result => result); } + + async $closeGroup(groupIds: number[], preserveFocus?: boolean): Promise<boolean> { + const groupCloseResults: boolean[] = []; + for (const groupId of groupIds) { + const group = this._editorGroupsService.getGroup(groupId); + if (group) { + groupCloseResults.push(await group.closeAllEditors()); + // Make sure group is empty but still there before removing it + if (group.count === 0 && this._editorGroupsService.getGroup(group.id)) { + this._editorGroupsService.removeGroup(group); + } + } + } + return groupCloseResults.every(result => result); + } //#endregion } diff --git a/src/vs/workbench/api/browser/mainThreadExtensionService.ts b/src/vs/workbench/api/browser/mainThreadExtensionService.ts index 4bc58f95e00..41309ab2bd9 100644 --- a/src/vs/workbench/api/browser/mainThreadExtensionService.ts +++ b/src/vs/workbench/api/browser/mainThreadExtensionService.ts @@ -26,6 +26,7 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { IRemoteConnectionData } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { URI, UriComponents } from 'vs/base/common/uri'; import { FileAccess } from 'vs/base/common/network'; +import { IExtensionDescriptionDelta } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; @extHostNamedCustomer(MainContext.MainThreadExtensionService) export class MainThreadExtensionService implements MainThreadExtensionServiceShape { @@ -57,6 +58,9 @@ export class MainThreadExtensionService implements MainThreadExtensionServiceSha public dispose(): void { } + $getExtension(extensionId: string) { + return this._extensionService.getExtension(extensionId); + } $activateExtension(extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise<void> { return this._internalExtensionService._activateById(extensionId, reason); } @@ -201,8 +205,8 @@ class ExtensionHostProxy implements IExtensionHostProxy { const uriComponents = await this._actual.$getCanonicalURI(remoteAuthority, uri); return (uriComponents ? URI.revive(uriComponents) : uriComponents); } - startExtensionHost(enabledExtensionIds: ExtensionIdentifier[]): Promise<void> { - return this._actual.$startExtensionHost(enabledExtensionIds); + startExtensionHost(extensionsDelta: IExtensionDescriptionDelta): Promise<void> { + return this._actual.$startExtensionHost(extensionsDelta); } extensionTestsExecute(): Promise<number> { return this._actual.$extensionTestsExecute(); @@ -222,8 +226,8 @@ class ExtensionHostProxy implements IExtensionHostProxy { updateRemoteConnectionData(connectionData: IRemoteConnectionData): Promise<void> { return this._actual.$updateRemoteConnectionData(connectionData); } - deltaExtensions(toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[]): Promise<void> { - return this._actual.$deltaExtensions(toAdd, toRemove); + deltaExtensions(extensionsDelta: IExtensionDescriptionDelta): Promise<void> { + return this._actual.$deltaExtensions(extensionsDelta); } test_latency(n: number): Promise<number> { return this._actual.$test_latency(n); diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index be97123b3c9..3b5e9139ca3 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -3,39 +3,32 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle'; -import { distinct } from 'vs/base/common/arrays'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { CancellationError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; -import { ITextModel } from 'vs/editor/common/model'; +import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { revive } from 'vs/base/common/marshalling'; +import { mixin } from 'vs/base/common/objects'; +import { URI } from 'vs/base/common/uri'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; +import { Position as EditorPosition } from 'vs/editor/common/core/position'; +import { IRange, Range as EditorRange } from 'vs/editor/common/core/range'; +import { Selection } from 'vs/editor/common/core/selection'; import * as languages from 'vs/editor/common/languages'; -import * as search from 'vs/workbench/contrib/search/common/search'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { IPosition, Position as EditorPosition } from 'vs/editor/common/core/position'; -import { Range as EditorRange, IRange } from 'vs/editor/common/core/range'; -import { ExtHostContext, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape, MainContext, ILanguageConfigurationDto, IRegExpDto, IIndentationRuleDto, IOnEnterRuleDto, ILocationDto, IWorkspaceSymbolDto, reviveWorkspaceEditDto, IDocumentFilterDto, ILocationLinkDto, ISignatureHelpProviderMetadataDto, ILinkDto, ICallHierarchyItemDto, ISuggestDataDto, ICodeActionDto, ISuggestDataDtoField, ISuggestResultDtoField, ICodeActionProviderMetadataDto, ILanguageWordDefinitionDto, IdentifiableInlineCompletions, IdentifiableInlineCompletion, ITypeHierarchyItemDto, IInlayHintDto } from '../common/extHost.protocol'; -import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; -import { LanguageConfiguration, IndentationRule, OnEnterRule } from 'vs/editor/common/languages/languageConfiguration'; import { ILanguageService } from 'vs/editor/common/languages/language'; -import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; -import { URI } from 'vs/base/common/uri'; -import { Selection } from 'vs/editor/common/core/selection'; +import { IndentationRule, LanguageConfiguration, OnEnterRule } from 'vs/editor/common/languages/languageConfiguration'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; +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 { 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 { mixin } from 'vs/base/common/objects'; -import { decodeSemanticTokensDto } from 'vs/editor/common/services/semanticTokensDto'; -import { revive } from 'vs/base/common/marshalling'; -import { CancellationError } from 'vs/base/common/errors'; -import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; -import { Mimes } from 'vs/base/common/mime'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { performSnippetEdit } from 'vs/editor/contrib/snippet/browser/snippetController2'; -import { DataTransferConverter } from 'vs/workbench/api/common/shared/dataTransfer'; -import { extractEditorsDropData } from 'vs/workbench/browser/dnd'; -import { IDataTransfer, IDataTransferItem } from 'vs/workbench/common/dnd'; -import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +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'; @extHostNamedCustomer(MainContext.MainThreadLanguageFeatures) export class MainThreadLanguageFeatures extends Disposable implements MainThreadLanguageFeaturesShape { @@ -50,8 +43,6 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread @ILanguageService private readonly _languageService: ILanguageService, @ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService, @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, - @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, - @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { super(); @@ -84,24 +75,6 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread }); updateAllWordDefinitions(); } - - if (this._codeEditorService) { - const registerDropListenerOnEditor = (editor: ICodeEditor) => { - this._dropIntoEditorListeners.get(editor)?.dispose(); - this._dropIntoEditorListeners.set(editor, editor.onDropIntoEditor(e => this.onDropIntoEditor(editor, e.position, e.event))); - }; - - this._register(this._codeEditorService.onCodeEditorAdd(registerDropListenerOnEditor)); - - this._register(this._codeEditorService.onCodeEditorRemove(editor => { - this._dropIntoEditorListeners.get(editor)?.dispose(); - this._dropIntoEditorListeners.delete(editor); - })); - - for (const editor of this._codeEditorService.listCodeEditors()) { - registerDropListenerOnEditor(editor); - } - } } override dispose(): void { @@ -514,7 +487,7 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread }; } - $registerSuggestSupport(handle: number, selector: IDocumentFilterDto[], triggerCharacters: string[], supportsResolveDetails: boolean, displayName: string): void { + $registerCompletionsProvider(handle: number, selector: IDocumentFilterDto[], triggerCharacters: string[], supportsResolveDetails: boolean, displayName: string): void { const provider: languages.CompletionItemProvider = { triggerCharacters, _debugDisplayName: displayName, @@ -899,58 +872,6 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread } })); } - - private async onDropIntoEditor(editor: ICodeEditor, position: IPosition, dragEvent: DragEvent) { - if (!dragEvent.dataTransfer || !editor.hasModel()) { - return; - } - - const model = editor.getModel(); - const modelVersionNow = model.getVersionId(); - - const textEditorDataTransfer: IDataTransfer = new Map<string, IDataTransferItem>(); - for (const item of dragEvent.dataTransfer.items) { - if (item.kind === 'string') { - const type = item.type; - const asStringValue = new Promise<string>(resolve => item.getAsString(resolve)); - textEditorDataTransfer.set(type, { - asString: () => asStringValue, - value: undefined - }); - } - } - - if (!textEditorDataTransfer.has(Mimes.uriList.toLowerCase())) { - const editorData = (await this._instantiationService.invokeFunction(extractEditorsDropData, dragEvent)) - .filter(input => input.resource) - .map(input => input.resource!.toString()); - - if (editorData.length) { - const str = distinct(editorData).join('\n'); - textEditorDataTransfer.set(Mimes.uriList.toLowerCase(), { - asString: () => Promise.resolve(str), - value: undefined - }); - } - } - - if (textEditorDataTransfer.size === 0) { - return; - } - - const ordered = this._languageFeaturesService.documentOnDropEditProvider.ordered(model); - for (const provider of ordered) { - const edit = await provider.provideDocumentOnDropEdits(model, position, textEditorDataTransfer, CancellationToken.None); - if (editor.getModel().getVersionId() !== modelVersionNow) { - return; - } - - if (edit) { - performSnippetEdit(editor, edit); - return; - } - } - } } export class MainThreadDocumentSemanticTokensProvider implements languages.DocumentSemanticTokensProvider { diff --git a/src/vs/workbench/api/browser/mainThreadLanguages.ts b/src/vs/workbench/api/browser/mainThreadLanguages.ts index 71c8c4ab1c2..e8316e42dfb 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguages.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguages.ts @@ -68,8 +68,8 @@ export class MainThreadLanguages implements MainThreadLanguagesShape { if (!model) { return undefined; } - model.tokenizeIfCheap(position.lineNumber); - const tokens = model.getLineTokens(position.lineNumber); + model.tokenization.tokenizeIfCheap(position.lineNumber); + const tokens = model.tokenization.getLineTokens(position.lineNumber); const idx = tokens.findTokenIndexAtOffset(position.column - 1); return { type: tokens.getStandardTokenType(idx), diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index b22dce389de..6b522da7c97 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -200,14 +200,14 @@ CommandsRegistry.registerCommand('_executeDataToNotebook', async (accessor, ...a } const dto = await info.serializer.dataToNotebook(bytes); - return NotebookDto.toNotebookDataDto(dto); + return new SerializableObjectWithBuffers(NotebookDto.toNotebookDataDto(dto)); }); CommandsRegistry.registerCommand('_executeNotebookToData', async (accessor, ...args) => { const [notebookType, dto] = args; assertType(typeof notebookType === 'string', 'string'); - assertType(typeof dto === 'object', 'NotebookDataDto'); + assertType(typeof dto === 'object'); const notebookService = accessor.get(INotebookService); const info = await notebookService.withNotebookDataProvider(notebookType); @@ -215,7 +215,7 @@ CommandsRegistry.registerCommand('_executeNotebookToData', async (accessor, ...a return; } - const data = NotebookDto.fromNotebookDataDto(dto); + const data = NotebookDto.fromNotebookDataDto(dto.value); const bytes = await info.serializer.notebookToData(data); return bytes; }); diff --git a/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts b/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts index 46febd36991..a8809c58016 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts @@ -15,11 +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 { INotebookKernel, INotebookKernelChangeEvent, INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { IResolvedNotebookKernel, INotebookKernelChangeEvent, INotebookKernelService, NotebookKernelType } 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'; -abstract class MainThreadKernel implements INotebookKernel { +abstract class MainThreadKernel implements IResolvedNotebookKernel { + readonly type: NotebookKernelType.Resolved = NotebookKernelType.Resolved; private readonly _onDidChange = new Emitter<INotebookKernelChangeEvent>(); private readonly preloads: { uri: URI; provides: string[] }[]; diff --git a/src/vs/workbench/api/browser/mainThreadNotebookProxyKernels.ts b/src/vs/workbench/api/browser/mainThreadNotebookProxyKernels.ts new file mode 100644 index 00000000000..74e7290161d --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadNotebookProxyKernels.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 { 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 2c82719f4e8..34859bb4235 100644 --- a/src/vs/workbench/api/browser/mainThreadOutputService.ts +++ b/src/vs/workbench/api/browser/mainThreadOutputService.ts @@ -4,8 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Registry } from 'vs/platform/registry/common/platform'; -import { IOutputService, IOutputChannel, OUTPUT_VIEW_ID, OutputChannelUpdateMode } from 'vs/workbench/contrib/output/common/output'; -import { Extensions, IOutputChannelRegistry } from 'vs/workbench/services/output/common/output'; +import { Extensions, IOutputChannelRegistry, IOutputService, IOutputChannel, OUTPUT_VIEW_ID, OutputChannelUpdateMode } from 'vs/workbench/services/output/common/output'; import { MainThreadOutputServiceShape, MainContext, ExtHostOutputServiceShape, ExtHostContext } from '../common/extHost.protocol'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { UriComponents, URI } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/api/browser/mainThreadSecretState.ts b/src/vs/workbench/api/browser/mainThreadSecretState.ts index 1a131d41518..acd6e3db0c1 100644 --- a/src/vs/workbench/api/browser/mainThreadSecretState.ts +++ b/src/vs/workbench/api/browser/mainThreadSecretState.ts @@ -8,6 +8,7 @@ import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/ext import { ICredentialsService } from 'vs/platform/credentials/common/credentials'; import { IEncryptionService } from 'vs/workbench/services/encryption/common/encryptionService'; import { ExtHostContext, ExtHostSecretStateShape, MainContext, MainThreadSecretStateShape } from '../common/extHost.protocol'; +import { ILogService } from 'vs/platform/log/common/log'; @extHostNamedCustomer(MainContext.MainThreadSecretState) export class MainThreadSecretState extends Disposable implements MainThreadSecretStateShape { @@ -19,6 +20,7 @@ export class MainThreadSecretState extends Disposable implements MainThreadSecre extHostContext: IExtHostContext, @ICredentialsService private readonly credentialsService: ICredentialsService, @IEncryptionService private readonly encryptionService: IEncryptionService, + @ILogService private readonly logService: ILogService, ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostSecretState); @@ -36,7 +38,26 @@ export class MainThreadSecretState extends Disposable implements MainThreadSecre async $getPassword(extensionId: string, key: string): Promise<string | undefined> { const fullKey = await this.getFullKey(extensionId); const password = await this.credentialsService.getPassword(fullKey, key); - const decrypted = password && await this.encryptionService.decrypt(password); + if (!password) { + return undefined; + } + + let decrypted: string | null; + try { + decrypted = await this.encryptionService.decrypt(password); + } catch (e) { + this.logService.error(e); + + // If we are on a platform that newly started encrypting secrets before storing them, + // then passwords previously stored were stored un-encrypted (NOTE: but still being stored in a secure keyring). + // When we try to decrypt a password that wasn't encrypted previously, the encryption service will throw. + // To recover gracefully, we first try to encrypt & store the password (essentially migrating the secret to the new format) + // and then we try to read it and decrypt again. + const encryptedForSet = await this.encryptionService.encrypt(password); + await this.credentialsService.setPassword(fullKey, key, encryptedForSet); + const passwordEncrypted = await this.credentialsService.getPassword(fullKey, key); + decrypted = passwordEncrypted && await this.encryptionService.decrypt(passwordEncrypted); + } if (decrypted) { try { @@ -44,7 +65,8 @@ export class MainThreadSecretState extends Disposable implements MainThreadSecre if (value.extensionId === extensionId) { return value.content; } - } catch (_) { + } catch (e) { + this.logService.error(e); throw new Error('Cannot get password'); } } @@ -59,7 +81,7 @@ export class MainThreadSecretState extends Disposable implements MainThreadSecre content: value }); const encrypted = await this.encryptionService.encrypt(toEncrypt); - return this.credentialsService.setPassword(fullKey, key, encrypted); + return await this.credentialsService.setPassword(fullKey, key, encrypted); } async $deletePassword(extensionId: string, key: string): Promise<void> { diff --git a/src/vs/workbench/api/browser/mainThreadTask.ts b/src/vs/workbench/api/browser/mainThreadTask.ts index 60575f37b1f..9cd5c8235f9 100644 --- a/src/vs/workbench/api/browser/mainThreadTask.ts +++ b/src/vs/workbench/api/browser/mainThreadTask.ts @@ -29,7 +29,7 @@ import { ExtHostContext, MainThreadTaskShape, ExtHostTaskShape, MainContext } fr import { TaskDefinitionDTO, TaskExecutionDTO, ProcessExecutionOptionsDTO, TaskPresentationOptionsDTO, ProcessExecutionDTO, ShellExecutionDTO, ShellExecutionOptionsDTO, CustomExecutionDTO, TaskDTO, TaskSourceDTO, TaskHandleDTO, TaskFilterDTO, TaskProcessStartedDTO, TaskProcessEndedDTO, TaskSystemInfoDTO, - RunOptionsDTO + RunOptionsDTO, TaskGroupDTO } from 'vs/workbench/api/common/shared/tasks'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; @@ -320,7 +320,7 @@ namespace TaskDTO { hasDefinedMatchers: ContributedTask.is(task) ? task.hasDefinedMatchers : false, runOptions: RunOptionsDTO.from(task.runOptions), }; - result.group = TaskGroup.from(task.configurationProperties.group); + result.group = TaskGroupDTO.from(task.configurationProperties.group); if (task.configurationProperties.detail) { result.detail = task.configurationProperties.detail; @@ -389,6 +389,18 @@ namespace TaskDTO { } } +namespace TaskGroupDTO { + export function from(value: string | TaskGroup | undefined): TaskGroupDTO | undefined { + if (value === undefined) { + return undefined; + } + return { + _id: (typeof value === 'string') ? value : value._id, + isDefault: (typeof value === 'string') ? false : ((typeof value.isDefault === 'string') ? false : value.isDefault) + }; + } +} + namespace TaskFilterDTO { export function from(value: TaskFilter): TaskFilterDTO { return value; diff --git a/src/vs/workbench/api/browser/mainThreadTelemetry.ts b/src/vs/workbench/api/browser/mainThreadTelemetry.ts index 58a919fc1e6..ac89aca9280 100644 --- a/src/vs/workbench/api/browser/mainThreadTelemetry.ts +++ b/src/vs/workbench/api/browser/mainThreadTelemetry.ts @@ -3,15 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ITelemetryService, TelemetryLevel, TELEMETRY_OLD_SETTING_ID, TELEMETRY_SETTING_ID } from 'vs/platform/telemetry/common/telemetry'; -import { MainThreadTelemetryShape, MainContext, ExtHostTelemetryShape, ExtHostContext } from '../common/extHost.protocol'; -import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; -import { ClassifiedEvent, StrictPropertyCheck, GDPRClassification } from 'vs/platform/telemetry/common/gdprTypings'; import { Disposable } from 'vs/base/common/lifecycle'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IProductService } from 'vs/platform/product/common/productService'; -import { getTelemetryLevel, supportsTelemetry } from 'vs/platform/telemetry/common/telemetryUtils'; +import { ClassifiedEvent, GDPRClassification, StrictPropertyCheck } from 'vs/platform/telemetry/common/gdprTypings'; +import { ITelemetryService, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; +import { supportsTelemetry } from 'vs/platform/telemetry/common/telemetryUtils'; +import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; +import { ExtHostContext, ExtHostTelemetryShape, MainContext, MainThreadTelemetryShape } from '../common/extHost.protocol'; @extHostNamedCustomer(MainContext.MainThreadTelemetry) export class MainThreadTelemetry extends Disposable implements MainThreadTelemetryShape { @@ -22,7 +21,6 @@ export class MainThreadTelemetry extends Disposable implements MainThreadTelemet constructor( extHostContext: IExtHostContext, @ITelemetryService private readonly _telemetryService: ITelemetryService, - @IConfigurationService private readonly _configurationService: IConfigurationService, @IEnvironmentService private readonly _environmentService: IEnvironmentService, @IProductService private readonly _productService: IProductService ) { @@ -31,10 +29,8 @@ export class MainThreadTelemetry extends Disposable implements MainThreadTelemet this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTelemetry); if (supportsTelemetry(this._productService, this._environmentService)) { - this._register(this._configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(TELEMETRY_SETTING_ID) || e.affectsConfiguration(TELEMETRY_OLD_SETTING_ID)) { - this._proxy.$onDidChangeTelemetryLevel(this.telemetryLevel); - } + this._register(_telemetryService.telemetryLevel.onDidChange(level => { + this._proxy.$onDidChangeTelemetryLevel(level); })); } @@ -46,7 +42,7 @@ export class MainThreadTelemetry extends Disposable implements MainThreadTelemet return TelemetryLevel.NONE; } - return getTelemetryLevel(this._configurationService); + return this._telemetryService.telemetryLevel.value; } $publicLog(eventName: string, data: any = Object.create(null)): void { @@ -55,7 +51,7 @@ export class MainThreadTelemetry extends Disposable implements MainThreadTelemet this._telemetryService.publicLog(eventName, data); } - $publicLog2<E extends ClassifiedEvent<T> = never, T extends GDPRClassification<T> = never>(eventName: string, data: StrictPropertyCheck<T, E>): void { + $publicLog2<E extends ClassifiedEvent<T> = never, T extends GDPRClassification<T> = never>(eventName: string, data?: StrictPropertyCheck<T, E>): void { this.$publicLog(eventName, data as any); } } diff --git a/src/vs/workbench/api/browser/mainThreadTimeline.ts b/src/vs/workbench/api/browser/mainThreadTimeline.ts index 6636909bbdf..d6547f29752 100644 --- a/src/vs/workbench/api/browser/mainThreadTimeline.ts +++ b/src/vs/workbench/api/browser/mainThreadTimeline.ts @@ -9,7 +9,7 @@ import { URI } from 'vs/base/common/uri'; import { ILogService } from 'vs/platform/log/common/log'; import { MainContext, MainThreadTimelineShape, ExtHostTimelineShape, ExtHostContext } from 'vs/workbench/api/common/extHost.protocol'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; -import { TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor, ITimelineService, InternalTimelineOptions, Timeline } from 'vs/workbench/contrib/timeline/common/timeline'; +import { TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor, ITimelineService, Timeline } from 'vs/workbench/contrib/timeline/common/timeline'; import { revive } from 'vs/base/common/marshalling'; @extHostNamedCustomer(MainContext.MainThreadTimeline) @@ -40,8 +40,8 @@ export class MainThreadTimeline implements MainThreadTimelineShape { this._timelineService.registerTimelineProvider({ ...provider, onDidChange: onDidChange.event, - async provideTimeline(uri: URI, options: TimelineOptions, token: CancellationToken, internalOptions?: InternalTimelineOptions) { - return revive<Timeline>(await proxy.$getTimeline(provider.id, uri, options, token, internalOptions)); + async provideTimeline(uri: URI, options: TimelineOptions, token: CancellationToken) { + return revive<Timeline>(await proxy.$getTimeline(provider.id, uri, options, token)); }, dispose() { emitters.delete(provider.id); diff --git a/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts b/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts index 3ca501b079b..6659298ef14 100644 --- a/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts +++ b/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts @@ -174,8 +174,10 @@ export class MainThreadWebviewPanels extends Disposable implements extHostProtoc } as const; type Classification = { - extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'mjbvz'; comment: 'Id of the extension that created the webview panel' }; - viewType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'mjbvz'; comment: 'Id of the webview' }; + extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Id of the extension that created the webview panel' }; + viewType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Id of the webview' }; + owner: 'mjbvz'; + comment: 'Triggered when a webview is created. Records the type of webview and the extension which created it'; }; this._telemetryService.publicLog2<typeof payload, Classification>('webviews:createWebviewPanel', payload); @@ -275,6 +277,7 @@ export class MainThreadWebviewPanels extends Disposable implements extHostProtoc state, panelOptions: webviewInput.webview.options, webviewOptions: webviewInput.webview.contentOptions, + active: webviewInput === this._editorService.activeEditor, }, editorGroupToColumn(this._editorGroupService, webviewInput.group || 0)); } catch (error) { onUnexpectedError(error); diff --git a/src/vs/workbench/api/browser/mainThreadWebviews.ts b/src/vs/workbench/api/browser/mainThreadWebviews.ts index 15e71145210..ef088e5c054 100644 --- a/src/vs/workbench/api/browser/mainThreadWebviews.ts +++ b/src/vs/workbench/api/browser/mainThreadWebviews.ts @@ -65,8 +65,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma public async $postMessage(handle: extHostProtocol.WebviewHandle, jsonMessage: string, ...buffers: VSBuffer[]): Promise<boolean> { const webview = this.getWebview(handle); const { message, arrayBuffers } = deserializeWebviewMessage(jsonMessage, buffers); - webview.postMessage(message, arrayBuffers); - return true; + return webview.postMessage(message, arrayBuffers); } private hookupWebviewEventDelegate(handle: extHostProtocol.WebviewHandle, webview: IOverlayWebview, options: { serializeBuffersForPostMessage: boolean }) { diff --git a/src/vs/workbench/api/browser/viewsExtensionPoint.ts b/src/vs/workbench/api/browser/viewsExtensionPoint.ts index f839ed68780..489f34bb0fd 100644 --- a/src/vs/workbench/api/browser/viewsExtensionPoint.ts +++ b/src/vs/workbench/api/browser/viewsExtensionPoint.ts @@ -510,7 +510,7 @@ class ViewsExtensionHandler implements IWorkbenchContribution { containerTitle: item.contextualTitle || viewContainer?.title, canToggleVisibility: true, canMoveView: viewContainer?.id !== REMOTE, - treeView: type === ViewType.Tree ? this.instantiationService.createInstance(CustomTreeView, item.id, item.name) : undefined, + treeView: type === ViewType.Tree ? this.instantiationService.createInstance(CustomTreeView, item.id, item.name, extension.description.identifier.value) : undefined, collapsed: this.showCollapsed(container) || initialVisibility === InitialVisibility.Collapsed, order: order, extensionId: extension.description.identifier, diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index e59da75b06d..864b227de8e 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -70,7 +70,6 @@ import { IExtHostTunnelService } from 'vs/workbench/api/common/extHostTunnelServ import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService'; import { ExtHostAuthentication } from 'vs/workbench/api/common/extHostAuthentication'; import { ExtHostTimeline } from 'vs/workbench/api/common/extHostTimeline'; -import { ExtHostNotebookConcatDocument } from 'vs/workbench/api/common/extHostNotebookConcatDocument'; import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths'; import { IExtHostConsumerFileSystem } from 'vs/workbench/api/common/extHostFileSystemConsumer'; import { ExtHostWebviewViews } from 'vs/workbench/api/common/extHostWebviewView'; @@ -92,11 +91,17 @@ import { ExtHostNotebookEditors } from 'vs/workbench/api/common/extHostNotebookE import { ExtHostNotebookDocuments } from 'vs/workbench/api/common/extHostNotebookDocuments'; import { ExtHostInteractive } from 'vs/workbench/api/common/extHostInteractive'; import { combinedDisposable } from 'vs/base/common/lifecycle'; -import { checkProposedApiEnabled, isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; +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'; + +export interface IExtensionRegistries { + mine: ExtensionDescriptionRegistry; + all: ExtensionDescriptionRegistry; +} export interface IExtensionApiFactory { - (extension: IExtensionDescription, registry: ExtensionDescriptionRegistry, configProvider: ExtHostConfigProvider): typeof vscode; + (extension: IExtensionDescription, extensionInfo: IExtensionRegistries, configProvider: ExtHostConfigProvider): typeof vscode; } /** @@ -156,6 +161,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostNotebookDocuments = rpcProtocol.set(ExtHostContext.ExtHostNotebookDocuments, new ExtHostNotebookDocuments(extHostNotebook)); const extHostNotebookEditors = rpcProtocol.set(ExtHostContext.ExtHostNotebookEditors, new ExtHostNotebookEditors(extHostLogService, rpcProtocol, 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)); @@ -195,7 +201,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I // Register API-ish commands ExtHostApiCommands.register(extHostCommands); - return function (extension: IExtensionDescription, extensionRegistry: ExtensionDescriptionRegistry, configProvider: ExtHostConfigProvider): typeof vscode { + return function (extension: IExtensionDescription, extensionInfo: IExtensionRegistries, configProvider: ExtHostConfigProvider): typeof vscode { // Check document selectors for being overly generic. Technically this isn't a problem but // in practice many extensions say they support `fooLang` but need fs-access to do so. Those @@ -359,10 +365,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I Object.freeze(env); } - const extensionKind = initData.remote.isRemote - ? extHostTypes.ExtensionKind.Workspace - : extHostTypes.ExtensionKind.UI; - + // namespace: tests const tests: typeof vscode.tests = { createTestController(provider, label, refreshHandler?: (token: vscode.CancellationToken) => Thenable<void> | void) { return extHostTesting.createTestController(provider, label, refreshHandler); @@ -386,19 +389,49 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I }; // namespace: extensions + const extensionKind = initData.remote.isRemote + ? extHostTypes.ExtensionKind.Workspace + : extHostTypes.ExtensionKind.UI; + const extensions: typeof vscode.extensions = { - getExtension(extensionId: string): vscode.Extension<any> | undefined { - const desc = extensionRegistry.getExtensionDescription(extensionId); - if (desc) { - return new Extension(extensionService, extension.identifier, desc, extensionKind); + getExtension(extensionId: string, includeFromDifferentExtensionHosts?: boolean): vscode.Extension<any> | undefined { + if (!isProposedApiEnabled(extension, 'extensionsAny')) { + includeFromDifferentExtensionHosts = false; + } + const mine = extensionInfo.mine.getExtensionDescription(extensionId); + if (mine) { + return new Extension(extensionService, extension.identifier, mine, extensionKind, false); + } + if (includeFromDifferentExtensionHosts) { + const foreign = extensionInfo.all.getExtensionDescription(extensionId); + if (foreign) { + return new Extension(extensionService, extension.identifier, foreign, extensionKind /* TODO@alexdima THIS IS WRONG */, true); + } } return undefined; }, get all(): vscode.Extension<any>[] { - return extensionRegistry.getAllExtensionDescriptions().map((desc) => new Extension(extensionService, extension.identifier, desc, extensionKind)); + const result: vscode.Extension<any>[] = []; + for (const desc of extensionInfo.mine.getAllExtensionDescriptions()) { + result.push(new Extension(extensionService, extension.identifier, desc, extensionKind, false)); + } + return result; + }, + get allAcrossExtensionHosts(): vscode.Extension<any>[] { + checkProposedApiEnabled(extension, 'extensionsAny'); + const local = new ExtensionIdentifierSet(extensionInfo.mine.getAllExtensionDescriptions().map(desc => desc.identifier)); + const result: vscode.Extension<any>[] = []; + for (const desc of extensionInfo.all.getAllExtensionDescriptions()) { + const isFromDifferentExtensionHost = !local.has(desc.identifier); + result.push(new Extension(extensionService, extension.identifier, desc, extensionKind /* TODO@alexdima THIS IS WRONG */, isFromDifferentExtensionHost)); + } + return result; }, get onDidChange() { - return extensionRegistry.onDidChange; + if (isProposedApiEnabled(extension, 'extensionsAny')) { + return Event.any(extensionInfo.mine.onDidChange, extensionInfo.all.onDidChange); + } + return extensionInfo.mine.onDidChange; } }; @@ -625,7 +658,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I showInputBox(options?: vscode.InputBoxOptions, token?: vscode.CancellationToken) { if (options?.validateInput2) { checkProposedApiEnabled(extension, 'inputBoxSeverity'); - options.validateInput = options.validateInput2 as any; } return extHostQuickOpen.showInput(options, token); }, @@ -673,7 +705,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'editorInsets'); return extHostEditorInsets.createWebviewEditorInset(editor, line, height, options, extension); }, - createTerminal(nameOrOptions?: vscode.TerminalOptions | vscode.ExtensionTerminalOptions | string, shellPath?: string, shellArgs?: string[] | string): vscode.Terminal { + createTerminal(nameOrOptions?: vscode.TerminalOptions | vscode.ExtensionTerminalOptions | string, shellPath?: string, shellArgs?: readonly string[] | string): vscode.Terminal { if (typeof nameOrOptions === 'object') { if ('pty' in nameOrOptions) { return extHostTerminalService.createExtensionTerminal(nameOrOptions); @@ -758,7 +790,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostUriOpeners.registerExternalUriOpener(extension.identifier, id, opener, metadata); }, get tabGroups(): vscode.TabGroups { - checkProposedApiEnabled(extension, 'tabs'); return extHostEditorTabs.tabGroups; }, getInlineCompletionItemController<T extends vscode.InlineCompletionItem>(provider: vscode.InlineCompletionItemProvider<T>): vscode.InlineCompletionController<T> { @@ -892,11 +923,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostNotebook.getNotebookDocument(uri).apiNotebook; }, onDidSaveNotebookDocument(listener, thisArg, disposables) { - checkProposedApiEnabled(extension, 'notebookDocumentEvents'); return extHostNotebookDocuments.onDidSaveNotebookDocument(listener, thisArg, disposables); }, onDidChangeNotebookDocument(listener, thisArg, disposables) { - checkProposedApiEnabled(extension, 'notebookDocumentEvents'); return extHostNotebookDocuments.onDidChangeNotebookDocument(listener, thisArg, disposables); }, get onDidOpenNotebookDocument(): Event<vscode.NotebookDocument> { @@ -1129,10 +1158,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'notebookCellExecutionState'); return extHostNotebookKernels.onDidChangeNotebookCellExecutionState(listener, thisArgs, disposables); }, - createConcatTextDocument(notebook, selector) { - checkProposedApiEnabled(extension, 'notebookConcatTextDocument'); - return new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook, selector); - }, + 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); + } }; return <typeof vscode>{ @@ -1314,13 +1343,13 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I LanguageStatusSeverity: extHostTypes.LanguageStatusSeverity, QuickPickItemKind: extHostTypes.QuickPickItemKind, InputBoxValidationSeverity: extHostTypes.InputBoxValidationSeverity, - TabKindText: extHostTypes.TextTabInput, - TabKindTextDiff: extHostTypes.TextDiffTabInput, - TabKindCustom: extHostTypes.CustomEditorTabInput, - TabKindNotebook: extHostTypes.NotebookEditorTabInput, - TabKindNotebookDiff: extHostTypes.NotebookDiffEditorTabInput, - TabKindWebview: extHostTypes.WebviewEditorTabInput, - TabKindTerminal: extHostTypes.TerminalEditorTabInput + TabInputText: extHostTypes.TextTabInput, + TabInputTextDiff: extHostTypes.TextDiffTabInput, + TabInputCustom: extHostTypes.CustomEditorTabInput, + TabInputNotebook: extHostTypes.NotebookEditorTabInput, + TabInputNotebookDiff: extHostTypes.NotebookDiffEditorTabInput, + TabInputWebview: extHostTypes.WebviewEditorTabInput, + TabInputTerminal: extHostTypes.TerminalEditorTabInput }; }; } diff --git a/src/vs/workbench/api/common/extHost.common.services.ts b/src/vs/workbench/api/common/extHost.common.services.ts index e8ec83f8d66..9ffb9e748b6 100644 --- a/src/vs/workbench/api/common/extHost.common.services.ts +++ b/src/vs/workbench/api/common/extHost.common.services.ts @@ -26,6 +26,7 @@ import { ExtHostEditorTabs, IExtHostEditorTabs } from 'vs/workbench/api/common/e import { ExtHostLoggerService } from 'vs/workbench/api/common/extHostLoggerService'; import { ILoggerService, ILogService } from 'vs/platform/log/common/log'; import { ExtHostLogService } from 'vs/workbench/api/common/extHostLogService'; +import { ExtHostVariableResolverProviderService, IExtHostVariableResolverProvider } from 'vs/workbench/api/common/extHostVariableResolverService'; registerSingleton(ILoggerService, ExtHostLoggerService); registerSingleton(ILogService, ExtHostLogService); @@ -48,3 +49,4 @@ registerSingleton(IExtHostWorkspace, ExtHostWorkspace); registerSingleton(IExtHostSecretState, ExtHostSecretState); registerSingleton(IExtHostTelemetry, ExtHostTelemetry); registerSingleton(IExtHostEditorTabs, ExtHostEditorTabs); +registerSingleton(IExtHostVariableResolverProvider, ExtHostVariableResolverProviderService); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index c33567f8d99..4787ccb401b 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -53,16 +53,16 @@ import * as notebookCommon from 'vs/workbench/contrib/notebook/common/notebookCo import { CellExecutionUpdateType } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; import { ICellExecutionComplete, ICellExecutionStateUpdate } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; -import { OutputChannelUpdateMode } from 'vs/workbench/contrib/output/common/output'; +import { OutputChannelUpdateMode } from 'vs/workbench/services/output/common/output'; import { InputValidationType } from 'vs/workbench/contrib/scm/common/scm'; import { IWorkspaceSymbol } from 'vs/workbench/contrib/search/common/search'; import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { CoverageDetails, ExtensionRunTestsRequest, IFileCoverage, ISerializedTestResults, ITestItem, ITestMessage, ITestRunProfile, ITestRunTask, ResolvedTestRunRequest, RunTestForControllerRequest, TestResultState, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testTypes'; -import { InternalTimelineOptions, Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor } from 'vs/workbench/contrib/timeline/common/timeline'; +import { Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor } from 'vs/workbench/contrib/timeline/common/timeline'; import { TypeHierarchyItem } from 'vs/workbench/contrib/typeHierarchy/common/typeHierarchy'; import { AuthenticationProviderInformation, AuthenticationSession, AuthenticationSessionsChangeEvent } from 'vs/workbench/services/authentication/common/authentication'; import { EditorGroupColumn } from 'vs/workbench/services/editor/common/editorGroupColumn'; -import { IStaticWorkspaceData } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; +import { IExtensionDescriptionDelta, IStaticWorkspaceData } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; import { IResolveAuthorityResult } from 'vs/workbench/services/extensions/common/extensionHostProxy'; import { ActivationKind, ExtensionActivationReason, MissingExtensionDependency } from 'vs/workbench/services/extensions/common/extensions'; import { createProxyIdentifier, Dto, IRPCProtocol, SerializableObjectWithBuffers } from 'vs/workbench/services/extensions/common/proxyIdentifier'; @@ -257,7 +257,7 @@ export interface MainThreadTextEditorsShape extends IDisposable { } export interface MainThreadTreeViewsShape extends IDisposable { - $registerTreeViewDataProvider(treeViewId: string, options: { showCollapseAll: boolean; canSelectMany: boolean; dropMimeTypes: string[]; dragMimeTypes: string[]; hasHandleDrag: boolean; hasHandleDrop: boolean }): Promise<void>; + $registerTreeViewDataProvider(treeViewId: string, options: { showCollapseAll: boolean; canSelectMany: boolean; dropMimeTypes: readonly string[]; dragMimeTypes: readonly string[]; hasHandleDrag: boolean; hasHandleDrop: boolean }): Promise<void>; $refresh(treeViewId: string, itemsToRefresh?: { [treeItemHandle: string]: ITreeItem }): Promise<void>; $reveal(treeViewId: string, itemInfo: { item: ITreeItem; parentChain: ITreeItem[] } | undefined, options: IRevealOptions): Promise<void>; $setMessage(treeViewId: string, message: string): void; @@ -378,7 +378,7 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $registerDocumentSemanticTokensProvider(handle: number, selector: IDocumentFilterDto[], legend: languages.SemanticTokensLegend, eventHandle: number | undefined): void; $emitDocumentSemanticTokensEvent(eventHandle: number): void; $registerDocumentRangeSemanticTokensProvider(handle: number, selector: IDocumentFilterDto[], legend: languages.SemanticTokensLegend): void; - $registerSuggestSupport(handle: number, selector: IDocumentFilterDto[], triggerCharacters: string[], supportsResolveDetails: boolean, displayName: string): void; + $registerCompletionsProvider(handle: number, selector: IDocumentFilterDto[], triggerCharacters: string[], supportsResolveDetails: boolean, displayName: string): void; $registerInlineCompletionsSupport(handle: number, selector: IDocumentFilterDto[]): void; $registerSignatureHelpProvider(handle: number, selector: IDocumentFilterDto[], metadata: ISignatureHelpProviderMetadataDto): void; $registerInlayHintsProvider(handle: number, selector: IDocumentFilterDto[], supportsResolve: boolean, eventHandle: number | undefined, displayName: string | undefined): void; @@ -625,7 +625,8 @@ export const enum TabInputKind { export const enum TabModelOperationKind { TAB_OPEN, TAB_CLOSE, - TAB_UPDATE + TAB_UPDATE, + TAB_MOVE } export interface UnknownInputDto { @@ -677,6 +678,7 @@ export interface MainThreadEditorTabsShape extends IDisposable { // manage tabs: move, close, rearrange etc $moveTab(tabId: string, index: number, viewColumn: EditorGroupColumn, preserveFocus?: boolean): void; $closeTab(tabIds: string[], preserveFocus?: boolean): Promise<boolean>; + $closeGroup(groupIds: number[], preservceFocus?: boolean): Promise<boolean>; } export interface IEditorTabGroupDto { @@ -689,11 +691,12 @@ export interface IEditorTabGroupDto { } export interface TabOperation { - readonly kind: TabModelOperationKind.TAB_OPEN | TabModelOperationKind.TAB_CLOSE | TabModelOperationKind.TAB_UPDATE; + readonly kind: TabModelOperationKind.TAB_OPEN | TabModelOperationKind.TAB_CLOSE | TabModelOperationKind.TAB_UPDATE | TabModelOperationKind.TAB_MOVE; // TODO @lramos15 Possibly get rid of index for tab update, it's only needed for open and close readonly index: number; readonly tabDto: IEditorTabDto; readonly groupId: number; + readonly oldIndex?: number; } export interface IEditorTabDto { @@ -865,6 +868,7 @@ export interface ExtHostWebviewPanelsShape { state: any; webviewOptions: IWebviewContentOptions; panelOptions: IWebviewPanelOptions; + active: boolean; }, position: EditorGroupColumn, ): Promise<void>; @@ -978,6 +982,17 @@ export interface INotebookKernelDto2 { preloads?: { uri: UriComponents; provides: string[] }[]; } +export interface INotebookProxyKernelDto { + id: string; + notebookType: string; + extensionId: ExtensionIdentifier; + extensionLocation: UriComponents; + label: string; + detail?: string; + description?: string; + kind?: string; +} + export interface ICellExecuteOutputEditDto { editType: CellExecutionUpdateType.Output; append?: boolean; @@ -1011,6 +1026,12 @@ 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>; } @@ -1105,6 +1126,7 @@ export interface MainThreadTaskShape extends IDisposable { } export interface MainThreadExtensionServiceShape extends IDisposable { + $getExtension(extensionId: string): Promise<Dto<IExtensionDescription> | undefined>; $activateExtension(extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise<void>; $onWillActivateExtension(extensionId: ExtensionIdentifier): Promise<void>; $onDidActivateExtension(extensionId: ExtensionIdentifier, codeLoadingTime: number, activateCallTime: number, activateResolvedTime: number, activationReason: ExtensionActivationReason): void; @@ -1412,7 +1434,7 @@ export interface ExtHostExtensionServiceShape { * Returns `null` if no resolver for `remoteAuthority` is found. */ $getCanonicalURI(remoteAuthority: string, uri: UriComponents): Promise<UriComponents | null>; - $startExtensionHost(enabledExtensionIds: ExtensionIdentifier[]): Promise<void>; + $startExtensionHost(extensionsDelta: IExtensionDescriptionDelta): Promise<void>; $extensionTestsExecute(): Promise<number>; $extensionTestsExit(code: number): Promise<void>; $activateByEvent(activationEvent: string, activationKind: ActivationKind): Promise<void>; @@ -1420,7 +1442,7 @@ export interface ExtHostExtensionServiceShape { $setRemoteEnvironment(env: { [key: string]: string | null }): Promise<void>; $updateRemoteConnectionData(connectionData: IRemoteConnectionData): Promise<void>; - $deltaExtensions(toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[]): Promise<void>; + $deltaExtensions(extensionsDelta: IExtensionDescriptionDelta): Promise<void>; $test_latency(n: number): Promise<number>; $test_up(b: VSBuffer): Promise<number>; @@ -2098,6 +2120,10 @@ 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; @@ -2133,7 +2159,7 @@ export interface ExtHostTunnelServiceShape { } export interface ExtHostTimelineShape { - $getTimeline(source: string, uri: UriComponents, options: TimelineOptions, token: CancellationToken, internalOptions?: InternalTimelineOptions): Promise<Dto<Timeline> | undefined>; + $getTimeline(source: string, uri: UriComponents, options: TimelineOptions, token: CancellationToken): Promise<Dto<Timeline> | undefined>; } export const enum ExtHostTestingResource { @@ -2274,6 +2300,7 @@ 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'), @@ -2326,6 +2353,7 @@ 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/extHostApiDeprecationService.ts b/src/vs/workbench/api/common/extHostApiDeprecationService.ts index a407b241c33..59841770307 100644 --- a/src/vs/workbench/api/common/extHostApiDeprecationService.ts +++ b/src/vs/workbench/api/common/extHostApiDeprecationService.ts @@ -47,8 +47,10 @@ export class ExtHostApiDeprecationService implements IExtHostApiDeprecationServi apiId: string; }; type DeprecationTelemetryMeta = { - extensionId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; owner: 'mjbvz'; comment: 'The id of the extension that is using the deprecated API' }; - apiId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; owner: 'mjbvz'; comment: 'The id of the deprecated API' }; + extensionId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The id of the extension that is using the deprecated API' }; + apiId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The id of the deprecated API' }; + owner: 'mjbvz'; + comment: 'Helps us gain insights on extensions using deprecated API so we can assist in migration to new API'; }; this._telemetryShape.$publicLog2<DeprecationTelemetry, DeprecationTelemetryMeta>('extHostDeprecatedApiUsage', { extensionId: extension.identifier.value, diff --git a/src/vs/workbench/api/common/extHostCommands.ts b/src/vs/workbench/api/common/extHostCommands.ts index c8fb9cf0ed6..d0ae8f53ff4 100644 --- a/src/vs/workbench/api/common/extHostCommands.ts +++ b/src/vs/workbench/api/common/extHostCommands.ts @@ -8,7 +8,7 @@ import { ICommandHandlerDescription } from 'vs/platform/commands/common/commands import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; import * as extHostTypeConverter from 'vs/workbench/api/common/extHostTypeConverters'; import { cloneAndChange } from 'vs/base/common/objects'; -import { MainContext, MainThreadCommandsShape, ExtHostCommandsShape, ICommandDto, ICommandHandlerDescriptionDto } from './extHost.protocol'; +import { MainContext, MainThreadCommandsShape, ExtHostCommandsShape, ICommandDto, ICommandHandlerDescriptionDto, MainThreadTelemetryShape } from './extHost.protocol'; import { isNonEmptyArray } from 'vs/base/common/arrays'; import * as languages from 'vs/editor/common/languages'; import type * as vscode from 'vscode'; @@ -46,6 +46,7 @@ export class ExtHostCommands implements ExtHostCommandsShape { private readonly _commands = new Map<string, CommandHandler>(); private readonly _apiCommands = new Map<string, ApiCommand>(); + #telemetry: MainThreadTelemetryShape; private readonly _logService: ILogService; private readonly _argumentProcessors: ArgumentProcessor[]; @@ -58,6 +59,7 @@ export class ExtHostCommands implements ExtHostCommandsShape { ) { this.#proxy = extHostRpc.getProxy(MainContext.MainThreadCommands); this._logService = logService; + this.#telemetry = extHostRpc.getProxy(MainContext.MainThreadTelemetry); this.converter = new CommandsConverter( this, id => { @@ -219,6 +221,7 @@ export class ExtHostCommands implements ExtHostCommandsShape { if (!command) { throw new Error('Unknown command'); } + this._reportTelemetry(command, id); let { callback, thisArg, description } = command; if (description) { for (let i = 0; i < description.args.length; i++) { @@ -257,6 +260,26 @@ export class ExtHostCommands implements ExtHostCommandsShape { } } + private _reportTelemetry(command: CommandHandler, id: string) { + if (!command.extension || command.extension.isBuiltin) { + return; + } + type ExtensionActionTelemetry = { + extensionId: string; + id: string; + }; + type ExtensionActionTelemetryMeta = { + extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The id of the extension handling the command, informing which extensions provide most-used functionality.' }; + id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The id of the command, to understand which specific extension features are most popular.' }; + owner: 'digitarald'; + comment: 'Used to gain insight on the most popular commands used from extensions'; + }; + this.#telemetry.$publicLog2<ExtensionActionTelemetry, ExtensionActionTelemetryMeta>('Extension:ActionExecuted', { + extensionId: command.extension.identifier.value, + id: id, + }); + } + $executeContributedCommand(id: string, ...args: any[]): Promise<unknown> { this._logService.trace('ExtHostCommands#$executeContributedCommand', id); diff --git a/src/vs/workbench/api/common/extHostCustomEditors.ts b/src/vs/workbench/api/common/extHostCustomEditors.ts index 7c9e25f6126..24ee34186c0 100644 --- a/src/vs/workbench/api/common/extHostCustomEditors.ts +++ b/src/vs/workbench/api/common/extHostCustomEditors.ts @@ -267,7 +267,7 @@ export class ExtHostCustomEditors implements extHostProtocol.ExtHostCustomEditor const viewColumn = typeConverters.ViewColumn.to(position); const webview = this._extHostWebview.createNewWebview(handle, initData.webviewOptions, entry.extension); - const panel = this._extHostWebviewPanels.createNewWebviewPanel(handle, viewType, initData.title, viewColumn, initData.panelOptions, webview); + const panel = this._extHostWebviewPanels.createNewWebviewPanel(handle, viewType, initData.title, viewColumn, initData.panelOptions, webview, true); const revivedResource = URI.revive(resource); diff --git a/src/vs/workbench/api/common/extHostDebugService.ts b/src/vs/workbench/api/common/extHostDebugService.ts index 228920010ae..178ab59c5ef 100644 --- a/src/vs/workbench/api/common/extHostDebugService.ts +++ b/src/vs/workbench/api/common/extHostDebugService.ts @@ -3,36 +3,29 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as path from 'vs/base/common/path'; -import { URI, UriComponents } from 'vs/base/common/uri'; -import { Event, Emitter } from 'vs/base/common/event'; import { asPromise } from 'vs/base/common/async'; -import { - MainContext, MainThreadDebugServiceShape, ExtHostDebugServiceShape, DebugSessionUUID, - IBreakpointsDeltaDto, ISourceMultiBreakpointDto, IFunctionBreakpointDto, IDebugSessionDto -} from 'vs/workbench/api/common/extHost.protocol'; -import { Disposable, Position, Location, SourceBreakpoint, FunctionBreakpoint, DebugAdapterServer, DebugAdapterExecutable, DataBreakpoint, DebugConsoleMode, DebugAdapterInlineImplementation, DebugAdapterNamedPipeServer, TextDiffTabInput, NotebookDiffEditorTabInput, TextTabInput, NotebookEditorTabInput, CustomEditorTabInput } from 'vs/workbench/api/common/extHostTypes'; -import { AbstractDebugAdapter } from 'vs/workbench/contrib/debug/common/abstractDebugAdapter'; -import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; -import { IExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; -import { ExtHostDocumentsAndEditors, IExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; -import { IDebuggerContribution, IConfig, IDebugAdapter, IDebugAdapterServer, IDebugAdapterExecutable, IAdapterDescriptor, IDebugAdapterNamedPipeServer } from 'vs/workbench/contrib/debug/common/debug'; -import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { AbstractVariableResolverService } from 'vs/workbench/services/configurationResolver/common/variableResolver'; -import { ExtHostConfigProvider, IExtHostConfiguration } from '../common/extHostConfiguration'; -import { convertToVSCPaths, convertToDAPaths, isDebuggerMainContribution } from 'vs/workbench/contrib/debug/common/debugUtils'; -import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; -import { ISignService } from 'vs/platform/sign/common/sign'; -import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; -import type * as vscode from 'vscode'; +import { Emitter, Event } from 'vs/base/common/event'; +import { withNullAsUndefined } from 'vs/base/common/types'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { withNullAsUndefined } from 'vs/base/common/types'; -import * as process from 'vs/base/common/process'; +import { ISignService } from 'vs/platform/sign/common/sign'; +import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { DebugSessionUUID, ExtHostDebugServiceShape, IBreakpointsDeltaDto, IDebugSessionDto, IFunctionBreakpointDto, ISourceMultiBreakpointDto, MainContext, MainThreadDebugServiceShape } from 'vs/workbench/api/common/extHost.protocol'; import { IExtHostEditorTabs } from 'vs/workbench/api/common/extHostEditorTabs'; +import { IExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; +import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; +import { DataBreakpoint, DebugAdapterExecutable, DebugAdapterInlineImplementation, DebugAdapterNamedPipeServer, DebugAdapterServer, DebugConsoleMode, Disposable, FunctionBreakpoint, Location, Position, SourceBreakpoint } from 'vs/workbench/api/common/extHostTypes'; +import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; +import { AbstractDebugAdapter } from 'vs/workbench/contrib/debug/common/abstractDebugAdapter'; +import { IAdapterDescriptor, IConfig, IDebugAdapter, IDebugAdapterExecutable, IDebugAdapterNamedPipeServer, IDebugAdapterServer, IDebuggerContribution } from 'vs/workbench/contrib/debug/common/debug'; +import { convertToDAPaths, convertToVSCPaths, isDebuggerMainContribution } from 'vs/workbench/contrib/debug/common/debugUtils'; +import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; import { Dto } from 'vs/workbench/services/extensions/common/proxyIdentifier'; +import type * as vscode from 'vscode'; +import { IExtHostConfiguration } from '../common/extHostConfiguration'; +import { IExtHostVariableResolverProvider } from './extHostVariableResolverService'; export const IExtHostDebugService = createDecorator<IExtHostDebugService>('IExtHostDebugService'); @@ -101,17 +94,15 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E private _debugAdapters: Map<number, IDebugAdapter>; private _debugAdaptersTrackers: Map<number, vscode.DebugAdapterTracker>; - private _variableResolver: IConfigurationResolverService | undefined; - private _signService: ISignService | undefined; constructor( @IExtHostRpcService extHostRpcService: IExtHostRpcService, @IExtHostWorkspace protected _workspaceService: IExtHostWorkspace, @IExtHostExtensionService private _extensionService: IExtHostExtensionService, - @IExtHostDocumentsAndEditors private _editorsService: IExtHostDocumentsAndEditors, @IExtHostConfiguration protected _configurationService: IExtHostConfiguration, - @IExtHostEditorTabs protected _editorTabs: IExtHostEditorTabs + @IExtHostEditorTabs protected _editorTabs: IExtHostEditorTabs, + @IExtHostVariableResolverProvider private _variableResolver: IExtHostVariableResolverProvider, ) { this._configProviderHandleCounter = 0; this._configProviders = []; @@ -371,13 +362,7 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E return Promise.resolve(undefined); } - protected abstract createVariableResolver(folders: vscode.WorkspaceFolder[], editorService: ExtHostDocumentsAndEditors, configurationService: ExtHostConfigProvider): AbstractVariableResolverService; - public async $substituteVariables(folderUri: UriComponents | undefined, config: IConfig): Promise<IConfig> { - if (!this._variableResolver) { - const [workspaceFolders, configProvider] = await Promise.all([this._workspaceService.getWorkspaceFolders2(), this._configurationService.getConfigProvider()]); - this._variableResolver = this.createVariableResolver(workspaceFolders || [], this._editorsService, configProvider!); - } let ws: IWorkspaceFolder | undefined; const folder = await this.getFolder(folderUri); if (folder) { @@ -390,7 +375,8 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E } }; } - return this._variableResolver.resolveAnyAsync(ws, config); + const variableResolver = await this._variableResolver.getResolver(); + return variableResolver.resolveAnyAsync(ws, config); } protected createDebugAdapter(adapter: IAdapterDescriptor, session: ExtHostDebugSession): AbstractDebugAdapter | undefined { @@ -939,94 +925,6 @@ export class ExtHostDebugConsole { } } -export class ExtHostVariableResolverService extends AbstractVariableResolverService { - - constructor(folders: vscode.WorkspaceFolder[], - editorService: ExtHostDocumentsAndEditors | undefined, - configurationService: ExtHostConfigProvider, - editorTabs: IExtHostEditorTabs, - workspaceService?: IExtHostWorkspace, - userHome?: string) { - function getActiveUri(): URI | undefined { - if (editorService) { - const activeEditor = editorService.activeEditor(); - if (activeEditor) { - return activeEditor.document.uri; - } - const activeTab = editorTabs.tabGroups.groups.find(group => group.isActive)?.activeTab; - if (activeTab !== undefined) { - // Resolve a resource from the tab - if (activeTab.kind instanceof TextDiffTabInput || activeTab.kind instanceof NotebookDiffEditorTabInput) { - return activeTab.kind.modified; - } else if (activeTab.kind instanceof TextTabInput || activeTab.kind instanceof NotebookEditorTabInput || activeTab.kind instanceof CustomEditorTabInput) { - return activeTab.kind.uri; - } - } - } - return undefined; - } - - super({ - getFolderUri: (folderName: string): URI | undefined => { - const found = folders.filter(f => f.name === folderName); - if (found && found.length > 0) { - return found[0].uri; - } - return undefined; - }, - getWorkspaceFolderCount: (): number => { - return folders.length; - }, - getConfigurationValue: (folderUri: URI | undefined, section: string): string | undefined => { - return configurationService.getConfiguration(undefined, folderUri).get<string>(section); - }, - getAppRoot: (): string | undefined => { - return process.cwd(); - }, - getExecPath: (): string | undefined => { - return process.env['VSCODE_EXEC_PATH']; - }, - getFilePath: (): string | undefined => { - const activeUri = getActiveUri(); - if (activeUri) { - return path.normalize(activeUri.fsPath); - } - return undefined; - }, - getWorkspaceFolderPathForFile: (): string | undefined => { - if (workspaceService) { - const activeUri = getActiveUri(); - if (activeUri) { - const ws = workspaceService.getWorkspaceFolder(activeUri); - if (ws) { - return path.normalize(ws.uri.fsPath); - } - } - } - return undefined; - }, - getSelectedText: (): string | undefined => { - if (editorService) { - const activeEditor = editorService.activeEditor(); - if (activeEditor && !activeEditor.selection.isEmpty) { - return activeEditor.document.getText(activeEditor.selection); - } - } - return undefined; - }, - getLineNumber: (): string | undefined => { - if (editorService) { - const activeEditor = editorService.activeEditor(); - if (activeEditor) { - return String(activeEditor.selection.end.line + 1); - } - } - return undefined; - } - }, undefined, userHome ? Promise.resolve(userHome) : undefined, Promise.resolve(process.env)); - } -} - interface ConfigProviderTuple { type: string; handle: number; @@ -1108,14 +1006,10 @@ export class WorkerExtHostDebugService extends ExtHostDebugServiceBase { @IExtHostRpcService extHostRpcService: IExtHostRpcService, @IExtHostWorkspace workspaceService: IExtHostWorkspace, @IExtHostExtensionService extensionService: IExtHostExtensionService, - @IExtHostDocumentsAndEditors editorsService: IExtHostDocumentsAndEditors, @IExtHostConfiguration configurationService: IExtHostConfiguration, - @IExtHostEditorTabs editorTabs: IExtHostEditorTabs + @IExtHostEditorTabs editorTabs: IExtHostEditorTabs, + @IExtHostVariableResolverProvider variableResolver: IExtHostVariableResolverProvider ) { - super(extHostRpcService, workspaceService, extensionService, editorsService, configurationService, editorTabs); - } - - protected createVariableResolver(folders: vscode.WorkspaceFolder[], editorService: ExtHostDocumentsAndEditors, configurationService: ExtHostConfigProvider): AbstractVariableResolverService { - return new ExtHostVariableResolverService(folders, editorService, configurationService, this._editorTabs); + super(extHostRpcService, workspaceService, extensionService, configurationService, editorTabs, variableResolver); } } diff --git a/src/vs/workbench/api/common/extHostEditorTabs.ts b/src/vs/workbench/api/common/extHostEditorTabs.ts index cbd2effae0c..1fbc2d487de 100644 --- a/src/vs/workbench/api/common/extHostEditorTabs.ts +++ b/src/vs/workbench/api/common/extHostEditorTabs.ts @@ -9,9 +9,10 @@ import { IEditorTabDto, IEditorTabGroupDto, IExtHostEditorTabsShape, MainContext import { URI } from 'vs/base/common/uri'; import { Emitter } from 'vs/base/common/event'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { CustomEditorTabInput, NotebookDiffEditorTabInput, NotebookEditorTabInput, TerminalEditorTabInput, TextDiffTabInput, TextTabInput, ViewColumn, WebviewEditorTabInput } from 'vs/workbench/api/common/extHostTypes'; +import { CustomEditorTabInput, NotebookDiffEditorTabInput, NotebookEditorTabInput, TerminalEditorTabInput, TextDiffTabInput, TextTabInput, WebviewEditorTabInput } from 'vs/workbench/api/common/extHostTypes'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { assertIsDefined } from 'vs/base/common/types'; +import { diffSets } from 'vs/base/common/collections'; export interface IExtHostEditorTabs extends IExtHostEditorTabsShape { readonly _serviceBrand: undefined; @@ -47,7 +48,7 @@ class ExtHostEditorTab { get label() { return that._dto.label; }, - get kind() { + get input() { return that._input; }, get isDirty() { @@ -174,6 +175,17 @@ class ExtHostEditorTabGroup { this._activeTabId = ''; } return tab; + } else if (operation.kind === TabModelOperationKind.TAB_MOVE) { + if (operation.oldIndex === undefined) { + throw new Error('Invalid old index on move IPC'); + } + // Splice to remove at old index and insert at new index === moving the tab + const tab = this._tabs.splice(operation.oldIndex, 1)[0]; + if (!tab) { + throw new Error(`Tab move updated received for index ${operation.oldIndex} which does not exist`); + } + this._tabs.splice(operation.index, 0, tab); + return tab; } const tab = this._tabs.find(extHostTab => extHostTab.tabId === operation.tabDto.id); if (!tab) { @@ -201,8 +213,8 @@ export class ExtHostEditorTabs implements IExtHostEditorTabs { readonly _serviceBrand: undefined; private readonly _proxy: MainThreadEditorTabsShape; - private readonly _onDidChangeTabs = new Emitter<vscode.Tab[]>(); - private readonly _onDidChangeTabGroups = new Emitter<vscode.TabGroup[]>(); + private readonly _onDidChangeTabs = new Emitter<vscode.TabChangeEvent>(); + private readonly _onDidChangeTabGroups = new Emitter<vscode.TabGroupChangeEvent>(); // Have to use ! because this gets initialized via an RPC proxy private _activeGroupId!: number; @@ -223,7 +235,7 @@ export class ExtHostEditorTabs implements IExtHostEditorTabs { onDidChangeTabGroups: that._onDidChangeTabGroups.event, onDidChangeTabs: that._onDidChangeTabs.event, // dynamic -> getters - get groups() { + get all() { return Object.freeze(that._extHostTabGroups.map(group => group.apiObject)); }, get activeTabGroup() { @@ -231,47 +243,51 @@ export class ExtHostEditorTabs implements IExtHostEditorTabs { const activeTabGroup = assertIsDefined(that._extHostTabGroups.find(candidate => candidate.groupId === activeTabGroupId)?.apiObject); return activeTabGroup; }, - close: async (tab: vscode.Tab | vscode.Tab[], preserveFocus?: boolean) => { - const tabs = Array.isArray(tab) ? tab : [tab]; - const extHostTabIds: string[] = []; - for (const tab of tabs) { - const extHostTab = this._findExtHostTabFromApi(tab); - if (!extHostTab) { - throw new Error('Tab close: Invalid tab not found!'); - } - extHostTabIds.push(extHostTab.tabId); + close: async (tabOrTabGroup: vscode.Tab | readonly vscode.Tab[] | vscode.TabGroup | readonly vscode.TabGroup[], preserveFocus?: boolean) => { + const tabsOrTabGroups = Array.isArray(tabOrTabGroup) ? tabOrTabGroup : [tabOrTabGroup]; + if (!tabsOrTabGroups.length) { + return true; } - return this._proxy.$closeTab(extHostTabIds, preserveFocus); - }, - move: async (tab: vscode.Tab, viewColumn: ViewColumn, index: number, preservceFocus?: boolean) => { - const extHostTab = this._findExtHostTabFromApi(tab); - if (!extHostTab) { - throw new Error('Invalid tab'); + // Check which type was passed in and call the appropriate close + // Casting is needed as typescript doesn't seem to infer enough from this + if (isTabGroup(tabsOrTabGroups[0])) { + return this._closeGroups(tabsOrTabGroups as vscode.TabGroup[], preserveFocus); + } else { + return this._closeTabs(tabsOrTabGroups as vscode.Tab[], preserveFocus); } - this._proxy.$moveTab(extHostTab.tabId, index, typeConverters.ViewColumn.from(viewColumn), preservceFocus); - return; - } + }, + // move: async (tab: vscode.Tab, viewColumn: ViewColumn, index: number, preservceFocus?: boolean) => { + // const extHostTab = this._findExtHostTabFromApi(tab); + // if (!extHostTab) { + // throw new Error('Invalid tab'); + // } + // this._proxy.$moveTab(extHostTab.tabId, index, typeConverters.ViewColumn.from(viewColumn), preservceFocus); + // return; + // } }; this._apiObject = Object.freeze(obj); } return this._apiObject; } - private _findExtHostTabFromApi(apiTab: vscode.Tab): ExtHostEditorTab | undefined { - for (const group of this._extHostTabGroups) { - for (const tab of group.tabs) { - if (tab.apiObject === apiTab) { - return tab; - } - } - } - return; - } - $acceptEditorTabModel(tabGroups: IEditorTabGroupDto[]): void { + const groupIdsBefore = new Set(this._extHostTabGroups.map(group => group.groupId)); + const groupIdsAfter = new Set(tabGroups.map(dto => dto.groupId)); + const diff = diffSets(groupIdsBefore, groupIdsAfter); + + const closed: vscode.TabGroup[] = this._extHostTabGroups.filter(group => diff.removed.includes(group.groupId)).map(group => group.apiObject); + const opened: vscode.TabGroup[] = []; + const changed: vscode.TabGroup[] = []; + + this._extHostTabGroups = tabGroups.map(tabGroup => { const group = new ExtHostEditorTabGroup(tabGroup, this._proxy, () => this._activeGroupId); + if (diff.added.includes(group.groupId)) { + opened.push(group.apiObject); + } else { + changed.push(group.apiObject); + } return group; }); @@ -280,7 +296,7 @@ export class ExtHostEditorTabs implements IExtHostEditorTabs { if (activeTabGroupId !== undefined && this._activeGroupId !== activeTabGroupId) { this._activeGroupId = activeTabGroupId; } - this._onDidChangeTabGroups.fire(this._extHostTabGroups.map(g => g.apiObject)); + this._onDidChangeTabGroups.fire(Object.freeze({ opened, closed, changed })); } $acceptTabGroupUpdate(groupDto: IEditorTabGroupDto) { @@ -292,7 +308,7 @@ export class ExtHostEditorTabs implements IExtHostEditorTabs { if (groupDto.isActive) { this._activeGroupId = groupDto.groupId; } - this._onDidChangeTabGroups.fire([group.apiObject]); + this._onDidChangeTabGroups.fire(Object.freeze({ changed: [group.apiObject], opened: [], closed: [] })); } $acceptTabOperation(operation: TabOperation) { @@ -301,9 +317,80 @@ export class ExtHostEditorTabs implements IExtHostEditorTabs { throw new Error('Update Tabs IPC call received before group creation.'); } const tab = group.acceptTabOperation(operation); - // We don't want to fire a change event with a closed tab to prevent an invalid tabs from being received - if (operation.kind !== TabModelOperationKind.TAB_CLOSE) { - this._onDidChangeTabs.fire([tab.apiObject]); + + // Construct the tab change event based on the operation + switch (operation.kind) { + case TabModelOperationKind.TAB_OPEN: + this._onDidChangeTabs.fire(Object.freeze({ + opened: [tab.apiObject], + closed: [], + changed: [] + })); + return; + case TabModelOperationKind.TAB_CLOSE: + this._onDidChangeTabs.fire(Object.freeze({ + opened: [], + closed: [tab.apiObject], + changed: [] + })); + return; + case TabModelOperationKind.TAB_MOVE: + case TabModelOperationKind.TAB_UPDATE: + this._onDidChangeTabs.fire(Object.freeze({ + opened: [], + closed: [], + changed: [tab.apiObject] + })); + return; } } + + private _findExtHostTabFromApi(apiTab: vscode.Tab): ExtHostEditorTab | undefined { + for (const group of this._extHostTabGroups) { + for (const tab of group.tabs) { + if (tab.apiObject === apiTab) { + return tab; + } + } + } + return; + } + + private _findExtHostTabGroupFromApi(apiTabGroup: vscode.TabGroup): ExtHostEditorTabGroup | undefined { + return this._extHostTabGroups.find(candidate => candidate.apiObject === apiTabGroup); + } + + private async _closeTabs(tabs: vscode.Tab[], preserveFocus?: boolean): Promise<boolean> { + const extHostTabIds: string[] = []; + for (const tab of tabs) { + const extHostTab = this._findExtHostTabFromApi(tab); + if (!extHostTab) { + throw new Error('Tab close: Invalid tab not found!'); + } + extHostTabIds.push(extHostTab.tabId); + } + return this._proxy.$closeTab(extHostTabIds, preserveFocus); + } + + private async _closeGroups(groups: vscode.TabGroup[], preserverFoucs?: boolean): Promise<boolean> { + const extHostGroupIds: number[] = []; + for (const group of groups) { + const extHostGroup = this._findExtHostTabGroupFromApi(group); + if (!extHostGroup) { + throw new Error('Group close: Invalid group not found!'); + } + extHostGroupIds.push(extHostGroup.groupId); + } + return this._proxy.$closeGroup(extHostGroupIds, preserverFoucs); + } +} + +//#region Utils +function isTabGroup(obj: unknown): obj is vscode.TabGroup { + const tabGroup = obj as vscode.TabGroup; + if (tabGroup.tabs !== undefined) { + return true; + } + return false; } +//#endregion diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts index b3ff5c0c2b4..49eaa65d462 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts @@ -13,12 +13,12 @@ import { TernarySearchTree } from 'vs/base/common/map'; import { URI, UriComponents } from 'vs/base/common/uri'; import { ILogService } from 'vs/platform/log/common/log'; import { ExtHostExtensionServiceShape, MainContext, MainThreadExtensionServiceShape, MainThreadTelemetryShape, MainThreadWorkspaceShape } from 'vs/workbench/api/common/extHost.protocol'; -import { IExtensionHostInitData } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; +import { IExtensionDescriptionDelta, IExtensionHostInitData } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; import { ExtHostConfiguration, IExtHostConfiguration } from 'vs/workbench/api/common/extHostConfiguration'; import { ActivatedExtension, EmptyExtension, ExtensionActivationTimes, ExtensionActivationTimesBuilder, ExtensionsActivator, IExtensionAPI, IExtensionModule, HostExtension, ExtensionActivationTimesFragment } from 'vs/workbench/api/common/extHostExtensionActivator'; import { ExtHostStorage, IExtHostStorage } from 'vs/workbench/api/common/extHostStorage'; import { ExtHostWorkspace, IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; -import { MissingExtensionDependency, ActivationKind, checkProposedApiEnabled, isProposedApiEnabled, ExtensionActivationReason } from 'vs/workbench/services/extensions/common/extensions'; +import { MissingExtensionDependency, ActivationKind, checkProposedApiEnabled, isProposedApiEnabled, ExtensionActivationReason, extensionIdentifiersArrayToSet } from 'vs/workbench/services/extensions/common/extensions'; import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; import * as errors from 'vs/base/common/errors'; import type * as vscode from 'vscode'; @@ -100,16 +100,18 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme private readonly _readyToRunExtensions: Barrier; private readonly _eagerExtensionsActivated: Barrier; - protected readonly _registry: ExtensionDescriptionRegistry; + protected readonly _myRegistry: ExtensionDescriptionRegistry; + protected readonly _globalRegistry: ExtensionDescriptionRegistry; private readonly _storage: ExtHostStorage; private readonly _secretState: ExtHostSecretState; private readonly _storagePath: IExtensionStoragePaths; private readonly _activator: ExtensionsActivator; - private _extensionPathIndex: Promise<TernarySearchTree<URI, IExtensionDescription>> | null; + private _extensionPathIndex: Promise<ExtensionPaths> | null; private readonly _resolvers: { [authorityPrefix: string]: vscode.RemoteAuthorityResolver }; private _started: boolean; + private _isTerminating: boolean = false; private _remoteConnectionData: IRemoteConnectionData | null; constructor( @@ -143,7 +145,11 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme this._readyToStartExtensionHost = new Barrier(); this._readyToRunExtensions = new Barrier(); this._eagerExtensionsActivated = new Barrier(); - this._registry = new ExtensionDescriptionRegistry(this._initData.extensions); + this._globalRegistry = new ExtensionDescriptionRegistry(this._initData.allExtensions); + const myExtensionsSet = extensionIdentifiersArrayToSet(this._initData.myExtensions); + this._myRegistry = new ExtensionDescriptionRegistry( + filterExtensions(this._globalRegistry, myExtensionsSet) + ); this._storage = new ExtHostStorage(this._extHostContext); this._secretState = new ExtHostSecretState(this._extHostContext); this._storagePath = storagePath; @@ -153,24 +159,33 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme [IExtHostSecretState, this._secretState] )); - const hostExtensions = new Set<string>(); - this._initData.hostExtensions.forEach((extensionId) => hostExtensions.add(ExtensionIdentifier.toKey(extensionId))); + let resolvedExtensions: ExtensionIdentifier[] = []; + let hostExtensions: ExtensionIdentifier[] = []; + if (this._initData.remote.isRemote) { + resolvedExtensions = this._initData.allExtensions.filter(extension => !extension.main && !extension.browser).map(extension => extension.identifier); + hostExtensions = ( + this._initData.allExtensions + .filter(extension => !myExtensionsSet.has(ExtensionIdentifier.toKey(extension.identifier.value))) + .filter(extension => (extension.main || extension.browser) && extension.api === 'none').map(extension => extension.identifier) + ); + } + const hostExtensionsSet = extensionIdentifiersArrayToSet(hostExtensions); this._activator = this._register(new ExtensionsActivator( - this._registry, - this._initData.resolvedExtensions, - this._initData.hostExtensions, + this._myRegistry, + resolvedExtensions, + hostExtensions, { onExtensionActivationError: (extensionId: ExtensionIdentifier, error: Error, missingExtensionDependency: MissingExtensionDependency | null): void => { this._mainThreadExtensionsProxy.$onExtensionActivationError(extensionId, errors.transformErrorForSerialization(error), missingExtensionDependency); }, actualActivateExtension: async (extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise<ActivatedExtension> => { - if (hostExtensions.has(ExtensionIdentifier.toKey(extensionId))) { + if (hostExtensionsSet.has(ExtensionIdentifier.toKey(extensionId))) { await this._mainThreadExtensionsProxy.$activateExtension(extensionId, reason); return new HostExtension(); } - const extensionDescription = this._registry.getExtensionDescription(extensionId)!; + const extensionDescription = this._myRegistry.getExtensionDescription(extensionId)!; return this._activateExtension(extensionDescription, reason); } }, @@ -204,12 +219,12 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme } } - public async deactivateAll(): Promise<void> { + private async _deactivateAll(): Promise<void> { this._storagePath.onWillDeactivateAll(); let allPromises: Promise<void>[] = []; try { - const allExtensions = this._registry.getAllExtensionDescriptions(); + const allExtensions = this._myRegistry.getAllExtensionDescriptions(); const allExtensionsIds = allExtensions.map(ext => ext.identifier); const activatedExtensions = allExtensionsIds.filter(id => this.isActivated(id)); @@ -222,6 +237,40 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme await Promise.all(allPromises); } + public terminate(reason: string, code: number = 0): void { + if (this._isTerminating) { + // we are already shutting down... + return; + } + this._isTerminating = true; + this._logService.info(`Extension host terminating: ${reason}`); + this._logService.flush(); + + this._extHostTerminalService.dispose(); + this._activator.dispose(); + + errors.setUnexpectedErrorHandler((err) => { + this._logService.error(err); + }); + + // Invalidate all proxies + this._extHostContext.dispose(); + + const extensionsDeactivated = this._deactivateAll(); + + // Give extensions at most 5 seconds to wrap up any async deactivate, then exit + Promise.race([timeout(5000), extensionsDeactivated]).finally(() => { + if (this._hostUtils.pid) { + this._logService.info(`Extension host with pid ${this._hostUtils.pid} exiting with code ${code}`); + } else { + this._logService.info(`Extension host exiting with code ${code}`); + } + this._logService.flush(); + this._logService.dispose(); + this._hostUtils.exit(code); + }); + } + public isActivated(extensionId: ExtensionIdentifier): boolean { if (this._readyToRunExtensions.isOpen()) { return this._activator.isActivated(extensionId); @@ -229,6 +278,15 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme return false; } + public async getExtension(extensionId: string): Promise<IExtensionDescription | undefined> { + const ext = await this._mainThreadExtensionsProxy.$getExtension(extensionId); + return ext && { + ...ext, + identifier: new ExtensionIdentifier(ext.identifier.value), + extensionLocation: URI.revive(ext.extensionLocation), + }; + } + private _activateByEvent(activationEvent: string, startup: boolean): Promise<void> { return this._activator.activateByEvent(activationEvent, startup); } @@ -249,7 +307,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme } public getExtensionRegistry(): Promise<ExtensionDescriptionRegistry> { - return this._readyToRunExtensions.wait().then(_ => this._registry); + return this._readyToRunExtensions.wait().then(_ => this._myRegistry); } public getExtensionExports(extensionId: ExtensionIdentifier): IExtensionAPI | null | undefined { @@ -272,28 +330,35 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme } // create trie to enable fast 'filename -> extension id' look up - public async getExtensionPathIndex(): Promise<TernarySearchTree<URI, IExtensionDescription>> { + public async getExtensionPathIndex(): Promise<ExtensionPaths> { if (!this._extensionPathIndex) { - this._extensionPathIndex = (async () => { - const tst = TernarySearchTree.forUris<IExtensionDescription>(key => { - // using the default/biased extUri-util because the IExtHostFileSystemInfo-service - // isn't ready to be used yet, e.g the knowledge about `file` protocol and others - // comes in while this code runs - return extUriBiasedIgnorePathCase.ignorePathCasing(key); - }); - // const tst = TernarySearchTree.forUris<IExtensionDescription>(key => true); - for (const ext of this._registry.getAllExtensionDescriptions()) { - if (this._getEntryPoint(ext)) { - const uri = await this._realPathExtensionUri(ext.extensionLocation); - tst.set(uri, ext); - } - } - return tst; - })(); + this._extensionPathIndex = this._createExtensionPathIndex(this._myRegistry.getAllExtensionDescriptions()).then((searchTree) => { + return new ExtensionPaths(searchTree); + }); } return this._extensionPathIndex; } + /** + * create trie to enable fast 'filename -> extension id' look up + */ + private async _createExtensionPathIndex(extensions: IExtensionDescription[]): Promise<TernarySearchTree<URI, IExtensionDescription>> { + const tst = TernarySearchTree.forUris<IExtensionDescription>(key => { + // using the default/biased extUri-util because the IExtHostFileSystemInfo-service + // isn't ready to be used yet, e.g the knowledge about `file` protocol and others + // comes in while this code runs + return extUriBiasedIgnorePathCase.ignorePathCasing(key); + }); + // const tst = TernarySearchTree.forUris<IExtensionDescription>(key => true); + await Promise.all(extensions.map(async (ext) => { + if (this._getEntryPoint(ext)) { + const uri = await this._realPathExtensionUri(ext.extensionLocation); + tst.set(uri, ext); + } + })); + return tst; + } + private _deactivate(extensionId: ExtensionIdentifier): Promise<void> { let result = Promise.resolve(undefined); @@ -448,7 +513,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme get extensionMode() { return extensionMode; }, get extension() { if (extension === undefined) { - extension = new Extension(that, extensionDescription.identifier, extensionDescription, extensionKind); + extension = new Extension(that, extensionDescription.identifier, extensionDescription, extensionKind, false); } return extension; }, @@ -528,7 +593,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme // startup is considered finished this._mainThreadExtensionsProxy.$setPerformanceMarks(performance.getMarks()); - for (const desc of this._registry.getAllExtensionDescriptions()) { + for (const desc of this._myRegistry.getAllExtensionDescriptions()) { if (desc.activationEvents) { for (const activationEvent of desc.activationEvents) { if (activationEvent === 'onStartupFinished') { @@ -563,7 +628,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme } return Promise.all( - this._registry.getAllExtensionDescriptions().map((desc) => { + this._myRegistry.getAllExtensionDescriptions().map((desc) => { return this._handleWorkspaceContainsEagerExtension(folders, desc); }) ).then(() => { }); @@ -645,12 +710,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme } public async $extensionTestsExit(code: number): Promise<void> { - this._logService.info(`Extension host terminating: test runner requested exit with code ${code}`); - if (this._hostUtils.pid) { - this._logService.info(`Extension host with pid ${this._hostUtils.pid} exiting with code ${code}`); - } - this._logService.flush(); - this._hostUtils.exit(code); + this.terminate(`test runner requested exit with code ${code}`, code); } private _startExtensionHost(): Promise<void> { @@ -777,8 +837,29 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme return result; } - public $startExtensionHost(enabledExtensionIds: ExtensionIdentifier[]): Promise<void> { - this._registry.keepOnly(enabledExtensionIds); + private static _applyExtensionsDelta(oldGlobalRegistry: ExtensionDescriptionRegistry, oldMyRegistry: ExtensionDescriptionRegistry, extensionsDelta: IExtensionDescriptionDelta) { + const globalRegistry = new ExtensionDescriptionRegistry(oldGlobalRegistry.getAllExtensionDescriptions()); + globalRegistry.deltaExtensions(extensionsDelta.toAdd, extensionsDelta.toRemove); + + const myExtensionsSet = extensionIdentifiersArrayToSet(oldMyRegistry.getAllExtensionDescriptions().map(extension => extension.identifier)); + for (const extensionId of extensionsDelta.myToRemove) { + myExtensionsSet.delete(ExtensionIdentifier.toKey(extensionId)); + } + for (const extensionId of extensionsDelta.myToAdd) { + myExtensionsSet.add(ExtensionIdentifier.toKey(extensionId)); + } + const myExtensions = filterExtensions(globalRegistry, myExtensionsSet); + + return { globalRegistry, myExtensions }; + } + + public $startExtensionHost(extensionsDelta: IExtensionDescriptionDelta): Promise<void> { + extensionsDelta.toAdd.forEach((extension) => (<any>extension).extensionLocation = URI.revive(extension.extensionLocation)); + + const { globalRegistry, myExtensions } = AbstractExtHostExtensionService._applyExtensionsDelta(this._globalRegistry, this._myRegistry, extensionsDelta); + this._globalRegistry.set(globalRegistry.getAllExtensionDescriptions()); + this._myRegistry.set(myExtensions); + return this._startExtensionHost(); } @@ -795,7 +876,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme public async $activate(extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise<boolean> { await this._readyToRunExtensions.wait(); - if (!this._registry.getExtensionDescription(extensionId)) { + if (!this._myRegistry.getExtensionDescription(extensionId)) { // unknown extension => ignore return false; } @@ -803,24 +884,17 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme return true; } - public async $deltaExtensions(toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[]): Promise<void> { - toAdd.forEach((extension) => (<any>extension).extensionLocation = URI.revive(extension.extensionLocation)); + public async $deltaExtensions(extensionsDelta: IExtensionDescriptionDelta): Promise<void> { + extensionsDelta.toAdd.forEach((extension) => (<any>extension).extensionLocation = URI.revive(extension.extensionLocation)); - const trie = await this.getExtensionPathIndex(); + // First build up and update the trie and only afterwards apply the delta + const { globalRegistry, myExtensions } = AbstractExtHostExtensionService._applyExtensionsDelta(this._globalRegistry, this._myRegistry, extensionsDelta); + const newSearchTree = await this._createExtensionPathIndex(myExtensions); + const extensionsPaths = await this.getExtensionPathIndex(); + extensionsPaths.setSearchTree(newSearchTree); + this._globalRegistry.set(globalRegistry.getAllExtensionDescriptions()); + this._myRegistry.set(myExtensions); - await Promise.all(toRemove.map(async (extensionId) => { - const extensionDescription = this._registry.getExtensionDescription(extensionId); - if (extensionDescription) { - trie.delete(await this._realPathExtensionUri(extensionDescription.extensionLocation)); - } - })); - - await Promise.all(toAdd.map(async (extensionDescription) => { - const realpathUri = await this._realPathExtensionUri(extensionDescription.extensionLocation); - trie.set(realpathUri, extensionDescription); - })); - - this._registry.deltaExtensions(toAdd, toRemove); return Promise.resolve(undefined); } @@ -885,12 +959,13 @@ export const IExtHostExtensionService = createDecorator<IExtHostExtensionService export interface IExtHostExtensionService extends AbstractExtHostExtensionService { readonly _serviceBrand: undefined; initialize(): Promise<void>; + terminate(reason: string): void; + getExtension(extensionId: string): Promise<IExtensionDescription | undefined>; isActivated(extensionId: ExtensionIdentifier): boolean; activateByIdWithErrors(extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise<void>; - deactivateAll(): Promise<void>; getExtensionExports(extensionId: ExtensionIdentifier): IExtensionAPI | null | undefined; getExtensionRegistry(): Promise<ExtensionDescriptionRegistry>; - getExtensionPathIndex(): Promise<TernarySearchTree<URI, IExtensionDescription>>; + getExtensionPathIndex(): Promise<ExtensionPaths>; registerRemoteAuthorityResolver(authorityPrefix: string, resolver: vscode.RemoteAuthorityResolver): vscode.Disposable; onDidChangeRemoteConnectionData: Event<void>; @@ -908,8 +983,9 @@ export class Extension<T extends object | null | undefined> implements vscode.Ex readonly extensionPath: string; readonly packageJSON: IExtensionDescription; readonly extensionKind: vscode.ExtensionKind; + readonly isFromDifferentExtensionHost: boolean; - constructor(extensionService: IExtHostExtensionService, originExtensionId: ExtensionIdentifier, description: IExtensionDescription, kind: ExtensionKind) { + constructor(extensionService: IExtHostExtensionService, originExtensionId: ExtensionIdentifier, description: IExtensionDescription, kind: ExtensionKind, isFromDifferentExtensionHost: boolean) { this.#extensionService = extensionService; this.#originExtensionId = originExtensionId; this.#identifier = description.identifier; @@ -918,24 +994,36 @@ export class Extension<T extends object | null | undefined> implements vscode.Ex this.extensionPath = path.normalize(originalFSPath(description.extensionLocation)); this.packageJSON = description; this.extensionKind = kind; + this.isFromDifferentExtensionHost = isFromDifferentExtensionHost; } get isActive(): boolean { + // TODO@alexdima support this return this.#extensionService.isActivated(this.#identifier); } get exports(): T { - if (this.packageJSON.api === 'none') { + if (this.packageJSON.api === 'none' || this.isFromDifferentExtensionHost) { return undefined!; // Strict nulloverride - Public api } return <T>this.#extensionService.getExtensionExports(this.#identifier); } - activate(): Thenable<T> { - return this.#extensionService.activateByIdWithErrors(this.#identifier, { startup: false, extensionId: this.#originExtensionId, activationEvent: 'api' }).then(() => this.exports); + async activate(): Promise<T> { + if (this.isFromDifferentExtensionHost) { + throw new Error('Cannot activate foreign extension'); // TODO@alexdima support this + } + await this.#extensionService.activateByIdWithErrors(this.#identifier, { startup: false, extensionId: this.#originExtensionId, activationEvent: 'api' }); + return this.exports; } } +function filterExtensions(globalRegistry: ExtensionDescriptionRegistry, desiredExtensions: Set<string>): IExtensionDescription[] { + return globalRegistry.getAllExtensionDescriptions().filter( + extension => desiredExtensions.has(ExtensionIdentifier.toKey(extension.identifier)) + ); +} + function getRemoteAuthorityPrefix(remoteAuthority: string): string { const plusIndex = remoteAuthority.indexOf('+'); if (plusIndex === -1) { @@ -943,3 +1031,22 @@ function getRemoteAuthorityPrefix(remoteAuthority: string): string { } return remoteAuthority.substring(0, plusIndex); } + +export class ExtensionPaths { + + constructor( + private _searchTree: TernarySearchTree<URI, IExtensionDescription> + ) { } + + setSearchTree(searchTree: TernarySearchTree<URI, IExtensionDescription>): void { + this._searchTree = searchTree; + } + + findSubstr(key: URI): IExtensionDescription | undefined { + return this._searchTree.findSubstr(key); + } + + forEach(callback: (value: IExtensionDescription, index: URI) => any): void { + return this._searchTree.forEach(callback); + } +} diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 2cc57319c69..ef72e012647 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -42,13 +42,10 @@ import { Dto } from 'vs/workbench/services/extensions/common/proxyIdentifier'; class DocumentSymbolAdapter { - private _documents: ExtHostDocuments; - private _provider: vscode.DocumentSymbolProvider; - - constructor(documents: ExtHostDocuments, provider: vscode.DocumentSymbolProvider) { - this._documents = documents; - this._provider = provider; - } + constructor( + private readonly _documents: ExtHostDocuments, + private readonly _provider: vscode.DocumentSymbolProvider + ) { } async provideDocumentSymbols(resource: URI, token: CancellationToken): Promise<languages.DocumentSymbol[] | undefined> { const doc = this._documents.getDocument(resource); @@ -256,7 +253,7 @@ class HoverAdapter { private readonly _provider: vscode.HoverProvider, ) { } - public async provideHover(resource: URI, position: IPosition, token: CancellationToken): Promise<languages.Hover | undefined> { + async provideHover(resource: URI, position: IPosition, token: CancellationToken): Promise<languages.Hover | undefined> { const doc = this._documents.getDocument(resource); const pos = typeConvert.Position.to(position); @@ -464,7 +461,7 @@ class CodeActionAdapter { return { cacheId, actions }; } - public async resolveCodeAction(id: extHostProtocol.ChainedCacheId, token: CancellationToken): Promise<extHostProtocol.IWorkspaceEditDto | undefined> { + async resolveCodeAction(id: extHostProtocol.ChainedCacheId, token: CancellationToken): Promise<extHostProtocol.IWorkspaceEditDto | undefined> { const [sessionId, itemId] = id; const item = this._cache.get(sessionId, itemId); if (!item || CodeActionAdapter._isCommand(item)) { @@ -479,7 +476,7 @@ class CodeActionAdapter { : undefined; } - public releaseCodeActions(cachedId: number): void { + releaseCodeActions(cachedId: number): void { this._disposables.get(cachedId)?.dispose(); this._disposables.delete(cachedId); this._cache.delete(cachedId); @@ -697,8 +694,8 @@ class RenameAdapter { class SemanticTokensPreviousResult { constructor( - public readonly resultId: string | undefined, - public readonly tokens?: Uint32Array, + readonly resultId: string | undefined, + readonly tokens?: Uint32Array, ) { } } @@ -849,8 +846,7 @@ export class DocumentRangeSemanticTokensAdapter { constructor( private readonly _documents: ExtHostDocuments, private readonly _provider: vscode.DocumentRangeSemanticTokensProvider, - ) { - } + ) { } async provideDocumentRangeSemanticTokens(resource: URI, range: IRange, token: CancellationToken): Promise<VSBuffer | null> { const doc = this._documents.getDocument(resource); @@ -870,7 +866,7 @@ export class DocumentRangeSemanticTokensAdapter { } } -class SuggestAdapter { +class CompletionsAdapter { static supportsResolving(provider: vscode.CompletionItemProvider): boolean { return typeof provider.resolveCompletionItem === 'function'; @@ -915,7 +911,7 @@ class SuggestAdapter { const list = Array.isArray(itemsOrList) ? new CompletionList(itemsOrList) : itemsOrList; // keep result for providers that support resolving - const pid: number = SuggestAdapter.supportsResolving(this._provider) ? this._cache.add(list.items) : this._cache.add([]); + const pid: number = CompletionsAdapter.supportsResolving(this._provider) ? this._cache.add(list.items) : this._cache.add([]); const disposables = new DisposableStore(); this._disposables.set(pid, disposables); @@ -1027,9 +1023,13 @@ class SuggestAdapter { } class InlineCompletionAdapterBase { - public async provideInlineCompletions(resource: URI, position: IPosition, context: languages.InlineCompletionContext, token: CancellationToken): Promise<extHostProtocol.IdentifiableInlineCompletions | undefined> { + async provideInlineCompletions(resource: URI, position: IPosition, context: languages.InlineCompletionContext, token: CancellationToken): Promise<extHostProtocol.IdentifiableInlineCompletions | undefined> { return undefined; } + + disposeCompletions(pid: number): void { } + + handleDidShowCompletionItem(pid: number, idx: number): void { } } class InlineCompletionAdapter extends InlineCompletionAdapterBase { @@ -1044,7 +1044,7 @@ class InlineCompletionAdapter extends InlineCompletionAdapterBase { super(); } - public override async provideInlineCompletions(resource: URI, position: IPosition, context: languages.InlineCompletionContext, token: CancellationToken): Promise<extHostProtocol.IdentifiableInlineCompletions | undefined> { + override async provideInlineCompletions(resource: URI, position: IPosition, context: languages.InlineCompletionContext, token: CancellationToken): Promise<extHostProtocol.IdentifiableInlineCompletions | undefined> { const doc = this._documents.getDocument(resource); const pos = typeConvert.Position.to(position); @@ -1104,7 +1104,7 @@ class InlineCompletionAdapter extends InlineCompletionAdapterBase { }; } - public disposeCompletions(pid: number) { + override disposeCompletions(pid: number) { this._cache.delete(pid); const d = this._disposables.get(pid); if (d) { @@ -1113,7 +1113,7 @@ class InlineCompletionAdapter extends InlineCompletionAdapterBase { this._disposables.delete(pid); } - public handleDidShowCompletionItem(pid: number, idx: number): void { + override handleDidShowCompletionItem(pid: number, idx: number): void { const completionItem = this._cache.get(pid, idx); if (completionItem) { InlineCompletionController.get(this._provider).fireOnDidShowCompletionItem({ @@ -1124,8 +1124,10 @@ class InlineCompletionAdapter extends InlineCompletionAdapterBase { } class InlineCompletionAdapterNew extends InlineCompletionAdapterBase { - private readonly _cache = new Cache<vscode.InlineCompletionItemNew>('InlineCompletionItemNew'); - private readonly _disposables = new Map<number, DisposableStore>(); + private readonly _references = new ReferenceMap<{ + dispose(): void; + items: readonly vscode.InlineCompletionItemNew[]; + }>(); private readonly isAdditionProposedApiEnabled = isProposedApiEnabled(this.extension, 'inlineCompletionsAdditions'); @@ -1143,7 +1145,7 @@ class InlineCompletionAdapterNew extends InlineCompletionAdapterBase { [languages.InlineCompletionTriggerKind.Explicit]: InlineCompletionTriggerKindNew.Invoke, }; - public override async provideInlineCompletions(resource: URI, position: IPosition, context: languages.InlineCompletionContext, token: CancellationToken): Promise<extHostProtocol.IdentifiableInlineCompletions | undefined> { + override async provideInlineCompletions(resource: URI, position: IPosition, context: languages.InlineCompletionContext, token: CancellationToken): Promise<extHostProtocol.IdentifiableInlineCompletions | undefined> { const doc = this._documents.getDocument(resource); const pos = typeConvert.Position.to(position); @@ -1170,9 +1172,17 @@ class InlineCompletionAdapterNew extends InlineCompletionAdapterBase { } const normalizedResult = isArray(result) ? result : result.items; + const commands = isArray(result) ? [] : result.commands || []; - const pid = this._cache.add(normalizedResult); let disposableStore: DisposableStore | undefined = undefined; + const pid = this._references.createReferenceId({ + dispose() { + if (disposableStore) { + disposableStore.dispose(); + } + }, + items: normalizedResult + }); return { pid, @@ -1181,7 +1191,6 @@ class InlineCompletionAdapterNew extends InlineCompletionAdapterBase { if (item.command) { if (!disposableStore) { disposableStore = new DisposableStore(); - this._disposables.set(pid, disposableStore); } command = this._commands.toInternal(item.command, disposableStore); } @@ -1196,32 +1205,55 @@ class InlineCompletionAdapterNew extends InlineCompletionAdapterBase { completeBracketPairs: this.isAdditionProposedApiEnabled ? item.completeBracketPairs : false }); }), + commands: commands.map(c => { + if (!disposableStore) { + disposableStore = new DisposableStore(); + } + return this._commands.toInternal(c, disposableStore); + }) }; } - public disposeCompletions(pid: number) { - this._cache.delete(pid); - const d = this._disposables.get(pid); - if (d) { - d.clear(); - } - this._disposables.delete(pid); + override disposeCompletions(pid: number) { + const data = this._references.disposeReferenceId(pid); + data?.dispose(); } - public handleDidShowCompletionItem(pid: number, idx: number): void { - const completionItem = this._cache.get(pid, idx); + override handleDidShowCompletionItem(pid: number, idx: number): void { + const completionItem = this._references.get(pid)?.items[idx]; if (completionItem) { - if (this._provider.handleDidShowCompletionItem && isProposedApiEnabled(this.extension, 'inlineCompletionsAdditions')) { + if (this._provider.handleDidShowCompletionItem && this.isAdditionProposedApiEnabled) { this._provider.handleDidShowCompletionItem(completionItem); } } } } +class ReferenceMap<T> { + private readonly _references = new Map<number, T>(); + private _idPool = 1; + + createReferenceId(value: T): number { + const id = this._idPool++; + this._references.set(id, value); + return id; + } + + disposeReferenceId(referenceId: number): T | undefined { + const value = this._references.get(referenceId); + this._references.delete(referenceId); + return value; + } + + get(referenceId: number): T | undefined { + return this._references.get(referenceId); + } +} + export class InlineCompletionController<T extends vscode.InlineCompletionItem> implements vscode.InlineCompletionController<T> { private static readonly map = new WeakMap<vscode.InlineCompletionItemProvider<any>, InlineCompletionController<any>>(); - public static get<T extends vscode.InlineCompletionItem>(provider: vscode.InlineCompletionItemProvider<T>): InlineCompletionController<T> { + static get<T extends vscode.InlineCompletionItem>(provider: vscode.InlineCompletionItemProvider<T>): InlineCompletionController<T> { let existing = InlineCompletionController.map.get(provider); if (!existing) { existing = new InlineCompletionController(); @@ -1231,9 +1263,9 @@ export class InlineCompletionController<T extends vscode.InlineCompletionItem> i } private readonly _onDidShowCompletionItemEmitter = new Emitter<vscode.InlineCompletionItemDidShowEvent<T>>(); - public readonly onDidShowCompletionItem: vscode.Event<vscode.InlineCompletionItemDidShowEvent<T>> = this._onDidShowCompletionItemEmitter.event; + readonly onDidShowCompletionItem: vscode.Event<vscode.InlineCompletionItemDidShowEvent<T>> = this._onDidShowCompletionItemEmitter.event; - public fireOnDidShowCompletionItem(event: vscode.InlineCompletionItemDidShowEvent<T>): void { + fireOnDidShowCompletionItem(event: vscode.InlineCompletionItemDidShowEvent<T>): void { this._onDidShowCompletionItemEmitter.fire(event); } } @@ -1738,7 +1770,7 @@ class DocumentOnDropAdapter { type Adapter = DocumentSymbolAdapter | CodeLensAdapter | DefinitionAdapter | HoverAdapter | DocumentHighlightAdapter | ReferenceAdapter | CodeActionAdapter | DocumentFormattingAdapter | RangeFormattingAdapter | OnTypeFormattingAdapter | NavigateTypeAdapter | RenameAdapter - | SuggestAdapter | SignatureHelpAdapter | LinkProviderAdapter | ImplementationAdapter + | CompletionsAdapter | SignatureHelpAdapter | LinkProviderAdapter | ImplementationAdapter | TypeDefinitionAdapter | ColorProviderAdapter | FoldingProviderAdapter | DeclarationAdapter | SelectionRangeAdapter | CallHierarchyAdapter | TypeHierarchyAdapter | DocumentSemanticTokensAdapter | DocumentRangeSemanticTokensAdapter @@ -2158,21 +2190,21 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF // --- suggestion registerCompletionItemProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.CompletionItemProvider, triggerCharacters: string[]): vscode.Disposable { - const handle = this._addNewAdapter(new SuggestAdapter(this._documents, this._commands.converter, provider, this._apiDeprecation, extension), extension); - this._proxy.$registerSuggestSupport(handle, this._transformDocumentSelector(selector), triggerCharacters, SuggestAdapter.supportsResolving(provider), `${extension.identifier.value}(${triggerCharacters.join('')})`); + const handle = this._addNewAdapter(new CompletionsAdapter(this._documents, this._commands.converter, provider, this._apiDeprecation, extension), extension); + this._proxy.$registerCompletionsProvider(handle, this._transformDocumentSelector(selector), triggerCharacters, CompletionsAdapter.supportsResolving(provider), `${extension.identifier.value}(${triggerCharacters.join('')})`); return this._createDisposable(handle); } $provideCompletionItems(handle: number, resource: UriComponents, position: IPosition, context: languages.CompletionContext, token: CancellationToken): Promise<extHostProtocol.ISuggestResultDto | undefined> { - return this._withAdapter(handle, SuggestAdapter, adapter => adapter.provideCompletionItems(URI.revive(resource), position, context, token), undefined, token); + return this._withAdapter(handle, CompletionsAdapter, adapter => adapter.provideCompletionItems(URI.revive(resource), position, context, token), undefined, token); } $resolveCompletionItem(handle: number, id: extHostProtocol.ChainedCacheId, token: CancellationToken): Promise<extHostProtocol.ISuggestDataDto | undefined> { - return this._withAdapter(handle, SuggestAdapter, adapter => adapter.resolveCompletionItem(id, token), undefined, token); + return this._withAdapter(handle, CompletionsAdapter, adapter => adapter.resolveCompletionItem(id, token), undefined, token); } $releaseCompletionItems(handle: number, id: number): void { - this._withAdapter(handle, SuggestAdapter, adapter => adapter.releaseCompletionItems(id), undefined, undefined); + this._withAdapter(handle, CompletionsAdapter, adapter => adapter.releaseCompletionItems(id), undefined, undefined); } // --- ghost test @@ -2194,13 +2226,13 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF } $handleInlineCompletionDidShow(handle: number, pid: number, idx: number): void { - this._withAdapter(handle, InlineCompletionAdapter, async adapter => { + this._withAdapter(handle, InlineCompletionAdapterBase, async adapter => { adapter.handleDidShowCompletionItem(pid, idx); }, undefined, undefined); } $freeInlineCompletionsList(handle: number, pid: number): void { - this._withAdapter(handle, InlineCompletionAdapter, async adapter => { adapter.disposeCompletions(pid); }, undefined, undefined); + this._withAdapter(handle, InlineCompletionAdapterBase, async adapter => { adapter.disposeCompletions(pid); }, undefined, undefined); } // --- parameter hints diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index 6d68184d52c..7c9cc4c0eb0 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -523,12 +523,12 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { const commandDataToNotebook = new ApiCommand( 'vscode.executeDataToNotebook', '_executeDataToNotebook', 'Invoke notebook serializer', [notebookTypeArg, new ApiCommandArgument<Uint8Array, VSBuffer>('data', 'Bytes to convert to data', v => v instanceof Uint8Array, v => VSBuffer.wrap(v))], - new ApiCommandResult<NotebookDataDto, vscode.NotebookData>('Notebook Data', dto => typeConverters.NotebookData.to(dto)) + new ApiCommandResult<SerializableObjectWithBuffers<NotebookDataDto>, vscode.NotebookData>('Notebook Data', data => typeConverters.NotebookData.to(data.value)) ); const commandNotebookToData = new ApiCommand( 'vscode.executeNotebookToData', '_executeNotebookToData', 'Invoke notebook serializer', - [notebookTypeArg, new ApiCommandArgument<vscode.NotebookData, NotebookDataDto>('NotebookData', 'Notebook data to convert to bytes', v => true, v => typeConverters.NotebookData.from(v))], + [notebookTypeArg, new ApiCommandArgument<vscode.NotebookData, SerializableObjectWithBuffers<NotebookDataDto>>('NotebookData', 'Notebook data to convert to bytes', v => true, v => new SerializableObjectWithBuffers(typeConverters.NotebookData.from(v)))], new ApiCommandResult<VSBuffer, Uint8Array>('Bytes', dto => dto.buffer) ); diff --git a/src/vs/workbench/api/common/extHostNotebookConcatDocument.ts b/src/vs/workbench/api/common/extHostNotebookConcatDocument.ts deleted file mode 100644 index 65ecebc97c1..00000000000 --- a/src/vs/workbench/api/common/extHostNotebookConcatDocument.ts +++ /dev/null @@ -1,192 +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 types from 'vs/workbench/api/common/extHostTypes'; -import * as vscode from 'vscode'; -import { Event, Emitter } from 'vs/base/common/event'; -import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; -import { PrefixSumComputer } from 'vs/editor/common/model/prefixSumComputer'; -import { DisposableStore } from 'vs/base/common/lifecycle'; -import { score } from 'vs/editor/common/languageSelector'; -import { ResourceMap } from 'vs/base/common/map'; -import { URI } from 'vs/base/common/uri'; -import { generateUuid } from 'vs/base/common/uuid'; -import { ExtHostNotebookDocuments } from 'vs/workbench/api/common/extHostNotebookDocuments'; - -export class ExtHostNotebookConcatDocument implements vscode.NotebookConcatTextDocument { - - private _disposables = new DisposableStore(); - private _isClosed = false; - - private _cells!: vscode.NotebookCell[]; - private _cellUris!: ResourceMap<number>; - private _cellLengths!: PrefixSumComputer; - private _cellLines!: PrefixSumComputer; - private _versionId = 0; - - private readonly _onDidChange = new Emitter<void>(); - readonly onDidChange: Event<void> = this._onDidChange.event; - - readonly uri = URI.from({ scheme: 'vscode-concat-doc', path: generateUuid() }); - - constructor( - extHostNotebooks: ExtHostNotebookDocuments, - extHostDocuments: ExtHostDocuments, - private readonly _notebook: vscode.NotebookDocument, - private readonly _selector: vscode.DocumentSelector | undefined, - ) { - this._init(); - - this._disposables.add(extHostDocuments.onDidChangeDocument(e => { - const cellIdx = this._cellUris.get(e.document.uri); - if (cellIdx !== undefined) { - this._cellLengths.setValue(cellIdx, this._cells[cellIdx].document.getText().length + 1); - this._cellLines.setValue(cellIdx, this._cells[cellIdx].document.lineCount); - this._versionId += 1; - this._onDidChange.fire(undefined); - } - })); - const documentChange = (document: vscode.NotebookDocument) => { - if (document === this._notebook) { - this._init(); - this._versionId += 1; - this._onDidChange.fire(undefined); - } - }; - - this._disposables.add(extHostNotebooks.onDidChangeNotebookDocument(e => documentChange(e.notebook))); - } - - dispose(): void { - this._disposables.dispose(); - this._isClosed = true; - } - - get isClosed() { - return this._isClosed; - } - - private _init() { - this._cells = []; - this._cellUris = new ResourceMap(); - const cellLengths: number[] = []; - const cellLineCounts: number[] = []; - for (const cell of this._notebook.getCells()) { - if (cell.kind === types.NotebookCellKind.Code && (!this._selector || score(this._selector, cell.document.uri, cell.document.languageId, true, undefined))) { - this._cellUris.set(cell.document.uri, this._cells.length); - this._cells.push(cell); - cellLengths.push(cell.document.getText().length + 1); - cellLineCounts.push(cell.document.lineCount); - } - } - this._cellLengths = new PrefixSumComputer(new Uint32Array(cellLengths)); - this._cellLines = new PrefixSumComputer(new Uint32Array(cellLineCounts)); - } - - get version(): number { - return this._versionId; - } - - getText(range?: vscode.Range): string { - if (!range) { - let result = ''; - for (const cell of this._cells) { - result += cell.document.getText() + '\n'; - } - // remove last newline again - result = result.slice(0, -1); - return result; - } - - if (range.isEmpty) { - return ''; - } - - // get start and end locations and create substrings - const start = this.locationAt(range.start); - const end = this.locationAt(range.end); - - const startIdx = this._cellUris.get(start.uri); - const endIdx = this._cellUris.get(end.uri); - - if (startIdx === undefined || endIdx === undefined) { - return ''; - } - - if (startIdx === endIdx) { - return this._cells[startIdx].document.getText(new types.Range(start.range.start, end.range.end)); - } - - const parts = [this._cells[startIdx].document.getText(new types.Range(start.range.start, new types.Position(this._cells[startIdx].document.lineCount, 0)))]; - for (let i = startIdx + 1; i < endIdx; i++) { - parts.push(this._cells[i].document.getText()); - } - parts.push(this._cells[endIdx].document.getText(new types.Range(new types.Position(0, 0), end.range.end))); - return parts.join('\n'); - } - - offsetAt(position: vscode.Position): number { - const idx = this._cellLines.getIndexOf(position.line); - const offset1 = this._cellLengths.getPrefixSum(idx.index - 1); - const offset2 = this._cells[idx.index].document.offsetAt(position.with(idx.remainder)); - return offset1 + offset2; - } - - positionAt(locationOrOffset: vscode.Location | number): vscode.Position { - if (typeof locationOrOffset === 'number') { - const idx = this._cellLengths.getIndexOf(locationOrOffset); - const lineCount = this._cellLines.getPrefixSum(idx.index - 1); - return this._cells[idx.index].document.positionAt(idx.remainder).translate(lineCount); - } - - const idx = this._cellUris.get(locationOrOffset.uri); - if (idx !== undefined) { - const line = this._cellLines.getPrefixSum(idx - 1); - return new types.Position(line + locationOrOffset.range.start.line, locationOrOffset.range.start.character); - } - // do better? - // return undefined; - return new types.Position(0, 0); - } - - locationAt(positionOrRange: vscode.Range | vscode.Position): types.Location { - if (!types.Range.isRange(positionOrRange)) { - positionOrRange = new types.Range(<types.Position>positionOrRange, <types.Position>positionOrRange); - } - - const startIdx = this._cellLines.getIndexOf(positionOrRange.start.line); - let endIdx = startIdx; - if (!positionOrRange.isEmpty) { - endIdx = this._cellLines.getIndexOf(positionOrRange.end.line); - } - - const startPos = new types.Position(startIdx.remainder, positionOrRange.start.character); - const endPos = new types.Position(endIdx.remainder, positionOrRange.end.character); - const range = new types.Range(startPos, endPos); - - const startCell = this._cells[startIdx.index]; - return new types.Location(startCell.document.uri, <types.Range>startCell.document.validateRange(range)); - } - - contains(uri: vscode.Uri): boolean { - return this._cellUris.has(uri); - } - - validateRange(range: vscode.Range): vscode.Range { - const start = this.validatePosition(range.start); - const end = this.validatePosition(range.end); - return range.with(start, end); - } - - validatePosition(position: vscode.Position): vscode.Position { - const startIdx = this._cellLines.getIndexOf(position.line); - - const cellPosition = new types.Position(startIdx.remainder, position.character); - const validCellPosition = this._cells[startIdx.index].document.validatePosition(cellPosition); - - const line = this._cellLines.getPrefixSum(startIdx.index - 1); - return new types.Position(line + validCellPosition.line, validCellPosition.character); - } -} diff --git a/src/vs/workbench/api/common/extHostNotebookDocument.ts b/src/vs/workbench/api/common/extHostNotebookDocument.ts index e8cc8e027ff..b77133347e3 100644 --- a/src/vs/workbench/api/common/extHostNotebookDocument.ts +++ b/src/vs/workbench/api/common/extHostNotebookDocument.ts @@ -218,11 +218,11 @@ export class ExtHostNotebookDocument { const result = { notebook: this.apiNotebook, metadata: newMetadata, - cellChanges: <vscode.NotebookDocumentContentCellChange[]>[], + cellChanges: <vscode.NotebookDocumentCellChange[]>[], contentChanges: <vscode.NotebookDocumentContentChange[]>[], }; - type RelaxedCellChange = Partial<vscode.NotebookDocumentContentCellChange> & { cell: vscode.NotebookCell }; + type RelaxedCellChange = Partial<vscode.NotebookDocumentCellChange> & { cell: vscode.NotebookCell }; const relaxedCellChanges: RelaxedCellChange[] = []; // -- apply change and populate content changes diff --git a/src/vs/workbench/api/common/extHostNotebookKernels.ts b/src/vs/workbench/api/common/extHostNotebookKernels.ts index 05550b53475..f99ead59ac7 100644 --- a/src/vs/workbench/api/common/extHostNotebookKernels.ts +++ b/src/vs/workbench/api/common/extHostNotebookKernels.ts @@ -108,7 +108,7 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { const onDidReceiveMessage = new Emitter<{ editor: vscode.NotebookEditor; message: any }>(); const data: INotebookKernelDto2 = { - id: createKernelId(extension, id), + id: createKernelId(extension.identifier, id), notebookType: viewType, extensionId: extension.identifier, extensionLocation: extension.extensionLocation, @@ -218,7 +218,7 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { that._logService.trace(`NotebookController[${handle}] NOT associated to notebook, associated to THESE notebooks:`, Array.from(associatedNotebooks.keys()).map(u => u.toString())); throw new Error(`notebook controller is NOT associated to notebook: ${cell.notebook.uri.toString()}`); } - return that._createNotebookCellExecution(cell, createKernelId(extension, this.id)); + return that._createNotebookCellExecution(cell, createKernelId(extension.identifier, this.id)); }, dispose: () => { if (!isDisposed) { @@ -257,6 +257,15 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { return controller; } + getIdByController(controller: vscode.NotebookController) { + for (const [_, candidate] of this._kernelData) { + if (candidate.controller === controller) { + return createKernelId(candidate.extensionId, controller.id); + } + } + return null; + } + $acceptNotebookAssociation(handle: number, uri: UriComponents, value: boolean): void { const obj = this._kernelData.get(handle); if (obj) { @@ -583,6 +592,6 @@ class TimeoutBasedCollector<T> { } } -function createKernelId(extension: IExtensionDescription, id: string): string { - return `${extension.identifier.value}/${id}`; +export function createKernelId(extensionIdentifier: ExtensionIdentifier, id: string): string { + return `${extensionIdentifier.value}/${id}`; } diff --git a/src/vs/workbench/api/common/extHostNotebookProxyKernels.ts b/src/vs/workbench/api/common/extHostNotebookProxyKernels.ts new file mode 100644 index 00000000000..786101bf360 --- /dev/null +++ b/src/vs/workbench/api/common/extHostNotebookProxyKernels.ts @@ -0,0 +1,157 @@ +/*--------------------------------------------------------------------------------------------- + * 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/extHostOutput.ts b/src/vs/workbench/api/common/extHostOutput.ts index 12e6570161e..5c013fa2534 100644 --- a/src/vs/workbench/api/common/extHostOutput.ts +++ b/src/vs/workbench/api/common/extHostOutput.ts @@ -11,7 +11,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ILogger, ILoggerService } from 'vs/platform/log/common/log'; -import { OutputChannelUpdateMode } from 'vs/workbench/contrib/output/common/output'; +import { OutputChannelUpdateMode } from 'vs/workbench/services/output/common/output'; import { IExtHostConsumerFileSystem } from 'vs/workbench/api/common/extHostFileSystemConsumer'; import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; import { IExtHostFileSystemInfo } from 'vs/workbench/api/common/extHostFileSystemInfo'; diff --git a/src/vs/workbench/api/common/extHostQuickOpen.ts b/src/vs/workbench/api/common/extHostQuickOpen.ts index 0df03b79137..a3c48d6cc54 100644 --- a/src/vs/workbench/api/common/extHostQuickOpen.ts +++ b/src/vs/workbench/api/common/extHostQuickOpen.ts @@ -3,16 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { asPromise } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter } from 'vs/base/common/event'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import { IExtHostWorkspaceProvider } from 'vs/workbench/api/common/extHostWorkspace'; -import type { InputBox, InputBoxOptions, InputBoxValidationSeverity, QuickInput, QuickInputButton, QuickPick, QuickPickItem, QuickPickItemButtonEvent, QuickPickOptions, WorkspaceFolder, WorkspaceFolderPickOptions } from 'vscode'; +import { InputBox, InputBoxOptions, InputBoxValidationMessage, QuickInput, QuickInputButton, QuickPick, QuickPickItem, QuickPickItemButtonEvent, QuickPickOptions, WorkspaceFolder, WorkspaceFolderPickOptions } from 'vscode'; import { ExtHostQuickOpenShape, IMainContext, MainContext, TransferQuickInput, TransferQuickInputButton, TransferQuickPickItemOrSeparator } from './extHost.protocol'; import { URI } from 'vs/base/common/uri'; -import { ThemeIcon, QuickInputButtons, QuickPickItemKind } from 'vs/workbench/api/common/extHostTypes'; +import { ThemeIcon, QuickInputButtons, QuickPickItemKind, InputBoxValidationSeverity } from 'vs/workbench/api/common/extHostTypes'; import { isCancellationError } from 'vs/base/common/errors'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { coalesce } from 'vs/base/common/arrays'; @@ -46,7 +45,7 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx private _commands: ExtHostCommands; private _onDidSelectItem?: (handle: number) => void; - private _validateInput?: (input: string) => string | { content: string; severity: Severity } | undefined | null | Thenable<string | { content: string; severity: Severity } | undefined | null>; + private _validateInput?: (input: string) => string | InputBoxValidationMessage | undefined | null | Thenable<string | InputBoxValidationMessage | undefined | null>; private _sessions = new Map<number, ExtHostQuickInput>(); @@ -148,7 +147,7 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx showInput(options?: InputBoxOptions, token: CancellationToken = CancellationToken.None): Promise<string | undefined> { // global validate fn used in callback below - this._validateInput = options ? options.validateInput : undefined; + this._validateInput = options ? options.validateInput2 ?? options.validateInput : undefined; return proxy.$input(options, typeof this._validateInput === 'function', token) .then(undefined, err => { @@ -160,11 +159,36 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx }); } - $validateInput(input: string): Promise<string | { content: string; severity: Severity } | null | undefined> { - if (this._validateInput) { - return asPromise(() => this._validateInput!(input)); + async $validateInput(input: string): Promise<string | { content: string; severity: Severity } | null | undefined> { + if (!this._validateInput) { + return; + } + + const result = await this._validateInput(input); + if (!result || typeof result === 'string') { + return result; } - return Promise.resolve(undefined); + + let severity: Severity; + switch (result.severity) { + case InputBoxValidationSeverity.Info: + severity = Severity.Info; + break; + case InputBoxValidationSeverity.Warning: + severity = Severity.Warning; + break; + case InputBoxValidationSeverity.Error: + severity = Severity.Error; + break; + default: + severity = result.message ? Severity.Error : Severity.Ignore; + break; + } + + return { + content: result.message, + severity + }; } // ---- workspace folder picker @@ -675,7 +699,7 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx private _password = false; private _prompt: string | undefined; private _validationMessage: string | undefined; - private _validationMessage2: string | { content: string; severity: InputBoxValidationSeverity } | undefined; + private _validationMessage2: string | InputBoxValidationMessage | undefined; constructor(private readonly extension: IExtensionDescription, onDispose: () => void) { super(extension.identifier, onDispose); @@ -713,7 +737,7 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx return this._validationMessage2; } - set validationMessage2(validationMessage: string | { content: string; severity: InputBoxValidationSeverity } | undefined) { + set validationMessage2(validationMessage: string | InputBoxValidationMessage | undefined) { checkProposedApiEnabled(this.extension, 'inputBoxSeverity'); this._validationMessage2 = validationMessage; if (!validationMessage) { @@ -721,7 +745,7 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx } else if (typeof validationMessage === 'string') { this.update({ validationMessage, severity: Severity.Error }); } else { - this.update({ validationMessage: validationMessage.content, severity: validationMessage.severity ?? Severity.Error }); + this.update({ validationMessage: validationMessage.message, severity: validationMessage.severity ?? Severity.Error }); } } } diff --git a/src/vs/workbench/api/common/extHostRequireInterceptor.ts b/src/vs/workbench/api/common/extHostRequireInterceptor.ts index 9ce9a8f402f..4a072952392 100644 --- a/src/vs/workbench/api/common/extHostRequireInterceptor.ts +++ b/src/vs/workbench/api/common/extHostRequireInterceptor.ts @@ -4,19 +4,17 @@ *--------------------------------------------------------------------------------------------*/ import * as performance from 'vs/base/common/performance'; -import { TernarySearchTree } from 'vs/base/common/map'; import { URI } from 'vs/base/common/uri'; import { MainThreadTelemetryShape, MainContext } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostConfigProvider, IExtHostConfiguration } from 'vs/workbench/api/common/extHostConfiguration'; import { nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; -import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; import * as vscode from 'vscode'; -import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { IExtensionApiFactory } from 'vs/workbench/api/common/extHost.api.impl'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { IExtensionApiFactory, IExtensionRegistries } from 'vs/workbench/api/common/extHost.api.impl'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; +import { ExtensionPaths, IExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; import { platform } from 'vs/base/common/process'; import { ILogService } from 'vs/platform/log/common/log'; import { escapeRegExpCharacters } from 'vs/base/common/strings'; @@ -42,7 +40,7 @@ export abstract class RequireInterceptor { constructor( private _apiFactory: IExtensionApiFactory, - private _extensionRegistry: ExtensionDescriptionRegistry, + private _extensionRegistry: IExtensionRegistries, @IInstantiationService private readonly _instaService: IInstantiationService, @IExtHostConfiguration private readonly _extHostConfiguration: IExtHostConfiguration, @IExtHostExtensionService private readonly _extHostExtensionService: IExtHostExtensionService, @@ -156,8 +154,8 @@ class VSCodeNodeModuleFactory implements INodeModuleFactory { constructor( private readonly _apiFactory: IExtensionApiFactory, - private readonly _extensionPaths: TernarySearchTree<URI, IExtensionDescription>, - private readonly _extensionRegistry: ExtensionDescriptionRegistry, + private readonly _extensionPaths: ExtensionPaths, + private readonly _extensionRegistry: IExtensionRegistries, private readonly _configProvider: ExtHostConfigProvider, private readonly _logService: ILogService, ) { @@ -208,7 +206,7 @@ class KeytarNodeModuleFactory implements INodeModuleFactory { private _impl: IKeytarModule; constructor( - private readonly _extensionPaths: TernarySearchTree<URI, IExtensionDescription>, + private readonly _extensionPaths: ExtensionPaths, @IExtHostRpcService rpcService: IExtHostRpcService, @IExtHostInitDataService initData: IExtHostInitDataService, @@ -303,7 +301,7 @@ class OpenNodeModuleFactory implements INodeModuleFactory { private _mainThreadTelemetry: MainThreadTelemetryShape; constructor( - private readonly _extensionPaths: TernarySearchTree<URI, IExtensionDescription>, + private readonly _extensionPaths: ExtensionPaths, private readonly _appUriScheme: string, @IExtHostRpcService rpcService: IExtHostRpcService, ) { diff --git a/src/vs/workbench/api/common/extHostRpcService.ts b/src/vs/workbench/api/common/extHostRpcService.ts index 322fa65b374..c6df14903e4 100644 --- a/src/vs/workbench/api/common/extHostRpcService.ts +++ b/src/vs/workbench/api/common/extHostRpcService.ts @@ -17,12 +17,14 @@ export class ExtHostRpcService implements IExtHostRpcService { readonly getProxy: <T>(identifier: ProxyIdentifier<T>) => Proxied<T>; readonly set: <T, R extends T> (identifier: ProxyIdentifier<T>, instance: R) => R; + readonly dispose: () => void; readonly assertRegistered: (identifiers: ProxyIdentifier<any>[]) => void; readonly drain: () => Promise<void>; constructor(rpcProtocol: IRPCProtocol) { this.getProxy = rpcProtocol.getProxy.bind(rpcProtocol); this.set = rpcProtocol.set.bind(rpcProtocol); + this.dispose = rpcProtocol.dispose.bind(rpcProtocol); this.assertRegistered = rpcProtocol.assertRegistered.bind(rpcProtocol); this.drain = rpcProtocol.drain.bind(rpcProtocol); } diff --git a/src/vs/workbench/api/common/extHostTask.ts b/src/vs/workbench/api/common/extHostTask.ts index 37758d0d220..00d03d0b6ee 100644 --- a/src/vs/workbench/api/common/extHostTask.ts +++ b/src/vs/workbench/api/common/extHostTask.ts @@ -320,7 +320,7 @@ export namespace TaskDTO { result.group = types.TaskGroup.from(value.group._id); if (result.group && value.group.isDefault) { result.group = new types.TaskGroup(result.group.id, result.group.label); - if (value.group.isDefault) { + if (value.group.isDefault === true) { result.group.isDefault = value.group.isDefault; } } diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index 12643cef941..1aa1e678e00 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -38,7 +38,7 @@ export interface IExtHostTerminalService extends ExtHostTerminalServiceShape, ID onDidChangeTerminalState: Event<vscode.Terminal>; onDidWriteTerminalData: Event<vscode.TerminalDataWriteEvent>; - createTerminal(name?: string, shellPath?: string, shellArgs?: string[] | string): vscode.Terminal; + createTerminal(name?: string, shellPath?: string, shellArgs?: readonly string[] | string): vscode.Terminal; createTerminalFromOptions(options: vscode.TerminalOptions, internalOptions?: ITerminalInternalOptions): vscode.Terminal; createExtensionTerminal(options: vscode.ExtensionTerminalOptions): vscode.Terminal; attachPtyToTerminal(id: number, pty: vscode.Pseudoterminal): void; diff --git a/src/vs/workbench/api/common/extHostTimeline.ts b/src/vs/workbench/api/common/extHostTimeline.ts index baab688ad31..c9d9e611649 100644 --- a/src/vs/workbench/api/common/extHostTimeline.ts +++ b/src/vs/workbench/api/common/extHostTimeline.ts @@ -7,7 +7,7 @@ import * as vscode from 'vscode'; import { UriComponents, URI } from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ExtHostTimelineShape, MainThreadTimelineShape, IMainContext, MainContext } from 'vs/workbench/api/common/extHost.protocol'; -import { Timeline, TimelineItem, TimelineOptions, TimelineProvider, InternalTimelineOptions } from 'vs/workbench/contrib/timeline/common/timeline'; +import { Timeline, TimelineItem, TimelineOptions, TimelineProvider } from 'vs/workbench/contrib/timeline/common/timeline'; import { IDisposable, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { CancellationToken } from 'vs/base/common/cancellation'; import { CommandsConverter, ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; @@ -19,7 +19,7 @@ import { isString } from 'vs/base/common/types'; export interface IExtHostTimeline extends ExtHostTimelineShape { readonly _serviceBrand: undefined; - $getTimeline(id: string, uri: UriComponents, options: vscode.TimelineOptions, token: vscode.CancellationToken, internalOptions?: InternalTimelineOptions): Promise<Timeline | undefined>; + $getTimeline(id: string, uri: UriComponents, options: vscode.TimelineOptions, token: vscode.CancellationToken): Promise<Timeline | undefined>; } export const IExtHostTimeline = createDecorator<IExtHostTimeline>('IExtHostTimeline'); @@ -51,9 +51,9 @@ export class ExtHostTimeline implements IExtHostTimeline { }); } - async $getTimeline(id: string, uri: UriComponents, options: vscode.TimelineOptions, token: vscode.CancellationToken, internalOptions?: InternalTimelineOptions): Promise<Timeline | undefined> { + async $getTimeline(id: string, uri: UriComponents, options: vscode.TimelineOptions, token: vscode.CancellationToken): Promise<Timeline | undefined> { const provider = this._providers.get(id); - return provider?.provideTimeline(URI.revive(uri), options, token, internalOptions); + return provider?.provideTimeline(URI.revive(uri), options, token); } registerTimelineProvider(scheme: string | string[], provider: vscode.TimelineProvider, _extensionId: ExtensionIdentifier, commandConverter: CommandsConverter): IDisposable { @@ -71,8 +71,8 @@ export class ExtHostTimeline implements IExtHostTimeline { ...provider, scheme: scheme, onDidChange: undefined, - async provideTimeline(uri: URI, options: TimelineOptions, token: CancellationToken, internalOptions?: InternalTimelineOptions) { - if (internalOptions?.resetCache) { + async provideTimeline(uri: URI, options: TimelineOptions, token: CancellationToken) { + if (options?.resetCache) { timelineDisposables.clear(); // For now, only allow the caching of a single Uri @@ -87,7 +87,7 @@ export class ExtHostTimeline implements IExtHostTimeline { // TODO: Should we bother converting all the data if we aren't caching? Meaning it is being requested by an extension? - const convertItem = convertTimelineItem(uri, internalOptions); + const convertItem = convertTimelineItem(uri, options); return { ...result, source: provider.id, @@ -106,7 +106,7 @@ export class ExtHostTimeline implements IExtHostTimeline { } private convertTimelineItem(source: string, commandConverter: CommandsConverter, disposables: DisposableStore) { - return (uri: URI, options?: InternalTimelineOptions) => { + return (uri: URI, options?: TimelineOptions) => { let items: Map<string, vscode.TimelineItem> | undefined; if (options?.cacheResults) { let itemsByUri = this._itemsBySourceAndUriMap.get(source); diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 9dec8f6b163..979eebec86c 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -7,6 +7,7 @@ import { asArray, coalesce, isNonEmptyArray } from 'vs/base/common/arrays'; import { VSBuffer } from 'vs/base/common/buffer'; import * as htmlContent from 'vs/base/common/htmlContent'; import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ResourceSet } from 'vs/base/common/map'; import { marked } from 'vs/base/common/marked/marked'; import { parse } from 'vs/base/common/marshalling'; import { cloneAndChange } from 'vs/base/common/objects'; @@ -580,7 +581,17 @@ export namespace WorkspaceEdit { }; if (value instanceof types.WorkspaceEdit) { - for (let entry of value._allEntries()) { + + // collect all files that are to be created so that their version + // information (in case they exist as text model already) can be ignored + const toCreate = new ResourceSet(); + for (const entry of value._allEntries()) { + if (entry._type === types.FileEditType.File && URI.isUri(entry.to) && entry.from === undefined) { + toCreate.add(entry.to); + } + } + + for (const entry of value._allEntries()) { if (entry._type === types.FileEditType.File) { // file operation @@ -598,7 +609,7 @@ export namespace WorkspaceEdit { _type: extHostProtocol.WorkspaceEditType.Text, resource: entry.uri, edit: TextEdit.from(entry.edit), - modelVersionId: versionInfo?.getTextDocumentVersion(entry.uri), + modelVersionId: !toCreate.has(entry.uri) ? versionInfo?.getTextDocumentVersion(entry.uri) : undefined, metadata: entry.metadata }); } else if (entry._type === types.FileEditType.Cell) { @@ -1480,7 +1491,8 @@ export namespace LanguageSelector { language: filter.language, scheme: filter.scheme, pattern: GlobPattern.from(filter.pattern), - exclusive: filter.exclusive + exclusive: filter.exclusive, + notebookType: filter.notebookType }; } } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index ca19cdc010c..a69e00f05ec 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -1594,7 +1594,10 @@ export class CompletionList { @es5ClassCompat export class InlineSuggestion implements vscode.InlineCompletionItem { - insertText?: string; + + insertText?: string | SnippetString; + + filterText?: string; /** * @deprecated Use `insertText` instead. Will be removed eventually. @@ -1604,7 +1607,7 @@ export class InlineSuggestion implements vscode.InlineCompletionItem { range?: Range; command?: vscode.Command; - constructor(insertText: string, range?: Range, command?: vscode.Command) { + constructor(insertText: string | SnippetString, range?: Range, command?: vscode.Command) { this.insertText = insertText; this.range = range; this.command = command; @@ -1640,8 +1643,11 @@ export class InlineSuggestionNew implements vscode.InlineCompletionItemNew { export class InlineSuggestionsNew implements vscode.InlineCompletionListNew { items: vscode.InlineCompletionItemNew[]; - constructor(items: vscode.InlineCompletionItemNew[]) { + commands: vscode.Command[] | undefined; + + constructor(items: vscode.InlineCompletionItemNew[], commands?: vscode.Command[]) { this.items = items; + this.commands = commands; } } diff --git a/src/vs/workbench/api/common/extHostVariableResolverService.ts b/src/vs/workbench/api/common/extHostVariableResolverService.ts new file mode 100644 index 00000000000..7455de15e22 --- /dev/null +++ b/src/vs/workbench/api/common/extHostVariableResolverService.ts @@ -0,0 +1,167 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Lazy } from 'vs/base/common/lazy'; +import { Disposable } from 'vs/base/common/lifecycle'; +import * as path from 'vs/base/common/path'; +import * as process from 'vs/base/common/process'; +import { URI } from 'vs/base/common/uri'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; +import { IExtHostEditorTabs } from 'vs/workbench/api/common/extHostEditorTabs'; +import { IExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; +import { CustomEditorTabInput, NotebookDiffEditorTabInput, NotebookEditorTabInput, TextDiffTabInput, TextTabInput } from 'vs/workbench/api/common/extHostTypes'; +import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; +import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; +import { AbstractVariableResolverService } from 'vs/workbench/services/configurationResolver/common/variableResolver'; +import * as vscode from 'vscode'; +import { ExtHostConfigProvider, IExtHostConfiguration } from './extHostConfiguration'; + +export interface IExtHostVariableResolverProvider { + readonly _serviceBrand: undefined; + getResolver(): Promise<IConfigurationResolverService>; +} + +export const IExtHostVariableResolverProvider = createDecorator<IExtHostVariableResolverProvider>('IExtHostVariableResolverProvider'); + +interface DynamicContext { + folders: vscode.WorkspaceFolder[]; +} + +class ExtHostVariableResolverService extends AbstractVariableResolverService { + + constructor( + extensionService: IExtHostExtensionService, + workspaceService: IExtHostWorkspace, + editorService: IExtHostDocumentsAndEditors, + editorTabs: IExtHostEditorTabs, + configProvider: ExtHostConfigProvider, + context: DynamicContext, + homeDir: string | undefined, + ) { + function getActiveUri(): URI | undefined { + if (editorService) { + const activeEditor = editorService.activeEditor(); + if (activeEditor) { + return activeEditor.document.uri; + } + const activeTab = editorTabs.tabGroups.all.find(group => group.isActive)?.activeTab; + if (activeTab !== undefined) { + // Resolve a resource from the tab + if (activeTab.input instanceof TextDiffTabInput || activeTab.input instanceof NotebookDiffEditorTabInput) { + return activeTab.input.modified; + } else if (activeTab.input instanceof TextTabInput || activeTab.input instanceof NotebookEditorTabInput || activeTab.input instanceof CustomEditorTabInput) { + return activeTab.input.uri; + } + } + } + return undefined; + } + + super({ + getFolderUri: (folderName: string): URI | undefined => { + const found = context.folders.filter(f => f.name === folderName); + if (found && found.length > 0) { + return found[0].uri; + } + return undefined; + }, + getWorkspaceFolderCount: (): number => { + return context.folders.length; + }, + getConfigurationValue: (folderUri: URI | undefined, section: string): string | undefined => { + return configProvider.getConfiguration(undefined, folderUri).get<string>(section); + }, + getAppRoot: (): string | undefined => { + return process.cwd(); + }, + getExecPath: (): string | undefined => { + return process.env['VSCODE_EXEC_PATH']; + }, + getFilePath: (): string | undefined => { + const activeUri = getActiveUri(); + if (activeUri) { + return path.normalize(activeUri.fsPath); + } + return undefined; + }, + getWorkspaceFolderPathForFile: (): string | undefined => { + if (workspaceService) { + const activeUri = getActiveUri(); + if (activeUri) { + const ws = workspaceService.getWorkspaceFolder(activeUri); + if (ws) { + return path.normalize(ws.uri.fsPath); + } + } + } + return undefined; + }, + getSelectedText: (): string | undefined => { + if (editorService) { + const activeEditor = editorService.activeEditor(); + if (activeEditor && !activeEditor.selection.isEmpty) { + return activeEditor.document.getText(activeEditor.selection); + } + } + return undefined; + }, + getLineNumber: (): string | undefined => { + if (editorService) { + const activeEditor = editorService.activeEditor(); + if (activeEditor) { + return String(activeEditor.selection.end.line + 1); + } + } + return undefined; + }, + getExtension: (id) => { + return extensionService.getExtension(id); + }, + }, undefined, homeDir ? Promise.resolve(homeDir) : undefined, Promise.resolve(process.env)); + } +} + +export class ExtHostVariableResolverProviderService extends Disposable implements IExtHostVariableResolverProvider { + declare readonly _serviceBrand: undefined; + + private _resolver = new Lazy(async () => { + const configProvider = await this.configurationService.getConfigProvider(); + const folders = await this.workspaceService.getWorkspaceFolders2() || []; + + const dynamic: DynamicContext = { folders }; + this._register(this.workspaceService.onDidChangeWorkspace(async e => { + dynamic.folders = await this.workspaceService.getWorkspaceFolders2() || []; + })); + + return new ExtHostVariableResolverService( + this.extensionService, + this.workspaceService, + this.editorService, + this.editorTabs, + configProvider, + dynamic, + this.homeDir(), + ); + }); + + constructor( + @IExtHostExtensionService private readonly extensionService: IExtHostExtensionService, + @IExtHostWorkspace private readonly workspaceService: IExtHostWorkspace, + @IExtHostDocumentsAndEditors private readonly editorService: IExtHostDocumentsAndEditors, + @IExtHostConfiguration private readonly configurationService: IExtHostConfiguration, + @IExtHostEditorTabs private readonly editorTabs: IExtHostEditorTabs, + ) { + super(); + } + + public getResolver(): Promise<IConfigurationResolverService> { + return this._resolver.getValue(); + } + + protected homeDir(): string | undefined { + return undefined; + } +} diff --git a/src/vs/workbench/api/common/extHostWebviewPanels.ts b/src/vs/workbench/api/common/extHostWebviewPanels.ts index 2f2e575ed76..189d350428b 100644 --- a/src/vs/workbench/api/common/extHostWebviewPanels.ts +++ b/src/vs/workbench/api/common/extHostWebviewPanels.ts @@ -32,7 +32,7 @@ class ExtHostWebviewPanel extends Disposable implements vscode.WebviewPanel { #iconPath?: IconPath; #viewColumn: vscode.ViewColumn | undefined = undefined; #visible: boolean = true; - #active: boolean = true; + #active: boolean; #isDisposed: boolean = false; readonly #onDidDispose = this._register(new Emitter<void>()); @@ -44,20 +44,24 @@ class ExtHostWebviewPanel extends Disposable implements vscode.WebviewPanel { constructor( handle: extHostProtocol.WebviewHandle, proxy: extHostProtocol.MainThreadWebviewPanelsShape, - viewType: string, - title: string, - viewColumn: vscode.ViewColumn | undefined, - panelOptions: vscode.WebviewPanelOptions, - webview: ExtHostWebview + webview: ExtHostWebview, + params: { + viewType: string; + title: string; + viewColumn: vscode.ViewColumn | undefined; + panelOptions: vscode.WebviewPanelOptions; + active: boolean; + } ) { super(); this.#handle = handle; this.#proxy = proxy; - this.#viewType = viewType; - this.#options = panelOptions; - this.#viewColumn = viewColumn; - this.#title = title; this.#webview = webview; + this.#viewType = params.viewType; + this.#options = params.panelOptions; + this.#viewColumn = params.viewColumn; + this.#title = params.title; + this.#active = params.active; } public override dispose() { @@ -209,7 +213,7 @@ export class ExtHostWebviewPanels implements extHostProtocol.ExtHostWebviewPanel }, webviewShowOptions); const webview = this.webviews.createNewWebview(handle, options, extension); - const panel = this.createNewWebviewPanel(handle, viewType, title, viewColumn, options, webview); + const panel = this.createNewWebviewPanel(handle, viewType, title, viewColumn, options, webview, true); return panel; } @@ -283,6 +287,7 @@ export class ExtHostWebviewPanels implements extHostProtocol.ExtHostWebviewPanel state: any; webviewOptions: extHostProtocol.IWebviewContentOptions; panelOptions: extHostProtocol.IWebviewPanelOptions; + active: boolean; }, position: EditorGroupColumn ): Promise<void> { @@ -293,12 +298,12 @@ export class ExtHostWebviewPanels implements extHostProtocol.ExtHostWebviewPanel const { serializer, extension } = entry; const webview = this.webviews.createNewWebview(webviewHandle, initData.webviewOptions, extension); - const revivedPanel = this.createNewWebviewPanel(webviewHandle, viewType, initData.title, position, initData.panelOptions, webview); + const revivedPanel = this.createNewWebviewPanel(webviewHandle, viewType, initData.title, position, initData.panelOptions, webview, initData.active); await serializer.deserializeWebviewPanel(revivedPanel, initData.state); } - public createNewWebviewPanel(webviewHandle: string, viewType: string, title: string, position: vscode.ViewColumn, options: extHostProtocol.IWebviewPanelOptions, webview: ExtHostWebview) { - const panel = new ExtHostWebviewPanel(webviewHandle, this._proxy, viewType, title, position, options, webview); + public createNewWebviewPanel(webviewHandle: string, viewType: string, title: string, position: vscode.ViewColumn, options: extHostProtocol.IWebviewPanelOptions, webview: ExtHostWebview, active: boolean) { + const panel = new ExtHostWebviewPanel(webviewHandle, this._proxy, webview, { viewType, title, viewColumn: position, panelOptions: options, active }); this._webviewPanels.set(webviewHandle, panel); return panel; } diff --git a/src/vs/workbench/api/common/extHostWebviewView.ts b/src/vs/workbench/api/common/extHostWebviewView.ts index 884e05f44a3..fbce7f6e892 100644 --- a/src/vs/workbench/api/common/extHostWebviewView.ts +++ b/src/vs/workbench/api/common/extHostWebviewView.ts @@ -7,7 +7,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { ExtHostWebview, ExtHostWebviews, toExtensionData } from 'vs/workbench/api/common/extHostWebview'; +import { ExtHostWebview, ExtHostWebviews, toExtensionData, shouldSerializeBuffersForPostMessage } from 'vs/workbench/api/common/extHostWebview'; import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import { ViewBadge } from 'vs/workbench/api/common/extHostTypeConverters'; import type * as vscode from 'vscode'; @@ -173,7 +173,7 @@ export class ExtHostWebviewViews implements extHostProtocol.ExtHostWebviewViewsS this._viewProviders.set(viewType, { provider, extension }); this._proxy.$registerWebviewViewProvider(toExtensionData(extension), viewType, { retainContextWhenHidden: webviewOptions?.retainContextWhenHidden, - serializeBuffersForPostMessage: false, + serializeBuffersForPostMessage: shouldSerializeBuffersForPostMessage(extension), }); return new extHostTypes.Disposable(() => { diff --git a/src/vs/workbench/api/common/extensionHostMain.ts b/src/vs/workbench/api/common/extensionHostMain.ts index 80f8d74b967..0eabc9e29cc 100644 --- a/src/vs/workbench/api/common/extensionHostMain.ts +++ b/src/vs/workbench/api/common/extensionHostMain.ts @@ -3,10 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { timeout } from 'vs/base/common/async'; import * as errors from 'vs/base/common/errors'; import * as performance from 'vs/base/common/performance'; -import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { IURITransformer } from 'vs/base/common/uriIpc'; import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; @@ -23,7 +21,6 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IExtHostRpcService, ExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { IURITransformerService, URITransformerService } from 'vs/workbench/api/common/extHostUriTransformerService'; import { IExtHostExtensionService, IHostUtils } from 'vs/workbench/api/common/extHostExtensionService'; -import { IExtHostTerminalService } from 'vs/workbench/api/common/extHostTerminalService'; export interface IExitFn { (code?: number): any; @@ -35,12 +32,10 @@ export interface IConsolePatchFn { export class ExtensionHostMain { - private _isTerminating: boolean; private readonly _hostUtils: IHostUtils; private readonly _rpcProtocol: RPCProtocol; private readonly _extensionService: IExtHostExtensionService; private readonly _logService: ILogService; - private readonly _disposables = new DisposableStore(); constructor( protocol: IMessagePassingProtocol, @@ -49,7 +44,6 @@ export class ExtensionHostMain { uriTransformer: IURITransformer | null, messagePorts?: ReadonlyMap<string, MessagePort> ) { - this._isTerminating = false; this._hostUtils = hostUtils; this._rpcProtocol = new RPCProtocol(protocol, null, uriTransformer); @@ -66,9 +60,6 @@ export class ExtensionHostMain { const instaService: IInstantiationService = new InstantiationService(services, true); // ugly self - inject - this._disposables.add(instaService.invokeFunction(accessor => accessor.get(IExtHostTerminalService))); - this._disposables.add(instaService.invokeFunction(accessor => accessor.get(IExtHostExtensionService))); - this._logService = instaService.invokeFunction(accessor => accessor.get(ILogService)); performance.mark(`code/extHost/didCreateServices`); @@ -125,40 +116,11 @@ export class ExtensionHostMain { } terminate(reason: string): void { - if (this._isTerminating) { - // we are already shutting down... - return; - } - this._isTerminating = true; - this._logService.info(`extension host terminating: ${reason}`); - this._logService.flush(); - - this._disposables.dispose(); - - errors.setUnexpectedErrorHandler((err) => { - this._logService.error(err); - }); - - // Invalidate all proxies - this._rpcProtocol.dispose(); - - const extensionsDeactivated = this._extensionService.deactivateAll(); - - // Give extensions at most 5 seconds to wrap up any async deactivate, then exit - Promise.race([timeout(5000), extensionsDeactivated]).finally(() => { - if (this._hostUtils.pid) { - this._logService.info(`Extension host with pid ${this._hostUtils.pid} exiting with code 0`); - } else { - this._logService.info(`Extension host exiting with code 0`); - } - this._logService.flush(); - this._logService.dispose(); - this._hostUtils.exit(0); - }); + this._extensionService.terminate(reason); } private static _transform(initData: IExtensionHostInitData, rpcProtocol: RPCProtocol): IExtensionHostInitData { - initData.extensions.forEach((ext) => (<any>ext).extensionLocation = URI.revive(rpcProtocol.transformIncomingURIs(ext.extensionLocation))); + initData.allExtensions.forEach((ext) => (<any>ext).extensionLocation = URI.revive(rpcProtocol.transformIncomingURIs(ext.extensionLocation))); initData.environment.appRoot = URI.revive(rpcProtocol.transformIncomingURIs(initData.environment.appRoot)); const extDevLocs = initData.environment.extensionDevelopmentLocationURI; if (extDevLocs) { diff --git a/src/vs/workbench/api/node/extHost.node.services.ts b/src/vs/workbench/api/node/extHost.node.services.ts index 5bc2abbca64..0cbd7e57c22 100644 --- a/src/vs/workbench/api/node/extHost.node.services.ts +++ b/src/vs/workbench/api/node/extHost.node.services.ts @@ -20,6 +20,8 @@ import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePa import { ExtensionStoragePaths } from 'vs/workbench/api/node/extHostStoragePaths'; import { ExtHostLoggerService } from 'vs/workbench/api/node/extHostLoggerService'; import { ILoggerService } from 'vs/platform/log/common/log'; +import { NodeExtHostVariableResolverProviderService } from 'vs/workbench/api/node/extHostVariableResolverService'; +import { IExtHostVariableResolverProvider } from 'vs/workbench/api/common/extHostVariableResolverService'; // ######################################################################### // ### ### @@ -36,3 +38,4 @@ registerSingleton(IExtHostSearch, NativeExtHostSearch); registerSingleton(IExtHostTask, ExtHostTask); registerSingleton(IExtHostTerminalService, ExtHostTerminalService); registerSingleton(IExtHostTunnelService, ExtHostTunnelService); +registerSingleton(IExtHostVariableResolverProvider, NodeExtHostVariableResolverProviderService); diff --git a/src/vs/workbench/api/node/extHostDebugService.ts b/src/vs/workbench/api/node/extHostDebugService.ts index b235b34a8fc..d2127334678 100644 --- a/src/vs/workbench/api/node/extHostDebugService.ts +++ b/src/vs/workbench/api/node/extHostDebugService.ts @@ -3,31 +3,29 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; -import type * as vscode from 'vscode'; -import { homedir } from 'os'; +import { createCancelablePromise, firstParallel } from 'vs/base/common/async'; +import { IDisposable } from 'vs/base/common/lifecycle'; import * as platform from 'vs/base/common/platform'; +import * as nls from 'vs/nls'; +import { IExternalTerminalService } from 'vs/platform/externalTerminal/common/externalTerminal'; +import { LinuxExternalTerminalService, MacExternalTerminalService, WindowsExternalTerminalService } from 'vs/platform/externalTerminal/node/externalTerminalService'; +import { ISignService } from 'vs/platform/sign/common/sign'; +import { SignService } from 'vs/platform/sign/node/signService'; +import { ExtHostDebugServiceBase, ExtHostDebugSession } from 'vs/workbench/api/common/extHostDebugService'; +import { IExtHostEditorTabs } from 'vs/workbench/api/common/extHostEditorTabs'; +import { IExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; +import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; +import { IExtHostTerminalService } from 'vs/workbench/api/common/extHostTerminalService'; import { DebugAdapterExecutable, ThemeIcon } from 'vs/workbench/api/common/extHostTypes'; -import { ExecutableDebugAdapter, SocketDebugAdapter, NamedPipeDebugAdapter } from 'vs/workbench/contrib/debug/node/debugAdapter'; -import { AbstractDebugAdapter } from 'vs/workbench/contrib/debug/common/abstractDebugAdapter'; +import { IExtHostVariableResolverProvider } from 'vs/workbench/api/common/extHostVariableResolverService'; import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; -import { IExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; -import { IExtHostDocumentsAndEditors, ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; +import { AbstractDebugAdapter } from 'vs/workbench/contrib/debug/common/abstractDebugAdapter'; import { IAdapterDescriptor } from 'vs/workbench/contrib/debug/common/debug'; -import { IExtHostConfiguration, ExtHostConfigProvider } from '../common/extHostConfiguration'; -import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; -import { IExtHostTerminalService } from 'vs/workbench/api/common/extHostTerminalService'; -import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; -import { ExtHostDebugServiceBase, ExtHostDebugSession, ExtHostVariableResolverService } from 'vs/workbench/api/common/extHostDebugService'; -import { ISignService } from 'vs/platform/sign/common/sign'; -import { SignService } from 'vs/platform/sign/node/signService'; -import { IDisposable } from 'vs/base/common/lifecycle'; -import { AbstractVariableResolverService } from 'vs/workbench/services/configurationResolver/common/variableResolver'; -import { createCancelablePromise, firstParallel } from 'vs/base/common/async'; +import { ExecutableDebugAdapter, NamedPipeDebugAdapter, SocketDebugAdapter } from 'vs/workbench/contrib/debug/node/debugAdapter'; import { hasChildProcesses, prepareCommand } from 'vs/workbench/contrib/debug/node/terminals'; -import { IExtHostEditorTabs } from 'vs/workbench/api/common/extHostEditorTabs'; -import { IExternalTerminalService } from 'vs/platform/externalTerminal/common/externalTerminal'; -import { WindowsExternalTerminalService, MacExternalTerminalService, LinuxExternalTerminalService } from 'vs/platform/externalTerminal/node/externalTerminalService'; +import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; +import type * as vscode from 'vscode'; +import { ExtHostConfigProvider, IExtHostConfiguration } from '../common/extHostConfiguration'; export class ExtHostDebugService extends ExtHostDebugServiceBase { @@ -40,12 +38,12 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { @IExtHostRpcService extHostRpcService: IExtHostRpcService, @IExtHostWorkspace workspaceService: IExtHostWorkspace, @IExtHostExtensionService extensionService: IExtHostExtensionService, - @IExtHostDocumentsAndEditors editorsService: IExtHostDocumentsAndEditors, @IExtHostConfiguration configurationService: IExtHostConfiguration, @IExtHostTerminalService private _terminalService: IExtHostTerminalService, - @IExtHostEditorTabs editorTabs: IExtHostEditorTabs + @IExtHostEditorTabs editorTabs: IExtHostEditorTabs, + @IExtHostVariableResolverProvider variableResolver: IExtHostVariableResolverProvider, ) { - super(extHostRpcService, workspaceService, extensionService, editorsService, configurationService, editorTabs); + super(extHostRpcService, workspaceService, extensionService, configurationService, editorTabs, variableResolver); } protected override createDebugAdapter(adapter: IAdapterDescriptor, session: ExtHostDebugSession): AbstractDebugAdapter | undefined { @@ -154,10 +152,6 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { } return super.$runInTerminal(args, sessionId); } - - protected createVariableResolver(folders: vscode.WorkspaceFolder[], editorService: ExtHostDocumentsAndEditors, configurationService: ExtHostConfigProvider): AbstractVariableResolverService { - return new ExtHostVariableResolverService(folders, editorService, configurationService, this._editorTabs, this._workspaceService, homedir()); - } } let externalTerminalService: IExternalTerminalService | undefined = undefined; diff --git a/src/vs/workbench/api/node/extHostExtensionService.ts b/src/vs/workbench/api/node/extHostExtensionService.ts index 481409b79d5..62e0d6b4998 100644 --- a/src/vs/workbench/api/node/extHostExtensionService.ts +++ b/src/vs/workbench/api/node/extHostExtensionService.ts @@ -72,7 +72,7 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { } // Module loading tricks - const interceptor = this._instaService.createInstance(NodeModuleRequireInterceptor, extensionApiFactory, this._registry); + const interceptor = this._instaService.createInstance(NodeModuleRequireInterceptor, extensionApiFactory, { mine: this._myRegistry, all: this._globalRegistry }); await interceptor.install(); performance.mark('code/extHost/didInitAPI'); diff --git a/src/vs/workbench/api/node/extHostTask.ts b/src/vs/workbench/api/node/extHostTask.ts index c47e3203461..7afdfc40ea0 100644 --- a/src/vs/workbench/api/node/extHostTask.ts +++ b/src/vs/workbench/api/node/extHostTask.ts @@ -11,7 +11,6 @@ import * as types from 'vs/workbench/api/common/extHostTypes'; import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; import type * as vscode from 'vscode'; import * as tasks from '../common/shared/tasks'; -import { ExtHostVariableResolverService } from 'vs/workbench/api/common/extHostDebugService'; import { IExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { IExtHostConfiguration } from 'vs/workbench/api/common/extHostConfiguration'; import { IWorkspaceFolder, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; @@ -23,13 +22,11 @@ import { ExtHostTaskBase, TaskHandleDTO, TaskDTO, CustomExecutionDTO, HandlerDat import { Schemas } from 'vs/base/common/network'; import { ILogService } from 'vs/platform/log/common/log'; import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService'; -import { IExtHostEditorTabs } from 'vs/workbench/api/common/extHostEditorTabs'; import * as resources from 'vs/base/common/resources'; import { homedir } from 'os'; +import { IExtHostVariableResolverProvider } from 'vs/workbench/api/common/extHostVariableResolverService'; export class ExtHostTask extends ExtHostTaskBase { - private _variableResolver: ExtHostVariableResolverService | undefined; - constructor( @IExtHostRpcService extHostRpc: IExtHostRpcService, @IExtHostInitDataService initData: IExtHostInitDataService, @@ -39,7 +36,7 @@ export class ExtHostTask extends ExtHostTaskBase { @IExtHostTerminalService extHostTerminalService: IExtHostTerminalService, @ILogService logService: ILogService, @IExtHostApiDeprecationService deprecationService: IExtHostApiDeprecationService, - @IExtHostEditorTabs private readonly editorTabs: IExtHostEditorTabs, + @IExtHostVariableResolverProvider private readonly variableResolver: IExtHostVariableResolverProvider, ) { super(extHostRpc, initData, workspaceService, editorService, configurationService, extHostTerminalService, logService, deprecationService); if (initData.remote.isRemote && initData.remote.authority) { @@ -128,14 +125,6 @@ export class ExtHostTask extends ExtHostTaskBase { return resolvedTaskDTO; } - private async getVariableResolver(workspaceFolders: vscode.WorkspaceFolder[]): Promise<ExtHostVariableResolverService> { - if (this._variableResolver === undefined) { - const configProvider = await this._configurationService.getConfigProvider(); - this._variableResolver = new ExtHostVariableResolverService(workspaceFolders, this._editorService, configProvider, this.editorTabs, this.workspaceService, homedir()); - } - return this._variableResolver; - } - private async getAFolder(workspaceFolders: vscode.WorkspaceFolder[] | undefined): Promise<IWorkspaceFolder> { let folder = (workspaceFolders && workspaceFolders.length > 0) ? workspaceFolders[0] : undefined; if (!folder) { @@ -161,7 +150,7 @@ export class ExtHostTask extends ExtHostTaskBase { const workspaceFolder = await this._workspaceProvider.resolveWorkspaceFolder(uri); const workspaceFolders = (await this._workspaceProvider.getWorkspaceFolders2()) ?? []; - const resolver = await this.getVariableResolver(workspaceFolders); + const resolver = await this.variableResolver.getResolver(); const ws: IWorkspaceFolder = workspaceFolder ? { uri: workspaceFolder.uri, name: workspaceFolder.name, diff --git a/src/vs/workbench/api/node/extHostVariableResolverService.ts b/src/vs/workbench/api/node/extHostVariableResolverService.ts new file mode 100644 index 00000000000..c6439fd4e03 --- /dev/null +++ b/src/vs/workbench/api/node/extHostVariableResolverService.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 { homedir } from 'os'; +import { ExtHostVariableResolverProviderService } from 'vs/workbench/api/common/extHostVariableResolverService'; + +export class NodeExtHostVariableResolverProviderService extends ExtHostVariableResolverProviderService { + protected override homeDir(): string | undefined { + return homedir(); + } +} diff --git a/src/vs/workbench/api/test/browser/extHostAuthentication.test.ts b/src/vs/workbench/api/test/browser/extHostAuthentication.test.ts index 0a9919154a3..c8dd0d0f7c6 100644 --- a/src/vs/workbench/api/test/browser/extHostAuthentication.test.ts +++ b/src/vs/workbench/api/test/browser/extHostAuthentication.test.ts @@ -22,9 +22,8 @@ import { AuthenticationService } from 'vs/workbench/services/authentication/brow import { IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; import { IExtensionService, nullExtensionDescription as extensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; -import { TestRemoteAgentService } from 'vs/workbench/services/remote/test/common/testServices'; import { TestRPCProtocol } from 'vs/workbench/api/test/common/testRPCProtocol'; -import { TestQuickInputService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestQuickInputService, TestRemoteAgentService } from 'vs/workbench/test/browser/workbenchTestServices'; import { TestActivityService, TestExtensionService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; import type { AuthenticationProvider, AuthenticationSession } from 'vscode'; diff --git a/src/vs/workbench/api/test/browser/extHostDiagnostics.test.ts b/src/vs/workbench/api/test/browser/extHostDiagnostics.test.ts index 6b4487af06e..32ccfd141ff 100644 --- a/src/vs/workbench/api/test/browser/extHostDiagnostics.test.ts +++ b/src/vs/workbench/api/test/browser/extHostDiagnostics.test.ts @@ -392,6 +392,7 @@ suite('ExtHostDiagnostics', () => { set(): any { return null; } + dispose() { } assertRegistered(): void { } @@ -444,6 +445,7 @@ suite('ExtHostDiagnostics', () => { set(): any { return null; } + dispose() { } assertRegistered(): void { } @@ -484,6 +486,7 @@ suite('ExtHostDiagnostics', () => { set(): any { return null; } + dispose() { } assertRegistered(): void { } diff --git a/src/vs/workbench/api/test/browser/extHostEditorTabs.test.ts b/src/vs/workbench/api/test/browser/extHostEditorTabs.test.ts index a9ff3908319..1e385fc213f 100644 --- a/src/vs/workbench/api/test/browser/extHostEditorTabs.test.ts +++ b/src/vs/workbench/api/test/browser/extHostEditorTabs.test.ts @@ -7,7 +7,7 @@ import type * as vscode from 'vscode'; import assert = require('assert'); import { URI } from 'vs/base/common/uri'; import { mock } from 'vs/base/test/common/mock'; -import { IEditorTabDto, MainThreadEditorTabsShape, TabInputKind, TabModelOperationKind, TextInputDto } from 'vs/workbench/api/common/extHost.protocol'; +import { IEditorTabDto, IEditorTabGroupDto, MainThreadEditorTabsShape, TabInputKind, TabModelOperationKind, TextInputDto } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostEditorTabs } from 'vs/workbench/api/common/extHostEditorTabs'; import { SingleProxyRPCProtocol } from 'vs/workbench/api/test/common/testRPCProtocol'; import { TextTabInput } from 'vs/workbench/api/common/extHostTypes'; @@ -35,7 +35,7 @@ suite('ExtHostEditorTabs', function () { }) ); - assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 0); + assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 0); // Active group should never be undefined (there is always an active group). Ensure accessing it undefined throws. // TODO @lramos15 Add a throw on the main side when a model is sent without an active group assert.throws(() => extHostEditorTabs.tabGroups.activeTabGroup); @@ -63,8 +63,8 @@ suite('ExtHostEditorTabs', function () { groupId: 12, tabs: [tab] }]); - assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 1); - const [first] = extHostEditorTabs.tabGroups.groups; + assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1); + const [first] = extHostEditorTabs.tabGroups.all; assert.ok(first.activeTab); assert.strictEqual(first.tabs.indexOf(first.activeTab), 0); @@ -75,8 +75,8 @@ suite('ExtHostEditorTabs', function () { groupId: 12, tabs: [tab] }]); - assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 1); - const [first] = extHostEditorTabs.tabGroups.groups; + assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1); + const [first] = extHostEditorTabs.tabGroups.all; assert.ok(first.activeTab); assert.strictEqual(first.tabs.indexOf(first.activeTab), 0); } @@ -95,8 +95,8 @@ suite('ExtHostEditorTabs', function () { groupId: 12, tabs: [] }]); - assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 1); - const [first] = extHostEditorTabs.tabGroups.groups; + assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1); + const [first] = extHostEditorTabs.tabGroups.all; assert.strictEqual(first.activeTab, undefined); assert.strictEqual(first.tabs.length, 0); }); @@ -121,11 +121,65 @@ suite('ExtHostEditorTabs', function () { }]); assert.ok(extHostEditorTabs.tabGroups.activeTabGroup); const activeTabGroup: vscode.TabGroup = extHostEditorTabs.tabGroups.activeTabGroup; - assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 1); + assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1); assert.strictEqual(activeTabGroup.tabs.length, 0); assert.strictEqual(count, 1); }); + test('Check TabGroupChangeEvent properties', function () { + const extHostEditorTabs = new ExtHostEditorTabs( + SingleProxyRPCProtocol(new class extends mock<MainThreadEditorTabsShape>() { + // override/implement $moveTab or $closeTab + }) + ); + + const group1Data: IEditorTabGroupDto = { + isActive: true, + viewColumn: 0, + groupId: 12, + tabs: [] + }; + const group2Data: IEditorTabGroupDto = { ...group1Data, groupId: 13 }; + + const events: vscode.TabGroupChangeEvent[] = []; + extHostEditorTabs.tabGroups.onDidChangeTabGroups(e => events.push(e)); + // OPEN + extHostEditorTabs.$acceptEditorTabModel([group1Data]); + assert.deepStrictEqual(events, [{ + changed: [], + closed: [], + opened: [extHostEditorTabs.tabGroups.activeTabGroup] + }]); + + // OPEN, CHANGE + events.length = 0; + extHostEditorTabs.$acceptEditorTabModel([{ ...group1Data, isActive: false }, group2Data]); + assert.deepStrictEqual(events, [{ + changed: [extHostEditorTabs.tabGroups.all[0]], + closed: [], + opened: [extHostEditorTabs.tabGroups.all[1]] + }]); + + // CHANGE + events.length = 0; + extHostEditorTabs.$acceptEditorTabModel([group1Data, { ...group2Data, isActive: false }]); + assert.deepStrictEqual(events, [{ + changed: extHostEditorTabs.tabGroups.all, + closed: [], + opened: [] + }]); + + // CLOSE, CHANGE + events.length = 0; + const oldActiveGroup = extHostEditorTabs.tabGroups.activeTabGroup; + extHostEditorTabs.$acceptEditorTabModel([group2Data]); + assert.deepStrictEqual(events, [{ + changed: extHostEditorTabs.tabGroups.all, + closed: [oldActiveGroup], + opened: [] + }]); + }); + test('Ensure reference equality for activeTab and activeGroup', function () { const extHostEditorTabs = new ExtHostEditorTabs( SingleProxyRPCProtocol(new class extends mock<MainThreadEditorTabsShape>() { @@ -147,8 +201,8 @@ suite('ExtHostEditorTabs', function () { groupId: 12, tabs: [tab] }]); - assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 1); - const [first] = extHostEditorTabs.tabGroups.groups; + assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1); + const [first] = extHostEditorTabs.tabGroups.all; assert.ok(first.activeTab); assert.strictEqual(first.tabs.indexOf(first.activeTab), 0); assert.strictEqual(first.activeTab, first.tabs[0]); @@ -172,13 +226,13 @@ suite('ExtHostEditorTabs', function () { groupId: 12, tabs: [tabDto] }]); - let all = extHostEditorTabs.tabGroups.groups.map(group => group.tabs).flat(); + let all = extHostEditorTabs.tabGroups.all.map(group => group.tabs).flat(); assert.strictEqual(all.length, 1); const apiTab1 = all[0]; - assert.ok(apiTab1.kind instanceof TextTabInput); + assert.ok(apiTab1.input instanceof TextTabInput); assert.strictEqual(tabDto.input.kind, TabInputKind.TextInput); const dtoResource = (tabDto.input as TextInputDto).uri; - assert.strictEqual(apiTab1.kind.uri.toString(), URI.revive(dtoResource).toString()); + assert.strictEqual(apiTab1.input.uri.toString(), URI.revive(dtoResource).toString()); assert.strictEqual(apiTab1.isDirty, true); @@ -193,11 +247,11 @@ suite('ExtHostEditorTabs', function () { groupId: 12 }); - all = extHostEditorTabs.tabGroups.groups.map(group => group.tabs).flat(); + all = extHostEditorTabs.tabGroups.all.map(group => group.tabs).flat(); assert.strictEqual(all.length, 1); const apiTab2 = all[0]; - assert.ok(apiTab1.kind instanceof TextTabInput); - assert.strictEqual(apiTab1.kind.uri.toString(), URI.revive(dtoResource).toString()); + assert.ok(apiTab1.input instanceof TextTabInput); + assert.strictEqual(apiTab1.input.uri.toString(), URI.revive(dtoResource).toString()); assert.strictEqual(apiTab2.isDirty, false); assert.strictEqual(apiTab1 === apiTab2, true); @@ -239,14 +293,14 @@ suite('ExtHostEditorTabs', function () { tabs: [tabDtoAAA, tabDtoBBB] }]); - let all = extHostEditorTabs.tabGroups.groups.map(group => group.tabs).flat(); + let all = extHostEditorTabs.tabGroups.all.map(group => group.tabs).flat(); assert.strictEqual(all.length, 2); const activeTab1 = extHostEditorTabs.tabGroups.activeTabGroup?.activeTab; - assert.ok(activeTab1?.kind instanceof TextTabInput); + assert.ok(activeTab1?.input instanceof TextTabInput); assert.strictEqual(tabDtoAAA.input.kind, TabInputKind.TextInput); const dtoAAAResource = (tabDtoAAA.input as TextInputDto).uri; - assert.strictEqual(activeTab1?.kind?.uri.toString(), URI.revive(dtoAAAResource)?.toString()); + assert.strictEqual(activeTab1?.input?.uri.toString(), URI.revive(dtoAAAResource)?.toString()); assert.strictEqual(activeTab1?.isActive, true); extHostEditorTabs.$acceptTabOperation({ @@ -257,10 +311,10 @@ suite('ExtHostEditorTabs', function () { }); const activeTab2 = extHostEditorTabs.tabGroups.activeTabGroup?.activeTab; - assert.ok(activeTab2?.kind instanceof TextTabInput); + assert.ok(activeTab2?.input instanceof TextTabInput); assert.strictEqual(tabDtoBBB.input.kind, TabInputKind.TextInput); const dtoBBBResource = (tabDtoBBB.input as TextInputDto).uri; - assert.strictEqual(activeTab2?.kind?.uri.toString(), URI.revive(dtoBBBResource)?.toString()); + assert.strictEqual(activeTab2?.input?.uri.toString(), URI.revive(dtoBBBResource)?.toString()); assert.strictEqual(activeTab2?.isActive, true); assert.strictEqual(activeTab1?.isActive, false); }); @@ -279,7 +333,7 @@ suite('ExtHostEditorTabs', function () { }); assert.throws(() => { // @ts-expect-error write to readonly prop - extHostEditorTabs.tabGroups.groups.length = 0; + extHostEditorTabs.tabGroups.all.length = 0; }); assert.throws(() => { // @ts-expect-error write to readonly prop @@ -317,7 +371,7 @@ suite('ExtHostEditorTabs', function () { groupId: 12, tabs: [tab] }]); - assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 1); + assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1); const activeTab = extHostEditorTabs.tabGroups.activeTabGroup?.activeTab; assert.ok(activeTab); extHostEditorTabs.tabGroups.close(activeTab, false); @@ -356,12 +410,12 @@ suite('ExtHostEditorTabs', function () { tabs: [tabDto] }]); - assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 1); - assert.strictEqual(extHostEditorTabs.tabGroups.groups.map(g => g.tabs).flat().length, 1); + assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1); + assert.strictEqual(extHostEditorTabs.tabGroups.all.map(g => g.tabs).flat().length, 1); - const tab = extHostEditorTabs.tabGroups.groups[0].tabs[0]; + const tab = extHostEditorTabs.tabGroups.all[0].tabs[0]; - const p = new Promise<vscode.Tab[]>(resolve => extHostEditorTabs.tabGroups.onDidChangeTabs(resolve)); + const p = new Promise<vscode.TabChangeEvent>(resolve => extHostEditorTabs.tabGroups.onDidChangeTabs(resolve)); extHostEditorTabs.$acceptTabOperation({ groupId: 12, @@ -370,7 +424,7 @@ suite('ExtHostEditorTabs', function () { tabDto: { ...tabDto, label: 'NEW LABEL' } }); - const changedTab = (await p)[0]; + const changedTab = (await p).changed[0]; assert.ok(tab === changedTab); assert.strictEqual(changedTab.label, 'NEW LABEL'); @@ -410,8 +464,8 @@ suite('ExtHostEditorTabs', function () { tabs: [tab1, tab2, tab3] }]); - assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 1); - assert.strictEqual(extHostEditorTabs.tabGroups.groups.map(g => g.tabs).flat().length, 3); + assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1); + assert.strictEqual(extHostEditorTabs.tabGroups.all.map(g => g.tabs).flat().length, 3); // Active tab is correct assert.strictEqual(extHostEditorTabs.tabGroups.activeTabGroup?.activeTab, extHostEditorTabs.tabGroups.activeTabGroup?.tabs[0]); @@ -441,8 +495,8 @@ suite('ExtHostEditorTabs', function () { groupId: 12, tabs: [tab3] }]); - assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 1); - assert.strictEqual(extHostEditorTabs.tabGroups.groups.map(g => g.tabs).flat().length, 1); + assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1); + assert.strictEqual(extHostEditorTabs.tabGroups.all.map(g => g.tabs).flat().length, 1); assert.strictEqual(extHostEditorTabs.tabGroups.activeTabGroup?.activeTab, extHostEditorTabs.tabGroups.activeTabGroup?.tabs[0]); // Closing out all tabs returns undefine active tab @@ -452,8 +506,8 @@ suite('ExtHostEditorTabs', function () { groupId: 12, tabs: [] }]); - assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 1); - assert.strictEqual(extHostEditorTabs.tabGroups.groups.map(g => g.tabs).flat().length, 0); + assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1); + assert.strictEqual(extHostEditorTabs.tabGroups.all.map(g => g.tabs).flat().length, 0); assert.strictEqual(extHostEditorTabs.tabGroups.activeTabGroup?.activeTab, undefined); }); @@ -489,8 +543,8 @@ suite('ExtHostEditorTabs', function () { tabs: [tab1, tab2, tab3] }]); - assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 1); - assert.strictEqual(extHostEditorTabs.tabGroups.groups.map(g => g.tabs).flat().length, 3); + assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1); + assert.strictEqual(extHostEditorTabs.tabGroups.all.map(g => g.tabs).flat().length, 3); // Close tab 2 extHostEditorTabs.$acceptTabOperation({ @@ -499,8 +553,8 @@ suite('ExtHostEditorTabs', function () { kind: TabModelOperationKind.TAB_CLOSE, tabDto: tab2 }); - assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 1); - assert.strictEqual(extHostEditorTabs.tabGroups.groups.map(g => g.tabs).flat().length, 2); + assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1); + assert.strictEqual(extHostEditorTabs.tabGroups.all.map(g => g.tabs).flat().length, 2); // Close active tab and update tab 3 to be active extHostEditorTabs.$acceptTabOperation({ @@ -509,8 +563,8 @@ suite('ExtHostEditorTabs', function () { kind: TabModelOperationKind.TAB_CLOSE, tabDto: tab1 }); - assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 1); - assert.strictEqual(extHostEditorTabs.tabGroups.groups.map(g => g.tabs).flat().length, 1); + assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1); + assert.strictEqual(extHostEditorTabs.tabGroups.all.map(g => g.tabs).flat().length, 1); tab3.isActive = true; extHostEditorTabs.$acceptTabOperation({ groupId: 12, @@ -518,9 +572,9 @@ suite('ExtHostEditorTabs', function () { kind: TabModelOperationKind.TAB_UPDATE, tabDto: tab3 }); - assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 1); - assert.strictEqual(extHostEditorTabs.tabGroups.groups.map(g => g.tabs).flat().length, 1); - assert.strictEqual(extHostEditorTabs.tabGroups.groups[0]?.activeTab?.label, 'label3'); + assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1); + assert.strictEqual(extHostEditorTabs.tabGroups.all.map(g => g.tabs).flat().length, 1); + assert.strictEqual(extHostEditorTabs.tabGroups.all[0]?.activeTab?.label, 'label3'); // Open tab 2 back extHostEditorTabs.$acceptTabOperation({ @@ -529,8 +583,70 @@ suite('ExtHostEditorTabs', function () { kind: TabModelOperationKind.TAB_OPEN, tabDto: tab2 }); - assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 1); - assert.strictEqual(extHostEditorTabs.tabGroups.groups.map(g => g.tabs).flat().length, 2); - assert.strictEqual(extHostEditorTabs.tabGroups.groups[0]?.tabs[1]?.label, 'label2'); + assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1); + assert.strictEqual(extHostEditorTabs.tabGroups.all.map(g => g.tabs).flat().length, 2); + assert.strictEqual(extHostEditorTabs.tabGroups.all[0]?.tabs[1]?.label, 'label2'); + }); + + test('Tab operations patches move correctly', function () { + const extHostEditorTabs = new ExtHostEditorTabs( + SingleProxyRPCProtocol(new class extends mock<MainThreadEditorTabsShape>() { + // override/implement $moveTab or $closeTab + }) + ); + + const tab1: IEditorTabDto = createTabDto({ + id: 'uniquestring', + isActive: true, + label: 'label1', + }); + + const tab2: IEditorTabDto = createTabDto({ + isActive: false, + id: 'uniquestring2', + label: 'label2', + }); + + const tab3: IEditorTabDto = createTabDto({ + isActive: false, + id: 'uniquestring3', + label: 'label3', + }); + + extHostEditorTabs.$acceptEditorTabModel([{ + isActive: true, + viewColumn: 0, + groupId: 12, + tabs: [tab1, tab2, tab3] + }]); + + assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1); + assert.strictEqual(extHostEditorTabs.tabGroups.all.map(g => g.tabs).flat().length, 3); + + // Move tab 2 to index 0 + extHostEditorTabs.$acceptTabOperation({ + groupId: 12, + index: 0, + oldIndex: 1, + kind: TabModelOperationKind.TAB_MOVE, + tabDto: tab2 + }); + assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1); + assert.strictEqual(extHostEditorTabs.tabGroups.all.map(g => g.tabs).flat().length, 3); + assert.strictEqual(extHostEditorTabs.tabGroups.all[0]?.tabs[0]?.label, 'label2'); + + // Move tab 3 to index 1 + extHostEditorTabs.$acceptTabOperation({ + groupId: 12, + index: 1, + oldIndex: 2, + kind: TabModelOperationKind.TAB_MOVE, + tabDto: tab3 + }); + assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1); + assert.strictEqual(extHostEditorTabs.tabGroups.all.map(g => g.tabs).flat().length, 3); + assert.strictEqual(extHostEditorTabs.tabGroups.all[0]?.tabs[1]?.label, 'label3'); + assert.strictEqual(extHostEditorTabs.tabGroups.all[0]?.tabs[0]?.label, 'label2'); + assert.strictEqual(extHostEditorTabs.tabGroups.all[0]?.tabs[2]?.label, 'label1'); }); }); diff --git a/src/vs/workbench/api/test/browser/extHostFileSystemEventService.test.ts b/src/vs/workbench/api/test/browser/extHostFileSystemEventService.test.ts index 110fa1c2b6f..64d35e85b79 100644 --- a/src/vs/workbench/api/test/browser/extHostFileSystemEventService.test.ts +++ b/src/vs/workbench/api/test/browser/extHostFileSystemEventService.test.ts @@ -14,6 +14,7 @@ suite('ExtHostFileSystemEventService', () => { const protocol: IMainContext = { getProxy: () => { return undefined!; }, set: undefined!, + dispose: undefined!, assertRegistered: undefined!, drain: undefined! }; diff --git a/src/vs/workbench/api/test/browser/extHostNotebookConcatDocument.test.ts b/src/vs/workbench/api/test/browser/extHostNotebookConcatDocument.test.ts deleted file mode 100644 index 6267d249941..00000000000 --- a/src/vs/workbench/api/test/browser/extHostNotebookConcatDocument.test.ts +++ /dev/null @@ -1,620 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as assert from 'assert'; -import { TestRPCProtocol } from 'vs/workbench/api/test/common/testRPCProtocol'; -import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; -import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; -import { NullLogService } from 'vs/platform/log/common/log'; -import { ExtHostNotebookConcatDocument } from 'vs/workbench/api/common/extHostNotebookConcatDocument'; -import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook'; -import { ExtHostNotebookDocument } from 'vs/workbench/api/common/extHostNotebookDocument'; -import { URI } from 'vs/base/common/uri'; -import { CellKind, CellUri, NotebookCellsChangeType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { Position, Location, Range } from 'vs/workbench/api/common/extHostTypes'; -import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; -import { nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; -import * as vscode from 'vscode'; -import { mock } from 'vs/workbench/test/common/workbenchTestServices'; -import { MainContext, MainThreadCommandsShape, MainThreadNotebookShape } from 'vs/workbench/api/common/extHost.protocol'; -import { DisposableStore } from 'vs/base/common/lifecycle'; -import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths'; -import { generateUuid } from 'vs/base/common/uuid'; -import { ExtHostNotebookDocuments } from 'vs/workbench/api/common/extHostNotebookDocuments'; -import { SerializableObjectWithBuffers } from 'vs/workbench/services/extensions/common/proxyIdentifier'; - -suite('NotebookConcatDocument', function () { - - let rpcProtocol: TestRPCProtocol; - let notebook: ExtHostNotebookDocument; - let extHostDocumentsAndEditors: ExtHostDocumentsAndEditors; - let extHostDocuments: ExtHostDocuments; - let extHostNotebooks: ExtHostNotebookController; - let extHostNotebookDocuments: ExtHostNotebookDocuments; - - const notebookUri = URI.parse('test:///notebook.file'); - const disposables = new DisposableStore(); - - setup(async function () { - disposables.clear(); - - rpcProtocol = new TestRPCProtocol(); - rpcProtocol.set(MainContext.MainThreadCommands, new class extends mock<MainThreadCommandsShape>() { - override $registerCommand() { } - }); - rpcProtocol.set(MainContext.MainThreadNotebook, new class extends mock<MainThreadNotebookShape>() { - override async $registerNotebookProvider() { } - override async $unregisterNotebookProvider() { } - }); - extHostDocumentsAndEditors = new ExtHostDocumentsAndEditors(rpcProtocol, new NullLogService()); - extHostDocuments = new ExtHostDocuments(rpcProtocol, extHostDocumentsAndEditors); - const extHostStoragePaths = new class extends mock<IExtensionStoragePaths>() { - override workspaceValue() { - return URI.from({ scheme: 'test', path: generateUuid() }); - } - }; - 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>() { - // async openNotebook() { } - }); - extHostNotebooks.$acceptDocumentAndEditorsDelta(new SerializableObjectWithBuffers({ - addedDocuments: [{ - uri: notebookUri, - viewType: 'test', - cells: [{ - handle: 0, - uri: CellUri.generate(notebookUri, 0), - source: ['### Heading'], - eol: '\n', - language: 'markdown', - cellKind: CellKind.Markup, - outputs: [], - }], - versionId: 0 - }], - addedEditors: [{ - documentUri: notebookUri, - id: '_notebook_editor_0', - selections: [{ start: 0, end: 1 }], - visibleRanges: [] - }] - })); - extHostNotebooks.$acceptDocumentAndEditorsDelta(new SerializableObjectWithBuffers({ newActiveEditor: '_notebook_editor_0' })); - - notebook = extHostNotebooks.notebookDocuments[0]!; - - disposables.add(reg); - disposables.add(notebook); - disposables.add(extHostDocuments); - }); - - test('empty', function () { - let doc = new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook.apiNotebook, undefined); - assert.strictEqual(doc.getText(), ''); - assert.strictEqual(doc.version, 0); - - // assert.strictEqual(doc.locationAt(new Position(0, 0)), undefined); - // assert.strictEqual(doc.positionAt(SOME_FAKE_LOCATION?), undefined); - }); - - - function assertLocation(doc: vscode.NotebookConcatTextDocument, pos: Position, expected: Location, reverse = true) { - const actual = doc.locationAt(pos); - assert.strictEqual(actual.uri.toString(), expected.uri.toString()); - assert.strictEqual(actual.range.isEqual(expected.range), true); - - if (reverse) { - // reverse - offset - const offset = doc.offsetAt(pos); - assert.strictEqual(doc.positionAt(offset).isEqual(pos), true); - - // reverse - pos - const actualPosition = doc.positionAt(actual); - assert.strictEqual(actualPosition.isEqual(pos), true); - } - } - - function assertLines(doc: vscode.NotebookConcatTextDocument, ...lines: string[]) { - let actual = doc.getText().split(/\r\n|\n|\r/); - assert.deepStrictEqual(actual, lines); - } - - test('contains', function () { - - const cellUri1 = CellUri.generate(notebook.uri, 1); - const cellUri2 = CellUri.generate(notebook.uri, 2); - - extHostNotebookDocuments.$acceptModelChanged(notebookUri, new SerializableObjectWithBuffers({ - versionId: notebook.apiNotebook.version + 1, - rawEvents: [{ - kind: NotebookCellsChangeType.ModelChange, - changes: [[0, 0, [{ - handle: 1, - uri: cellUri1, - source: ['Hello', 'World', 'Hello World!'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }, { - handle: 2, - uri: cellUri2, - source: ['Hallo', 'Welt', 'Hallo Welt!'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }]] - ] - }] - }), false); - - - assert.strictEqual(notebook.apiNotebook.cellCount, 1 + 2); // markdown and code - - let doc = new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook.apiNotebook, undefined); - - assert.strictEqual(doc.contains(cellUri1), true); - assert.strictEqual(doc.contains(cellUri2), true); - assert.strictEqual(doc.contains(URI.parse('some://miss/path')), false); - }); - - test('location, position mapping', function () { - - extHostNotebookDocuments.$acceptModelChanged(notebookUri, new SerializableObjectWithBuffers({ - versionId: notebook.apiNotebook.version + 1, - rawEvents: [ - { - kind: NotebookCellsChangeType.ModelChange, - changes: [[0, 0, [{ - handle: 1, - uri: CellUri.generate(notebook.uri, 1), - source: ['Hello', 'World', 'Hello World!'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }, { - handle: 2, - uri: CellUri.generate(notebook.uri, 2), - source: ['Hallo', 'Welt', 'Hallo Welt!'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }]]] - } - ] - }), false); - - - assert.strictEqual(notebook.apiNotebook.cellCount, 1 + 2); // markdown and code - - let doc = new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook.apiNotebook, undefined); - assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!'); - - assertLocation(doc, new Position(0, 0), new Location(notebook.apiNotebook.cellAt(0).document.uri, new Position(0, 0))); - assertLocation(doc, new Position(4, 0), new Location(notebook.apiNotebook.cellAt(1).document.uri, new Position(1, 0))); - assertLocation(doc, new Position(4, 3), new Location(notebook.apiNotebook.cellAt(1).document.uri, new Position(1, 3))); - assertLocation(doc, new Position(5, 11), new Location(notebook.apiNotebook.cellAt(1).document.uri, new Position(2, 11))); - assertLocation(doc, new Position(5, 12), new Location(notebook.apiNotebook.cellAt(1).document.uri, new Position(2, 11)), false); // don't check identity because position will be clamped - }); - - - test('location, position mapping, cell changes', function () { - - let doc = new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook.apiNotebook, undefined); - - // UPDATE 1 - extHostNotebookDocuments.$acceptModelChanged(notebookUri, new SerializableObjectWithBuffers({ - versionId: notebook.apiNotebook.version + 1, - rawEvents: [ - { - kind: NotebookCellsChangeType.ModelChange, - changes: [[0, 0, [{ - handle: 1, - uri: CellUri.generate(notebook.uri, 1), - source: ['Hello', 'World', 'Hello World!'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }]]] - } - ] - }), false); - assert.strictEqual(notebook.apiNotebook.cellCount, 1 + 1); - assert.strictEqual(doc.version, 1); - assertLines(doc, 'Hello', 'World', 'Hello World!'); - - assertLocation(doc, new Position(0, 0), new Location(notebook.apiNotebook.cellAt(0).document.uri, new Position(0, 0))); - assertLocation(doc, new Position(2, 2), new Location(notebook.apiNotebook.cellAt(0).document.uri, new Position(2, 2))); - assertLocation(doc, new Position(4, 0), new Location(notebook.apiNotebook.cellAt(0).document.uri, new Position(2, 12)), false); // clamped - - - // UPDATE 2 - extHostNotebookDocuments.$acceptModelChanged(notebookUri, new SerializableObjectWithBuffers({ - versionId: notebook.apiNotebook.version + 1, - rawEvents: [ - { - kind: NotebookCellsChangeType.ModelChange, - changes: [[1, 0, [{ - handle: 2, - uri: CellUri.generate(notebook.uri, 2), - source: ['Hallo', 'Welt', 'Hallo Welt!'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }]]] - } - ] - }), false); - - assert.strictEqual(notebook.apiNotebook.cellCount, 1 + 2); - assert.strictEqual(doc.version, 2); - assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!'); - assertLocation(doc, new Position(0, 0), new Location(notebook.apiNotebook.cellAt(0).document.uri, new Position(0, 0))); - assertLocation(doc, new Position(4, 0), new Location(notebook.apiNotebook.cellAt(1).document.uri, new Position(1, 0))); - assertLocation(doc, new Position(4, 3), new Location(notebook.apiNotebook.cellAt(1).document.uri, new Position(1, 3))); - assertLocation(doc, new Position(5, 11), new Location(notebook.apiNotebook.cellAt(1).document.uri, new Position(2, 11))); - assertLocation(doc, new Position(5, 12), new Location(notebook.apiNotebook.cellAt(1).document.uri, new Position(2, 11)), false); // don't check identity because position will be clamped - - // UPDATE 3 (remove cell #2 again) - extHostNotebookDocuments.$acceptModelChanged(notebookUri, new SerializableObjectWithBuffers({ - versionId: notebook.apiNotebook.version + 1, - rawEvents: [ - { - kind: NotebookCellsChangeType.ModelChange, - changes: [[1, 1, []]] - } - ] - }), false); - assert.strictEqual(notebook.apiNotebook.cellCount, 1 + 1); - assert.strictEqual(doc.version, 3); - assertLines(doc, 'Hello', 'World', 'Hello World!'); - assertLocation(doc, new Position(0, 0), new Location(notebook.apiNotebook.cellAt(0).document.uri, new Position(0, 0))); - assertLocation(doc, new Position(2, 2), new Location(notebook.apiNotebook.cellAt(0).document.uri, new Position(2, 2))); - assertLocation(doc, new Position(4, 0), new Location(notebook.apiNotebook.cellAt(0).document.uri, new Position(2, 12)), false); // clamped - }); - - test('location, position mapping, cell-document changes', function () { - - let doc = new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook.apiNotebook, undefined); - - // UPDATE 1 - extHostNotebookDocuments.$acceptModelChanged(notebookUri, new SerializableObjectWithBuffers({ - versionId: notebook.apiNotebook.version + 1, - rawEvents: [ - { - - kind: NotebookCellsChangeType.ModelChange, - changes: [[0, 0, [{ - handle: 1, - uri: CellUri.generate(notebook.uri, 1), - source: ['Hello', 'World', 'Hello World!'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }, { - handle: 2, - uri: CellUri.generate(notebook.uri, 2), - source: ['Hallo', 'Welt', 'Hallo Welt!'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }]]] - } - ] - }), false); - assert.strictEqual(notebook.apiNotebook.cellCount, 1 + 2); - assert.strictEqual(doc.version, 1); - - assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!'); - assertLocation(doc, new Position(0, 0), new Location(notebook.apiNotebook.cellAt(0).document.uri, new Position(0, 0))); - assertLocation(doc, new Position(2, 2), new Location(notebook.apiNotebook.cellAt(0).document.uri, new Position(2, 2))); - assertLocation(doc, new Position(2, 12), new Location(notebook.apiNotebook.cellAt(0).document.uri, new Position(2, 12))); - assertLocation(doc, new Position(4, 0), new Location(notebook.apiNotebook.cellAt(1).document.uri, new Position(1, 0))); - assertLocation(doc, new Position(4, 3), new Location(notebook.apiNotebook.cellAt(1).document.uri, new Position(1, 3))); - - // offset math - let cell1End = doc.offsetAt(new Position(2, 12)); - assert.strictEqual(doc.positionAt(cell1End).isEqual(new Position(2, 12)), true); - - extHostDocuments.$acceptModelChanged(notebook.apiNotebook.cellAt(0).document.uri, { - versionId: 0, - eol: '\n', - changes: [{ - range: { startLineNumber: 3, startColumn: 1, endLineNumber: 3, endColumn: 6 }, - rangeLength: 6, - rangeOffset: 12, - text: 'Hi' - }], - isRedoing: false, - isUndoing: false, - }, false); - assertLines(doc, 'Hello', 'World', 'Hi World!', 'Hallo', 'Welt', 'Hallo Welt!'); - assertLocation(doc, new Position(2, 12), new Location(notebook.apiNotebook.cellAt(0).document.uri, new Position(2, 9)), false); - - assert.strictEqual(doc.positionAt(cell1End).isEqual(new Position(3, 2)), true); - - }); - - test('selector', function () { - - extHostNotebookDocuments.$acceptModelChanged(notebookUri, new SerializableObjectWithBuffers({ - versionId: notebook.apiNotebook.version + 1, - rawEvents: [ - { - kind: NotebookCellsChangeType.ModelChange, - changes: [[0, 0, [{ - handle: 1, - uri: CellUri.generate(notebook.uri, 1), - source: ['fooLang-document'], - eol: '\n', - language: 'fooLang', - cellKind: CellKind.Code, - outputs: [], - }, { - handle: 2, - uri: CellUri.generate(notebook.uri, 2), - source: ['barLang-document'], - eol: '\n', - language: 'barLang', - cellKind: CellKind.Code, - outputs: [], - }]]] - } - ] - }), false); - - const mixedDoc = new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook.apiNotebook, undefined); - const fooLangDoc = new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook.apiNotebook, 'fooLang'); - const barLangDoc = new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook.apiNotebook, 'barLang'); - - assertLines(mixedDoc, 'fooLang-document', 'barLang-document'); - assertLines(fooLangDoc, 'fooLang-document'); - assertLines(barLangDoc, 'barLang-document'); - - extHostNotebookDocuments.$acceptModelChanged(notebookUri, new SerializableObjectWithBuffers({ - versionId: notebook.apiNotebook.version + 1, - rawEvents: [ - { - kind: NotebookCellsChangeType.ModelChange, - changes: [[2, 0, [{ - handle: 3, - uri: CellUri.generate(notebook.uri, 3), - source: ['barLang-document2'], - eol: '\n', - language: 'barLang', - cellKind: CellKind.Code, - outputs: [], - }]]] - } - ] - }), false); - - assertLines(mixedDoc, 'fooLang-document', 'barLang-document', 'barLang-document2'); - assertLines(fooLangDoc, 'fooLang-document'); - assertLines(barLangDoc, 'barLang-document', 'barLang-document2'); - }); - - function assertOffsetAtPosition(doc: vscode.NotebookConcatTextDocument, offset: number, expected: { line: number; character: number }, reverse = true) { - const actual = doc.positionAt(offset); - - assert.strictEqual(actual.line, expected.line); - assert.strictEqual(actual.character, expected.character); - - if (reverse) { - const actualOffset = doc.offsetAt(actual); - assert.strictEqual(actualOffset, offset); - } - } - - - test('offsetAt(position) <-> positionAt(offset)', function () { - - extHostNotebookDocuments.$acceptModelChanged(notebookUri, new SerializableObjectWithBuffers({ - versionId: notebook.apiNotebook.version + 1, - rawEvents: [ - { - kind: NotebookCellsChangeType.ModelChange, - changes: [[0, 0, [{ - handle: 1, - uri: CellUri.generate(notebook.uri, 1), - source: ['Hello', 'World', 'Hello World!'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }, { - handle: 2, - uri: CellUri.generate(notebook.uri, 2), - source: ['Hallo', 'Welt', 'Hallo Welt!'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }]]] - } - ] - }), false); - - assert.strictEqual(notebook.apiNotebook.cellCount, 1 + 2); // markdown and code - - let doc = new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook.apiNotebook, undefined); - assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!'); - - assertOffsetAtPosition(doc, 0, { line: 0, character: 0 }); - assertOffsetAtPosition(doc, 1, { line: 0, character: 1 }); - assertOffsetAtPosition(doc, 9, { line: 1, character: 3 }); - assertOffsetAtPosition(doc, 32, { line: 4, character: 1 }); - assertOffsetAtPosition(doc, 47, { line: 5, character: 11 }); - }); - - - function assertLocationAtPosition(doc: vscode.NotebookConcatTextDocument, pos: { line: number; character: number }, expected: { uri: URI; line: number; character: number }, reverse = true) { - - const actual = doc.locationAt(new Position(pos.line, pos.character)); - assert.strictEqual(actual.uri.toString(), expected.uri.toString()); - assert.strictEqual(actual.range.start.line, expected.line); - assert.strictEqual(actual.range.end.line, expected.line); - assert.strictEqual(actual.range.start.character, expected.character); - assert.strictEqual(actual.range.end.character, expected.character); - - if (reverse) { - const actualPos = doc.positionAt(actual); - assert.strictEqual(actualPos.line, pos.line); - assert.strictEqual(actualPos.character, pos.character); - } - } - - test('locationAt(position) <-> positionAt(location)', function () { - - extHostNotebookDocuments.$acceptModelChanged(notebookUri, new SerializableObjectWithBuffers({ - versionId: notebook.apiNotebook.version + 1, - rawEvents: [ - { - kind: NotebookCellsChangeType.ModelChange, - changes: [[0, 0, [{ - handle: 1, - uri: CellUri.generate(notebook.uri, 1), - source: ['Hello', 'World', 'Hello World!'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }, { - handle: 2, - uri: CellUri.generate(notebook.uri, 2), - source: ['Hallo', 'Welt', 'Hallo Welt!'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }]]] - } - ] - }), false); - - assert.strictEqual(notebook.apiNotebook.cellCount, 1 + 2); // markdown and code - - let doc = new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook.apiNotebook, undefined); - assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!'); - - assertLocationAtPosition(doc, { line: 0, character: 0 }, { uri: notebook.apiNotebook.cellAt(0).document.uri, line: 0, character: 0 }); - assertLocationAtPosition(doc, { line: 2, character: 0 }, { uri: notebook.apiNotebook.cellAt(0).document.uri, line: 2, character: 0 }); - assertLocationAtPosition(doc, { line: 2, character: 12 }, { uri: notebook.apiNotebook.cellAt(0).document.uri, line: 2, character: 12 }); - assertLocationAtPosition(doc, { line: 3, character: 0 }, { uri: notebook.apiNotebook.cellAt(1).document.uri, line: 0, character: 0 }); - assertLocationAtPosition(doc, { line: 5, character: 0 }, { uri: notebook.apiNotebook.cellAt(1).document.uri, line: 2, character: 0 }); - assertLocationAtPosition(doc, { line: 5, character: 11 }, { uri: notebook.apiNotebook.cellAt(1).document.uri, line: 2, character: 11 }); - }); - - test('getText(range)', function () { - - extHostNotebookDocuments.$acceptModelChanged(notebookUri, new SerializableObjectWithBuffers({ - versionId: notebook.apiNotebook.version + 1, - rawEvents: [ - { - kind: NotebookCellsChangeType.ModelChange, - changes: [[0, 0, [{ - handle: 1, - uri: CellUri.generate(notebook.uri, 1), - source: ['Hello', 'World', 'Hello World!'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }, { - handle: 2, - uri: CellUri.generate(notebook.uri, 2), - source: ['Hallo', 'Welt', 'Hallo Welt!'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }, { - handle: 3, - uri: CellUri.generate(notebook.uri, 3), - source: ['Three', 'Drei', 'Drüü'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }]]] - } - ] - }), false); - - assert.strictEqual(notebook.apiNotebook.cellCount, 1 + 3); // markdown and code - - let doc = new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook.apiNotebook, undefined); - assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!', 'Three', 'Drei', 'Drüü'); - - assert.strictEqual(doc.getText(new Range(0, 0, 0, 0)), ''); - assert.strictEqual(doc.getText(new Range(0, 0, 1, 0)), 'Hello\n'); - assert.strictEqual(doc.getText(new Range(2, 0, 4, 0)), 'Hello World!\nHallo\n'); - assert.strictEqual(doc.getText(new Range(2, 0, 8, 0)), 'Hello World!\nHallo\nWelt\nHallo Welt!\nThree\nDrei\n'); - }); - - test('validateRange/Position', function () { - - extHostNotebookDocuments.$acceptModelChanged(notebookUri, new SerializableObjectWithBuffers({ - versionId: notebook.apiNotebook.version + 1, - rawEvents: [ - { - kind: NotebookCellsChangeType.ModelChange, - changes: [[0, 0, [{ - handle: 1, - uri: CellUri.generate(notebook.uri, 1), - source: ['Hello', 'World', 'Hello World!'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }, { - handle: 2, - uri: CellUri.generate(notebook.uri, 2), - source: ['Hallo', 'Welt', 'Hallo Welt!'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }]]] - } - ] - }), false); - - assert.strictEqual(notebook.apiNotebook.cellCount, 1 + 2); // markdown and code - - let doc = new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook.apiNotebook, undefined); - assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!'); - - - function assertPosition(actual: vscode.Position, expectedLine: number, expectedCh: number) { - assert.strictEqual(actual.line, expectedLine); - assert.strictEqual(actual.character, expectedCh); - } - - - // "fixed" - assertPosition(doc.validatePosition(new Position(0, 1000)), 0, 5); - assertPosition(doc.validatePosition(new Position(2, 1000)), 2, 12); - assertPosition(doc.validatePosition(new Position(5, 1000)), 5, 11); - assertPosition(doc.validatePosition(new Position(5000, 1000)), 5, 11); - - // "good" - assertPosition(doc.validatePosition(new Position(0, 1)), 0, 1); - assertPosition(doc.validatePosition(new Position(0, 5)), 0, 5); - assertPosition(doc.validatePosition(new Position(2, 8)), 2, 8); - assertPosition(doc.validatePosition(new Position(2, 12)), 2, 12); - assertPosition(doc.validatePosition(new Position(5, 11)), 5, 11); - - }); -}); diff --git a/src/vs/workbench/api/test/browser/extHostTypeConverter.test.ts b/src/vs/workbench/api/test/browser/extHostTypeConverter.test.ts index 20ef8c11b6c..fb2b02bc890 100644 --- a/src/vs/workbench/api/test/browser/extHostTypeConverter.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTypeConverter.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; -import { MarkdownString, NotebookCellOutputItem, NotebookData } from 'vs/workbench/api/common/extHostTypeConverters'; +import { MarkdownString, NotebookCellOutputItem, NotebookData, LanguageSelector } from 'vs/workbench/api/common/extHostTypeConverters'; import { isEmptyObject } from 'vs/base/common/types'; import { forEach } from 'vs/base/common/collections'; import { LogLevel as _MainLogLevel } from 'vs/platform/log/common/log'; @@ -111,4 +111,16 @@ suite('ExtHostTypeConverter', function () { assert.strictEqual(item2.mime, item.mime); assert.deepStrictEqual(Array.from(item2.data), Array.from(item.data)); }); + + test('LanguageSelector', function () { + const out = LanguageSelector.from({ language: 'bat', notebookType: 'xxx' }); + assert.ok(typeof out === 'object'); + assert.deepStrictEqual(out, { + language: 'bat', + notebookType: 'xxx', + scheme: undefined, + pattern: undefined, + exclusive: undefined, + }); + }); }); diff --git a/src/vs/workbench/api/test/browser/extHostWebview.test.ts b/src/vs/workbench/api/test/browser/extHostWebview.test.ts index cce4f74d246..50327ab707d 100644 --- a/src/vs/workbench/api/test/browser/extHostWebview.test.ts +++ b/src/vs/workbench/api/test/browser/extHostWebview.test.ts @@ -55,7 +55,8 @@ suite('ExtHostWebview', () => { title: 'title', state: {}, panelOptions: {}, - webviewOptions: {} + webviewOptions: {}, + active: true, }, 0 as EditorGroupColumn); assert.strictEqual(lastInvokedDeserializer, serializerA); @@ -71,7 +72,8 @@ suite('ExtHostWebview', () => { title: 'title', state: {}, panelOptions: {}, - webviewOptions: {} + webviewOptions: {}, + active: true, }, 0 as EditorGroupColumn); assert.strictEqual(lastInvokedDeserializer, serializerB); }); diff --git a/src/vs/workbench/api/test/browser/extHostWorkspace.test.ts b/src/vs/workbench/api/test/browser/extHostWorkspace.test.ts index 5c05a247db3..c5fa028c83c 100644 --- a/src/vs/workbench/api/test/browser/extHostWorkspace.test.ts +++ b/src/vs/workbench/api/test/browser/extHostWorkspace.test.ts @@ -290,6 +290,7 @@ suite('ExtHostWorkspace', function () { const protocol: IMainContext = { getProxy: () => { return undefined!; }, set: () => { return undefined!; }, + dispose: () => { }, assertRegistered: () => { }, drain: () => { return undefined!; }, }; diff --git a/src/vs/workbench/api/test/browser/mainThreadDiagnostics.test.ts b/src/vs/workbench/api/test/browser/mainThreadDiagnostics.test.ts index e30482a8eac..de58c41de23 100644 --- a/src/vs/workbench/api/test/browser/mainThreadDiagnostics.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadDiagnostics.test.ts @@ -30,6 +30,7 @@ suite('MainThreadDiagnostics', function () { new class implements IExtHostContext { remoteAuthority = ''; extensionHostKind = ExtensionHostKind.LocalProcess; + dispose() { } assertRegistered() { } set(v: any): any { return null; } getProxy(): any { @@ -71,6 +72,7 @@ suite('MainThreadDiagnostics', function () { new class implements IExtHostContext { remoteAuthority = ''; extensionHostKind = ExtensionHostKind.LocalProcess; + dispose() { } assertRegistered() { } set(v: any): any { return null; } getProxy(): any { @@ -134,6 +136,7 @@ suite('MainThreadDiagnostics', function () { new class implements IExtHostContext { remoteAuthority = ''; extensionHostKind = ExtensionHostKind.LocalProcess; + dispose() { } assertRegistered() { } set(v: any): any { return null; } getProxy(): any { diff --git a/src/vs/workbench/api/test/browser/mainThreadTreeViews.test.ts b/src/vs/workbench/api/test/browser/mainThreadTreeViews.test.ts index 0803d8aabba..1386a01b234 100644 --- a/src/vs/workbench/api/test/browser/mainThreadTreeViews.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadTreeViews.test.ts @@ -67,6 +67,7 @@ suite('MainThreadHostTreeView', function () { new class implements IExtHostContext { remoteAuthority = ''; extensionHostKind = ExtensionHostKind.LocalProcess; + dispose() { } assertRegistered() { } set(v: any): any { return null; } getProxy(): any { diff --git a/src/vs/workbench/api/test/common/testRPCProtocol.ts b/src/vs/workbench/api/test/common/testRPCProtocol.ts index 52a307debc9..2947d378288 100644 --- a/src/vs/workbench/api/test/common/testRPCProtocol.ts +++ b/src/vs/workbench/api/test/common/testRPCProtocol.ts @@ -21,6 +21,7 @@ export function SingleProxyRPCProtocol(thing: any): IExtHostContext & IExtHostRp set<T, R extends T>(identifier: ProxyIdentifier<T>, value: R): R { return value; }, + dispose: undefined!, assertRegistered: undefined!, drain: undefined!, extensionHostKind: ExtensionHostKind.LocalProcess @@ -135,6 +136,10 @@ export class TestRPCProtocol implements IExtHostContext, IExtHostRpcService { }); } + public dispose() { + throw new Error('Not implemented!'); + } + public assertRegistered(identifiers: ProxyIdentifier<any>[]): void { throw new Error('Not implemented!'); } diff --git a/src/vs/workbench/api/worker/extHostExtensionService.ts b/src/vs/workbench/api/worker/extHostExtensionService.ts index 68c7c834910..a74fcfbc69b 100644 --- a/src/vs/workbench/api/worker/extHostExtensionService.ts +++ b/src/vs/workbench/api/worker/extHostExtensionService.ts @@ -44,7 +44,7 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { // initialize API and register actors const apiFactory = this._instaService.invokeFunction(createApiFactoryAndRegisterActors); - this._fakeModules = this._instaService.createInstance(WorkerRequireInterceptor, apiFactory, this._registry); + this._fakeModules = this._instaService.createInstance(WorkerRequireInterceptor, apiFactory, { mine: this._myRegistry, all: this._globalRegistry }); await this._fakeModules.install(); performance.mark('code/extHost/didInitAPI'); diff --git a/src/vs/workbench/browser/actions/layoutActions.ts b/src/vs/workbench/browser/actions/layoutActions.ts index aaf84ac8ef7..8440c0f8e12 100644 --- a/src/vs/workbench/browser/actions/layoutActions.ts +++ b/src/vs/workbench/browser/actions/layoutActions.ts @@ -22,7 +22,7 @@ import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/b import { ToggleAuxiliaryBarAction } from 'vs/workbench/browser/parts/auxiliarybar/auxiliaryBarActions'; import { TogglePanelAction } from 'vs/workbench/browser/parts/panel/panelActions'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { AuxiliaryBarVisibleContext, PanelAlignmentContext, PanelVisibleContext, SideBarVisibleContext, FocusedViewContext, InEditorZenModeContext, IsCenteredLayoutContext, EditorAreaVisibleContext, IsFullscreenContext } from 'vs/workbench/common/contextkeys'; +import { AuxiliaryBarVisibleContext, PanelAlignmentContext, PanelVisibleContext, SideBarVisibleContext, FocusedViewContext, InEditorZenModeContext, IsCenteredLayoutContext, EditorAreaVisibleContext, IsFullscreenContext, PanelPositionContext } from 'vs/workbench/common/contextkeys'; import { Codicon } from 'vs/base/common/codicons'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; @@ -330,12 +330,8 @@ registerAction2(class extends Action2 { category: CATEGORIES.View, f1: true, toggled: EditorAreaVisibleContext, - // Remove from appearance menu - // menu: [{ - // id: MenuId.MenubarAppearanceMenu, - // group: '2_workbench_layout', - // order: 5 - // }] + // the workbench grid currently prevents us from supporting panel maximization with non-center panel alignment + precondition: ContextKeyExpr.or(PanelAlignmentContext.isEqualTo('center'), PanelPositionContext.notEqualsTo('bottom')) }); } @@ -505,13 +501,7 @@ registerAction2(class extends Action2 { original: 'Toggle Tab Visibility' }, category: CATEGORIES.View, - f1: true, - keybinding: { - weight: KeybindingWeight.WorkbenchContrib, - primary: undefined, - mac: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KeyW, }, - linux: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KeyW, } - } + f1: true }); } diff --git a/src/vs/workbench/browser/actions/quickAccessActions.ts b/src/vs/workbench/browser/actions/quickAccessActions.ts index f587eb23bf8..f3f41c78e1d 100644 --- a/src/vs/workbench/browser/actions/quickAccessActions.ts +++ b/src/vs/workbench/browser/actions/quickAccessActions.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { MenuRegistry, MenuId, Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +import { MenuId, Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { KeybindingsRegistry, KeybindingWeight, IKeybindingRule } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IQuickInputService, ItemActivation } from 'vs/platform/quickinput/common/quickInput'; @@ -22,21 +22,6 @@ const globalQuickAccessKeybinding = { mac: { primary: KeyMod.CtrlCmd | KeyCode.KeyP, secondary: undefined } }; -const QUICKACCESS_ACTION_ID = 'workbench.action.quickOpen'; - -MenuRegistry.appendMenuItem(MenuId.CommandPalette, { - command: { id: QUICKACCESS_ACTION_ID, title: { value: localize('quickOpen', "Go to File..."), original: 'Go to File...' } } -}); - -KeybindingsRegistry.registerKeybindingRule({ - id: QUICKACCESS_ACTION_ID, - weight: KeybindingWeight.WorkbenchContrib, - when: undefined, - primary: globalQuickAccessKeybinding.primary, - secondary: globalQuickAccessKeybinding.secondary, - mac: globalQuickAccessKeybinding.mac -}); - KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'workbench.action.closeQuickOpen', weight: KeybindingWeight.WorkbenchContrib, @@ -131,21 +116,41 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } }); -CommandsRegistry.registerCommand({ - id: QUICKACCESS_ACTION_ID, - handler: async function (accessor: ServicesAccessor, prefix: unknown) { - const quickInputService = accessor.get(IQuickInputService); +registerAction2(class QuickAccessAction extends Action2 { + constructor() { + super({ + id: 'workbench.action.quickOpen', + title: { + value: localize('quickOpen', "Go to File..."), + original: 'Go to File...' + }, + description: { + description: `Quick access`, + args: [{ + name: 'prefix', + schema: { + 'type': 'string' + } + }] + }, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: globalQuickAccessKeybinding.primary, + secondary: globalQuickAccessKeybinding.secondary, + mac: globalQuickAccessKeybinding.mac + }, + f1: true, + menu: { + id: MenuId.TitleMenuQuickPick, + group: '1/workspaceNav', + order: 1 + } + }); + } + run(accessor: ServicesAccessor, prefix: undefined): void { + const quickInputService = accessor.get(IQuickInputService); quickInputService.quickAccess.show(typeof prefix === 'string' ? prefix : undefined, { preserveValue: typeof prefix === 'string' /* preserve as is if provided */ }); - }, - description: { - description: `Quick access`, - args: [{ - name: 'prefix', - schema: { - 'type': 'string' - } - }] } }); diff --git a/src/vs/workbench/browser/actions/windowActions.ts b/src/vs/workbench/browser/actions/windowActions.ts index 748ababc6db..a0d14adf991 100644 --- a/src/vs/workbench/browser/actions/windowActions.ts +++ b/src/vs/workbench/browser/actions/windowActions.ts @@ -145,6 +145,7 @@ abstract class BaseOpenRecentAction extends Action2 { matchOnDescription: true, onKeyMods: mods => keyMods = mods, quickNavigate: this.isQuickNavigate() ? { keybindings: keybindingService.lookupKeybindings(this.desc.id) } : undefined, + hideInput: this.isQuickNavigate(), onDidTriggerItemButton: async context => { // Remove diff --git a/src/vs/workbench/browser/labels.ts b/src/vs/workbench/browser/labels.ts index e0b778dd82f..4cfe9f32bb5 100644 --- a/src/vs/workbench/browser/labels.ts +++ b/src/vs/workbench/browser/labels.ts @@ -272,14 +272,15 @@ class ResourceLabelWidget extends IconLabel { private readonly _onDidRender = this._register(new Emitter<void>()); readonly onDidRender = this._onDidRender.event; - private label?: IResourceLabelProps; + private label: IResourceLabelProps | undefined = undefined; private decoration = this._register(new MutableDisposable<IDecoration>()); - private options?: IResourceLabelOptions; - private computedIconClasses?: string[]; - private lastKnownDetectedLanguageId?: string; - private computedPathLabel?: string; + private options: IResourceLabelOptions | undefined = undefined; - private needsRedraw?: Redraw; + private computedIconClasses: string[] | undefined = undefined; + private computedLanguageId: string | undefined = undefined; + private computedPathLabel: string | undefined = undefined; + + private needsRedraw: Redraw | undefined = undefined; private isHidden: boolean = false; constructor( @@ -325,8 +326,8 @@ class ResourceLabelWidget extends IconLabel { } if (isEqual(model.uri, resource)) { - if (this.lastKnownDetectedLanguageId !== model.getLanguageId()) { - this.lastKnownDetectedLanguageId = model.getLanguageId(); + if (this.computedLanguageId !== model.getLanguageId()) { + this.computedLanguageId = model.getLanguageId(); this.render({ updateIcon: true, updateDecoration: false }); // update if the language id of the model has changed from our last known state } } @@ -444,8 +445,12 @@ class ResourceLabelWidget extends IconLabel { this.label = label; this.options = options; + if (hasResourceChanged) { + this.computedLanguageId = undefined; // reset computed language since resource changed + } + if (hasPathLabelChanged) { - this.computedPathLabel = undefined; // reset path label due to resource change + this.computedPathLabel = undefined; // reset path label due to resource/path-label change } this.render({ @@ -485,7 +490,7 @@ class ResourceLabelWidget extends IconLabel { clear(): void { this.label = undefined; this.options = undefined; - this.lastKnownDetectedLanguageId = undefined; + this.computedLanguageId = undefined; this.computedIconClasses = undefined; this.computedPathLabel = undefined; @@ -594,7 +599,7 @@ class ResourceLabelWidget extends IconLabel { this.label = undefined; this.options = undefined; - this.lastKnownDetectedLanguageId = undefined; + this.computedLanguageId = undefined; this.computedIconClasses = undefined; this.computedPathLabel = undefined; } diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 1bf6c4750e2..88e4d4f892d 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -617,16 +617,22 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi return { filesToOpenOrCreate: defaultLayout.editors.map<IPath>(file => { + const legacyOverride = file.openWith; + const legacySelection = file.selection && file.selection.start && isNumber(file.selection.start.line) ? { + startLineNumber: file.selection.start.line, + startColumn: isNumber(file.selection.start.column) ? file.selection.start.column : 1, + endLineNumber: isNumber(file.selection.end.line) ? file.selection.end.line : undefined, + endColumn: isNumber(file.selection.end.line) ? (isNumber(file.selection.end.column) ? file.selection.end.column : 1) : undefined, + } : undefined; + return { fileUri: URI.revive(file.uri), - selection: file.selection && file.selection.start && isNumber(file.selection.start.line) ? { - startLineNumber: file.selection.start.line, - startColumn: isNumber(file.selection.start.column) ? file.selection.start.column : 1, - endLineNumber: isNumber(file.selection.end.line) ? file.selection.end.line : undefined, - endColumn: isNumber(file.selection.end.line) ? (isNumber(file.selection.end.column) ? file.selection.end.column : 1) : undefined, - } : undefined, openOnlyIfExists: file.openOnlyIfExists, - editorOverrideId: file.openWith + options: { + selection: legacySelection, + override: legacyOverride, + ...file.options // keep at the end to override legacy selection/override that may be `undefined` + } }; }) }; @@ -2109,13 +2115,15 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi }; type StartupLayoutEventClassification = { - activityBarVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; - sideBarVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; - auxiliaryBarVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; - panelVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; - statusbarVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; - sideBarPosition: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; - panelPosition: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; + owner: 'sbatten'; + comment: 'Information about the layout of the workbench during statup'; + activityBarVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether or the not the activity bar is visible' }; + sideBarVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether or the not the primary side bar is visible' }; + auxiliaryBarVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether or the not the secondary side bar is visible' }; + panelVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether or the not the panel is visible' }; + statusbarVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether or the not the status bar is visible' }; + sideBarPosition: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the primary side bar is on the left or right' }; + panelPosition: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the panel is on the bottom, left, or right' }; }; const layoutDescriptor: StartupLayoutEvent = { diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index 69aad292e8a..c94c38b2e47 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -42,6 +42,8 @@ import { StringSHA1 } from 'vs/base/common/hash'; 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'; interface IPlaceholderViewContainer { readonly id: string; @@ -140,6 +142,15 @@ export class ActivitybarPart extends Part implements IPaneCompositeSelectorPart this.onDidRegisterViewContainers(this.getViewContainers()); this.registerListeners(); + + Registry.as<IProfileStorageRegistry>(Extensions.ProfileStorageRegistry) + .registerKeys([{ + key: ActivitybarPart.PINNED_VIEW_CONTAINERS, + description: localize('pinned view containers', "Activity bar entries visibility customizations") + }, { + key: AccountsActivityActionViewItem.ACCOUNTS_VISIBILITY_PREFERENCE_KEY, + description: localize('accounts visibility key', "Accounts entry visibility customization in the activity bar.") + }]); } private createCompositeBar() { diff --git a/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css b/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css index 169ba8a11f0..abe1427ca20 100644 --- a/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css +++ b/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css @@ -37,14 +37,6 @@ justify-content: space-between; } -/* - * Fix menu jumping and background inheritance in Safari - */ -.monaco-workbench.safari .activitybar > .content { - position: absolute; - background-color: inherit; -} - /** Viewlet Switcher */ .monaco-workbench .activitybar > .content .monaco-action-bar { diff --git a/src/vs/workbench/browser/parts/banner/bannerPart.ts b/src/vs/workbench/browser/parts/banner/bannerPart.ts index b28390d2680..7750ccf5fe3 100644 --- a/src/vs/workbench/browser/parts/banner/bannerPart.ts +++ b/src/vs/workbench/browser/parts/banner/bannerPart.ts @@ -43,7 +43,8 @@ registerThemingParticipant((theme, collector) => { collector.addRule(` .monaco-workbench .part.banner, .monaco-workbench .part.banner .action-container .codicon, - .monaco-workbench .part.banner .message-actions-container .monaco-link + .monaco-workbench .part.banner .message-actions-container .monaco-link, + .monaco-workbench .part.banner .message-container a { color: ${foregroundColor}; } `); } @@ -240,9 +241,8 @@ export class BannerPart extends Part implements IBannerService { messageContainer.appendChild(this.getBannerMessage(item.message)); // Message Actions + this.messageActionsContainer = append(this.element, $('div.message-actions-container')); if (item.actions) { - this.messageActionsContainer = append(this.element, $('div.message-actions-container')); - for (const action of item.actions) { this._register(this.instantiationService.createInstance(Link, this.messageActionsContainer, { ...action, tabIndex: -1 }, {})); } diff --git a/src/vs/workbench/browser/parts/banner/media/bannerpart.css b/src/vs/workbench/browser/parts/banner/media/bannerpart.css index 94d535fb462..fb2d5b4d1e8 100644 --- a/src/vs/workbench/browser/parts/banner/media/bannerpart.css +++ b/src/vs/workbench/browser/parts/banner/media/bannerpart.css @@ -54,6 +54,10 @@ text-decoration: underline; } +.monaco-workbench .part.banner .message-container a { + text-decoration: underline; +} + .monaco-workbench .part.banner .action-container { padding: 0 10px 0 6px; } diff --git a/src/vs/workbench/browser/parts/compositeBar.ts b/src/vs/workbench/browser/parts/compositeBar.ts index 68f6ebacaa8..de6e99067ef 100644 --- a/src/vs/workbench/browser/parts/compositeBar.ts +++ b/src/vs/workbench/browser/parts/compositeBar.ts @@ -227,6 +227,7 @@ export class CompositeBar extends Widget implements ICompositeBar { }, orientation: this.options.orientation, ariaLabel: localize('activityBarAriaLabel', "Active View Switcher"), + ariaRole: 'tablist', animated: false, preventLoopNavigation: this.options.preventLoopNavigation, triggerKeys: { keyDown: true } @@ -426,12 +427,6 @@ export class CompositeBar extends Widget implements ICompositeBar { this.options.openComposite(defaultCompositeId, true); } - // Case: we closed the last visible composite - // Solv: we hide the part - else if (this.visibleComposites.length <= 1) { - this.options.hidePart(); - } - // Case: we closed the default composite // Solv: we open the next visible composite from top else { diff --git a/src/vs/workbench/browser/parts/compositeBarActions.ts b/src/vs/workbench/browser/parts/compositeBarActions.ts index 3bfc0a6eca2..df8b6bc78d0 100644 --- a/src/vs/workbench/browser/parts/compositeBarActions.ts +++ b/src/vs/workbench/browser/parts/compositeBarActions.ts @@ -7,7 +7,7 @@ import { localize } from 'vs/nls'; import { Action, IAction, Separator } from 'vs/base/common/actions'; import { $, addDisposableListener, append, clearNode, EventHelper, EventType, getDomNodePagePosition, hide, show } from 'vs/base/browser/dom'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { dispose, toDisposable, MutableDisposable, IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { dispose, toDisposable, MutableDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IThemeService, IColorTheme, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { TextBadge, NumberBadge, IBadge, IconBadge, ProgressBadge } from 'vs/workbench/services/activity/common/activity'; @@ -21,7 +21,7 @@ import { CompositeDragAndDropObserver, ICompositeDragAndDrop, Before2D, toggleDr import { Color } from 'vs/base/common/color'; import { IBaseActionViewItemOptions, BaseActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { Codicon } from 'vs/base/common/codicons'; -import { IHoverService } from 'vs/workbench/services/hover/browser/hover'; +import { IHoverService, IHoverWidget } from 'vs/workbench/services/hover/browser/hover'; import { RunOnceScheduler } from 'vs/base/common/async'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; @@ -149,7 +149,7 @@ export class ActivityActionViewItem extends BaseActionViewItem { private keybindingLabel: string | undefined | null; private readonly hoverDisposables = this._register(new DisposableStore()); - private readonly hover = this._register(new MutableDisposable<IDisposable>()); + private lastHover: IHoverWidget | undefined; private readonly showHoverScheduler = new RunOnceScheduler(() => this.showHover(), 0); private static _hoverLeaveTime = 0; @@ -397,21 +397,21 @@ export class ActivityActionViewItem extends BaseActionViewItem { }, true)); this.hoverDisposables.add(addDisposableListener(this.container, EventType.MOUSE_LEAVE, () => { ActivityActionViewItem._hoverLeaveTime = Date.now(); - this.hover.value = undefined; + this.hoverService.hideHover(); this.showHoverScheduler.cancel(); }, true)); this.hoverDisposables.add(toDisposable(() => { - this.hover.value = undefined; + this.hoverService.hideHover(); this.showHoverScheduler.cancel(); })); } private showHover(skipFadeInAnimation: boolean = false): void { - if (this.hover.value) { + if (this.lastHover && !this.lastHover.isDisposed) { return; } const hoverPosition = this.options.hoverOptions!.position(); - this.hover.value = this.hoverService.showHover({ + this.lastHover = this.hoverService.showHover({ target: this.container, hoverPosition, content: this.computeTitle(), diff --git a/src/vs/workbench/browser/parts/editor/binaryEditor.ts b/src/vs/workbench/browser/parts/editor/binaryEditor.ts index 99701724a23..4f54228962d 100644 --- a/src/vs/workbench/browser/parts/editor/binaryEditor.ts +++ b/src/vs/workbench/browser/parts/editor/binaryEditor.ts @@ -3,26 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./media/binaryeditor'; import { localize } from 'vs/nls'; import { Emitter } from 'vs/base/common/event'; -import { IEditorOpenContext } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; -import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; -import { ScrollbarVisibility } from 'vs/base/common/scrollable'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { Dimension, size, clearNode } from 'vs/base/browser/dom'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { IStorageService } from 'vs/platform/storage/common/storage'; -import { assertIsDefined, assertAllDefined } from 'vs/base/common/types'; import { ByteSize } from 'vs/platform/files/common/files'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { Link } from 'vs/platform/opener/browser/link'; +import { EditorPlaceholder, IEditorPlaceholderContents } from 'vs/workbench/browser/parts/editor/editorPlaceholder'; export interface IOpenCallbacks { openInternal: (input: EditorInput, options: IEditorOptions | undefined) => Promise<void>; @@ -31,7 +22,7 @@ export interface IOpenCallbacks { /* * This class is only intended to be subclassed and not instantiated. */ -export abstract class BaseBinaryResourceEditor extends EditorPane { +export abstract class BaseBinaryResourceEditor extends EditorPlaceholder { private readonly _onDidChangeMetadata = this._register(new Emitter<void>()); readonly onDidChangeMetadata = this._onDidChangeMetadata.event; @@ -40,9 +31,6 @@ export abstract class BaseBinaryResourceEditor extends EditorPane { readonly onDidOpenInPlace = this._onDidOpenInPlace.event; private metadata: string | undefined; - private binaryContainer: HTMLElement | undefined; - private scrollbar: DomScrollableElement | undefined; - private inputDisposable = this._register(new MutableDisposable()); constructor( id: string, @@ -50,78 +38,44 @@ export abstract class BaseBinaryResourceEditor extends EditorPane { telemetryService: ITelemetryService, themeService: IThemeService, @IStorageService storageService: IStorageService, - @IInstantiationService private readonly instantiationService: IInstantiationService + @IInstantiationService instantiationService: IInstantiationService ) { - super(id, telemetryService, themeService, storageService); + super(id, telemetryService, themeService, storageService, instantiationService); } override getTitle(): string { return this.input ? this.input.getName() : localize('binaryEditor', "Binary Viewer"); } - protected createEditor(parent: HTMLElement): void { - - // Container for Binary - this.binaryContainer = document.createElement('div'); - this.binaryContainer.className = 'monaco-binary-resource-editor'; - this.binaryContainer.style.outline = 'none'; - this.binaryContainer.tabIndex = 0; // enable focus support from the editor part (do not remove) - - // Custom Scrollbars - this.scrollbar = this._register(new DomScrollableElement(this.binaryContainer, { horizontal: ScrollbarVisibility.Auto, vertical: ScrollbarVisibility.Auto })); - parent.appendChild(this.scrollbar.getDomNode()); - } - - override async setInput(input: EditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise<void> { - await super.setInput(input, options, context, token); + protected async getContents(input: EditorInput, options: IEditorOptions): Promise<IEditorPlaceholderContents> { const model = await input.resolve(); - // Check for cancellation - if (token.isCancellationRequested) { - return; - } - // Assert Model instance if (!(model instanceof BinaryEditorModel)) { throw new Error('Unable to open file as binary'); } - // Render Input - this.inputDisposable.value = this.renderInput(input, options, model); - } - - private renderInput(input: EditorInput, options: IEditorOptions | undefined, model: BinaryEditorModel): IDisposable { - const [binaryContainer, scrollbar] = assertAllDefined(this.binaryContainer, this.scrollbar); - - clearNode(binaryContainer); - - const disposables = new DisposableStore(); - - const label = document.createElement('p'); - label.textContent = localize('nativeBinaryError', "The file is not displayed in the editor because it is either binary or uses an unsupported text encoding."); - binaryContainer.appendChild(label); - - this._register(this.instantiationService.createInstance(Link, label, { - label: localize('openAsText', "Do you want to open it anyway?"), - href: '' - }, { - opener: async () => { - - // Open in place - await this.callbacks.openInternal(input, options); - - // Signal to listeners that the binary editor has been opened in-place - this._onDidOpenInPlace.fire(); - } - })); - - scrollbar.scanDomNode(); - // Update metadata const size = model.getSize(); this.handleMetadataChanged(typeof size === 'number' ? ByteSize.formatSize(size) : ''); - return disposables; + return { + icon: '$(warning)', + label: localize('binaryError', "The file is not displayed in the editor because it is either binary or uses an unsupported text encoding."), + actions: [ + { + label: localize('openAnyway', "Open Anyway"), + run: async () => { + + // Open in place + await this.callbacks.openInternal(input, options); + + // Signal to listeners that the binary editor has been opened in-place + this._onDidOpenInPlace.fire(); + } + } + ] + }; } private handleMetadataChanged(meta: string | undefined): void { @@ -133,38 +87,4 @@ export abstract class BaseBinaryResourceEditor extends EditorPane { getMetadata(): string | undefined { return this.metadata; } - - override clearInput(): void { - - // Clear Meta - this.handleMetadataChanged(undefined); - - // Clear the rest - if (this.binaryContainer) { - clearNode(this.binaryContainer); - } - this.inputDisposable.clear(); - - super.clearInput(); - } - - layout(dimension: Dimension): void { - - // Pass on to Binary Container - const [binaryContainer, scrollbar] = assertAllDefined(this.binaryContainer, this.scrollbar); - size(binaryContainer, dimension.width, dimension.height); - scrollbar.scanDomNode(); - } - - override focus(): void { - const binaryContainer = assertIsDefined(this.binaryContainer); - - binaryContainer.focus(); - } - - override dispose(): void { - this.binaryContainer?.remove(); - - super.dispose(); - } } diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts index 276c37b1bd0..a969831de5b 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts @@ -14,8 +14,7 @@ import { extUri } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import 'vs/css!./media/breadcrumbscontrol'; import { localize } from 'vs/nls'; -import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; -import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; @@ -494,26 +493,32 @@ export class BreadcrumbsControl { //#region commands // toggle command -MenuRegistry.appendMenuItem(MenuId.CommandPalette, { - command: { - id: 'breadcrumbs.toggle', - title: { value: localize('cmd.toggle', "Toggle Breadcrumbs"), original: 'Toggle Breadcrumbs' }, - category: CATEGORIES.View +registerAction2(class ToggleBreadcrumb extends Action2 { + + constructor() { + super({ + id: 'breadcrumbs.toggle', + title: { + value: localize('cmd.toggle', "Toggle Breadcrumbs"), + mnemonicTitle: localize('miShowBreadcrumbs', "Show &&Breadcrumbs"), + original: 'Toggle Breadcrumbs', + }, + category: CATEGORIES.View, + toggled: ContextKeyExpr.equals('config.breadcrumbs.enabled', true), + menu: [ + { id: MenuId.CommandPalette }, + { id: MenuId.MenubarViewMenu, group: '5_editor', order: 3 }, + { id: MenuId.NotebookToolbar, group: 'notebookLayout', order: 2 } + ] + }); } -}); -MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '5_editor', - order: 3, - command: { - id: 'breadcrumbs.toggle', - title: localize('miShowBreadcrumbs', "Show &&Breadcrumbs"), - toggled: ContextKeyExpr.equals('config.breadcrumbs.enabled', true) + + run(accessor: ServicesAccessor): void { + let config = accessor.get(IConfigurationService); + let value = BreadcrumbsConfig.IsEnabled.bindTo(config).getValue(); + BreadcrumbsConfig.IsEnabled.bindTo(config).updateValue(!value); } -}); -CommandsRegistry.registerCommand('breadcrumbs.toggle', accessor => { - let config = accessor.get(IConfigurationService); - let value = BreadcrumbsConfig.IsEnabled.bindTo(config).getValue(); - BreadcrumbsConfig.IsEnabled.bindTo(config).updateValue(!value); + }); // focus/focus-and-select @@ -530,20 +535,27 @@ function focusAndSelectHandler(accessor: ServicesAccessor, select: boolean): voi } } } -MenuRegistry.appendMenuItem(MenuId.CommandPalette, { - command: { - id: 'breadcrumbs.focusAndSelect', - title: { value: localize('cmd.focus', "Focus Breadcrumbs"), original: 'Focus Breadcrumbs' }, - precondition: BreadcrumbsControl.CK_BreadcrumbsVisible +registerAction2(class FocusAndSelectBreadcrumbs extends Action2 { + constructor() { + super({ + id: 'breadcrumbs.focusAndSelect', + title: { + value: localize('cmd.focus', "Focus Breadcrumbs"), + original: 'Focus Breadcrumbs' + }, + precondition: BreadcrumbsControl.CK_BreadcrumbsVisible, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Period, + when: BreadcrumbsControl.CK_BreadcrumbsPossible, + } + }); + } + run(accessor: ServicesAccessor, ...args: any[]): void { + focusAndSelectHandler(accessor, true); } }); -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: 'breadcrumbs.focusAndSelect', - weight: KeybindingWeight.WorkbenchContrib, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Period, - when: BreadcrumbsControl.CK_BreadcrumbsPossible, - handler: accessor => focusAndSelectHandler(accessor, true) -}); + KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'breadcrumbs.focus', weight: KeybindingWeight.WorkbenchContrib, diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index 318e7deb266..8e0c175392e 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -314,6 +314,9 @@ 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 }); diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index 0995d4e6e48..a993829e847 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -19,7 +19,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; -import { IFileDialogService, ConfirmResult } from 'vs/platform/dialogs/common/dialogs'; +import { IFileDialogService, ConfirmResult, IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { ItemActivation, IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { AllEditorsByMostRecentlyUsedQuickAccess, ActiveGroupEditorsByMostRecentlyUsedQuickAccess, AllEditorsByAppearanceQuickAccess } from 'vs/workbench/browser/parts/editor/editorQuickAccess'; import { Codicon } from 'vs/base/common/codicons'; @@ -1483,13 +1483,26 @@ export class ClearRecentFilesAction extends Action { id: string, label: string, @IWorkspacesService private readonly workspacesService: IWorkspacesService, - @IHistoryService private readonly historyService: IHistoryService + @IHistoryService private readonly historyService: IHistoryService, + @IDialogService private readonly dialogService: IDialogService ) { super(id, label); } override async run(): Promise<void> { + // Ask for confirmation + const { confirmed } = await this.dialogService.confirm({ + message: localize('confirmClearRecentsMessage', "Do you want to clear all recently opened files and workspaces?"), + detail: localize('confirmClearDetail', "This action is irreversible!"), + primaryButton: localize({ key: 'clearButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Clear"), + type: 'warning' + }); + + if (!confirmed) { + return; + } + // Clear global recently opened this.workspacesService.clearRecentlyOpened(); @@ -1746,14 +1759,27 @@ export class ClearEditorHistoryAction extends Action { constructor( id: string, label: string, - @IHistoryService private readonly historyService: IHistoryService + @IHistoryService private readonly historyService: IHistoryService, + @IDialogService private readonly dialogService: IDialogService ) { super(id, label); } override async run(): Promise<void> { - // Editor history + // Ask for confirmation + const { confirmed } = await this.dialogService.confirm({ + message: localize('confirmClearEditorHistoryMessage', "Do you want to clear the history of recently opened editors?"), + detail: localize('confirmClearDetail', "This action is irreversible!"), + primaryButton: localize({ key: 'clearButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Clear"), + type: 'warning' + }); + + if (!confirmed) { + return; + } + + // Clear editor history this.historyService.clear(); } } diff --git a/src/vs/workbench/browser/parts/editor/editorCommands.ts b/src/vs/workbench/browser/parts/editor/editorCommands.ts index cb67d57d434..3624936e41f 100644 --- a/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/editorCommands.ts @@ -844,7 +844,8 @@ function registerCloseEditorCommands() { handler: (accessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { return Promise.all(getEditorsContext(accessor, resourceOrContext, context).groups.map(async group => { if (group) { - return group.closeAllEditors({ excludeSticky: true }); + await group.closeAllEditors({ excludeSticky: true }); + return; } })); } diff --git a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts index 2fa3f3998d6..0ee0ad2f093 100644 --- a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts +++ b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts @@ -4,34 +4,37 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/editordroptarget'; -import { Extensions as DragAndDropExtensions, LocalSelectionTransfer, DraggedEditorIdentifier, ResourcesDropHandler, DraggedEditorGroupIdentifier, containsDragType, CodeDataTransfers, DraggedTreeItemsIdentifier, extractTreeDropData, IDragAndDropContributionRegistry } from 'vs/workbench/browser/dnd'; -import { addDisposableListener, EventType, EventHelper, isAncestor, DragAndDropObserver } from 'vs/base/browser/dom'; -import { IEditorGroupsAccessor, IEditorGroupView, fillActiveEditorViewState } from 'vs/workbench/browser/parts/editor/editor'; -import { EDITOR_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/common/theme'; -import { IThemeService, Themable } from 'vs/platform/theme/common/themeService'; -import { activeContrastBorder } from 'vs/platform/theme/common/colorRegistry'; -import { IEditorIdentifier, EditorInputCapabilities, IUntypedEditorInput } from 'vs/workbench/common/editor'; -import { isMacintosh, isWeb } from 'vs/base/common/platform'; -import { GroupDirection, IEditorGroupsService, IMergeGroupOptions, MergeGroupMode } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { DataTransfers } from 'vs/base/browser/dnd'; +import { addDisposableListener, DragAndDropObserver, EventHelper, EventType, isAncestor } from 'vs/base/browser/dom'; +import { renderFormattedText } from 'vs/base/browser/formattedTextRenderer'; +import { RunOnceScheduler } from 'vs/base/common/async'; import { toDisposable } from 'vs/base/common/lifecycle'; +import { isMacintosh, isWeb } from 'vs/base/common/platform'; +import { assertAllDefined, assertIsDefined } from 'vs/base/common/types'; +import { localize } from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { RunOnceScheduler } from 'vs/base/common/async'; -import { DataTransfers } from 'vs/base/browser/dnd'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { activeContrastBorder } from 'vs/platform/theme/common/colorRegistry'; +import { IThemeService, Themable } from 'vs/platform/theme/common/themeService'; +import { isTemporaryWorkspace, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { CodeDataTransfers, containsDragType, DraggedEditorGroupIdentifier, DraggedEditorIdentifier, DraggedTreeItemsIdentifier, Extensions as DragAndDropExtensions, extractTreeDropData, IDragAndDropContributionRegistry, LocalSelectionTransfer, ResourcesDropHandler } from 'vs/workbench/browser/dnd'; +import { fillActiveEditorViewState, IEditorGroupsAccessor, IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; +import { EditorInputCapabilities, IEditorIdentifier, IUntypedEditorInput } from 'vs/workbench/common/editor'; +import { EDITOR_DRAG_AND_DROP_BACKGROUND, EDITOR_DROP_INTO_PROMPT_BACKGROUND, EDITOR_DROP_INTO_PROMPT_BORDER, EDITOR_DROP_INTO_PROMPT_FOREGROUND } from 'vs/workbench/common/theme'; +import { GroupDirection, IEditorGroupsService, IMergeGroupOptions, MergeGroupMode } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { assertIsDefined, assertAllDefined } from 'vs/base/common/types'; import { ITreeViewsService } from 'vs/workbench/services/views/browser/treeViewsService'; -import { isTemporaryWorkspace, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { Registry } from 'vs/platform/registry/common/platform'; interface IDropOperation { splitDirection?: GroupDirection; } -function isDragIntoEditorEvent(configurationService: IConfigurationService, e: DragEvent): boolean { - if (!configurationService.getValue<boolean>('workbench.experimental.editor.dragAndDropIntoEditor.enabled')) { - return false; - } +function isDropIntoEditorEnabledGlobally(configurationService: IConfigurationService) { + return configurationService.getValue<boolean>('workbench.experimental.editor.dropIntoEditor.enabled'); +} + +function isDragIntoEditorEvent(e: DragEvent): boolean { return e.shiftKey; } @@ -41,9 +44,12 @@ class DropOverlay extends Themable { private container: HTMLElement | undefined; private overlay: HTMLElement | undefined; + private dropIntoPromptElement?: HTMLSpanElement; private currentDropOperation: IDropOperation | undefined; + private _disposed: boolean | undefined; + get disposed(): boolean { return !!this._disposed; } private cleanupOverlayScheduler: RunOnceScheduler; @@ -51,6 +57,8 @@ class DropOverlay extends Themable { private readonly groupTransfer = LocalSelectionTransfer.getInstance<DraggedEditorGroupIdentifier>(); private readonly treeItemsTransfer = LocalSelectionTransfer.getInstance<DraggedTreeItemsIdentifier>(); + private readonly enableDropIntoEditor: boolean; + constructor( private accessor: IEditorGroupsAccessor, private groupView: IEditorGroupView, @@ -66,11 +74,9 @@ class DropOverlay extends Themable { this.cleanupOverlayScheduler = this._register(new RunOnceScheduler(() => this.dispose(), 300)); - this.create(); - } + this.enableDropIntoEditor = isDropIntoEditorEnabledGlobally(this.configurationService) && this.isDropIntoActiveEditorEnabled(); - get disposed(): boolean { - return !!this._disposed; + this.create(); } private create(): void { @@ -94,6 +100,12 @@ class DropOverlay extends Themable { this.overlay.classList.add('editor-group-overlay-indicator'); container.appendChild(this.overlay); + if (this.enableDropIntoEditor) { + this.dropIntoPromptElement = renderFormattedText(localize('dropIntoEditorPrompt', "Hold __{0}__ to drop into editor", isMacintosh ? '⇧' : 'Shift'), {}); + this.dropIntoPromptElement.classList.add('editor-group-overlay-drop-into-prompt'); + this.overlay.appendChild(this.dropIntoPromptElement); + } + // Overlay Event Handling this.registerListeners(container); @@ -113,13 +125,27 @@ class DropOverlay extends Themable { overlay.style.outlineOffset = activeContrastBorderColor ? '-2px' : ''; overlay.style.outlineStyle = activeContrastBorderColor ? 'dashed' : ''; overlay.style.outlineWidth = activeContrastBorderColor ? '2px' : ''; + + if (this.dropIntoPromptElement) { + this.dropIntoPromptElement.style.backgroundColor = this.getColor(EDITOR_DROP_INTO_PROMPT_BACKGROUND) ?? ''; + this.dropIntoPromptElement.style.color = this.getColor(EDITOR_DROP_INTO_PROMPT_FOREGROUND) ?? ''; + + const borderColor = this.getColor(EDITOR_DROP_INTO_PROMPT_BORDER); + if (borderColor) { + this.dropIntoPromptElement.style.borderWidth = '1px'; + this.dropIntoPromptElement.style.borderStyle = 'solid'; + this.dropIntoPromptElement.style.borderColor = borderColor; + } else { + this.dropIntoPromptElement.style.borderWidth = '0'; + } + } } private registerListeners(container: HTMLElement): void { this._register(new DragAndDropObserver(container, { onDragEnter: e => undefined, onDragOver: e => { - if (isDragIntoEditorEvent(this.configurationService, e)) { + if (this.enableDropIntoEditor && isDragIntoEditorEvent(e)) { this.dispose(); return; } @@ -199,6 +225,10 @@ class DropOverlay extends Themable { })); } + private isDropIntoActiveEditorEnabled(): boolean { + return !!this.groupView.activeEditor?.hasCapability(EditorInputCapabilities.CanDropIntoEditor); + } + private findSourceGroupView(): IEditorGroupView | undefined { // Check for group transfer @@ -444,18 +474,23 @@ class DropOverlay extends Themable { switch (splitDirection) { case GroupDirection.UP: this.doPositionOverlay({ top: '0', left: '0', width: '100%', height: '50%' }); + this.toggleDropIntoPrompt(false); break; case GroupDirection.DOWN: this.doPositionOverlay({ top: '50%', left: '0', width: '100%', height: '50%' }); + this.toggleDropIntoPrompt(false); break; case GroupDirection.LEFT: this.doPositionOverlay({ top: '0', left: '0', width: '50%', height: '100%' }); + this.toggleDropIntoPrompt(false); break; case GroupDirection.RIGHT: this.doPositionOverlay({ top: '0', left: '50%', width: '50%', height: '100%' }); + this.toggleDropIntoPrompt(false); break; default: this.doPositionOverlay({ top: '0', left: '0', width: '100%', height: '100%' }); + this.toggleDropIntoPrompt(true); } // Make sure the overlay is visible now @@ -510,6 +545,13 @@ class DropOverlay extends Themable { this.currentDropOperation = undefined; } + private toggleDropIntoPrompt(showing: boolean) { + if (!this.dropIntoPromptElement) { + return; + } + this.dropIntoPromptElement.style.opacity = showing ? '1' : '0'; + } + contains(element: HTMLElement): boolean { return element === this.container || element === this.overlay; } @@ -566,7 +608,7 @@ export class EditorDropTarget extends Themable { } private onDragEnter(event: DragEvent): void { - if (isDragIntoEditorEvent(this.configurationService, event)) { + if (isDropIntoEditorEnabledGlobally(this.configurationService) && isDragIntoEditorEvent(event)) { return; } diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 0286fc0b08f..7ea0ca109d4 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -9,7 +9,7 @@ import { GroupIdentifier, CloseDirection, IEditorCloseEvent, IEditorPane, SaveRe import { ActiveEditorGroupLockedContext, ActiveEditorDirtyContext, EditorGroupEditorsCountContext, ActiveEditorStickyContext, ActiveEditorPinnedContext, ActiveEditorLastInGroupContext, ActiveEditorFirstInGroupContext } from 'vs/workbench/common/contextkeys'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; -import { Event, Emitter, Relay } from 'vs/base/common/event'; +import { Emitter, Relay } from 'vs/base/common/event'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Dimension, trackFocus, addDisposableListener, EventType, EventHelper, findParentWithClass, clearNode, isAncestor, asCSSUrl } from 'vs/base/browser/dom'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; @@ -26,10 +26,7 @@ import { IEditorProgressService } from 'vs/platform/progress/common/progress'; import { EditorProgressIndicator } from 'vs/workbench/services/progress/browser/progressIndicator'; import { localize } from 'vs/nls'; import { coalesce, firstOrDefault } from 'vs/base/common/arrays'; -import { isCancellationError } from 'vs/base/common/errors'; import { combinedDisposable, dispose, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { Severity, INotificationService } from 'vs/platform/notification/common/notification'; -import { toErrorMessage, isErrorWithActions } from 'vs/base/common/errorMessage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { DeferredPromise, Promises, RunOnceWorker } from 'vs/base/common/async'; import { EventType as TouchEventType, GestureEvent } from 'vs/base/browser/touch'; @@ -48,9 +45,8 @@ import { hash } from 'vs/base/common/hash'; import { getMimeTypes } from 'vs/editor/common/services/languagesAssociations'; import { extname, isEqual } from 'vs/base/common/resources'; import { FileAccess, Schemas } from 'vs/base/common/network'; -import { EditorActivation, EditorOpenSource, IEditorOptions } from 'vs/platform/editor/common/editor'; -import { IDialogService, IFileDialogService, ConfirmResult } from 'vs/platform/dialogs/common/dialogs'; -import { ILogService } from 'vs/platform/log/common/log'; +import { EditorActivation, IEditorOptions } from 'vs/platform/editor/common/editor'; +import { IFileDialogService, ConfirmResult } from 'vs/platform/dialogs/common/dialogs'; import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { withNullAsUndefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; @@ -134,7 +130,6 @@ export class EditorGroupView extends Themable implements IEditorGroupView { private readonly whenRestoredPromise = new DeferredPromise<void>(); readonly whenRestored = this.whenRestoredPromise.p; - private isRestored = false; constructor( private accessor: IEditorGroupsAccessor, @@ -143,14 +138,11 @@ export class EditorGroupView extends Themable implements IEditorGroupView { @IInstantiationService private readonly instantiationService: IInstantiationService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IThemeService themeService: IThemeService, - @INotificationService private readonly notificationService: INotificationService, - @IDialogService private readonly dialogService: IDialogService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IKeybindingService private readonly keybindingService: IKeybindingService, @IMenuService private readonly menuService: IMenuService, @IContextMenuService private readonly contextMenuService: IContextMenuService, @IFileDialogService private readonly fileDialogService: IFileDialogService, - @ILogService private readonly logService: ILogService, @IEditorService private readonly editorService: EditorServiceImpl, @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService @@ -235,7 +227,6 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Signal restored once editors have restored restoreEditorsPromise.finally(() => { - this.isRestored = true; this.whenRestoredPromise.complete(); }); @@ -1105,9 +1096,9 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this._onDidActiveEditorChange.fire({ editor }); } - // Handle errors but do not bubble them up + // Indicate error as an event but do not bubble them up if (error) { - await this.doHandleOpenEditorError(error, editor, options); + this._onDidOpenEditorFail.fire(editor); } // Without an editor pane, recover by closing the active editor @@ -1132,84 +1123,6 @@ export class EditorGroupView extends Themable implements IEditorGroupView { return openEditorPromise; } - private async doHandleOpenEditorError(error: Error, editor: EditorInput, options?: IEditorOptions): Promise<void> { - - // Report error only if we are not told to ignore errors that occur from opening an editor - if (!isCancellationError(error) && !options?.ignoreError) { - - // Always log the error to figure out what is going on - this.logService.error(error); - - // Since it is more likely that errors fail to open when restoring them e.g. - // because files got deleted or moved meanwhile, we do not show any notifications - // if we are still restoring editors. - if (this.isRestored) { - - // Extract possible error actions from the error - let errorActions: readonly IAction[] | undefined = undefined; - if (isErrorWithActions(error)) { - errorActions = error.actions; - } - - // If the context is USER, we try to show a modal dialog instead of a background notification - if (options?.source === EditorOpenSource.USER) { - const buttons: string[] = []; - if (Array.isArray(errorActions) && errorActions.length > 0) { - for (const errorAction of errorActions) { - buttons.push(errorAction.label); - } - } else { - buttons.push(localize('ok', 'OK')); - } - - let cancelId: number | undefined = undefined; - if (buttons.length === 1) { - buttons.push(localize('cancel', "Cancel")); - cancelId = 1; - } - - const result = await this.dialogService.show( - Severity.Error, - localize('editorOpenErrorDialog', "Unable to open '{0}'", editor.getName()), - buttons, - { - detail: toErrorMessage(error), - cancelId - } - ); - - // Make sure to run any error action if present - if (result.choice !== cancelId && Array.isArray(errorActions)) { - const errorAction = errorActions[result.choice]; - if (errorAction) { - errorAction.run(); - } - } - } - - // Otherwise, show a background notification. - else { - const actions = { primary: [] as readonly IAction[] }; - if (Array.isArray(errorActions)) { - actions.primary = errorActions; - } - - const handle = this.notificationService.notify({ - id: `${hash(editor.resource?.toString())}`, // unique per editor - severity: Severity.Error, - message: localize('editorOpenError', "Unable to open '{0}': {1}.", editor.getName(), toErrorMessage(error)), - actions - }); - - Event.once(handle.onDidClose)(() => actions.primary && dispose(actions.primary)); - } - } - } - - // Event - this._onDidOpenEditorFail.fire(editor); - } - //#endregion //#region openEditors() @@ -1786,7 +1699,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { //#region closeAllEditors() - async closeAllEditors(options?: ICloseAllEditorsOptions): Promise<void> { + async closeAllEditors(options?: ICloseAllEditorsOptions): Promise<boolean> { if (this.isEmpty) { // If the group is empty and the request is to close all editors, we still close @@ -1796,17 +1709,19 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this.accessor.removeGroup(this); } - return; + return true; } // Check for dirty and veto const veto = await this.handleDirtyClosing(this.model.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE, options)); if (veto) { - return; + return false; } // Do close this.doCloseAllEditors(options); + + return true; } private doCloseAllEditors(options?: ICloseAllEditorsOptions): void { diff --git a/src/vs/workbench/browser/parts/editor/editorPanes.ts b/src/vs/workbench/browser/parts/editor/editorPanes.ts index a587913b667..268c214a232 100644 --- a/src/vs/workbench/browser/parts/editor/editorPanes.ts +++ b/src/vs/workbench/browser/parts/editor/editorPanes.ts @@ -3,6 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize } from 'vs/nls'; +import { IAction } from 'vs/base/common/actions'; +import { Emitter } from 'vs/base/common/event'; +import Severity from 'vs/base/common/severity'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { EditorExtensions, EditorInputCapabilities, IEditorOpenContext, IVisibleEditorPane } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; @@ -14,12 +18,14 @@ import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IEditorProgressService, LongRunningOperation } from 'vs/platform/progress/common/progress'; import { IEditorGroupView, DEFAULT_EDITOR_MIN_DIMENSIONS, DEFAULT_EDITOR_MAX_DIMENSIONS } from 'vs/workbench/browser/parts/editor/editor'; -import { Emitter } from 'vs/base/common/event'; import { assertIsDefined } from 'vs/base/common/types'; import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; -import { UnavailableResourceErrorEditor, UnknownErrorEditor, WorkspaceTrustRequiredEditor } from 'vs/workbench/browser/parts/editor/editorPlaceholder'; -import { IEditorOptions } from 'vs/platform/editor/common/editor'; -import { FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; +import { ErrorPlaceholderEditor, IErrorEditorPlaceholderOptions, WorkspaceTrustRequiredPlaceholderEditor } from 'vs/workbench/browser/parts/editor/editorPlaceholder'; +import { EditorOpenSource, IEditorOptions } from 'vs/platform/editor/common/editor'; +import { isCancellationError } from 'vs/base/common/errors'; +import { isErrorWithActions, toErrorMessage } from 'vs/base/common/errorMessage'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; export interface IOpenEditorResult { @@ -88,7 +94,9 @@ export class EditorPanes extends Disposable { @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IEditorProgressService private readonly editorProgressService: IEditorProgressService, - @IWorkspaceTrustManagementService private readonly workspaceTrustService: IWorkspaceTrustManagementService + @IWorkspaceTrustManagementService private readonly workspaceTrustService: IWorkspaceTrustManagementService, + @ILogService private readonly logService: ILogService, + @IDialogService private readonly dialogService: IDialogService ) { super(); @@ -118,26 +126,91 @@ export class EditorPanes extends Disposable { return await this.doOpenEditor(this.getEditorPaneDescriptor(editor), editor, options, context); } catch (error) { - // We failed to open an editor and thus fallback to show a generic - // editor in error state as a way for the user to be aware. - // Previously we would immediately close the editor and show a - // error notification which was easy to not see. - // - // Besides, we want to preserve the users editor UI state as much - // as possible, so closing editors is never really an option. + // First check if caller instructed us to ignore error handling + if (options?.ignoreError) { + return { error }; + } + + // In case of an error when opening an editor, we still want to show + // an editor in the desired location to preserve the user intent and + // view state (e.g. when restoring). // - // Related issues: - // - https://github.com/microsoft/vscode/issues/110062 - // - https://github.com/microsoft/vscode/issues/142875 + // For that reason we have place holder editors that can convey a + // message with actions the user can click on. + + return this.doShowError(error, editor, options, context); + } + } + + private async doShowError(error: Error, editor: EditorInput, options?: IEditorOptions, context?: IEditorOpenContext): Promise<IOpenEditorResult> { + + // Always log the error to figure out what is going on + this.logService.error(error); + + // Show as modal dialog when explicit user action + let errorHandled = false; + if (options?.source === EditorOpenSource.USER) { - const isUnavailableResource = (<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND; - const editorPlaceholder = isUnavailableResource ? UnavailableResourceErrorEditor.DESCRIPTOR : UnknownErrorEditor.DESCRIPTOR; + // Extract possible error actions from the error + let errorActions: readonly IAction[] | undefined = undefined; + if (isErrorWithActions(error)) { + errorActions = error.actions; + } + + const buttons: string[] = []; + if (errorActions && errorActions.length > 0) { + for (const errorAction of errorActions) { + buttons.push(errorAction.label); + } + } else { + buttons.push(localize('ok', 'OK')); + } - return { - ...(await this.doOpenEditor(editorPlaceholder, editor, options, context)), - error - }; + let cancelId: number | undefined = undefined; + if (buttons.length === 1) { + buttons.push(localize('cancel', "Cancel")); + cancelId = 1; + } + + const result = await this.dialogService.show( + Severity.Error, + localize('editorOpenErrorDialog', "Unable to open '{0}'", editor.getName()), + buttons, + { + detail: toErrorMessage(error), + cancelId + } + ); + + // Make sure to run any error action if present + if (result.choice !== cancelId && errorActions) { + const errorAction = errorActions[result.choice]; + if (errorAction) { + const result = errorAction.run(); + if (result instanceof Promise) { + result.catch(error => this.dialogService.show(Severity.Error, toErrorMessage(error))); + } + + errorHandled = true; // consider the error as handled! + } + } } + + // Return early if the user dealt with the error already + if (errorHandled) { + return { error }; + } + + // Show as editor placeholder: pass over the error to display + const editorPlaceholderOptions: IErrorEditorPlaceholderOptions = { ...options }; + if (!isCancellationError(error)) { + editorPlaceholderOptions.error = error; + } + + return { + ...(await this.doOpenEditor(ErrorPlaceholderEditor.DESCRIPTOR, editor, editorPlaceholderOptions, context)), + error + }; } private async doOpenEditor(descriptor: IEditorPaneDescriptor, editor: EditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext = Object.create(null)): Promise<IOpenEditorResult> { @@ -165,7 +238,7 @@ export class EditorPanes extends Disposable { // but the current workspace is untrusted, we fallback to a generic // editor descriptor to indicate this an do NOT load the registered // editor. - return WorkspaceTrustRequiredEditor.DESCRIPTOR; + return WorkspaceTrustRequiredPlaceholderEditor.DESCRIPTOR; } return assertIsDefined(this.editorPanesRegistry.getEditorPane(editor)); diff --git a/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts b/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts index af524fb057f..0464f14265b 100644 --- a/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts +++ b/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts @@ -5,14 +5,15 @@ import 'vs/css!./media/editorplaceholder'; import { localize } from 'vs/nls'; +import Severity from 'vs/base/common/severity'; import { IEditorOpenContext } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import { ScrollbarVisibility } from 'vs/base/common/scrollable'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { Dimension, size, clearNode } from 'vs/base/browser/dom'; +import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { Dimension, size, clearNode, $ } from 'vs/base/browser/dom'; import { CancellationToken } from 'vs/base/common/cancellation'; import { DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { IStorageService } from 'vs/platform/storage/common/storage'; @@ -23,8 +24,29 @@ import { EditorOpenSource, IEditorOptions } from 'vs/platform/editor/common/edit import { computeEditorAriaLabel, EditorPaneDescriptor } from 'vs/workbench/browser/editor'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Link } from 'vs/platform/opener/browser/link'; +import { SimpleIconLabel } from 'vs/base/browser/ui/iconLabel/simpleIconLabel'; +import { editorErrorForeground, editorInfoForeground, editorWarningForeground } from 'vs/platform/theme/common/colorRegistry'; +import { Codicon } from 'vs/base/common/codicons'; +import { FileChangeType, FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files'; +import { isErrorWithActions, toErrorMessage } from 'vs/base/common/errorMessage'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; + +export interface IEditorPlaceholderContents { + icon: string; + label: string; + actions: IEditorPlaceholderContentsAction[]; +} + +export interface IEditorPlaceholderContentsAction { + label: string; + run: () => unknown; +} -abstract class EditorPlaceholderPane extends EditorPane { +export interface IErrorEditorPlaceholderOptions extends IEditorOptions { + error?: Error; +} + +export abstract class EditorPlaceholder extends EditorPane { private container: HTMLElement | undefined; private scrollbar: DomScrollableElement | undefined; @@ -32,18 +54,14 @@ abstract class EditorPlaceholderPane extends EditorPane { constructor( id: string, - private readonly title: string, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, - @IStorageService storageService: IStorageService + @IStorageService storageService: IStorageService, + @IInstantiationService private readonly instantiationService: IInstantiationService ) { super(id, telemetryService, themeService, storageService); } - override getTitle(): string { - return this.title; - } - protected createEditor(parent: HTMLElement): void { // Container @@ -66,21 +84,43 @@ abstract class EditorPlaceholderPane extends EditorPane { } // Render Input - this.inputDisposable.value = this.renderInput(input); + this.inputDisposable.value = await this.renderInput(input, options); } - private renderInput(input: EditorInput): IDisposable { + private async renderInput(input: EditorInput, options: IEditorOptions | undefined): Promise<IDisposable> { const [container, scrollbar] = assertAllDefined(this.container, this.scrollbar); // Reset any previous contents clearNode(container); - // Update ARIA label - container.setAttribute('aria-label', computeEditorAriaLabel(input, undefined, this.group, undefined)); - - // Delegate to implementation + // Delegate to implementation for contents const disposables = new DisposableStore(); - this.renderBody(container, disposables); + const { icon, label, actions } = await this.getContents(input, options, disposables); + + // Icon + const iconContainer = container.appendChild($('.editor-placeholder-icon-container')); + const iconWidget = new SimpleIconLabel(iconContainer); + iconWidget.text = icon; + + // Label + const labelContainer = container.appendChild($('.editor-placeholder-label-container')); + const labelWidget = document.createElement('span'); + labelWidget.textContent = label; + labelContainer.appendChild(labelWidget); + + // ARIA label + container.setAttribute('aria-label', `${computeEditorAriaLabel(input, undefined, this.group, undefined)}, ${label}`); + + // Actions + const actionsContainer = container.appendChild($('.editor-placeholder-actions-container')); + for (const action of actions) { + disposables.add(this.instantiationService.createInstance(Link, actionsContainer, { + label: action.label, + href: '' + }, { + opener: () => action.run() + })); + } // Adjust scrollbar scrollbar.scanDomNode(); @@ -88,7 +128,7 @@ abstract class EditorPlaceholderPane extends EditorPane { return disposables; } - protected abstract renderBody(container: HTMLElement, disposables: DisposableStore): void; + protected abstract getContents(input: EditorInput, options: IEditorOptions | undefined, disposables: DisposableStore): Promise<IEditorPlaceholderContents>; override clearInput(): void { if (this.container) { @@ -108,6 +148,9 @@ abstract class EditorPlaceholderPane extends EditorPane { // Adjust scrollbar scrollbar.scanDomNode(); + + // Toggle responsive class + container.classList.toggle('max-height-200px', dimension.height <= 200); } override focus(): void { @@ -123,11 +166,12 @@ abstract class EditorPlaceholderPane extends EditorPane { } } -export class WorkspaceTrustRequiredEditor extends EditorPlaceholderPane { +export class WorkspaceTrustRequiredPlaceholderEditor extends EditorPlaceholder { static readonly ID = 'workbench.editors.workspaceTrustRequiredEditor'; - static readonly LABEL = localize('trustRequiredEditor', "Workspace Trust Required"); - static readonly DESCRIPTOR = EditorPaneDescriptor.create(WorkspaceTrustRequiredEditor, WorkspaceTrustRequiredEditor.ID, WorkspaceTrustRequiredEditor.LABEL); + private static readonly LABEL = localize('trustRequiredEditor', "Workspace Trust Required"); + + static readonly DESCRIPTOR = EditorPaneDescriptor.create(WorkspaceTrustRequiredPlaceholderEditor, WorkspaceTrustRequiredPlaceholderEditor.ID, WorkspaceTrustRequiredPlaceholderEditor.LABEL); constructor( @ITelemetryService telemetryService: ITelemetryService, @@ -135,95 +179,128 @@ export class WorkspaceTrustRequiredEditor extends EditorPlaceholderPane { @ICommandService private readonly commandService: ICommandService, @IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService, @IStorageService storageService: IStorageService, - @IInstantiationService private readonly instantiationService: IInstantiationService + @IInstantiationService instantiationService: IInstantiationService ) { - super(WorkspaceTrustRequiredEditor.ID, WorkspaceTrustRequiredEditor.LABEL, telemetryService, themeService, storageService); + super(WorkspaceTrustRequiredPlaceholderEditor.ID, telemetryService, themeService, storageService, instantiationService); } - protected renderBody(container: HTMLElement, disposables: DisposableStore): void { - const label = container.appendChild(document.createElement('p')); - label.textContent = isSingleFolderWorkspaceIdentifier(toWorkspaceIdentifier(this.workspaceService.getWorkspace())) ? - localize('requiresFolderTrustText', "The file is not displayed in the editor because trust has not been granted to the folder.") : - localize('requiresWorkspaceTrustText', "The file is not displayed in the editor because trust has not been granted to the workspace."); + override getTitle(): string { + return WorkspaceTrustRequiredPlaceholderEditor.LABEL; + } - disposables.add(this.instantiationService.createInstance(Link, label, { - label: localize('manageTrust', "Manage Workspace Trust"), - href: '' - }, { - opener: () => this.commandService.executeCommand('workbench.trust.manage') - })); + protected async getContents(): Promise<IEditorPlaceholderContents> { + return { + icon: '$(workspace-untrusted)', + label: isSingleFolderWorkspaceIdentifier(toWorkspaceIdentifier(this.workspaceService.getWorkspace())) ? + localize('requiresFolderTrustText', "The file is not displayed in the editor because trust has not been granted to the folder.") : + localize('requiresWorkspaceTrustText', "The file is not displayed in the editor because trust has not been granted to the workspace."), + actions: [ + { + label: localize('manageTrust', "Manage Workspace Trust"), + run: () => this.commandService.executeCommand('workbench.trust.manage') + } + ] + }; } } -abstract class AbstractErrorEditor extends EditorPlaceholderPane { +export class ErrorPlaceholderEditor extends EditorPlaceholder { + + private static readonly ID = 'workbench.editors.errorEditor'; + private static readonly LABEL = localize('errorEditor', "Error Editor"); + + static readonly DESCRIPTOR = EditorPaneDescriptor.create(ErrorPlaceholderEditor, ErrorPlaceholderEditor.ID, ErrorPlaceholderEditor.LABEL); constructor( - id: string, - label: string, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IStorageService storageService: IStorageService, - @IInstantiationService private readonly instantiationService: IInstantiationService + @IInstantiationService instantiationService: IInstantiationService, + @IFileService private readonly fileService: IFileService, + @IDialogService private readonly dialogService: IDialogService ) { - super(id, label, telemetryService, themeService, storageService); + super(ErrorPlaceholderEditor.ID, telemetryService, themeService, storageService, instantiationService); } - protected abstract getErrorMessage(): string; - - protected renderBody(container: HTMLElement, disposables: DisposableStore): void { - const label = container.appendChild(document.createElement('p')); - label.textContent = this.getErrorMessage(); - - // Offer to re-open + protected async getContents(input: EditorInput, options: IErrorEditorPlaceholderOptions, disposables: DisposableStore): Promise<IEditorPlaceholderContents> { + const resource = input.resource; const group = this.group; - const input = this.input; - if (group && input) { - disposables.add(this.instantiationService.createInstance(Link, label, { - label: localize('retry', "Try Again"), - href: '' - }, { - opener: () => group.openEditor(input, { ...this.options, source: EditorOpenSource.USER /* explicit user gesture */ }) - })); + const error = options.error; + const isFileNotFound = (<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND; + + // Error Label + let label: string; + if (isFileNotFound) { + label = localize('unavailableResourceErrorEditorText', "The editor could not be opened because the file was not found."); + } else if (error) { + label = localize('unknownErrorEditorTextWithError', "The editor could not be opened due to an unexpected error: {0}", toErrorMessage(error)); + } else { + label = localize('unknownErrorEditorTextWithoutError', "The editor could not be opened due to an unexpected error."); } - } -} - -export class UnknownErrorEditor extends AbstractErrorEditor { - static readonly ID = 'workbench.editors.unknownErrorEditor'; - static readonly LABEL = localize('unknownErrorEditor', "Unknown Error Editor"); - static readonly DESCRIPTOR = EditorPaneDescriptor.create(UnknownErrorEditor, UnknownErrorEditor.ID, UnknownErrorEditor.LABEL); + // Actions + let actions: IEditorPlaceholderContentsAction[] | undefined = undefined; + if (isErrorWithActions(error) && error.actions.length > 0) { + actions = error.actions.map(action => { + return { + label: action.label, + run: () => { + const result = action.run(); + if (result instanceof Promise) { + result.catch(error => this.dialogService.show(Severity.Error, toErrorMessage(error))); + } + } + }; + }); + } else if (group) { + actions = [ + { + label: localize('retry', "Try Again"), + run: () => group.openEditor(input, { ...options, source: EditorOpenSource.USER /* explicit user gesture */ }) + } + ]; + } - constructor( - @ITelemetryService telemetryService: ITelemetryService, - @IThemeService themeService: IThemeService, - @IStorageService storageService: IStorageService, - @IInstantiationService instantiationService: IInstantiationService - ) { - super(UnknownErrorEditor.ID, UnknownErrorEditor.LABEL, telemetryService, themeService, storageService, instantiationService); - } + // Auto-reload when file is added + if (group && isFileNotFound && resource && this.fileService.hasProvider(resource)) { + disposables.add(this.fileService.onDidFilesChange(e => { + if (e.contains(resource, FileChangeType.ADDED, FileChangeType.UPDATED)) { + group.openEditor(input, options); + } + })); + } - protected override getErrorMessage(): string { - return localize('unknownErrorEditorText', "The editor could not be opened due to an unexpected error."); + return { icon: '$(error)', label, actions: actions ?? [] }; } } -export class UnavailableResourceErrorEditor extends AbstractErrorEditor { +registerThemingParticipant((theme, collector) => { - static readonly ID = 'workbench.editors.unavailableResourceErrorEditor'; - static readonly LABEL = localize('unavailableResourceErrorEditor', "Unavailable Resource Error Editor"); - static readonly DESCRIPTOR = EditorPaneDescriptor.create(UnavailableResourceErrorEditor, UnavailableResourceErrorEditor.ID, UnavailableResourceErrorEditor.LABEL); + // Editor Placeholder Error Icon + const editorErrorIconForegroundColor = theme.getColor(editorErrorForeground); + if (editorErrorIconForegroundColor) { + collector.addRule(` + .monaco-editor-pane-placeholder .editor-placeholder-icon-container ${Codicon.error.cssSelector} { + color: ${editorErrorIconForegroundColor}; + }`); + } - constructor( - @ITelemetryService telemetryService: ITelemetryService, - @IThemeService themeService: IThemeService, - @IStorageService storageService: IStorageService, - @IInstantiationService instantiationService: IInstantiationService - ) { - super(UnavailableResourceErrorEditor.ID, UnavailableResourceErrorEditor.LABEL, telemetryService, themeService, storageService, instantiationService); + // Editor Placeholder Warning Icon + const editorWarningIconForegroundColor = theme.getColor(editorWarningForeground); + if (editorWarningIconForegroundColor) { + collector.addRule(` + .monaco-editor-pane-placeholder .editor-placeholder-icon-container ${Codicon.warning.cssSelector} { + color: ${editorWarningIconForegroundColor}; + }`); } - protected override getErrorMessage(): string { - return localize('unavailableResourceErrorEditorText', "The editor could not be opened because the file was not found."); + // Editor Placeholder Info/Trust Icon + const editorInfoIconForegroundColor = theme.getColor(editorInfoForeground); + if (editorInfoIconForegroundColor) { + collector.addRule(` + .monaco-editor-pane-placeholder .editor-placeholder-icon-container ${Codicon.info.cssSelector}, + .monaco-editor-pane-placeholder .editor-placeholder-icon-container ${Codicon.workspaceUntrusted.cssSelector} { + color: ${editorInfoIconForegroundColor}; + }`); } -} +}); diff --git a/src/vs/workbench/browser/parts/editor/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts index 92091e6054d..7571481ae97 100644 --- a/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/src/vs/workbench/browser/parts/editor/editorStatus.ts @@ -1202,7 +1202,7 @@ export class ChangeLanguageAction extends Action { if (resource) { // Detect languages since we are in an untitled file let languageId: string | undefined = withNullAsUndefined(this.languageService.guessLanguageIdByFilepathOrFirstLine(resource, textModel.getLineContent(1))); - if (!languageId) { + if (!languageId || languageId === 'unknown') { detectedLanguage = await this.languageDetectionService.detectLanguage(resource); languageId = detectedLanguage; } diff --git a/src/vs/workbench/browser/parts/editor/media/editordroptarget.css b/src/vs/workbench/browser/parts/editor/media/editordroptarget.css index d21f31445b3..8a50b279785 100644 --- a/src/vs/workbench/browser/parts/editor/media/editordroptarget.css +++ b/src/vs/workbench/browser/parts/editor/media/editordroptarget.css @@ -20,6 +20,28 @@ pointer-events: none; /* very important to not take events away from the parent */ opacity: 0; /* hidden initially */ transition: opacity 150ms ease-out; + + display: flex; + align-items: center; + justify-content: center; +} + +#monaco-workbench-editor-drop-overlay .editor-group-overlay-drop-into-prompt { + text-align: center; + padding: 0.6em; + margin: 0.2em; + line-height: normal; + opacity: 0; /* hidden initially */ + transition: opacity 150ms ease-out; +} + +#monaco-workbench-editor-drop-overlay .editor-group-overlay-drop-into-prompt i /* Style keybinding */ { + padding: 0 8px; + border: 1px solid hsla(0,0%,80%,.4); + margin: 0 1px; + border-radius: 5px; + background-color: rgba(255, 255, 255, 0.05); + font-style: normal; } #monaco-workbench-editor-drop-overlay > .editor-group-overlay-indicator.overlay-move-transition { diff --git a/src/vs/workbench/browser/parts/editor/media/editorplaceholder.css b/src/vs/workbench/browser/parts/editor/media/editorplaceholder.css index a066aaec52c..692baa8c240 100644 --- a/src/vs/workbench/browser/parts/editor/media/editorplaceholder.css +++ b/src/vs/workbench/browser/parts/editor/media/editorplaceholder.css @@ -3,17 +3,38 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +.monaco-editor-pane-placeholder { + padding: 0 16px 0 16px; + box-sizing: border-box; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 10px; +} + .monaco-editor-pane-placeholder:focus { outline: none !important; } -.monaco-editor-pane-placeholder { - padding: 0 0 0 16px; - box-sizing: border-box; +.monaco-editor-pane-placeholder .editor-placeholder-icon-container .codicon { + font-size: 48px; +} + +.monaco-editor-pane-placeholder.max-height-200px .editor-placeholder-icon-container { + /* Hide the icon when height is limited */ + display: none; +} + +.monaco-editor-pane-placeholder .editor-placeholder-label-container { + font-size: 14px; + max-width: 450px; + text-align: center } .monaco-editor-pane-placeholder .monaco-link, .monaco-editor-pane-placeholder .monaco-link:hover { + font-size: 14px; cursor: pointer; text-decoration: underline; margin-left: 5px; diff --git a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index 5da06c5f378..6d834b41807 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -62,7 +62,7 @@ interface EditorInputLabel { ariaLabel?: string; } -interface ITabsTitleControlLayoutOtions { +interface ITabsTitleControlLayoutOptions { /** * Whether to force revealing the active tab, even when @@ -77,7 +77,7 @@ interface IScheduledTabsTitleControlLayout extends IDisposable { /** * Associated options with the layout call. */ - options?: ITabsTitleControlLayoutOtions; + options?: ITabsTitleControlLayoutOptions; } type EditorInputLabelAndEditor = EditorInputLabel & { editor: EditorInput }; @@ -1078,11 +1078,15 @@ export class TabsTitleControl extends TitleControl { } } - private redraw(options?: ITabsTitleControlLayoutOtions): void { + private redraw(options?: ITabsTitleControlLayoutOptions): void { - // Border below tabs if any - const tabsContainerBorderColor = this.getColor(EDITOR_GROUP_HEADER_TABS_BORDER); + // Border below tabs if any with explicit high contrast support if (this.tabsAndActionsContainer) { + let tabsContainerBorderColor = this.getColor(EDITOR_GROUP_HEADER_TABS_BORDER); + if (!tabsContainerBorderColor && isHighContrast(this.theme.type)) { + tabsContainerBorderColor = this.getColor(TAB_BORDER) || this.getColor(contrastBorder); + } + if (tabsContainerBorderColor) { this.tabsAndActionsContainer.classList.add('tabs-border-bottom'); this.tabsAndActionsContainer.style.setProperty('--tabs-border-bottom-color', tabsContainerBorderColor.toString()); @@ -1386,7 +1390,7 @@ export class TabsTitleControl extends TitleControl { return { total, offset }; } - layout(dimensions: ITitleControlDimensions, options?: ITabsTitleControlLayoutOtions): Dimension { + layout(dimensions: ITitleControlDimensions, options?: ITabsTitleControlLayoutOptions): Dimension { // Remember dimensions that we get Object.assign(this.dimensions, dimensions); @@ -1420,7 +1424,7 @@ export class TabsTitleControl extends TitleControl { return this.dimensions.used; } - private doLayout(dimensions: ITitleControlDimensions, options?: ITabsTitleControlLayoutOtions): void { + private doLayout(dimensions: ITitleControlDimensions, options?: ITabsTitleControlLayoutOptions): void { // Only layout if we have valid tab index and dimensions const activeTabAndIndex = this.group.activeEditor ? this.getTabAndIndex(this.group.activeEditor) : undefined; @@ -1459,7 +1463,7 @@ export class TabsTitleControl extends TitleControl { } } - private doLayoutTabs(activeTab: HTMLElement, activeIndex: number, dimensions: ITitleControlDimensions, options?: ITabsTitleControlLayoutOtions): void { + private doLayoutTabs(activeTab: HTMLElement, activeIndex: number, dimensions: ITitleControlDimensions, options?: ITabsTitleControlLayoutOptions): void { // Always first layout tabs with wrapping support even if wrapping // is disabled. The result indicates if tabs wrap and if not, we @@ -1601,7 +1605,7 @@ export class TabsTitleControl extends TitleControl { return tabsWrapMultiLine; } - private doLayoutTabsNonWrapping(activeTab: HTMLElement, activeIndex: number, options?: ITabsTitleControlLayoutOtions): void { + private doLayoutTabsNonWrapping(activeTab: HTMLElement, activeIndex: number, options?: ITabsTitleControlLayoutOptions): void { const [tabsContainer, tabsScrollbar] = assertAllDefined(this.tabsContainer, this.tabsScrollbar); // @@ -1881,18 +1885,6 @@ export class TabsTitleControl extends TitleControl { registerThemingParticipant((theme, collector) => { - // Add border between tabs and breadcrumbs in high contrast mode. - if (isHighContrast(theme.type)) { - const borderColor = (theme.getColor(TAB_BORDER) || theme.getColor(contrastBorder)); - if (borderColor) { - collector.addRule(` - .monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container { - border-bottom: 1px solid ${borderColor}; - } - `); - } - } - // Add bottom border to tabs when wrapping const borderColor = theme.getColor(TAB_BORDER); if (borderColor) { diff --git a/src/vs/workbench/browser/parts/notifications/notificationsActions.ts b/src/vs/workbench/browser/parts/notifications/notificationsActions.ts index 3b0d34ea4af..3402b0d5987 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsActions.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsActions.ts @@ -154,10 +154,12 @@ interface NotificationActionMetrics { } type NotificationActionMetricsClassification = { - id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'bpasero'; comment: 'The identifier of the action that was run from a notification.' }; - actionLabel: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'bpasero'; comment: 'The label of the action that was run from a notification.' }; - source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'bpasero'; comment: 'The source of the notification where an action was run.' }; - silent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'bpasero'; comment: 'Whether the notification where an action was run is silent or not.' }; + id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The identifier of the action that was run from a notification.' }; + actionLabel: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The label of the action that was run from a notification.' }; + source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The source of the notification where an action was run.' }; + silent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the notification where an action was run is silent or not.' }; + owner: 'bpasero'; + comment: 'Tracks when actions are fired from notifcations and how they were fired.'; }; export class NotificationActionRunner extends ActionRunner { diff --git a/src/vs/workbench/browser/parts/notifications/notificationsList.ts b/src/vs/workbench/browser/parts/notifications/notificationsList.ts index 58e32d35e78..9989466693c 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsList.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsList.ts @@ -301,10 +301,9 @@ registerThemingParticipant((theme, collector) => { // Notification Error Icon const notificationErrorIconForegroundColor = theme.getColor(NOTIFICATIONS_ERROR_ICON_FOREGROUND); if (notificationErrorIconForegroundColor) { - const errorCodiconSelector = Codicon.error.cssSelector; collector.addRule(` - .monaco-workbench .notifications-center ${errorCodiconSelector}, - .monaco-workbench .notifications-toasts ${errorCodiconSelector} { + .monaco-workbench .notifications-center ${Codicon.error.cssSelector}, + .monaco-workbench .notifications-toasts ${Codicon.error.cssSelector} { color: ${notificationErrorIconForegroundColor}; }`); } @@ -312,10 +311,9 @@ registerThemingParticipant((theme, collector) => { // Notification Warning Icon const notificationWarningIconForegroundColor = theme.getColor(NOTIFICATIONS_WARNING_ICON_FOREGROUND); if (notificationWarningIconForegroundColor) { - const warningCodiconSelector = Codicon.warning.cssSelector; collector.addRule(` - .monaco-workbench .notifications-center ${warningCodiconSelector}, - .monaco-workbench .notifications-toasts ${warningCodiconSelector} { + .monaco-workbench .notifications-center ${Codicon.warning.cssSelector}, + .monaco-workbench .notifications-toasts ${Codicon.warning.cssSelector} { color: ${notificationWarningIconForegroundColor}; }`); } @@ -323,10 +321,9 @@ registerThemingParticipant((theme, collector) => { // Notification Info Icon const notificationInfoIconForegroundColor = theme.getColor(NOTIFICATIONS_INFO_ICON_FOREGROUND); if (notificationInfoIconForegroundColor) { - const infoCodiconSelector = Codicon.info.cssSelector; collector.addRule(` - .monaco-workbench .notifications-center ${infoCodiconSelector}, - .monaco-workbench .notifications-toasts ${infoCodiconSelector} { + .monaco-workbench .notifications-center ${Codicon.info.cssSelector}, + .monaco-workbench .notifications-toasts ${Codicon.info.cssSelector} { color: ${notificationInfoIconForegroundColor}; }`); } diff --git a/src/vs/workbench/browser/parts/notifications/notificationsTelemetry.ts b/src/vs/workbench/browser/parts/notifications/notificationsTelemetry.ts index b498207420e..4d6e3823333 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsTelemetry.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsTelemetry.ts @@ -16,9 +16,11 @@ export interface NotificationMetrics { } export type NotificationMetricsClassification = { - id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'bpasero'; comment: 'The identifier of the source of the notification.' }; - silent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'bpasero'; comment: 'Whethe the notification is silent or not.' }; - source?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'bpasero'; comment: 'The source of the notification.' }; + id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The identifier of the source of the notification.' }; + silent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the notification is silent or not.' }; + source?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The source of the notification.' }; + owner: 'bpasero'; + comment: 'Helps us gain insights to what notifications are being shown, how many, and if they are silent or not.'; }; export function notificationToMetrics(message: NotificationMessage, source: string | undefined, silent: boolean): NotificationMetrics { diff --git a/src/vs/workbench/browser/parts/panel/panelPart.ts b/src/vs/workbench/browser/parts/panel/panelPart.ts index 3e87cd1f5fd..ec28dd8769c 100644 --- a/src/vs/workbench/browser/parts/panel/panelPart.ts +++ b/src/vs/workbench/browser/parts/panel/panelPart.ts @@ -45,6 +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'; interface ICachedPanel { id: string; @@ -208,6 +209,12 @@ export abstract class BasePanelPart extends CompositePart<PaneComposite> impleme // Global Panel Actions this.globalActions = this._register(this.instantiationService.createInstance(CompositeMenuActions, partId === Parts.PANEL_PART ? MenuId.PanelTitle : MenuId.AuxiliaryBarTitle, undefined, undefined)); this._register(this.globalActions.onDidChange(() => this.updateGlobalToolbarActions())); + + Registry.as<IProfileStorageRegistry>(Extensions.ProfileStorageRegistry) + .registerKeys([{ + key: this.pinnedPanelsKey, + description: localize('pinned view containers', "Panel entries visibility customizations") + }]); } protected abstract getActivityHoverOptions(): IActivityHoverOptions; @@ -370,6 +377,14 @@ export abstract class BasePanelPart extends CompositePart<PaneComposite> impleme if (viewContainerModel.activeViewDescriptors.length) { contextKey.set(true); this.compositeBar.addComposite({ id: viewContainer.id, name: viewContainer.title, order: viewContainer.order, requestedIndex: viewContainer.requestedIndex }); + + const activeComposite = this.getActiveComposite(); + if (activeComposite === undefined || activeComposite.getId() === viewContainer.id) { + this.compositeBar.activateComposite(viewContainer.id); + } + + this.layoutCompositeBar(); + this.layoutEmptyMessage(); } else if (viewContainer.hideIfEmpty) { contextKey.set(false); this.hideComposite(viewContainer.id); diff --git a/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css b/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css index 8a387c00f71..8363eed17f6 100644 --- a/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css +++ b/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css @@ -127,7 +127,7 @@ } .monaco-workbench .part.statusbar > .items-container > .statusbar-item.compact-right > a { - padding: 0 3px 0 5px; + padding: 0 3px; } .monaco-workbench .part.statusbar > .items-container > .statusbar-item > a:hover:not(.disabled) { diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts b/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts index 09f69549739..f3ea7a8e5f6 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts @@ -8,6 +8,9 @@ import { isStatusbarEntryLocation, IStatusbarEntryLocation, StatusbarAlignment } 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 { localize } from 'vs/nls'; export interface IStatusbarEntryPriority { @@ -64,6 +67,12 @@ export class StatusbarViewModel extends Disposable { this.restoreState(); this.registerListeners(); + + Registry.as<IProfileStorageRegistry>(Extensions.ProfileStorageRegistry) + .registerKeys([{ + key: StatusbarViewModel.HIDDEN_ENTRIES_KEY, + description: localize('statusbar.hidden', "Status bar entries visibility customizations"), + }]); } private restoreState(): void { diff --git a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css index 2471bcee7f8..088a77c1acd 100644 --- a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css +++ b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css @@ -3,27 +3,51 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-workbench .part.titlebar > .titlebar-container { +/* Part Element */ +.monaco-workbench .part.titlebar { + display: flex; + flex-direction: row; +} + +.monaco-workbench.mac .part.titlebar { + flex-direction: row-reverse; +} + +/* Root Container */ +.monaco-workbench .part.titlebar>.titlebar-container { box-sizing: border-box; - width: 100%; - padding: 0 70px; overflow: hidden; - flex-shrink: 0; + flex-shrink: 1; + flex-grow: 1; align-items: center; justify-content: center; user-select: none; -webkit-user-select: none; - zoom: 1; /* prevent zooming */ - line-height: 22px; - height: 22px; display: flex; + height: 100%; + width: 100%; } -.monaco-workbench .part.titlebar > .titlebar-container { - transform-origin: 0 0; +/* Account for zooming */ +.monaco-workbench .part.titlebar>.titlebar-container.counter-zoom { + zoom: calc(1.0 / var(--zoom-factor)); } -.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-drag-region { +/* Platform specific root element */ +.monaco-workbench.mac .part.titlebar>.titlebar-container { + line-height: 22px; +} + +.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: 30px; + justify-content: left; +} + +/* Draggable region */ +.monaco-workbench .part.titlebar>.titlebar-container>.titlebar-drag-region { top: 0; left: 0; display: block; @@ -33,7 +57,43 @@ -webkit-app-region: drag; } -.monaco-workbench .part.titlebar > .titlebar-container > .window-title { +/* Command Center */ +.monaco-workbench .part.titlebar>.titlebar-container>.title-menu .action-item.quickopen { + color: var(--vscode-input-foreground); + border: 1px solid var(--vscode-dropdown-border); + height: 20px; + line-height: 20px; + width: 38vw; + max-width: 600px; + margin: 4px 4px; + flex-direction: row; + justify-content: center; + overflow: hidden; +} + +.monaco-workbench .part.titlebar>.titlebar-container>.title-menu .action-item.quickopen:HOVER, +.monaco-workbench .part.titlebar>.titlebar-container>.title-menu .action-item.quickopen .action-container .action-label:HOVER { + background-color: var(--vscode-dropdown-border); + line-height: 18px; +} + +.monaco-workbench .part.titlebar>.titlebar-container>.title-menu .action-item.quickopen .action-container { + flex: 1 0 auto; + display: flex; + justify-content: center; +} + +.monaco-workbench .part.titlebar>.titlebar-container>.title-menu .action-item.quickopen .action-container .keybinding { + font-size: 11px; + padding: 3px; +} + +.monaco-workbench .part.titlebar>.titlebar-container>.title-menu .action-item.quickopen .dropdown-action-container { + margin-left: auto; +} + +/* Window title text */ +.monaco-workbench .part.titlebar>.titlebar-container>.window-title { flex: 0 1 auto; font-size: 12px; overflow: hidden; @@ -41,13 +101,12 @@ text-overflow: ellipsis; margin-left: auto; margin-right: auto; - zoom: 1; /* prevent zooming */ } /* Windows/Linux: Rules for custom title (icon, window controls) */ -.monaco-workbench.web .part.titlebar > .titlebar-container, -.monaco-workbench.windows .part.titlebar > .titlebar-container, -.monaco-workbench.linux .part.titlebar > .titlebar-container { +.monaco-workbench.web .part.titlebar>.titlebar-container, +.monaco-workbench.windows .part.titlebar>.titlebar-container, +.monaco-workbench.linux .part.titlebar>.titlebar-container { padding: 0; height: 30px; line-height: 30px; @@ -55,23 +114,31 @@ overflow: visible; } -.monaco-workbench.web .part.titlebar > .titlebar-container > .window-title, -.monaco-workbench.windows .part.titlebar > .titlebar-container > .window-title, -.monaco-workbench.linux .part.titlebar > .titlebar-container > .window-title { +.monaco-workbench.web .part.titlebar>.titlebar-container>.window-title, +.monaco-workbench.windows .part.titlebar>.titlebar-container>.window-title, +.monaco-workbench.linux .part.titlebar>.titlebar-container>.window-title { cursor: default; } -.monaco-workbench .part.titlebar > .titlebar-container > .menubar { - /* move menubar above drag region as negative z-index on drag region cause greyscale AA */ - z-index: 2500; +.monaco-workbench .part.titlebar>.titlebar-container.enable-title-menu>.window-title { + display: none; } -.monaco-workbench.linux .part.titlebar > .titlebar-container > .window-title { +.monaco-workbench.linux .part.titlebar>.titlebar-container>.window-title { font-size: inherit; + /* see #55435 */ +} + +/* Menubar */ +.monaco-workbench .part.titlebar>.titlebar-container>.menubar { + /* move menubar above drag region as negative z-index on drag region cause greyscale AA */ + z-index: 2500; + min-width: 36px; } -.monaco-workbench.windows .part.titlebar > .titlebar-container > .resizer, -.monaco-workbench.linux .part.titlebar > .titlebar-container > .resizer { +/* Resizer */ +.monaco-workbench.windows .part.titlebar>.titlebar-container>.resizer, +.monaco-workbench.linux .part.titlebar>.titlebar-container>.resizer { -webkit-app-region: no-drag; position: absolute; top: 0; @@ -79,12 +146,13 @@ height: 4px; } -.monaco-workbench.windows.fullscreen .part.titlebar > .titlebar-container > .resizer, -.monaco-workbench.linux.fullscreen .part.titlebar > .titlebar-container > .resizer { +.monaco-workbench.windows.fullscreen .part.titlebar>.titlebar-container>.resizer, +.monaco-workbench.linux.fullscreen .part.titlebar>.titlebar-container>.resizer { display: none; } -.monaco-workbench .part.titlebar > .titlebar-container > .window-appicon { +/* App Icon */ +.monaco-workbench .part.titlebar>.titlebar-container>.window-appicon { width: 35px; height: 100%; position: relative; @@ -92,20 +160,29 @@ flex-shrink: 0; } -.monaco-workbench .part.titlebar > .titlebar-container > .window-appicon:not(.codicon) { +.monaco-workbench .part.titlebar>.titlebar-container>.window-appicon:not(.codicon) { background-image: url('../../../media/code-icon.svg'); background-repeat: no-repeat; background-position: center center; background-size: 16px; } -.monaco-workbench .part.titlebar > .titlebar-container .window-appicon > .home-bar-icon-badge { +.monaco-workbench .part.titlebar>.titlebar-container>.window-appicon.codicon { + line-height: 30px; +} + +.monaco-workbench.fullscreen .part.titlebar>.titlebar-container>.window-appicon { + display: none; +} + +.monaco-workbench .part.titlebar>.titlebar-container .window-appicon>.home-bar-icon-badge { position: absolute; right: 9px; bottom: 6px; width: 8px; height: 8px; - z-index: 1; /* on top of home indicator */ + z-index: 1; + /* on top of home indicator */ background-image: url('../../../media/code-icon.svg'); background-repeat: no-repeat; background-position: center center; @@ -115,71 +192,32 @@ border-left: 1px solid transparent; } -.monaco-workbench .part.titlebar > .titlebar-container > .window-appicon.codicon { - line-height: 30px; -} - -.monaco-workbench.fullscreen .part.titlebar > .titlebar-container > .window-appicon { - display: none; -} - -.monaco-workbench .part.titlebar > .titlebar-container > .window-controls-container { +/* Window Controls (Minimize, Max/Restore, Close) */ +.monaco-workbench .part.titlebar>.window-controls-container { display: flex; flex-grow: 0; flex-shrink: 0; text-align: center; - position: relative; z-index: 3000; -webkit-app-region: no-drag; - height: 100%; - min-width: 138px; - margin-left: auto; -} - -.monaco-workbench .part.titlebar > .titlebar-container > .window-controls-container.show-layout-control { - min-width: 160px; -} - -.monaco-workbench.web .part.titlebar > .titlebar-container > .window-controls-container.show-layout-control { - min-width: 28px; - padding-right: 8px; -} - -.monaco-workbench.mac:not(.web) .part.titlebar > .titlebar-container > .window-controls-container { - position: absolute; - right: 8px; - min-width: 28px; - display: none; + height: 30px; + width: 138px; + zoom: calc(1 / var(--zoom-factor)); } -.monaco-workbench.mac:not(.web) .part.titlebar > .titlebar-container > .window-controls-container.show-layout-control { - display: flex; +.monaco-workbench.mac .part.titlebar>.window-controls-container { + width: 70px; + height: env(titlebar-area-width, 28px); } -.monaco-workbench.fullscreen .part.titlebar > .titlebar-container > .window-controls-container { +.monaco-workbench.web .part.titlebar>.window-controls-container, +.monaco-workbench.fullscreen .part.titlebar>.window-controls-container { display: none; background-color: transparent; } -.monaco-workbench .part.titlebar > .titlebar-container > .window-controls-container > .layout-dropdown-container { - padding-right: 2px; - display: none; -} - -.monaco-workbench .part.titlebar > .titlebar-container > .window-controls-container.show-layout-control > .layout-dropdown-container { - display: flex; - justify-content: center; -} - -.monaco-workbench:not(.mac):not(.web) .part.titlebar > .titlebar-container > .window-controls-container.show-layout-control > .layout-dropdown-container { - min-width: 46px; -} - -.monaco-workbench .part.titlebar > .titlebar-container > .window-controls-container.show-layout-control > .layout-dropdown-container .codicon { - color: inherit; -} - -.monaco-workbench .part.titlebar > .titlebar-container > .window-controls-container > .window-icon { +/* Window Control Icons */ +.monaco-workbench .part.titlebar>.window-controls-container>.window-icon { display: inline-block; line-height: 30px; height: 100%; @@ -187,18 +225,47 @@ font-size: 16px; } -.monaco-workbench .part.titlebar > .titlebar-container > .window-controls-container > .window-icon:hover { +.monaco-workbench .part.titlebar>.window-controls-container>.window-icon:hover { background-color: rgba(255, 255, 255, 0.1); } -.monaco-workbench .part.titlebar.light > .titlebar-container > .window-controls-container > .window-icon:hover { +.monaco-workbench .part.titlebar.light>.window-controls-container>.window-icon:hover { background-color: rgba(0, 0, 0, 0.1); } -.monaco-workbench .part.titlebar > .titlebar-container > .window-controls-container > .window-icon.window-close:hover { +.monaco-workbench .part.titlebar>.window-controls-container>.window-icon.window-close:hover { background-color: rgba(232, 17, 35, 0.9); } -.monaco-workbench .part.titlebar > .titlebar-container > .window-controls-container .window-icon.window-close:hover { +.monaco-workbench .part.titlebar>.window-controls-container .window-icon.window-close:hover { color: white; } + +/* Layout Controls */ +.monaco-workbench .part.titlebar>.titlebar-container>.layout-controls-container { + display: none; + padding-right: 2px; + flex-grow: 0; + flex-shrink: 0; + text-align: center; + position: relative; + z-index: 3000; + -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; + right: 8px; +} + +.monaco-workbench .part.titlebar>.titlebar-container>.layout-controls-container.show-layout-control { + display: flex; + justify-content: center; +} + +.monaco-workbench .part.titlebar>.titlebar-container>.layout-controls-container .codicon { + color: inherit; +} diff --git a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts index fd8c91724bf..88e69dc8ceb 100644 --- a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts +++ b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts @@ -921,10 +921,6 @@ export class CustomMenubarControl extends MenubarControl { } layout(dimension: Dimension) { - if (this.container) { - this.container.style.height = `${dimension.height}px`; - } - this.menubar?.update(this.getMenuBarOptions()); } diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index 4934df5077c..2dbbcce47c8 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -12,10 +12,10 @@ import { getZoomFactor } from 'vs/base/browser/browser'; import { MenuBarVisibility, getTitleBarStyle, getMenuBarVisibility } from 'vs/platform/window/common/window'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; -import { IAction } from 'vs/base/common/actions'; +import { IAction, toAction } from 'vs/base/common/actions'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { DisposableStore, dispose } from 'vs/base/common/lifecycle'; +import { DisposableStore, dispose, toDisposable } from 'vs/base/common/lifecycle'; import { EditorResourceAccessor, Verbosity, SideBySideEditor } from 'vs/workbench/common/editor'; import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; import { IWorkspaceContextService, WorkbenchState, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; @@ -25,7 +25,7 @@ import { isMacintosh, isWindows, isLinux, isWeb } from 'vs/base/common/platform' import { URI } from 'vs/base/common/uri'; import { Color } from 'vs/base/common/color'; import { trim } from 'vs/base/common/strings'; -import { EventType, EventHelper, Dimension, isAncestor, append, $, addDisposableListener, runAtThisOrScheduleAtNextAnimationFrame, prepend } from 'vs/base/browser/dom'; +import { EventType, EventHelper, Dimension, isAncestor, append, $, addDisposableListener, runAtThisOrScheduleAtNextAnimationFrame, prepend, clearNode } from 'vs/base/browser/dom'; import { CustomMenubarControl } from 'vs/workbench/browser/parts/titlebar/menubarControl'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { template } from 'vs/base/common/labels'; @@ -34,8 +34,8 @@ import { Emitter } from 'vs/base/common/event'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { Parts, IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { RunOnceScheduler } from 'vs/base/common/async'; -import { createActionViewItem, createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { IMenuService, IMenu, MenuId } from 'vs/platform/actions/common/actions'; +import { createActionViewItem, createAndFillInContextMenuActions, DropdownWithDefaultActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IMenuService, IMenu, MenuId, SubmenuItemAction, MenuRegistry } from 'vs/platform/actions/common/actions'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IProductService } from 'vs/platform/product/common/productService'; @@ -45,6 +45,7 @@ import { Codicon } from 'vs/base/common/codicons'; import { getVirtualWorkspaceLocation } from 'vs/platform/workspace/common/virtualWorkspace'; import { getIconRegistry } from 'vs/platform/theme/common/iconRegistry'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; export class TitlebarPart extends Part implements ITitleService { @@ -57,7 +58,7 @@ 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); } + get minimumHeight(): number { return 30 / (this.currentMenubarVisibility === 'hidden' || getZoomFactor() < 1 ? getZoomFactor() : 1); } get maximumHeight(): number { return this.minimumHeight; } //#endregion @@ -68,15 +69,19 @@ export class TitlebarPart extends Part implements ITitleService { declare readonly _serviceBrand: undefined; protected rootContainer!: HTMLElement; + protected windowControls: HTMLElement | undefined; + protected readonly titleMenuElement: HTMLElement = $('div.title-menu'); protected title!: HTMLElement; protected customMenubar: CustomMenubarControl | undefined; protected appIcon: HTMLElement | undefined; private appIconBadge: HTMLElement | undefined; protected menubar?: HTMLElement; - protected windowControls: HTMLElement | undefined; + protected layoutControls: HTMLElement | undefined; private layoutToolbar: ToolBar | undefined; protected lastLayoutDimensions: Dimension | undefined; + + private readonly titleMenuDisposables = this._register(new DisposableStore()); private titleBarStyle: 'native' | 'custom'; private pendingTitle: string | undefined; @@ -87,6 +92,7 @@ export class TitlebarPart extends Part implements ITitleService { private readonly activeEditorListeners = this._register(new DisposableStore()); private readonly titleUpdater = this._register(new RunOnceScheduler(() => this.doUpdateTitle(), 0)); + private readonly onDidUpdateTitle = new Emitter<void>(); private contextMenu: IMenu; @@ -105,6 +111,7 @@ export class TitlebarPart extends Part implements ITitleService { @IContextKeyService private readonly contextKeyService: IContextKeyService, @IHostService private readonly hostService: IHostService, @IProductService private readonly productService: IProductService, + @IKeybindingService private readonly keybindingService: IKeybindingService, ) { super(Parts.TITLEBAR_PART, { hasTitle: false }, themeService, storageService, layoutService); @@ -150,8 +157,12 @@ export class TitlebarPart extends Part implements ITitleService { } } - if (this.titleBarStyle !== 'native' && this.windowControls && event.affectsConfiguration('workbench.layoutControl.enabled')) { - this.windowControls.classList.toggle('show-layout-control', this.layoutControlEnabled); + if (this.titleBarStyle !== 'native' && this.layoutControls && event.affectsConfiguration('workbench.layoutControl.enabled')) { + this.layoutControls.classList.toggle('show-layout-control', this.layoutControlEnabled); + } + + if (event.affectsConfiguration('window.experimental.titleMenu')) { + this.updateTitleMenu(); } } @@ -196,11 +207,11 @@ export class TitlebarPart extends Part implements ITitleService { this.pendingTitle = title; } - if ((isWeb || isWindows || isLinux) && this.title) { - if (this.lastLayoutDimensions) { - this.updateLayout(this.lastLayoutDimensions); - } + if (this.lastLayoutDimensions) { + this.updateLayout(this.lastLayoutDimensions); } + + this.onDidUpdateTitle.fire(); } private getWindowTitle(): string { @@ -356,7 +367,7 @@ export class TitlebarPart extends Part implements ITitleService { this.customMenubar = this._register(this.instantiationService.createInstance(CustomMenubarControl)); - this.menubar = this.rootContainer.insertBefore($('div.menubar'), this.title); + this.menubar = this.rootContainer.insertBefore($('div.menubar'), this.titleMenuElement); this.menubar.setAttribute('role', 'menubar'); this.customMenubar.create(this.menubar); @@ -364,9 +375,54 @@ export class TitlebarPart extends Part implements ITitleService { this._register(this.customMenubar.onVisibilityChange(e => this.onMenubarVisibilityChanged(e))); } + private updateTitleMenu(): void { + this.titleMenuDisposables.clear(); + const enableTitleMenu = this.configurationService.getValue<boolean>('window.experimental.titleMenu'); + this.rootContainer.classList.toggle('enable-title-menu', enableTitleMenu); + if (!enableTitleMenu) { + return; + } + + + const that = this; + const titleToolbar = new ToolBar(this.titleMenuElement, this.contextMenuService, { + actionViewItemProvider: (action) => { + + if (action instanceof SubmenuItemAction && action.item.submenu === MenuId.TitleMenuQuickPick) { + class QuickInputDropDown extends DropdownWithDefaultActionViewItem { + override render(container: HTMLElement): void { + super.render(container); + container.classList.add('quickopen'); + container.title = that.getWindowTitle(); + this._store.add(that.onDidUpdateTitle.event(() => container.title = that.getWindowTitle())); + } + } + return that.instantiationService.createInstance(QuickInputDropDown, action, { + keybindingProvider: action => that.keybindingService.lookupKeybinding(action.id), + renderKeybindingWithDefaultActionLabel: true + }); + } + return undefined; + } + }); + const titleMenu = this.titleMenuDisposables.add(this.menuService.createMenu(MenuId.TitleMenu, this.contextKeyService)); + const titleMenuDisposables = this.titleMenuDisposables.add(new DisposableStore()); + const updateTitleMenu = () => { + titleMenuDisposables.clear(); + const actions: IAction[] = []; + titleMenuDisposables.add(createAndFillInContextMenuActions(titleMenu, undefined, actions)); + titleToolbar.setActions(actions); + }; + this.titleMenuDisposables.add(titleMenu.onDidChange(updateTitleMenu)); + this.titleMenuDisposables.add(this.keybindingService.onDidUpdateKeybindings(updateTitleMenu)); + this.titleMenuDisposables.add(toDisposable(() => clearNode(this.titleMenuElement))); + updateTitleMenu(); + } + override createContentArea(parent: HTMLElement): HTMLElement { this.element = parent; this.rootContainer = append(parent, $('.titlebar-container')); + append(this.rootContainer, this.titleMenuElement); // App Icon (Native Windows/Linux and Web) if (!isMacintosh || isWeb) { @@ -395,6 +451,9 @@ export class TitlebarPart extends Part implements ITitleService { this.installMenubar(); } + // Title Menu + this.updateTitleMenu(); + // Title this.title = append(this.rootContainer, $('div.window-title')); if (this.pendingTitle) { @@ -404,16 +463,22 @@ export class TitlebarPart extends Part implements ITitleService { } if (this.titleBarStyle !== 'native') { - this.windowControls = append(this.rootContainer, $('div.window-controls-container')); - this.windowControls.classList.toggle('show-layout-control', this.layoutControlEnabled); + this.layoutControls = append(this.rootContainer, $('div.layout-controls-container')); + this.layoutControls.classList.toggle('show-layout-control', this.layoutControlEnabled); - const layoutDropdownContainer = append(this.windowControls, $('div.layout-dropdown-container')); - this.layoutToolbar = new ToolBar(layoutDropdownContainer, this.contextMenuService, { + this.layoutToolbar = new ToolBar(this.layoutControls, this.contextMenuService, { actionViewItemProvider: action => { return createActionViewItem(this.instantiationService, action); - } + }, + allowContextMenu: true }); + this._register(addDisposableListener(this.layoutControls, EventType.CONTEXT_MENU, e => { + EventHelper.stop(e); + + this.onLayoutControlContextMenu(e, this.layoutControls!); + })); + const menu = this._register(this.menuService.createMenu(MenuId.LayoutControlMenu, this.contextKeyService)); const updateLayoutMenu = () => { @@ -433,6 +498,8 @@ export class TitlebarPart extends Part implements ITitleService { updateLayoutMenu(); } + this.windowControls = append(this.element, $('div.window-controls-container')); + // Context menu on title [EventType.CONTEXT_MENU, EventType.MOUSE_DOWN].forEach(event => { this._register(addDisposableListener(this.title, event, e => { @@ -455,6 +522,10 @@ export class TitlebarPart extends Part implements ITitleService { return; } + if (e.target && isAncestor(e.target as HTMLElement, this.titleMenuElement)) { + return; + } + const active = document.activeElement; setTimeout(() => { if (active instanceof HTMLElement) { @@ -507,7 +578,6 @@ export class TitlebarPart extends Part implements ITitleService { } private onContextMenu(e: MouseEvent): void { - // Find target anchor const event = new StandardMouseEvent(e); const anchor = { x: event.posx, y: event.posy }; @@ -524,20 +594,41 @@ export class TitlebarPart extends Part implements ITitleService { }); } - protected adjustTitleMarginToCenter(): void { - if (this.customMenubar && this.menubar) { - const leftMarker = (this.appIcon ? this.appIcon.clientWidth : 0) + this.menubar.clientWidth + 10; - const rightMarker = this.element.clientWidth - 10; - - // Not enough space to center the titlebar within window, - // Center between menu and window controls - if (leftMarker > (this.element.clientWidth - this.title.clientWidth) / 2 || - rightMarker < (this.element.clientWidth + this.title.clientWidth) / 2) { - this.title.style.position = ''; - this.title.style.left = ''; - this.title.style.transform = ''; - return; + private onLayoutControlContextMenu(e: MouseEvent, el: HTMLElement): void { + // Find target anchor + const event = new StandardMouseEvent(e); + const anchor = { x: event.posx, y: event.posy }; + + const actions: IAction[] = []; + actions.push(toAction({ + id: 'layoutControl.hide', + label: localize('layoutControl.hide', "Hide Layout Control"), + run: () => { + this.configurationService.updateValue('workbench.layoutControl.enabled', false); } + })); + + // Show it + this.contextMenuService.showContextMenu({ + getAnchor: () => anchor, + getActions: () => actions, + domForShadowRoot: el + }); + } + + protected adjustTitleMarginToCenter(): void { + const base = isMacintosh ? (this.windowControls?.clientWidth ?? 0) : 0; + const leftMarker = base + (this.appIcon?.clientWidth ?? 0) + (this.menubar?.clientWidth ?? 0) + 10; + const rightMarker = base + this.rootContainer.clientWidth - (this.layoutControls?.clientWidth ?? 0) - 10; + + // Not enough space to center the titlebar within window, + // Center between left and right controls + if (leftMarker > (this.rootContainer.clientWidth + (this.windowControls?.clientWidth ?? 0) - this.title.clientWidth) / 2 || + rightMarker < (this.rootContainer.clientWidth + (this.windowControls?.clientWidth ?? 0) + this.title.clientWidth) / 2) { + this.title.style.position = ''; + this.title.style.left = ''; + this.title.style.transform = ''; + return; } this.title.style.position = 'absolute'; @@ -557,16 +648,13 @@ export class TitlebarPart extends Part implements ITitleService { this.lastLayoutDimensions = dimension; if (getTitleBarStyle(this.configurationService) === 'custom') { - // Only prevent zooming behavior on macOS or when the menubar is not visible - if ((!isWeb && isMacintosh) || this.currentMenubarVisibility === 'hidden') { - this.rootContainer.style.height = `${100.0 * getZoomFactor()}%`; - this.rootContainer.style.width = `${100.0 * getZoomFactor()}%`; - this.rootContainer.style.transform = `scale(${1 / getZoomFactor()})`; - } else { - this.rootContainer.style.height = '100%'; - this.rootContainer.style.width = '100%'; - this.rootContainer.style.transform = ''; - } + // Prevent zooming behavior if any of the following conditions are met: + // 1. Native macOS + // 2. Menubar is hidden + // 3. Shrinking below the window control size (zoom < 1) + const zoomFactor = getZoomFactor(); + this.element.style.setProperty('--zoom-factor', zoomFactor.toString()); + this.rootContainer.classList.toggle('counter-zoom', zoomFactor < 1 || (!isWeb && isMacintosh) || this.currentMenubarVisibility === 'hidden'); runAtThisOrScheduleAtNextAnimationFrame(() => this.adjustTitleMarginToCenter()); @@ -609,3 +697,9 @@ registerThemingParticipant((theme, collector) => { `); } }); + +MenuRegistry.appendMenuItem(MenuId.TitleMenu, { + submenu: MenuId.TitleMenuQuickPick, + title: localize('title', "Select Mode"), + order: Number.MAX_SAFE_INTEGER +}); diff --git a/src/vs/workbench/browser/parts/views/treeView.ts b/src/vs/workbench/browser/parts/views/treeView.ts index 91df2786f62..e8f931902e7 100644 --- a/src/vs/workbench/browser/parts/views/treeView.ts +++ b/src/vs/workbench/browser/parts/views/treeView.ts @@ -65,6 +65,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { Mimes } from 'vs/base/common/mime'; import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; import { IDataTransfer } from 'vs/workbench/common/dnd'; +import { ThemeSettings } from 'vs/workbench/services/themes/common/workbenchThemeService'; export class TreeViewPane extends ViewPane { @@ -967,12 +968,14 @@ class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyS if (!(node instanceof ResolvableTreeItem) || !node.hasResolve) { if (resource && !node.tooltip) { return undefined; - } else if (!node.tooltip) { + } else if (node.tooltip === undefined) { return label; } else if (!isString(node.tooltip)) { return { markdown: node.tooltip, markdownNotSupportedFallback: resource ? undefined : renderMarkdownAsPlaintext(node.tooltip) }; // Passing undefined as the fallback for a resource falls back to the old native hover - } else { + } else if (node.tooltip !== '') { return node.tooltip; + } else { + return undefined; } } @@ -1018,13 +1021,13 @@ class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyS templateData.actionBar.clear(); templateData.icon.style.color = ''; - if (resource || this.isFileKindThemeIcon(node.themeIcon)) { + if (resource) { const fileDecorations = this.configurationService.getValue<{ colors: boolean; badges: boolean }>('explorer.decorations'); const labelResource = resource ? resource : URI.parse('missing:_icon_resource'); templateData.resourceLabel.setResource({ name: label, description, resource: labelResource }, { fileKind: this.getFileKind(node), title, - hideIcon: !!iconUrl || !!node.themeIcon, + hideIcon: !!iconUrl || this.shouldShowThemeIcon(!!resource, node.themeIcon), fileDecorations, extraClasses: ['custom-view-tree-node-item-resourceLabel'], matches: matches ? matches : createMatches(element.filterData), @@ -1047,7 +1050,7 @@ class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyS 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 (node.themeIcon && (!resource || !this.isFolderThemeIcon(node.themeIcon) || this.themeService.getFileIconTheme().hasFolderIcons)) { + if (this.shouldShowThemeIcon(!!resource, node.themeIcon)) { iconClass = ThemeIcon.asClassName(node.themeIcon); if (node.themeIcon.color) { templateData.icon.style.color = this.themeService.getColorTheme().getColor(node.themeIcon.color.id)?.toString() ?? ''; @@ -1080,6 +1083,27 @@ class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyS container.parentElement!.classList.toggle('align-icon-with-twisty', this.aligner.alignIconWithTwisty(treeItem)); } + 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(); + } + private isFolderThemeIcon(icon: ThemeIcon | undefined): boolean { return icon?.id === FolderThemeIcon.id; } @@ -1253,6 +1277,7 @@ export class CustomTreeView extends AbstractTreeView { constructor( id: string, title: string, + private readonly extensionId: string, @IThemeService themeService: IThemeService, @IInstantiationService instantiationService: IInstantiationService, @ICommandService commandService: ICommandService, @@ -1265,13 +1290,28 @@ export class CustomTreeView extends AbstractTreeView { @IContextKeyService contextKeyService: IContextKeyService, @IHoverService hoverService: IHoverService, @IExtensionService private readonly extensionService: IExtensionService, - @IActivityService activityService: IActivityService + @IActivityService activityService: IActivityService, + @ITelemetryService private readonly telemetryService: ITelemetryService ) { super(id, title, themeService, instantiationService, commandService, configurationService, progressService, contextMenuService, keybindingService, notificationService, viewDescriptorService, hoverService, contextKeyService, activityService); } protected activate() { if (!this.activated) { + type ExtensionViewTelemetry = { + extensionId: string; + id: string; + }; + type ExtensionViewTelemetryMeta = { + extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Id of the extension' }; + id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Id of the view' }; + owner: 'digitarald'; + comment: 'Helps to gain insights on what extension contributed views are most popular'; + }; + this.telemetryService.publicLog2<ExtensionViewTelemetry, ExtensionViewTelemetryMeta>('Extension:ViewActivate', { + extensionId: this.extensionId, + id: this.id, + }); this.createTree(); this.progressService.withProgress({ location: this.id }, () => this.extensionService.activateByEvent(`onView:${this.id}`)) .then(() => timeout(2000)) @@ -1391,13 +1431,7 @@ export class CustomTreeViewDragAndDrop implements ITreeDragAndDrop<ITreeItem> { } } - private debugLog(originalEvent: DragEvent) { - const types: Set<string> = new Set(); - originalEvent.dataTransfer?.types.forEach((value, index) => { - if ((originalEvent.dataTransfer?.items[index].kind === 'string') && (INTERNAL_MIME_TYPES.indexOf(value) < 0)) { - types.add(this.convertKnownMimes(value).type); - } - }); + private debugLog(types: Set<string>) { if (types.size) { this.logService.debug(`TreeView dragged mime types: ${Array.from(types).join(', ')}`); } else { @@ -1406,12 +1440,19 @@ export class CustomTreeViewDragAndDrop implements ITreeDragAndDrop<ITreeItem> { } onDragOver(data: IDragAndDropData, targetElement: ITreeItem, targetIndex: number, originalEvent: DragEvent): boolean | ITreeDragOverReaction { - this.debugLog(originalEvent); + 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); + } + }); + + this.debugLog(types); const dndController = this.dndController; if (!dndController || !originalEvent.dataTransfer || (dndController.dropMimeTypes.length === 0)) { return false; } - const dragContainersSupportedType = originalEvent.dataTransfer.types.some((value, index) => { + const dragContainersSupportedType = Array.from(types).some((value, index) => { if (value === this.treeMimeType) { return true; } else { @@ -1442,15 +1483,15 @@ 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, value?: string): { type: string; value?: string } { - let convertedValue = value; + private convertKnownMimes(type: string, kind?: string, value?: string | FileSystemHandle): { type: string; value?: string } { + let convertedValue = undefined; let convertedType = type; - switch (type) { - case DataTransfers.RESOURCES.toLowerCase(): { - convertedValue = value ? convertResourceUrlsToUriList(value) : undefined; - convertedType = Mimes.uriList; - break; - } + 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 }; } @@ -1461,8 +1502,9 @@ export class CustomTreeViewDragAndDrop implements ITreeDragAndDrop<ITreeItem> { return; } const treeDataTransfer: IDataTransfer = new Map(); - let stringCount = Array.from(originalEvent.dataTransfer.items).reduce((previous, current) => { - if (current.kind === 'string') { + 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; @@ -1475,8 +1517,15 @@ export class CustomTreeViewDragAndDrop implements ITreeDragAndDrop<ITreeItem> { } await new Promise<void>(resolve => { function decrementStringCount() { - stringCount--; - if (stringCount === 0) { + 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')), + value: undefined + }); + } resolve(); } } @@ -1486,17 +1535,17 @@ export class CustomTreeViewDragAndDrop implements ITreeDragAndDrop<ITreeItem> { } for (const dataItem of originalEvent.dataTransfer.items) { const type = dataItem.type; - const convertedType = this.convertKnownMimes(type).type; - if (dataItem.kind === 'string') { - if ((convertedType === this.treeMimeType) || (dndController.dropMimeTypes.indexOf(convertedType) >= 0)) { + 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 - && (INTERNAL_MIME_TYPES.indexOf(convertedType) < 0) - && ((convertedType === this.treeMimeType) || (dndController.dropMimeTypes.indexOf(convertedType) >= 0))) { - const converted = this.convertKnownMimes(type, dataValue); + if (dataValue) { + const converted = this.convertKnownMimes(type, kind, dataValue); treeDataTransfer.set(converted.type, { asString: () => Promise.resolve(converted.value!), value: undefined @@ -1504,9 +1553,15 @@ export class CustomTreeViewDragAndDrop implements ITreeDragAndDrop<ITreeItem> { } decrementStringCount(); }); - } else { + } else if (dataItem.kind === 'file') { + const dataValue = dataItem.getAsFile(); + if (dataValue) { + uris.push(URI.file(dataValue.path)); + } decrementStringCount(); } + } else if (dataItem.kind === 'string' || dataItem.kind === 'file') { + decrementStringCount(); } } }); diff --git a/src/vs/workbench/browser/web.api.ts b/src/vs/workbench/browser/web.api.ts index ddceb3aeeb8..effacb4a75f 100644 --- a/src/vs/workbench/browser/web.api.ts +++ b/src/vs/workbench/browser/web.api.ts @@ -14,6 +14,10 @@ import type { IWorkspaceProvider } from 'vs/workbench/services/host/browser/brow import type { IProductConfiguration } from 'vs/base/common/product'; import type { ICredentialsProvider } from 'vs/platform/credentials/common/credentials'; import type { TunnelProviderFeatures } from 'vs/platform/tunnel/common/tunnel'; +import type { IProgress, IProgressCompositeOptions, IProgressDialogOptions, IProgressNotificationOptions, IProgressOptions, IProgressStep, IProgressWindowOptions } from 'vs/platform/progress/common/progress'; +import { IObservableValue } from 'vs/base/common/observableValue'; +import { TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; /** * The `IWorkbench` interface is the API facade for web embedders @@ -41,7 +45,7 @@ export interface IWorkbench { * @returns the scheme to use for opening the associated desktop * experience via protocol handler. */ - readonly uriScheme: string; + getUriScheme(): Promise<string>; /** * Retrieve performance marks that have been collected during startup. This function @@ -61,6 +65,25 @@ export interface IWorkbench { * workbench. */ openUri(target: URI): Promise<boolean>; + + /** + * Current workbench telemetry level. + */ + readonly telemetryLevel: IObservableValue<TelemetryLevel>; + }; + + window: { + /** + * Show progress in the editor. Progress is shown while running the given callback + * and while the promise it returned isn't resolved nor rejected. + * + * @param task A callback returning a promise. + * @return A promise that resolves to the returned value of the given task result. + */ + withProgress<R>( + options: IProgressOptions | IProgressDialogOptions | IProgressNotificationOptions | IProgressWindowOptions | IProgressCompositeOptions, + task: (progress: IProgress<IProgressStep>) => Promise<R> + ): Promise<R>; }; /** @@ -538,32 +561,42 @@ export interface IDefaultView { readonly id: string; } +/** + * @deprecated use `IDefaultEditor.options` instead + */ export interface IPosition { readonly line: number; readonly column: number; } +/** + * @deprecated use `IDefaultEditor.options` instead + */ export interface IRange { - - /** - * The start position. It is before or equal to end position. - */ readonly start: IPosition; - - /** - * The end position. It is after or equal to start position. - */ readonly end: IPosition; } export interface IDefaultEditor { + readonly uri: UriComponents; - readonly selection?: IRange; + readonly options?: IEditorOptions; + readonly openOnlyIfExists?: boolean; + + /** + * @deprecated use `options` instead + */ + readonly selection?: IRange; + + /** + * @deprecated use `options.override` instead + */ readonly openWith?: string; } export interface IDefaultLayout { + readonly views?: IDefaultView[]; readonly editors?: IDefaultEditor[]; @@ -627,4 +660,3 @@ export interface IDevelopmentOptions { */ readonly enableSmokeTestDriver?: boolean; } - diff --git a/src/vs/workbench/browser/web.factory.ts b/src/vs/workbench/browser/web.factory.ts index 4b271e566d4..49c7eb489e9 100644 --- a/src/vs/workbench/browser/web.factory.ts +++ b/src/vs/workbench/browser/web.factory.ts @@ -12,6 +12,9 @@ import { mark, PerformanceMark } from 'vs/base/common/performance'; import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; import { DeferredPromise } from 'vs/base/common/async'; import { asArray } from 'vs/base/common/arrays'; +import { IProgress, IProgressCompositeOptions, IProgressDialogOptions, IProgressNotificationOptions, IProgressOptions, IProgressStep, IProgressWindowOptions } from 'vs/platform/progress/common/progress'; +import { IObservableValue } from 'vs/base/common/observableValue'; +import { TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; let created = false; const workbenchPromise = new DeferredPromise<IWorkbench>(); @@ -108,7 +111,7 @@ export namespace env { export async function getUriScheme(): Promise<string> { const workbench = await workbenchPromise.p; - return workbench.env.uriScheme; + return workbench.env.getUriScheme(); } /** @@ -119,4 +122,22 @@ export namespace env { return workbench.env.openUri(target); } + + export const telemetryLevel: Promise<IObservableValue<TelemetryLevel>> = + workbenchPromise.p.then(workbench => workbench.env.telemetryLevel); +} + +export namespace window { + + /** + * {@linkcode IWorkbench.window IWorkbench.window.withProgress} + */ + export async function withProgress<R>( + options: IProgressOptions | IProgressDialogOptions | IProgressNotificationOptions | IProgressWindowOptions | IProgressCompositeOptions, + task: (progress: IProgress<IProgressStep>) => Promise<R> + ): Promise<R> { + const workbench = await workbenchPromise.p; + + return workbench.window.withProgress(options, task); + } } diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index 260d8438a27..e8ca913a532 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -69,6 +69,8 @@ import { IndexedDB } from 'vs/base/browser/indexedDB'; import { BrowserCredentialsService } from 'vs/workbench/services/credentials/browser/credentialsService'; import { IWorkspace } from 'vs/workbench/services/host/browser/browserHostService'; import { WebFileSystemAccess } from 'vs/platform/files/browser/webFileSystemAccess'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IProgressService } from 'vs/platform/progress/common/progress'; export class BrowserMain extends Disposable { @@ -116,13 +118,18 @@ export class BrowserMain extends Disposable { const timerService = accessor.get(ITimerService); const openerService = accessor.get(IOpenerService); const productService = accessor.get(IProductService); + const telemetryService = accessor.get(ITelemetryService); + const progessService = accessor.get(IProgressService); return { commands: { executeCommand: (command, ...args) => commandService.executeCommand(command, ...args) }, env: { - uriScheme: productService.urlProtocol, + telemetryLevel: telemetryService.telemetryLevel, + async getUriScheme(): Promise<string> { + return productService.urlProtocol; + }, async retrievePerformanceMarks() { await timerService.whenReady(); @@ -132,6 +139,9 @@ export class BrowserMain extends Disposable { return openerService.open(uri, {}); } }, + window: { + withProgress: (options, task) => progessService.withProgress(options, task) + }, shutdown: () => lifecycleService.shutdown() }; }); @@ -243,7 +253,7 @@ export class BrowserMain extends Disposable { const workspaceTrustEnablementService = new WorkspaceTrustEnablementService(configurationService, environmentService); serviceCollection.set(IWorkspaceTrustEnablementService, workspaceTrustEnablementService); - const workspaceTrustManagementService = new WorkspaceTrustManagementService(configurationService, remoteAuthorityResolverService, storageService, uriIdentityService, environmentService, configurationService, workspaceTrustEnablementService, logService); + const workspaceTrustManagementService = new WorkspaceTrustManagementService(configurationService, remoteAuthorityResolverService, storageService, uriIdentityService, environmentService, configurationService, workspaceTrustEnablementService); serviceCollection.set(IWorkspaceTrustManagementService, workspaceTrustManagementService); // Update workspace trust so that configuration is updated accordingly diff --git a/src/vs/workbench/browser/window.ts b/src/vs/workbench/browser/window.ts index 5e94ea2bba1..7d15b8bf58f 100644 --- a/src/vs/workbench/browser/window.ts +++ b/src/vs/workbench/browser/window.ts @@ -18,6 +18,7 @@ import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { registerWindowDriver } from 'vs/platform/driver/browser/driver'; 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'; import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { BrowserLifecycleService } from 'vs/workbench/services/lifecycle/browser/lifecycleService'; @@ -30,6 +31,7 @@ export class BrowserWindow extends Disposable { @ILifecycleService private readonly lifecycleService: BrowserLifecycleService, @IDialogService private readonly dialogService: IDialogService, @ILabelService private readonly labelService: ILabelService, + @IProductService private readonly productService: IProductService, @IBrowserWorkbenchEnvironmentService private readonly environmentService: IBrowserWorkbenchEnvironmentService, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService ) { @@ -65,9 +67,9 @@ export class BrowserWindow extends Disposable { this._register(addDisposableListener(this.layoutService.container, EventType.DROP, e => EventHelper.stop(e, true))); // Fullscreen (Browser) - [EventType.FULLSCREEN_CHANGE, EventType.WK_FULLSCREEN_CHANGE].forEach(event => { + for (const event of [EventType.FULLSCREEN_CHANGE, EventType.WK_FULLSCREEN_CHANGE]) { this._register(addDisposableListener(document, event, () => setFullscreen(!!detectFullscreen()))); - }); + } // Fullscreen (Native) this._register(addDisposableThrottledListener(viewport, EventType.RESIZE, () => { @@ -123,7 +125,7 @@ export class BrowserWindow extends Disposable { } private setupDriver(): void { - if (this.environmentService.options?.developmentOptions?.enableSmokeTestDriver) { + if (this.environmentService.enableSmokeTestDriver) { registerWindowDriver(); } } @@ -189,7 +191,35 @@ export class BrowserWindow extends Disposable { // but make sure to signal this as an expected unload and disable unload // handling explicitly to prevent the workbench from going down. else { - this.lifecycleService.withExpectedShutdown({ disableShutdownHandling: true }, () => window.location.href = href); + const invokeProtocolHandler = () => { + this.lifecycleService.withExpectedShutdown({ disableShutdownHandling: true }, () => window.location.href = href); + }; + + invokeProtocolHandler(); + + // We cannot know whether the protocol handler succeeded. + // Display guidance in case it did not, e.g. the app is not installed locally. + if (matchesScheme(href, this.productService.urlProtocol)) { + const showResult = await this.dialogService.show( + Severity.Info, + localize('openExternalDialogTitle', "All done. You can close this tab now."), + [ + localize('openExternalDialogButtonRetry', "Try again"), + localize('openExternalDialogButtonInstall', "Install {0}", this.productService.nameLong), + localize('openExternalDialogButtonContinue', "Continue here") + ], + { + cancelId: 2, + detail: localize('openExternalDialogDetail', "We tried opening {0} on your computer.", this.productService.nameLong) + }, + ); + + if (showResult.choice === 0) { + invokeProtocolHandler(); + } else if (showResult.choice === 1) { + await this.openerService.open(URI.parse(`http://aka.ms/vscode-install`)); + } + } } return true; diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index 5d43bf45a05..1bf0fcae7f5 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -111,6 +111,19 @@ const registry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Con tags: ['experimental'], description: localize('workbench.editor.preferBasedLanguageDetection', "When enabled, a language detection model that takes into account editor history will be given higher precedence."), }, + 'workbench.editor.languageDetectionHints': { + type: 'string', + default: 'always', + tags: ['experimental'], + enum: ['always', 'notebookEditors', 'textEditors', 'never'], + description: localize('workbench.editor.showLanguageDetectionHints', "When enabled, shows a status bar quick fix when the editor language doesn't match detected content language."), + enumDescriptions: [ + localize('workbench.editor.showLanguageDetectionHints.always', "Show show language detection quick fixes in both notebooks and untitled editors"), + localize('workbench.editor.showLanguageDetectionHints.notebook', "Only show language detection quick fixes in notebooks"), + localize('workbench.editor.showLanguageDetectionHints.editors', "Only show language detection quick fixes in untitled editors"), + localize('workbench.editor.showLanguageDetectionHints.never', "Never show language quick fixes"), + ] + }, 'workbench.editor.tabCloseButton': { 'type': 'string', 'enum': ['left', 'right', 'off'], @@ -464,11 +477,11 @@ const registry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Con 'description': localize('layoutControlType', "Controls whether the layout control in the custom title bar is displayed as a single menu button or with multiple UI toggles."), 'markdownDeprecationMessage': localize({ key: 'layoutControlTypeDeprecation', comment: ['{0} is a placeholder for a setting identifier.'] }, "This setting has been deprecated in favor of {0}", '`#workbench.layoutControl.type#`') }, - 'workbench.experimental.editor.dragAndDropIntoEditor.enabled': { + 'workbench.experimental.editor.dropIntoEditor.enabled': { 'type': 'boolean', + 'default': true, 'tags': ['experimental'], - 'default': false, - 'description': localize('dragAndDropIntoEditor', "Controls whether you can drag and drop a file into an editor by holding down shift (instead of opening the file in an editor)."), + 'markdownDescription': localize('dropIntoEditor', "Controls whether you can drag and drop a file into a text editor by holding down `shift` (instead of opening the file in an editor)."), } } }); @@ -520,6 +533,11 @@ const registry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Con 'default': isMacintosh ? ' \u2014 ' : ' - ', 'markdownDescription': localize("window.titleSeparator", "Separator used by `window.title`.") }, + 'window.experimental.titleMenu': { + type: 'boolean', + default: false, + description: localize('window.experimental.titleMenu', "Show window title as menu") + }, 'window.menuBarVisibility': { 'type': 'string', 'enum': ['classic', 'visible', 'toggle', 'hidden', 'compact'], @@ -586,14 +604,21 @@ const registry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Con 'type': 'string', 'enum': ['always', 'keyboardOnly', 'never'], 'enumDescriptions': [ - localize('window.confirmBeforeClose.always', "Always try to ask for confirmation. Note that browsers may still decide to close a tab or window without confirmation."), - localize('window.confirmBeforeClose.keyboardOnly', "Only ask for confirmation if a keybinding was detected. Note that detection may not be possible in some cases."), - localize('window.confirmBeforeClose.never', "Never explicitly ask for confirmation unless data loss is imminent.") + isWeb ? + localize('window.confirmBeforeClose.always.web', "Always try to ask for confirmation. Note that browsers may still decide to close a tab or window without confirmation.") : + localize('window.confirmBeforeClose.always', "Always ask for confirmation."), + isWeb ? + localize('window.confirmBeforeClose.keyboardOnly.web', "Only ask for confirmation if a keybinding was used to close the window. Note that detection may not be possible in some cases.") : + localize('window.confirmBeforeClose.keyboardOnly', "Only ask for confirmation if a keybinding was used."), + isWeb ? + localize('window.confirmBeforeClose.never.web', "Never explicitly ask for confirmation unless data loss is imminent.") : + localize('window.confirmBeforeClose.never', "Never explicitly ask for confirmation.") ], - 'default': isWeb && !isStandalone() ? 'keyboardOnly' : 'never', // on by default in web, unless PWA - 'description': localize('confirmBeforeCloseWeb', "Controls whether to show a confirmation dialog before closing the browser tab or window. Note that even if enabled, browsers may still decide to close a tab or window without confirmation and that this setting is only a hint that may not work in all cases."), - 'scope': ConfigurationScope.APPLICATION, - 'included': isWeb + 'default': (isWeb && !isStandalone()) ? 'keyboardOnly' : 'never', // on by default in web, unless PWA, never on desktop + 'markdownDescription': isWeb ? + localize('confirmBeforeCloseWeb', "Controls whether to show a confirmation dialog before closing the browser tab or window. Note that even if enabled, browsers may still decide to close a tab or window without confirmation and that this setting is only a hint that may not work in all cases.") : + localize('confirmBeforeClose', "Controls whether to show a confirmation dialog before closing the window or quitting the application."), + 'scope': ConfigurationScope.APPLICATION } } }); diff --git a/src/vs/workbench/browser/workbench.ts b/src/vs/workbench/browser/workbench.ts index f2c5bf51ade..27c50770822 100644 --- a/src/vs/workbench/browser/workbench.ts +++ b/src/vs/workbench/browser/workbench.ts @@ -336,7 +336,7 @@ export class Workbench extends Layout { this.restoreFontInfo(storageService, configurationService); // Create Parts - [ + for (const { id, role, classes, options } of [ { id: Parts.TITLEBAR_PART, role: 'contentinfo', classes: ['titlebar'] }, { id: Parts.BANNER_PART, role: 'banner', classes: ['banner'] }, { id: Parts.ACTIVITYBAR_PART, role: 'none', classes: ['activitybar', this.getSideBarPosition() === Position.LEFT ? 'left' : 'right'] }, // Use role 'none' for some parts to make screen readers less chatty #114892 @@ -345,11 +345,11 @@ export class Workbench extends Layout { { id: Parts.PANEL_PART, role: 'none', classes: ['panel', 'basepanel', positionToString(this.getPanelPosition())] }, { id: Parts.AUXILIARYBAR_PART, role: 'none', classes: ['auxiliarybar', 'basepanel', this.getSideBarPosition() === Position.LEFT ? 'right' : 'left'] }, { id: Parts.STATUSBAR_PART, role: 'status', classes: ['statusbar'] } - ].forEach(({ id, role, classes, options }) => { + ]) { const partContainer = this.createPart(id, role, classes); this.getPart(id).create(partContainer, options); - }); + } // Notification Handlers this.createNotificationsHandlers(instantiationService, notificationService); diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index bd610ebd12d..b12e6477782 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -9,7 +9,7 @@ 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 { IEditorOptions, ITextEditorOptions, IResourceEditorInput, ITextResourceEditorInput, IBaseTextResourceEditorInput, IBaseUntypedEditorInput } from 'vs/platform/editor/common/editor'; +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'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -675,7 +675,13 @@ export const enum EditorInputCapabilities { * component may decide to hide the description portion * for brevity. */ - ForceDescription = 1 << 6 + ForceDescription = 1 << 6, + + /** + * Signals that the editor supports dropping into the + * editor by holding shift. + */ + CanDropIntoEditor = 1 << 7, } export type IUntypedEditorInput = IResourceEditorInput | ITextResourceEditorInput | IUntitledTextResourceEditorInput | IResourceDiffEditorInput | IResourceSideBySideEditorInput; @@ -1334,10 +1340,9 @@ export async function pathsToEditors(paths: IPathData[] | undefined, fileService return; } - const options: ITextEditorOptions = { - selection: exists ? path.selection : undefined, - pinned: true, - override: path.editorOverrideId + const options: IEditorOptions = { + ...path.options, + pinned: true }; let input: IResourceEditorInput | IUntitledTextResourceEditorInput; diff --git a/src/vs/workbench/common/editor/diffEditorInput.ts b/src/vs/workbench/common/editor/diffEditorInput.ts index 838a0136fd8..878ebaca825 100644 --- a/src/vs/workbench/common/editor/diffEditorInput.ts +++ b/src/vs/workbench/common/editor/diffEditorInput.ts @@ -105,7 +105,10 @@ export class DiffEditorInput extends SideBySideEditorInput implements IDiffEdito // a label that resembles the difference between the two const originalMediumDescription = this.original.getDescription(Verbosity.MEDIUM); const modifiedMediumDescription = this.modified.getDescription(Verbosity.MEDIUM); - if (typeof originalMediumDescription === 'string' && typeof modifiedMediumDescription === 'string') { + if ( + (typeof originalMediumDescription === 'string' && typeof modifiedMediumDescription === 'string') && // we can only `shorten` when both sides are strings... + (originalMediumDescription || modifiedMediumDescription) // ...however never when both sides are empty strings + ) { const [shortenedOriginalMediumDescription, shortenedModifiedMediumDescription] = shorten([originalMediumDescription, modifiedMediumDescription]); mediumDescription = this.computeLabel(shortenedOriginalMediumDescription, shortenedModifiedMediumDescription); } diff --git a/src/vs/workbench/common/editor/editorGroupModel.ts b/src/vs/workbench/common/editor/editorGroupModel.ts index 3b3159e971e..8aab2bcbf6a 100644 --- a/src/vs/workbench/common/editor/editorGroupModel.ts +++ b/src/vs/workbench/common/editor/editorGroupModel.ts @@ -567,7 +567,7 @@ export class EditorGroupModel extends Disposable { kind: GroupModelChangeKind.EDITOR_MOVE, editor, oldEditorIndex: index, - editorIndex: toIndex, + editorIndex: toIndex }; this._onDidModelChange.fire(event); @@ -735,7 +735,8 @@ export class EditorGroupModel extends Disposable { this.pin(editor); // Move editor to be the last sticky editor - this.moveEditor(editor, this.sticky + 1); + const newEditorIndex = this.sticky + 1; + this.moveEditor(editor, newEditorIndex); // Adjust sticky index this.sticky++; @@ -744,7 +745,7 @@ export class EditorGroupModel extends Disposable { const event: IGroupEditorChangeEvent = { kind: GroupModelChangeKind.EDITOR_STICKY, editor, - editorIndex + editorIndex: newEditorIndex }; this._onDidModelChange.fire(event); } @@ -768,7 +769,8 @@ export class EditorGroupModel extends Disposable { } // Move editor to be the first non-sticky editor - this.moveEditor(editor, this.sticky); + const newEditorIndex = this.sticky; + this.moveEditor(editor, newEditorIndex); // Adjust sticky index this.sticky--; @@ -777,7 +779,7 @@ export class EditorGroupModel extends Disposable { const event: IGroupEditorChangeEvent = { kind: GroupModelChangeKind.EDITOR_STICKY, editor, - editorIndex + editorIndex: newEditorIndex }; this._onDidModelChange.fire(event); } diff --git a/src/vs/workbench/common/editor/resourceEditorInput.ts b/src/vs/workbench/common/editor/resourceEditorInput.ts index 062f9a50439..190a6e63db5 100644 --- a/src/vs/workbench/common/editor/resourceEditorInput.ts +++ b/src/vs/workbench/common/editor/resourceEditorInput.ts @@ -26,6 +26,10 @@ export abstract class AbstractResourceEditorInput extends EditorInput implements capabilities |= EditorInputCapabilities.Untitled; } + if (!(capabilities & EditorInputCapabilities.Readonly)) { + capabilities |= EditorInputCapabilities.CanDropIntoEditor; + } + return capabilities; } diff --git a/src/vs/workbench/common/theme.ts b/src/vs/workbench/common/theme.ts index fb7dcc2c042..bf7c818fb94 100644 --- a/src/vs/workbench/common/theme.ts +++ b/src/vs/workbench/common/theme.ts @@ -279,6 +279,27 @@ export const EDITOR_DRAG_AND_DROP_BACKGROUND = registerColor('editorGroup.dropBa hcLight: Color.fromHex('#0F4A85').transparent(0.50) }, localize('editorDragAndDropBackground', "Background color when dragging editors around. The color should have transparency so that the editor contents can still shine through.")); +export const EDITOR_DROP_INTO_PROMPT_FOREGROUND = registerColor('editorGroup.dropIntoPromptForeground', { + dark: editorWidgetForeground, + light: editorWidgetForeground, + hcDark: editorWidgetForeground, + hcLight: editorWidgetForeground +}, localize('editorDropIntoPromptForeground', "Foreground color of text shown over editors when dragging files. This text informs the user that they can hold shift to drop into the editor.")); + +export const EDITOR_DROP_INTO_PROMPT_BACKGROUND = registerColor('editorGroup.dropIntoPromptBackground', { + dark: editorWidgetBackground, + light: editorWidgetBackground, + hcDark: editorWidgetBackground, + hcLight: editorWidgetBackground +}, localize('editorDropIntoPromptBackground', "Background color of text shown over editors when dragging files. This text informs the user that they can hold shift to drop into the editor.")); + +export const EDITOR_DROP_INTO_PROMPT_BORDER = registerColor('editorGroup.dropIntoPromptBorder', { + dark: null, + light: null, + hcDark: contrastBorder, + hcLight: contrastBorder +}, localize('editorDropIntoPromptBorder', "Border color of text shown over editors when dragging files. This text informs the user that they can hold shift to drop into the editor.")); + export const SIDE_BY_SIDE_EDITOR_HORIZONTAL_BORDER = registerColor('sideBySideEditor.horizontalBorder', { dark: EDITOR_GROUP_BORDER, light: EDITOR_GROUP_BORDER, @@ -594,7 +615,7 @@ export const ACTIVITY_BAR_BADGE_BACKGROUND = registerColor('activityBarBadge.bac dark: '#007ACC', light: '#007ACC', hcDark: '#000000', - hcLight: '#007ACC' + hcLight: '#0F4A85' }, localize('activityBarBadgeBackground', "Activity notification badge background color. The activity bar is showing on the far left or right and allows to switch between views of the side bar.")); export const ACTIVITY_BAR_BADGE_FOREGROUND = registerColor('activityBarBadge.foreground', { diff --git a/src/vs/workbench/common/webview.ts b/src/vs/workbench/common/webview.ts index aa598f1133d..1db1d451aff 100644 --- a/src/vs/workbench/common/webview.ts +++ b/src/vs/workbench/common/webview.ts @@ -22,7 +22,7 @@ export const webviewResourceBaseHost = 'vscode-cdn.net'; export const webviewRootResourceAuthority = `vscode-resource.${webviewResourceBaseHost}`; -export const webviewGenericCspSource = `https://*.${webviewResourceBaseHost}`; +export const webviewGenericCspSource = `'self' https://*.${webviewResourceBaseHost}`; /** * Construct a uri that can load resources inside a webview diff --git a/src/vs/workbench/contrib/audioCues/browser/audioCues.contribution.ts b/src/vs/workbench/contrib/audioCues/browser/audioCues.contribution.ts index 4122bc5a7dd..ef9e3d8358a 100644 --- a/src/vs/workbench/contrib/audioCues/browser/audioCues.contribution.ts +++ b/src/vs/workbench/contrib/audioCues/browser/audioCues.contribution.ts @@ -41,7 +41,7 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).regis 'type': 'number', 'minimum': 0, 'maximum': 100, - 'default': 50 + 'default': 70 }, 'audioCues.lineHasBreakpoint': { 'description': localize('audioCues.lineHasBreakpoint', "Plays a sound when the active line has a breakpoint."), diff --git a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts index c796f5e0ce4..ca1856c6312 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts @@ -227,14 +227,14 @@ export class BulkEditDataSource implements IAsyncDataSource<BulkFileOperations, const range = Range.lift(edit.textEdit.textEdit.range); //prefix-math - let startTokens = textModel.getLineTokens(range.startLineNumber); + let 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; prefixLen < 50 && idx >= 0; idx--) { prefixLen = range.startColumn - startTokens.getStartOffset(idx); } //suffix-math - let endTokens = textModel.getLineTokens(range.endLineNumber); + let endTokens = textModel.tokenization.getLineTokens(range.endLineNumber); let suffixLen = 0; for (let idx = endTokens.findTokenIndexAtOffset(range.endColumn); suffixLen < 50 && idx < endTokens.getCount(); idx++) { suffixLen += endTokens.getEndOffset(idx) - endTokens.getStartOffset(idx); diff --git a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.css b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.css index bdd496042ce..46b11d7eedf 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.css +++ b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.css @@ -15,6 +15,10 @@ padding: 0 10px 10px; } +.simple-find-part .monaco-inputbox > .ibwrapper > input { + text-overflow: clip; +} + .monaco-workbench .simple-find-part { visibility: hidden; /* Use visibility to maintain flex layout while hidden otherwise interferes with transition */ z-index: 10; @@ -57,7 +61,11 @@ cursor: pointer; } -.monaco-workbench .simple-find-part .button.disabled { - opacity: 0.3; +.monaco-workbench div.simple-find-part div.button.disabled { + opacity: 0.3 !important; cursor: default; } + +div.simple-find-part-wrapper div.button { + border-radius: 5px; +} diff --git a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts index 1e220e2de14..ff599441598 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts @@ -12,16 +12,19 @@ import { Delayer } from 'vs/base/common/async'; import { KeyCode } from 'vs/base/common/keyCodes'; import { FindReplaceState } from 'vs/editor/contrib/find/browser/findState'; import { IMessage as InputBoxMessage } from 'vs/base/browser/ui/inputbox/inputBox'; -import { SimpleButton, findPreviousMatchIcon, findNextMatchIcon } from 'vs/editor/contrib/find/browser/findWidget'; +import { SimpleButton, findPreviousMatchIcon, findNextMatchIcon, NLS_NO_RESULTS, NLS_MATCHES_LOCATION } from 'vs/editor/contrib/find/browser/findWidget'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; -import { editorWidgetBackground, inputActiveOptionBorder, inputActiveOptionBackground, inputActiveOptionForeground, inputBackground, inputBorder, inputForeground, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, inputValidationInfoBackground, inputValidationInfoBorder, inputValidationInfoForeground, inputValidationWarningBackground, inputValidationWarningBorder, inputValidationWarningForeground, widgetShadow, editorWidgetForeground, errorForeground } from 'vs/platform/theme/common/colorRegistry'; +import { editorWidgetBackground, inputActiveOptionBorder, inputActiveOptionBackground, inputActiveOptionForeground, inputBackground, inputBorder, inputForeground, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, inputValidationInfoBackground, inputValidationInfoBorder, inputValidationInfoForeground, inputValidationWarningBackground, inputValidationWarningBorder, inputValidationWarningForeground, widgetShadow, editorWidgetForeground, errorForeground, toolbarHoverBackground, toolbarHoverOutline } from 'vs/platform/theme/common/colorRegistry'; import { IColorTheme, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { ContextScopedFindInput } from 'vs/platform/history/browser/contextScopedHistoryWidget'; import { widgetClose } from 'vs/platform/theme/common/iconRegistry'; +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'; const NLS_FIND_INPUT_LABEL = nls.localize('label.find', "Find"); -const NLS_FIND_INPUT_PLACEHOLDER = nls.localize('placeholder.find', "Find"); +const NLS_FIND_INPUT_PLACEHOLDER = nls.localize('placeholder.find', "Find (\u21C5 for history)"); const NLS_PREVIOUS_MATCH_BTN_LABEL = nls.localize('label.previousMatchButton', "Previous Match"); const NLS_NEXT_MATCH_BTN_LABEL = nls.localize('label.nextMatchButton', "Next Match"); const NLS_CLOSE_BTN_LABEL = nls.localize('label.closeButton', "Close"); @@ -30,6 +33,10 @@ interface IFindOptions { showOptionButtons?: boolean; checkImeCompletionState?: boolean; showResultCount?: boolean; + appendCaseSensitiveLabel?: string; + appendRegexLabel?: string; + appendWholeWordsLabel?: string; + type?: 'Terminal' | 'Webview'; } export abstract class SimpleFindWidget extends Widget { @@ -47,14 +54,15 @@ export abstract class SimpleFindWidget extends Widget { private _foundMatch: boolean = false; constructor( - @IContextViewService private readonly _contextViewService: IContextViewService, - @IContextKeyService contextKeyService: IContextKeyService, - private readonly _state: FindReplaceState = new FindReplaceState(), - private readonly _options: IFindOptions + state: FindReplaceState = new FindReplaceState(), + options: IFindOptions, + contextViewService: IContextViewService, + contextKeyService: IContextKeyService, + private readonly _keybindingService: IKeybindingService ) { super(); - this._findInput = this._register(new ContextScopedFindInput(null, this._contextViewService, { + this._findInput = this._register(new ContextScopedFindInput(null, contextViewService, { label: NLS_FIND_INPUT_LABEL, placeholder: NLS_FIND_INPUT_PLACEHOLDER, validation: (value: string): InputBoxMessage | null => { @@ -69,16 +77,19 @@ export abstract class SimpleFindWidget extends Widget { this.updateButtons(this._foundMatch); return { content: e.message }; } - } - }, contextKeyService, _options.showOptionButtons)); + }, + 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 + }, contextKeyService, options.showOptionButtons)); // Find History with update delayer this._updateHistoryDelayer = new Delayer<void>(500); this._register(this._findInput.onInput(async (e) => { - if (!_options.checkImeCompletionState || !this._findInput.isImeSessionInProgress) { + if (!options.checkImeCompletionState || !this._findInput.isImeSessionInProgress) { this._foundMatch = this._onInputChanged(); - if (this._options.showResultCount) { + if (options.showResultCount) { await this.updateResultCount(); } this.updateButtons(this._foundMatch); @@ -87,22 +98,22 @@ export abstract class SimpleFindWidget extends Widget { } })); - this._findInput.setRegex(!!this._state.isRegex); - this._findInput.setCaseSensitive(!!this._state.matchCase); - this._findInput.setWholeWords(!!this._state.wholeWord); + this._findInput.setRegex(!!state.isRegex); + this._findInput.setCaseSensitive(!!state.matchCase); + this._findInput.setWholeWords(!!state.wholeWord); this._register(this._findInput.onDidOptionChange(() => { - this._state.change({ + state.change({ isRegex: this._findInput.getRegex(), wholeWord: this._findInput.getWholeWords(), matchCase: this._findInput.getCaseSensitive() }, true); })); - this._register(this._state.onFindReplaceStateChange(() => { - this._findInput.setRegex(this._state.isRegex); - this._findInput.setWholeWords(this._state.wholeWord); - this._findInput.setCaseSensitive(this._state.matchCase); + this._register(state.onFindReplaceStateChange(() => { + this._findInput.setRegex(state.isRegex); + this._findInput.setWholeWords(state.wholeWord); + this._findInput.setCaseSensitive(state.matchCase); this.findFirst(); })); @@ -162,7 +173,7 @@ export abstract class SimpleFindWidget extends Widget { event.stopPropagation(); })); - if (_options?.showResultCount) { + if (options?.showResultCount) { this._domNode.classList.add('result-count'); this._register(this._findInput.onDidChange(() => { this.updateResultCount(); @@ -209,6 +220,14 @@ export abstract class SimpleFindWidget extends Widget { this._findInput.style(inputStyles); } + private _getKeybinding(actionId: string): string { + let kb = this._keybindingService?.lookupKeybinding(actionId); + if (!kb) { + return ''; + } + return ` (${kb.getLabel()})`; + } + override dispose() { super.dispose(); @@ -307,9 +326,17 @@ export abstract class SimpleFindWidget extends Widget { this._matchesCount.className = 'matchesCount'; } this._matchesCount.innerText = ''; - const label = count === undefined || count.resultCount === 0 ? `No Results` : `${count.resultIndex + 1} of ${count.resultCount}`; + let label = ''; + this._matchesCount.classList.toggle('no-results', false); + if (count?.resultCount && count?.resultCount <= 0) { + label = NLS_NO_RESULTS; + if (!!this.inputValue) { + this._matchesCount.classList.toggle('no-results', true); + } + } else if (count?.resultCount) { + label = strings.format(NLS_MATCHES_LOCATION, count.resultIndex + 1, count?.resultCount); + } this._matchesCount.appendChild(document.createTextNode(label)); - this._matchesCount.classList.toggle('no-results', !count || count.resultCount === 0); this._findInput?.domNode.insertAdjacentElement('afterend', this._matchesCount); this._foundMatch = !!count && count.resultCount > 0; } @@ -336,4 +363,23 @@ registerThemingParticipant((theme, collector) => { if (error) { collector.addRule(`.no-results.matchesCount { color: ${error}; }`); } + + const toolbarHoverBackgroundColor = theme.getColor(toolbarHoverBackground); + if (toolbarHoverBackgroundColor) { + collector.addRule(` + div.simple-find-part-wrapper div.button:hover:not(.disabled) { + background-color: ${toolbarHoverBackgroundColor}; + } + `); + } + + const toolbarHoverOutlineColor = theme.getColor(toolbarHoverOutline); + if (toolbarHoverOutlineColor) { + collector.addRule(` + div.simple-find-part-wrapper div.button:hover:not(.disabled) { + outline: 1px dashed ${toolbarHoverOutlineColor}; + outline-offset: -1px; + } + `); + } }); diff --git a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts index cec3c9c01f8..d2b4f005a8b 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts @@ -12,7 +12,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IQuickAccessRegistry, Extensions as QuickaccesExtensions } from 'vs/platform/quickinput/common/quickAccess'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkbenchEditorConfiguration } from 'vs/workbench/common/editor'; -import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; @@ -85,6 +85,11 @@ class GotoLineAction extends Action2 { when: null, primary: KeyMod.CtrlCmd | KeyCode.KeyG, mac: { primary: KeyMod.WinCtrl | KeyCode.KeyG } + }, + menu: { + id: MenuId.TitleMenuQuickPick, + group: '3/editorNav', + order: 2 } }); } diff --git a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts index 703b333927a..a8b90ea7be3 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts @@ -201,7 +201,7 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess item.highlights = undefined; return true; } - const score = fuzzyScore(picker.value, picker.value.toLowerCase(), 1 /*@-character*/, item.label, item.label.toLowerCase(), 0, true); + const score = fuzzyScore(picker.value, picker.value.toLowerCase(), 1 /*@-character*/, item.label, item.label.toLowerCase(), 0, { firstMatchCanBeWeak: true, boostFullMatch: true }); if (!score) { return false; } @@ -270,11 +270,15 @@ registerAction2(class GotoSymbolAction extends Action2 { weight: KeybindingWeight.WorkbenchContrib, primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyO }, - menu: { + menu: [{ id: MenuId.MenubarGoMenu, group: '4_symbol_nav', order: 1 - } + }, { + id: MenuId.TitleMenuQuickPick, + group: '3/editorNav', + order: 1 + }] }); } diff --git a/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts b/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts index 138f276bff7..9cadf75ecce 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts @@ -111,7 +111,7 @@ export class SuggestEnabledInput extends Widget implements IThemable { private readonly _onInputDidChange = new Emitter<string | undefined>(); readonly onInputDidChange: Event<string | undefined> = this._onInputDidChange.event; - protected readonly inputWidget: CodeEditorWidget; + readonly inputWidget: CodeEditorWidget; private readonly inputModel: ITextModel; protected stylingContainer: HTMLDivElement; private placeholderText: HTMLDivElement; diff --git a/src/vs/workbench/contrib/codeEditor/browser/untitledTextEditorHint.ts b/src/vs/workbench/contrib/codeEditor/browser/untitledTextEditorHint.ts index 306f9b93ce7..14c97806e09 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/untitledTextEditorHint.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/untitledTextEditorHint.ts @@ -19,6 +19,7 @@ import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { EventType as GestureEventType, Gesture } from 'vs/base/browser/touch'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; const $ = dom.$; @@ -32,6 +33,7 @@ export class UntitledTextEditorHintContribution implements IEditorContribution { constructor( private editor: ICodeEditor, + @IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService, @ICommandService private readonly commandService: ICommandService, @IConfigurationService private readonly configurationService: IConfigurationService, @IKeybindingService private readonly keybindingService: IKeybindingService, @@ -53,7 +55,7 @@ export class UntitledTextEditorHintContribution implements IEditorContribution { const model = this.editor.getModel(); if (model && model.uri.scheme === Schemas.untitled && model.getLanguageId() === PLAINTEXT_LANGUAGE_ID && configValue === 'text') { - this.untitledTextHintContentWidget = new UntitledTextEditorHintContentWidget(this.editor, this.commandService, this.configurationService, this.keybindingService); + this.untitledTextHintContentWidget = new UntitledTextEditorHintContentWidget(this.editor, this.editorGroupsService, this.commandService, this.configurationService, this.keybindingService); } } @@ -72,6 +74,7 @@ class UntitledTextEditorHintContentWidget implements IContentWidget { constructor( private readonly editor: ICodeEditor, + private readonly editorGroupsService: IEditorGroupsService, private readonly commandService: ICommandService, private readonly configurationService: IConfigurationService, private readonly keybindingService: IKeybindingService, @@ -103,6 +106,7 @@ class UntitledTextEditorHintContentWidget implements IContentWidget { if (!this.domNode) { this.domNode = $('.untitled-hint'); this.domNode.style.width = 'max-content'; + const language = $('a.language-mode'); language.style.cursor = 'pointer'; language.innerText = localize('selectAlanguage2', "Select a language"); @@ -112,10 +116,31 @@ class UntitledTextEditorHintContentWidget implements IContentWidget { language.title = localize('keyboardBindingTooltip', "{0}", languageKeybindingLabel); } this.domNode.appendChild(language); + + const or = $('span'); + or.innerText = localize('or', " or ",); + this.domNode.appendChild(or); + + const editorType = $('a.editor-type'); + editorType.style.cursor = 'pointer'; + editorType.innerText = localize('openADifferentEditor', "open a different editor"); + const selectEditorTypeKeyBinding = this.keybindingService.lookupKeybinding('welcome.showNewFileEntries'); + const selectEditorTypeKeybindingLabel = selectEditorTypeKeyBinding?.getLabel(); + if (selectEditorTypeKeybindingLabel) { + editorType.title = localize('keyboardBindingTooltip', "{0}", selectEditorTypeKeybindingLabel); + } + this.domNode.appendChild(editorType); + const toGetStarted = $('span'); - toGetStarted.innerText = localize('toGetStarted', " to get started. Start typing to dismiss, or ",); + toGetStarted.innerText = localize('toGetStarted', " to get started."); this.domNode.appendChild(toGetStarted); + this.domNode.appendChild($('br')); + + const startTyping = $('span'); + startTyping.innerText = localize('startTyping', "Start typing to dismiss or "); + this.domNode.appendChild(startTyping); + const dontShow = $('a'); dontShow.style.cursor = 'pointer'; dontShow.innerText = localize('dontshow', "don't show"); @@ -136,6 +161,21 @@ class UntitledTextEditorHintContentWidget implements IContentWidget { this.toDispose.push(dom.addDisposableListener(language, GestureEventType.Tap, languageOnClickOrTap)); this.toDispose.push(Gesture.addTarget(language)); + const chooseEditorOnClickOrTap = async (e: MouseEvent) => { + e.stopPropagation(); + + const activeEditorInput = this.editorGroupsService.activeGroup.activeEditor; + const newEditorSelected = await this.commandService.executeCommand('welcome.showNewFileEntries', { from: 'hint' }); + + // Close the active editor as long as it is untitled (swap the editors out) + if (newEditorSelected && activeEditorInput !== null && activeEditorInput.resource?.scheme === Schemas.untitled) { + this.editorGroupsService.activeGroup.closeEditor(activeEditorInput, { preserveFocus: true }); + } + }; + this.toDispose.push(dom.addDisposableListener(editorType, 'click', chooseEditorOnClickOrTap)); + this.toDispose.push(dom.addDisposableListener(editorType, GestureEventType.Tap, chooseEditorOnClickOrTap)); + this.toDispose.push(Gesture.addTarget(editorType)); + const dontShowOnClickOrTap = () => { this.configurationService.updateValue(untitledTextEditorHintSetting, 'hidden'); this.dispose(); diff --git a/src/vs/workbench/contrib/comments/browser/commentColors.ts b/src/vs/workbench/contrib/comments/browser/commentColors.ts index 3fc12217f70..127c30c86f8 100644 --- a/src/vs/workbench/contrib/comments/browser/commentColors.ts +++ b/src/vs/workbench/contrib/comments/browser/commentColors.ts @@ -5,12 +5,17 @@ import { Color } from 'vs/base/common/color'; import * as languages from 'vs/editor/common/languages'; +import { peekViewBorder } from 'vs/editor/contrib/peekView/browser/peekView'; import * as nls from 'vs/nls'; -import { contrastBorder, editorWarningForeground, editorWidgetForeground, registerColor } from 'vs/platform/theme/common/colorRegistry'; +import { contrastBorder, disabledForeground, registerColor, transparent } from 'vs/platform/theme/common/colorRegistry'; import { IColorTheme } from 'vs/platform/theme/common/themeService'; -export const resolvedCommentBorder = registerColor('comments.resolved.border', { dark: editorWidgetForeground, light: editorWidgetForeground, hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('resolvedCommentBorder', 'Color of borders and arrow for resolved comments.')); -export const unresolvedCommentBorder = registerColor('comments.unresolved.border', { dark: editorWarningForeground, light: editorWarningForeground, hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('unresolvedCommentBorder', 'Color of borders and arrow for unresolved comments.')); +const resolvedCommentBorder = registerColor('editorCommentsWidget.resolvedBorder', { dark: disabledForeground, light: disabledForeground, hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('resolvedCommentBorder', 'Color of borders and arrow for resolved comments.')); +const unresolvedCommentBorder = registerColor('editorCommentsWidget.unresolvedBorder', { dark: peekViewBorder, light: peekViewBorder, hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('unresolvedCommentBorder', 'Color of borders and arrow for unresolved comments.')); +export const commentThreadRangeBackground = registerColor('editorCommentsWidget.rangeBackground', { dark: transparent(unresolvedCommentBorder, .1), light: transparent(unresolvedCommentBorder, .1), hcDark: transparent(unresolvedCommentBorder, .1), hcLight: transparent(unresolvedCommentBorder, .1) }, nls.localize('commentThreadRangeBackground', 'Color of background for comment ranges.')); +export const commentThreadRangeBorder = registerColor('editorCommentsWidget.rangeBorder', { dark: transparent(unresolvedCommentBorder, .4), light: transparent(unresolvedCommentBorder, .4), hcDark: transparent(unresolvedCommentBorder, .4), hcLight: transparent(unresolvedCommentBorder, .4) }, nls.localize('commentThreadRangeBorder', 'Color of border for comment ranges.')); +export const commentThreadRangeActiveBackground = registerColor('editorCommentsWidget.rangeActiveBackground', { dark: transparent(unresolvedCommentBorder, .1), light: transparent(unresolvedCommentBorder, .1), hcDark: transparent(unresolvedCommentBorder, .1), hcLight: transparent(unresolvedCommentBorder, .1) }, nls.localize('commentThreadActiveRangeBackground', 'Color of background for currently selected or hovered comment range.')); +export const commentThreadRangeActiveBorder = registerColor('editorCommentsWidget.rangeActiveBorder', { dark: transparent(unresolvedCommentBorder, .4), light: transparent(unresolvedCommentBorder, .4), hcDark: transparent(unresolvedCommentBorder, .4), hcLight: transparent(unresolvedCommentBorder, .2) }, nls.localize('commentThreadActiveRangeBorder', 'Color of border for currently selected or hovered comment range.')); const commentThreadStateColors = new Map([ [languages.CommentThreadState.Unresolved, unresolvedCommentBorder], @@ -18,8 +23,10 @@ const commentThreadStateColors = new Map([ ]); export const commentThreadStateColorVar = '--comment-thread-state-color'; +export const commentViewThreadStateColorVar = '--comment-view-thread-state-color'; +export const commentThreadStateBackgroundColorVar = '--comment-thread-state-background-color'; -export function getCommentThreadStateColor(thread: languages.CommentThread, theme: IColorTheme): Color | undefined { - const colorId = thread.state !== undefined ? commentThreadStateColors.get(thread.state) : undefined; - return colorId !== undefined ? theme.getColor(colorId) : undefined; +export function getCommentThreadStateColor(state: languages.CommentThreadState | undefined, theme: IColorTheme): Color | undefined { + const colorId = (state !== undefined) ? commentThreadStateColors.get(state) : undefined; + return (colorId !== undefined) ? theme.getColor(colorId) : undefined; } diff --git a/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts b/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts index 71be0f04c36..7be06df3a41 100644 --- a/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts @@ -62,7 +62,7 @@ export class CommentGlyphWidget { return { position: { - lineNumber: range ? range.startLineNumber : this._lineNumber, + lineNumber: range ? range.endLineNumber : this._lineNumber, column: 1 }, preference: [ContentWidgetPositionPreference.EXACT] diff --git a/src/vs/workbench/contrib/comments/browser/commentNode.ts b/src/vs/workbench/contrib/comments/browser/commentNode.ts index d99f9c41a0b..db768308374 100644 --- a/src/vs/workbench/contrib/comments/browser/commentNode.ts +++ b/src/vs/workbench/contrib/comments/browser/commentNode.ts @@ -165,10 +165,11 @@ export class CommentNode<T extends IRange | ICellRange> extends Disposable { private createHeader(commentDetailsContainer: HTMLElement): void { const header = dom.append(commentDetailsContainer, dom.$(`div.comment-title.${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME}`)); - const author = dom.append(header, dom.$('strong.author')); + const infoContainer = dom.append(header, dom.$('comment-header-info')); + const author = dom.append(infoContainer, dom.$('strong.author')); author.innerText = this.comment.userName; - this.createTimestamp(header); - this._isPendingLabel = dom.append(header, dom.$('span.isPending')); + this.createTimestamp(infoContainer); + this._isPendingLabel = dom.append(infoContainer, dom.$('span.isPending')); if (this.comment.label) { this._isPendingLabel.innerText = this.comment.label; diff --git a/src/vs/workbench/contrib/comments/browser/commentReply.ts b/src/vs/workbench/contrib/comments/browser/commentReply.ts index 6b9c90cc2ba..cf2e81a3bb9 100644 --- a/src/vs/workbench/contrib/comments/browser/commentReply.ts +++ b/src/vs/workbench/contrib/comments/browser/commentReply.ts @@ -275,7 +275,6 @@ export class CommentReply<T extends IRange | ICellRange> extends Disposable { } private hideReplyArea() { - this.commentEditor.setValue(''); this.commentEditor.getDomNode()!.style.outline = ''; this._pendingComment = ''; this.form.classList.remove('expand'); diff --git a/src/vs/workbench/contrib/comments/browser/commentService.ts b/src/vs/workbench/contrib/comments/browser/commentService.ts index a11e4b8fb15..eed900b49eb 100644 --- a/src/vs/workbench/contrib/comments/browser/commentService.ts +++ b/src/vs/workbench/contrib/comments/browser/commentService.ts @@ -67,6 +67,7 @@ export interface ICommentService { readonly onDidUpdateCommentThreads: Event<ICommentThreadChangedEvent>; readonly onDidUpdateNotebookCommentThreads: Event<INotebookCommentThreadChangedEvent>; readonly onDidChangeActiveCommentThread: Event<CommentThread | null>; + readonly onDidChangeCurrentCommentThread: Event<CommentThread | undefined>; readonly onDidUpdateCommentingRanges: Event<{ owner: string }>; readonly onDidChangeActiveCommentingRange: Event<{ range: Range; commentingRangesInfo: CommentingRanges }>; readonly onDidSetDataProvider: Event<void>; @@ -90,6 +91,7 @@ export interface ICommentService { hasReactionHandler(owner: string): boolean; 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; } export class CommentService extends Disposable implements ICommentService { @@ -119,6 +121,9 @@ export class CommentService extends Disposable implements ICommentService { private readonly _onDidChangeActiveCommentThread = this._register(new Emitter<CommentThread | null>()); readonly onDidChangeActiveCommentThread = this._onDidChangeActiveCommentThread.event; + private readonly _onDidChangeCurrentCommentThread = this._register(new Emitter<CommentThread | undefined>()); + readonly onDidChangeCurrentCommentThread = this._onDidChangeCurrentCommentThread.event; + private readonly _onDidChangeActiveCommentingRange: Emitter<{ range: Range; commentingRangesInfo: CommentingRanges; @@ -137,6 +142,18 @@ export class CommentService extends Disposable implements ICommentService { super(); } + /** + * The current comment thread is the thread that has focus or is being hovered. + * @param commentThread + */ + setCurrentCommentThread(commentThread: CommentThread | undefined) { + this._onDidChangeCurrentCommentThread.fire(commentThread); + } + + /** + * The active comment thread is the the thread that is currently being edited. + * @param commentThread + */ setActiveCommentThread(commentThread: CommentThread | null) { this._onDidChangeActiveCommentThread.fire(commentThread); } diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadBody.ts b/src/vs/workbench/contrib/comments/browser/commentThreadBody.ts index fca40378daa..59a1d57b12b 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadBody.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadBody.ts @@ -209,8 +209,14 @@ export class CommentThreadBody<T extends IRange | ICellRange = IRange> extends D } private _updateAriaLabel() { - this._commentsElement.ariaLabel = nls.localize('commentThreadAria', "Comment thread with {0} comments. {1}.", - this._commentThread.comments?.length, this._commentThread.label); + if (this._commentThread.isDocumentCommentThread()) { + this._commentsElement.ariaLabel = nls.localize('commentThreadAria.withRange', "Comment thread with {0} comments on lines {1} through {2}. {3}.", + this._commentThread.comments?.length, this._commentThread.range.startLineNumber, this._commentThread.range.endLineNumber, + this._commentThread.label); + } else { + this._commentsElement.ariaLabel = nls.localize('commentThreadAria', "Comment thread with {0} comments. {1}.", + this._commentThread.comments?.length, this._commentThread.label); + } } private _setFocusedComment(value: number | undefined) { diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadRangeDecorator.ts b/src/vs/workbench/contrib/comments/browser/commentThreadRangeDecorator.ts new file mode 100644 index 00000000000..5af04a9fe0c --- /dev/null +++ b/src/vs/workbench/contrib/comments/browser/commentThreadRangeDecorator.ts @@ -0,0 +1,99 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { IRange } from 'vs/editor/common/core/range'; +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'; + +class CommentThreadRangeDecoration implements IModelDeltaDecoration { + private _decorationId: string | undefined; + + public get id(): string | undefined { + return this._decorationId; + } + + public set id(id: string | undefined) { + this._decorationId = id; + } + + constructor( + public readonly range: IRange, + public readonly options: ModelDecorationOptions) { + } +} + +export class CommentThreadRangeDecorator extends Disposable { + private static description = 'comment-thread-range-decorator'; + private decorationOptions: ModelDecorationOptions; + private activeDecorationOptions: ModelDecorationOptions; + private decorationIds: string[] = []; + private activeDecorationIds: string[] = []; + private editor: ICodeEditor | undefined; + + constructor(commentService: ICommentService) { + super(); + const decorationOptions: IModelDecorationOptions = { + description: CommentThreadRangeDecorator.description, + isWholeLine: false, + zIndex: 20, + className: 'comment-thread-range' + }; + + this.decorationOptions = ModelDecorationOptions.createDynamic(decorationOptions); + + const activeDecorationOptions: IModelDecorationOptions = { + description: CommentThreadRangeDecorator.description, + isWholeLine: false, + zIndex: 20, + className: 'comment-thread-range-current' + }; + + 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))) { + newDecoration.push(new CommentThreadRangeDecoration(range, this.activeDecorationOptions)); + } + } + this.activeDecorationIds = this.editor.deltaDecorations(this.activeDecorationIds, newDecoration); + newDecoration.forEach((decoration, index) => decoration.id = this.decorationIds[index]); + })); + } + + public update(editor: ICodeEditor, commentInfos: ICommentInfo[]) { + const model = editor.getModel(); + if (!model) { + return; + } + this.editor = editor; + + const commentThreadRangeDecorations: CommentThreadRangeDecoration[] = []; + for (const info of commentInfos) { + info.threads.forEach(thread => { + 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; + } + commentThreadRangeDecorations.push(new CommentThreadRangeDecoration(range, this.decorationOptions)); + }); + } + + this.decorationIds = editor.deltaDecorations(this.decorationIds, commentThreadRangeDecorations); + commentThreadRangeDecorations.forEach((decoration, index) => decoration.id = this.decorationIds[index]); + } +} diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts index b4989b77691..5759d2f3783 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts @@ -24,7 +24,7 @@ import { IColorTheme } from 'vs/platform/theme/common/themeService'; import { contrastBorder, focusBorder, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, textBlockQuoteBackground, textBlockQuoteBorder, textLinkActiveForeground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; import { PANEL_BORDER } from 'vs/workbench/common/theme'; import { IRange } from 'vs/editor/common/core/range'; -import { commentThreadStateColorVar } from 'vs/workbench/contrib/comments/browser/commentColors'; +import { commentThreadStateBackgroundColorVar, commentThreadStateColorVar } from 'vs/workbench/contrib/comments/browser/commentColors'; import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { FontInfo } from 'vs/editor/common/config/fontInfo'; @@ -109,6 +109,41 @@ export class CommentThreadWidget<T extends IRange | ICellRange = IRange> extends if (controller) { commentControllerKey.set(controller.contextValue); } + + this.currentThreadListeners(); + } + + private updateCurrentThread(hasMouse: boolean, hasFocus: boolean) { + if (hasMouse || hasFocus) { + this.commentService.setCurrentCommentThread(this.commentThread); + } else { + this.commentService.setCurrentCommentThread(undefined); + } + } + + private currentThreadListeners() { + let hasMouse = false; + let hasFocus = false; + this._register(dom.addDisposableListener(this.container, dom.EventType.MOUSE_ENTER, (e) => { + if ((<any>e).toElement === this.container) { + hasMouse = true; + this.updateCurrentThread(hasMouse, hasFocus); + } + }, true)); + this._register(dom.addDisposableListener(this.container, dom.EventType.MOUSE_LEAVE, (e) => { + if ((<any>e).fromElement === this.container) { + hasMouse = false; + this.updateCurrentThread(hasMouse, hasFocus); + } + }, true)); + this._register(dom.addDisposableListener(this.container, dom.EventType.FOCUS_IN, () => { + hasFocus = true; + this.updateCurrentThread(hasMouse, hasFocus); + }, true)); + this._register(dom.addDisposableListener(this.container, dom.EventType.FOCUS_OUT, () => { + hasFocus = false; + this.updateCurrentThread(hasMouse, hasFocus); + }, true)); } updateCommentThread(commentThread: languages.CommentThread<T>) { @@ -161,7 +196,10 @@ export class CommentThreadWidget<T extends IRange | ICellRange = IRange> extends this._onDidResize.fire(dimension); } - + override dispose() { + super.dispose(); + this.updateCurrentThread(false, false); + } private _bindCommentThreadListeners() { this._commentThreadDisposables.push(this._commentThread.onDidChangeCanReply(() => { @@ -248,6 +286,7 @@ export class CommentThreadWidget<T extends IRange | ICellRange = IRange> extends const content: string[] = []; content.push(`.monaco-editor .review-widget > .body { border-top: 1px solid var(${commentThreadStateColorVar}) }`); + content.push(`.monaco-editor .review-widget > .head { background-color: var(${commentThreadStateBackgroundColorVar}) }`); const linkColor = theme.getColor(textLinkForeground); if (linkColor) { diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts index 151a49ffbb6..c6a047179b0 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts @@ -23,10 +23,10 @@ import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { CommentThreadWidget } from 'vs/workbench/contrib/comments/browser/commentThreadWidget'; import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; -import { commentThreadStateColorVar, getCommentThreadStateColor } from 'vs/workbench/contrib/comments/browser/commentColors'; +import { commentThreadStateBackgroundColorVar, commentThreadStateColorVar, getCommentThreadStateColor } from 'vs/workbench/contrib/comments/browser/commentColors'; import { peekViewBorder } from 'vs/editor/contrib/peekView/browser/peekView'; -export function getCommentThreadWidgetStateColor(thread: languages.CommentThread, theme: IColorTheme): Color | undefined { +export function getCommentThreadWidgetStateColor(thread: languages.CommentThreadState | undefined, theme: IColorTheme): Color | undefined { return getCommentThreadStateColor(thread, theme) ?? theme.getColor(peekViewBorder); } @@ -85,7 +85,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget private readonly _globalToDispose = new DisposableStore(); private _commentThreadDisposables: IDisposable[] = []; private _contextKeyService: IContextKeyService; - private _scopedInstatiationService: IInstantiationService; + private _scopedInstantiationService: IInstantiationService; public get owner(): string { return this._owner; @@ -109,7 +109,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget super(editor, { keepEditorSelection: true }); this._contextKeyService = contextKeyService.createScoped(this.domNode); - this._scopedInstatiationService = instantiationService.createChild(new ServiceCollection( + this._scopedInstantiationService = instantiationService.createChild(new ServiceCollection( [IContextKeyService, this._contextKeyService] )); @@ -184,16 +184,16 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget protected _fillContainer(container: HTMLElement): void { this.setCssClass('review-widget'); - this._commentThreadWidget = this._scopedInstatiationService.createInstance( + this._commentThreadWidget = this._scopedInstantiationService.createInstance( CommentThreadWidget, container, this._owner, this.editor.getModel()!.uri, this._contextKeyService, - this._scopedInstatiationService, + this._scopedInstantiationService, this._commentThread as unknown as languages.CommentThread<IRange | ICellRange>, this._pendingComment, - { editor: this.editor }, + { editor: this.editor, codeBlockFontSize: '' }, this._commentOptions, { actionRunner: () => { @@ -201,7 +201,17 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget let newPosition = this.getPosition(); if (newPosition) { - this.commentService.updateCommentThreadTemplate(this.owner, this._commentThread.commentThreadHandle, new Range(newPosition.lineNumber, 1, newPosition.lineNumber, 1)); + let range: Range; + const originalRange = this._commentThread.range; + if (newPosition.lineNumber !== originalRange.endLineNumber) { + // The widget could have moved as a result of editor changes. + // We need to try to calculate the new, more correct, range for the comment. + const distance = newPosition.lineNumber - this._commentThread.range.endLineNumber; + range = new Range(originalRange.startLineNumber + distance, originalRange.startColumn, originalRange.endLineNumber + distance, originalRange.endColumn); + } else { + range = new Range(originalRange.startLineNumber, originalRange.startColumn, originalRange.endLineNumber, originalRange.endColumn); + } + this.commentService.updateCommentThreadTemplate(this.owner, this._commentThread.commentThreadHandle, range); } } }, @@ -261,7 +271,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget this._commentThreadWidget.updateCommentThread(commentThread); // Move comment glyph widget and show position if the line has changed. - const lineNumber = this._commentThread.range.startLineNumber; + const lineNumber = this._commentThread.range.endLineNumber; let shouldMoveWidget = false; if (this._commentGlyph) { if (this._commentGlyph.getPosition().position!.lineNumber !== lineNumber) { @@ -344,12 +354,13 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget this._commentThreadDisposables.push(this._commentThread.onDidChangeState(() => { const borderColor = - getCommentThreadWidgetStateColor(this._commentThread, this.themeService.getColorTheme()) || Color.transparent; + getCommentThreadWidgetStateColor(this._commentThread.state, this.themeService.getColorTheme()) || Color.transparent; this.style({ frameColor: borderColor, arrowColor: borderColor, }); this.container?.style.setProperty(commentThreadStateColorVar, `${borderColor}`); + this.container?.style.setProperty(commentThreadStateBackgroundColorVar, `${borderColor.transparent(.1)}`); })); } @@ -410,7 +421,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget } private _applyTheme(theme: IColorTheme) { - const borderColor = getCommentThreadWidgetStateColor(this._commentThread, this.themeService.getColorTheme()) || Color.transparent; + const borderColor = getCommentThreadWidgetStateColor(this._commentThread.state, this.themeService.getColorTheme()) || Color.transparent; this.style({ arrowColor: borderColor, frameColor: borderColor diff --git a/src/vs/workbench/contrib/comments/browser/comments.contribution.ts b/src/vs/workbench/contrib/comments/browser/comments.contribution.ts index 0d515f2ae79..2ba619a7c0c 100644 --- a/src/vs/workbench/contrib/comments/browser/comments.contribution.ts +++ b/src/vs/workbench/contrib/comments/browser/comments.contribution.ts @@ -24,9 +24,9 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).regis markdownDeprecationMessage: nls.localize('comments.openPanel.deprecated', "This setting is deprecated in favor of `comments.openView`.") }, 'comments.openView': { - enum: ['never', 'file'], - enumDescriptions: [nls.localize('comments.openView.never', "The comments view will never be opened."), nls.localize('comments.openView.file', "The comments view will open when a file with comments is active.")], - default: 'file', + enum: ['never', 'file', 'firstFile'], + enumDescriptions: [nls.localize('comments.openView.never', "The comments view will never be opened."), nls.localize('comments.openView.file', "The comments view will open when a file with comments is active."), nls.localize('comments.openView.firstFile', "If the comments view has not been opened yet during this session it will open the first time during a session that a file with comments is active.")], + default: 'firstFile', description: nls.localize('comments.openView', "Controls when the comments view should open."), restricted: false }, diff --git a/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts b/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts index a172f418ba0..dd4fa30d7ff 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts @@ -46,6 +46,10 @@ import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { Position } from 'vs/editor/common/core/position'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import { CommentThreadRangeDecorator } from 'vs/workbench/contrib/comments/browser/commentThreadRangeDecorator'; +import { commentThreadRangeActiveBackground, commentThreadRangeActiveBorder, commentThreadRangeBackground, commentThreadRangeBorder } from 'vs/workbench/contrib/comments/browser/commentColors'; +import { ICursorSelectionChangedEvent } from 'vs/editor/common/cursorEvents'; +import { CommentsPanel } from 'vs/workbench/contrib/comments/browser/commentsView'; export const ID = 'editor.contrib.review'; @@ -66,6 +70,13 @@ export class ReviewViewZone implements IViewZone { } } +interface CommentRangeAction { + ownerId: string; + extensionId: string | undefined; + label: string | undefined; + commentingRangesInfo: languages.CommentingRanges; +} + class CommentingRangeDecoration implements IModelDeltaDecoration { private _decorationId: string | undefined; private _startLineNumber: number; @@ -91,7 +102,7 @@ class CommentingRangeDecoration implements IModelDeltaDecoration { this._endLineNumber = _range.endLineNumber; } - public getCommentAction(): { ownerId: string; extensionId: string | undefined; label: string | undefined; commentingRangesInfo: languages.CommentingRanges } { + public getCommentAction(): CommentRangeAction { return { extensionId: this._extensionId, label: this._label, @@ -111,13 +122,16 @@ class CommentingRangeDecoration implements IModelDeltaDecoration { class CommentingRangeDecorator { public static description = 'commenting-range-decorator'; - private decorationOptions!: ModelDecorationOptions; - private hoverDecorationOptions!: ModelDecorationOptions; + private decorationOptions: ModelDecorationOptions; + private hoverDecorationOptions: ModelDecorationOptions; + private multilineDecorationOptions: ModelDecorationOptions; private commentingRangeDecorations: CommentingRangeDecoration[] = []; private decorationIds: string[] = []; private _editor: ICodeEditor | undefined; private _infos: ICommentInfo[] | undefined; private _lastHover: number = -1; + private _lastSelection: Range | undefined; + private _lastSelectionCursor: number | undefined; private _onDidChangeDecorationsCount: Emitter<number> = new Emitter(); public readonly onDidChangeDecorationsCount = this._onDidChangeDecorationsCount.event; @@ -137,6 +151,14 @@ class CommentingRangeDecorator { }; this.hoverDecorationOptions = ModelDecorationOptions.createDynamic(hoverDecorationOptions); + + const multilineDecorationOptions: IModelDecorationOptions = { + description: CommentingRangeDecorator.description, + isWholeLine: true, + linesDecorationsClassName: `comment-range-glyph comment-diff-added multiline-add` + }; + + this.multilineDecorationOptions = ModelDecorationOptions.createDynamic(multilineDecorationOptions); } public updateHover(hoverLine?: number) { @@ -146,27 +168,72 @@ class CommentingRangeDecorator { this._lastHover = hoverLine ?? -1; } + public updateSelection(cursorLine: number, range: Range = new Range(0, 0, 0, 0)) { + this._lastSelection = range.isEmpty() ? undefined : range; + this._lastSelectionCursor = range.isEmpty() ? undefined : cursorLine; + // Some scenarios: + // Selection is made. Emphasis should show on the drag/selection end location. + // Selection is made, then user clicks elsewhere. We should still show the decoration. + if (this._editor && this._infos) { + this._doUpdate(this._editor, this._infos, cursorLine, range); + } + } + public update(editor: ICodeEditor, commentInfos: ICommentInfo[]) { this._editor = editor; this._infos = commentInfos; this._doUpdate(editor, commentInfos); } - private _doUpdate(editor: ICodeEditor, commentInfos: ICommentInfo[], hoverLine: number = -1) { + private _doUpdate(editor: ICodeEditor, commentInfos: ICommentInfo[], emphasisLine: number = -1, selectionRange: Range | undefined = this._lastSelection) { let model = editor.getModel(); if (!model) { return; } + // If there's still a selection, use that. + emphasisLine = this._lastSelectionCursor ?? emphasisLine; + let commentingRangeDecorations: CommentingRangeDecoration[] = []; for (const info of commentInfos) { info.commentingRanges.ranges.forEach(range => { - if ((range.startLineNumber <= hoverLine) && (range.endLineNumber >= hoverLine)) { - const beforeRange = new Range(range.startLineNumber, 1, hoverLine, 1); - const hoverRange = new Range(hoverLine, 1, hoverLine, 1); - const afterRange = new Range(hoverLine, 1, range.endLineNumber, 1); + const rangeObject = new Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn); + let intersectingSelectionRange = selectionRange ? rangeObject.intersectRanges(selectionRange) : undefined; + if ((selectionRange && (emphasisLine >= 0) && intersectingSelectionRange) + // If there's only one selection line, then just drop into the else if and show an emphasis line. + && !((intersectingSelectionRange.startLineNumber === intersectingSelectionRange.endLineNumber) + && (emphasisLine === intersectingSelectionRange.startLineNumber))) { + // The emphasisLine should be the within the commenting range, even if the selection range stretches + // outside of the commenting range. + // Clip the emphasis and selection ranges to the commenting range + let intersectingEmphasisRange: Range; + if (emphasisLine <= intersectingSelectionRange.startLineNumber) { + intersectingEmphasisRange = intersectingSelectionRange.collapseToStart(); + intersectingSelectionRange = new Range(intersectingSelectionRange.startLineNumber + 1, 1, intersectingSelectionRange.endLineNumber, 1); + } else { + intersectingEmphasisRange = new Range(intersectingSelectionRange.endLineNumber, 1, intersectingSelectionRange.endLineNumber, 1); + intersectingSelectionRange = new Range(intersectingSelectionRange.startLineNumber, 1, intersectingSelectionRange.endLineNumber - 1, 1); + } + commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, intersectingSelectionRange, this.multilineDecorationOptions, info.commentingRanges, true)); + commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, intersectingEmphasisRange, this.hoverDecorationOptions, info.commentingRanges, true)); + + const beforeRangeEndLine = Math.min(intersectingEmphasisRange.startLineNumber, intersectingSelectionRange.startLineNumber) - 1; + const hasBeforeRange = rangeObject.startLineNumber <= beforeRangeEndLine; + const afterRangeStartLine = Math.max(intersectingEmphasisRange.endLineNumber, intersectingSelectionRange.endLineNumber) + 1; + const hasAfterRange = rangeObject.endLineNumber >= afterRangeStartLine; + if (hasBeforeRange) { + const beforeRange = new Range(range.startLineNumber, 1, beforeRangeEndLine, 1); + commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, beforeRange, this.decorationOptions, info.commentingRanges, true)); + } + if (hasAfterRange) { + const afterRange = new Range(afterRangeStartLine, 1, range.endLineNumber, 1); + commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, afterRange, this.decorationOptions, info.commentingRanges, true)); + } + } else if ((rangeObject.startLineNumber <= emphasisLine) && (emphasisLine <= rangeObject.endLineNumber)) { + const beforeRange = new Range(range.startLineNumber, 1, emphasisLine, 1); + const afterRange = new Range(emphasisLine, 1, range.endLineNumber, 1); commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, beforeRange, this.decorationOptions, info.commentingRanges, true)); - commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, hoverRange, this.hoverDecorationOptions, info.commentingRanges, true)); + commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, new Range(emphasisLine, 1, emphasisLine, 1), this.hoverDecorationOptions, info.commentingRanges, true)); commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, afterRange, this.decorationOptions, info.commentingRanges, true)); } else { commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, range, this.decorationOptions, info.commentingRanges)); @@ -184,28 +251,35 @@ class CommentingRangeDecorator { } } - public getMatchedCommentAction(line: number) { + public getMatchedCommentAction(commentRange: Range): CommentRangeAction[] { // keys is ownerId - const foundHoverActions = new Map<string, languages.CommentingRanges>(); - let result = []; + const foundHoverActions = new Map<string, { range: Range; action: CommentRangeAction }>(); for (const decoration of this.commentingRangeDecorations) { const range = decoration.getActiveRange(); - if (range && range.startLineNumber <= line && line <= range.endLineNumber) { - // We can have 3 commenting ranges that match from the same owner because of how - // the line hover decoration is done. We only want to use the action from 1 of them. + if (range && ((range.startLineNumber <= commentRange.startLineNumber) || (commentRange.endLineNumber <= range.endLineNumber))) { + // We can have several commenting ranges that match from the same owner because of how + // the line hover and selection decoration is done. + // The ranges must be merged so that we can see if the new commentRange fits within them. const action = decoration.getCommentAction(); - if (decoration.isHover) { - if (foundHoverActions.get(action.ownerId) === action.commentingRangesInfo) { - continue; - } else { - foundHoverActions.set(action.ownerId, action.commentingRangesInfo); - } + const alreadyFoundInfo = foundHoverActions.get(action.ownerId); + if (alreadyFoundInfo?.action.commentingRangesInfo === action.commentingRangesInfo) { + // Merge ranges. + const newRange = new Range( + range.startLineNumber < alreadyFoundInfo.range.startLineNumber ? range.startLineNumber : alreadyFoundInfo.range.startLineNumber, + range.startColumn < alreadyFoundInfo.range.startColumn ? range.startColumn : alreadyFoundInfo.range.startColumn, + range.endLineNumber > alreadyFoundInfo.range.endLineNumber ? range.endLineNumber : alreadyFoundInfo.range.endLineNumber, + range.endColumn > alreadyFoundInfo.range.endColumn ? range.endColumn : alreadyFoundInfo.range.endColumn + ); + foundHoverActions.set(action.ownerId, { range: newRange, action }); + } else { + foundHoverActions.set(action.ownerId, { range, action }); } - result.push(action); } } - return result; + return Array.from(foundHoverActions.values()).filter(action => { + return (action.range.startLineNumber <= commentRange.startLineNumber) && (commentRange.endLineNumber <= action.range.endLineNumber); + }).map(actions => actions.action); } public dispose(): void { @@ -225,11 +299,12 @@ export class CommentController implements IEditorContribution { private _commentWidgets: ReviewZoneWidget[]; private _commentInfos: ICommentInfo[]; private _commentingRangeDecorator!: CommentingRangeDecorator; + private _commentThreadRangeDecorator!: CommentThreadRangeDecorator; private mouseDownInfo: { lineNumber: number } | null = null; private _commentingRangeSpaceReserved = false; private _computePromise: CancelablePromise<Array<ICommentInfo | null>> | null; private _addInProgress!: boolean; - private _emptyThreadsToAddQueue: [number, IEditorMouseEvent | undefined][] = []; + private _emptyThreadsToAddQueue: [Range, IEditorMouseEvent | undefined][] = []; private _computeCommentingRangePromise!: CancelablePromise<ICommentInfo[]> | null; private _computeCommentingRangeScheduler!: Delayer<Array<ICommentInfo | null>> | null; private _pendingCommentCache: { [key: string]: { [key: string]: string } }; @@ -268,6 +343,8 @@ export class CommentController implements IEditorContribution { } })); + this.globalToDispose.add(this._commentThreadRangeDecorator = new CommentThreadRangeDecorator(this.commentService)); + this.globalToDispose.add(this.commentService.onDidDeleteDataProvider(ownerId => { delete this._pendingCommentCache[ownerId]; this.beginCompute(); @@ -292,6 +369,8 @@ export class CommentController implements IEditorContribution { this._editorDisposables.push(this.editor.onMouseMove(e => this.onEditorMouseMove(e))); this._editorDisposables.push(this.editor.onDidChangeCursorPosition(e => this.onEditorChangeCursorPosition(e.position))); this._editorDisposables.push(this.editor.onDidFocusEditorWidget(() => this.onEditorChangeCursorPosition(this.editor.getPosition()))); + this._editorDisposables.push(this.editor.onDidChangeCursorSelection(e => this.onEditorChangeCursorSelection(e))); + this._editorDisposables.push(this.editor.onDidBlurEditorWidget(() => this.onEditorChangeCursorSelection())); } private clearEditorListeners() { @@ -303,6 +382,13 @@ export class CommentController implements IEditorContribution { this._commentingRangeDecorator.updateHover(e.target.position?.lineNumber); } + private onEditorChangeCursorSelection(e?: ICursorSelectionChangedEvent): void { + const position = this.editor.getPosition()?.lineNumber; + if (position) { + this._commentingRangeDecorator.updateSelection(position, e?.selection); + } + } + private onEditorChangeCursorPosition(e: Position | null) { const decorations = e ? this.editor.getDecorationsInRange(Range.fromPositions(e, { column: -1, lineNumber: e.lineNumber })) : undefined; let hasCommentingRange = false; @@ -512,6 +598,13 @@ export class CommentController implements IEditorContribution { this._commentWidgets.splice(index, 1); matchedZone.dispose(); } + const infosThreads = this._commentInfos.filter(info => info.owner === e.owner)[0].threads; + for (let i = 0; i < infosThreads.length; i++) { + if (infosThreads[i] === thread) { + infosThreads.splice(i, 1); + i--; + } + } }); changed.forEach(thread => { @@ -538,21 +631,31 @@ export class CommentController implements IEditorContribution { this.displayCommentThread(e.owner, thread, pendingCommentText); this._commentInfos.filter(info => info.owner === e.owner)[0].threads.push(thread); }); - + this._commentThreadRangeDecorator.update(this.editor, commentInfo); })); - this.beginCompute().then(() => { - if (this._commentWidgets.length - && (this.configurationService.getValue<ICommentsConfiguration>(COMMENTS_SECTION).openView === 'file')) { - this.viewsService.openView(COMMENTS_VIEW_ID); + this.beginCompute(); + } + + private async openCommentsView() { + if (this._commentWidgets.length) { + if (this.configurationService.getValue<ICommentsConfiguration>(COMMENTS_SECTION).openView === 'file') { + return this.viewsService.openView(COMMENTS_VIEW_ID); + } else if (this.configurationService.getValue<ICommentsConfiguration>(COMMENTS_SECTION).openView === 'firstFile') { + const hasShownView = this.viewsService.getViewWithId<CommentsPanel>(COMMENTS_VIEW_ID)?.hasRendered; + if (!hasShownView) { + return this.viewsService.openView(COMMENTS_VIEW_ID); + } } - }); + } + return undefined; } private displayCommentThread(owner: string, thread: languages.CommentThread, pendingComment: string | null): void { const zoneWidget = this.instantiationService.createInstance(ReviewZoneWidget, this.editor, owner, thread, pendingComment); - zoneWidget.display(thread.range.startLineNumber); + zoneWidget.display(thread.range.endLineNumber); this._commentWidgets.push(zoneWidget); + this.openCommentsView(); } private onEditorMouseDown(e: IEditorMouseEvent): void { @@ -569,26 +672,33 @@ export class CommentController implements IEditorContribution { if (e.target.element.className.indexOf('comment-diff-added') >= 0) { const lineNumber = e.target.position!.lineNumber; - this.addOrToggleCommentAtLine(lineNumber, e); + // 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)); + } + this.addOrToggleCommentAtLine(range, e); } } - public async addOrToggleCommentAtLine(lineNumber: number, e: IEditorMouseEvent | undefined): Promise<void> { + public async addOrToggleCommentAtLine(commentRange: Range, e: IEditorMouseEvent | undefined): Promise<void> { // If an add is already in progress, queue the next add and process it after the current one finishes to // prevent empty comment threads from being added to the same line. if (!this._addInProgress) { this._addInProgress = true; // The widget's position is undefined until the widget has been displayed, so rely on the glyph position instead - const existingCommentsAtLine = this._commentWidgets.filter(widget => widget.getGlyphPosition() === lineNumber); + const existingCommentsAtLine = this._commentWidgets.filter(widget => widget.getGlyphPosition() === commentRange.endLineNumber); if (existingCommentsAtLine.length) { - existingCommentsAtLine.forEach(widget => widget.toggleExpand(lineNumber)); + existingCommentsAtLine.forEach(widget => widget.toggleExpand(commentRange.endLineNumber)); this.processNextThreadToAdd(); return; } else { - this.addCommentAtLine(lineNumber, e); + this.addCommentAtLine(commentRange, e); } } else { - this._emptyThreadsToAddQueue.push([lineNumber, e]); + this._emptyThreadsToAddQueue.push([commentRange, e]); } } @@ -600,8 +710,8 @@ export class CommentController implements IEditorContribution { } } - public addCommentAtLine(lineNumber: number, e: IEditorMouseEvent | undefined): Promise<void> { - const newCommentInfos = this._commentingRangeDecorator.getMatchedCommentAction(lineNumber); + public addCommentAtLine(range: Range, e: IEditorMouseEvent | undefined): Promise<void> { + const newCommentInfos = this._commentingRangeDecorator.getMatchedCommentAction(range); if (!newCommentInfos.length || !this.editor.hasModel()) { return Promise.resolve(); } @@ -612,7 +722,7 @@ export class CommentController implements IEditorContribution { this.contextMenuService.showContextMenu({ getAnchor: () => anchor, - getActions: () => this.getContextMenuActions(newCommentInfos, lineNumber), + getActions: () => this.getContextMenuActions(newCommentInfos, range), getActionsContext: () => newCommentInfos.length ? newCommentInfos[0] : undefined, onHide: () => { this._addInProgress = false; } }); @@ -629,7 +739,7 @@ export class CommentController implements IEditorContribution { if (commentInfos.length) { const { ownerId } = commentInfos[0]; - this.addCommentAtLine2(lineNumber, ownerId); + this.addCommentAtLine2(range, ownerId); } }).then(() => { this._addInProgress = false; @@ -637,7 +747,7 @@ export class CommentController implements IEditorContribution { } } else { const { ownerId } = newCommentInfos[0]!; - this.addCommentAtLine2(lineNumber, ownerId); + this.addCommentAtLine2(range, ownerId); } return Promise.resolve(); @@ -656,7 +766,7 @@ export class CommentController implements IEditorContribution { return picks; } - private getContextMenuActions(commentInfos: { ownerId: string; extensionId: string | undefined; label: string | undefined; commentingRangesInfo: languages.CommentingRanges }[], lineNumber: number): IAction[] { + private getContextMenuActions(commentInfos: { ownerId: string; extensionId: string | undefined; label: string | undefined; commentingRangesInfo: languages.CommentingRanges }[], commentRange: Range): IAction[] { const actions: IAction[] = []; commentInfos.forEach(commentInfo => { @@ -668,7 +778,7 @@ export class CommentController implements IEditorContribution { undefined, true, () => { - this.addCommentAtLine2(lineNumber, ownerId); + this.addCommentAtLine2(commentRange, ownerId); return Promise.resolve(); } )); @@ -676,8 +786,7 @@ export class CommentController implements IEditorContribution { return actions; } - public addCommentAtLine2(lineNumber: number, ownerId: string) { - const range = new Range(lineNumber, 1, lineNumber, 1); + public addCommentAtLine2(range: Range, ownerId: string) { this.commentService.createCommentThreadTemplate(ownerId, this.editor.getModel()!.uri, range); this.processNextThreadToAdd(); return; @@ -742,6 +851,7 @@ export class CommentController implements IEditorContribution { }); this._commentingRangeDecorator.update(this.editor, this._commentInfos); + this._commentThreadRangeDecorator.update(this.editor, this._commentInfos); } public closeWidget(): void { @@ -848,15 +958,15 @@ CommandsRegistry.registerCommand({ return Promise.resolve(); } - const position = activeEditor.getPosition(); - return controller.addOrToggleCommentAtLine(position.lineNumber, undefined); + const position = activeEditor.getSelection(); + return controller.addOrToggleCommentAtLine(position, undefined); } }); MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: ADD_COMMENT_COMMAND, - title: nls.localize('comments.addCommand', "Add Comment on Current Line"), + title: nls.localize('comments.addCommand', "Add Comment on Current Selection"), category: 'Comments' }, when: ActiveCursorHasCommentingRange @@ -980,4 +1090,32 @@ registerThemingParticipant((theme, collector) => { if (statusBarItemActiveBackground) { collector.addRule(`.review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label:active { background-color: ${statusBarItemActiveBackground}; border: 1px solid transparent;}`); } + + const commentThreadRangeBackgroundColor = theme.getColor(commentThreadRangeBackground); + if (commentThreadRangeBackgroundColor) { + collector.addRule(`.monaco-editor .comment-thread-range { background-color: ${commentThreadRangeBackgroundColor};}`); + } + + const commentThreadRangeBorderColor = theme.getColor(commentThreadRangeBorder); + if (commentThreadRangeBorderColor) { + collector.addRule(`.monaco-editor .comment-thread-range { + border-color: ${commentThreadRangeBorderColor}; + border-width: 1px; + border-style: solid; + box-sizing: border-box; }`); + } + + const commentThreadRangeActiveBackgroundColor = theme.getColor(commentThreadRangeActiveBackground); + if (commentThreadRangeActiveBackgroundColor) { + collector.addRule(`.monaco-editor .comment-thread-range-current { background-color: ${commentThreadRangeActiveBackgroundColor};}`); + } + + const commentThreadRangeActiveBorderColor = theme.getColor(commentThreadRangeActiveBorder); + if (commentThreadRangeActiveBorderColor) { + collector.addRule(`.monaco-editor .comment-thread-range-current { + border-color: ${commentThreadRangeActiveBorderColor}; + border-width: 1px; + border-style: solid; + box-sizing: border-box; }`); + } }); diff --git a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts index e291c7d32de..3166fd47195 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts @@ -18,12 +18,15 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { WorkbenchAsyncDataTree, IListService, IWorkbenchAsyncDataTreeOptions } from 'vs/platform/list/browser/listService'; -import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { IColorTheme, IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IColorMapping } from 'vs/platform/theme/common/styler'; import { TimestampWidget } from 'vs/workbench/contrib/comments/browser/timestamp'; import { Codicon } from 'vs/base/common/codicons'; import { IMarkdownString } from 'vs/base/common/htmlContent'; +import { commentViewThreadStateColorVar, getCommentThreadStateColor } from 'vs/workbench/contrib/comments/browser/commentColors'; +import { CommentThreadState } from 'vs/editor/common/languages'; +import { Color } from 'vs/base/common/color'; export const COMMENTS_VIEW_ID = 'workbench.panel.comments'; export const COMMENTS_VIEW_TITLE = 'Comments'; @@ -50,11 +53,12 @@ interface IResourceTemplateData { interface ICommentThreadTemplateData { threadMetadata: { - icon?: HTMLElement; + icon: HTMLElement; userNames: HTMLSpanElement; timestamp: TimestampWidget; separator: HTMLElement; commentPreview: HTMLSpanElement; + range: HTMLSpanElement; }; repliesMetadata: { container: HTMLElement; @@ -121,7 +125,8 @@ export class CommentNodeRenderer implements IListRenderer<ITreeNode<CommentNode> constructor( @IOpenerService private readonly openerService: IOpenerService, - @IConfigurationService private readonly configurationService: IConfigurationService + @IConfigurationService private readonly configurationService: IConfigurationService, + @IThemeService private themeService: IThemeService ) { } renderTemplate(container: HTMLElement) { @@ -134,7 +139,8 @@ export class CommentNodeRenderer implements IListRenderer<ITreeNode<CommentNode> userNames: dom.append(metadataContainer, dom.$('.user')), timestamp: new TimestampWidget(this.configurationService, dom.append(metadataContainer, dom.$('.timestamp-container'))), separator: dom.append(metadataContainer, dom.$('.separator')), - commentPreview: dom.append(metadataContainer, dom.$('.text')) + commentPreview: dom.append(metadataContainer, dom.$('.text')), + range: dom.append(metadataContainer, dom.$('.range')) }; data.threadMetadata.separator.innerText = '\u00b7'; @@ -185,11 +191,17 @@ export class CommentNodeRenderer implements IListRenderer<ITreeNode<CommentNode> renderElement(node: ITreeNode<CommentNode>, index: number, templateData: ICommentThreadTemplateData, height: number | undefined): void { const commentCount = node.element.replies.length + 1; templateData.threadMetadata.icon?.classList.add(...ThemeIcon.asClassNameArray((commentCount === 1) ? Codicon.comment : Codicon.commentDiscussion)); + if (node.element.threadState !== undefined) { + const color = this.getCommentThreadWidgetStateColor(node.element.threadState, this.themeService.getColorTheme()); + templateData.threadMetadata.icon.style.setProperty(commentViewThreadStateColorVar, `${color}`); + templateData.threadMetadata.icon.style.color = `var(${commentViewThreadStateColorVar}`; + } templateData.threadMetadata.userNames.textContent = node.element.comment.userName; templateData.threadMetadata.timestamp.setTimestamp(node.element.comment.timestamp ? new Date(node.element.comment.timestamp) : undefined); const originalComment = node.element; templateData.threadMetadata.commentPreview.innerText = ''; + templateData.threadMetadata.commentPreview.style.height = '22px'; if (typeof originalComment.comment.body === 'string') { templateData.threadMetadata.commentPreview.innerText = originalComment.comment.body; } else { @@ -201,6 +213,12 @@ export class CommentNodeRenderer implements IListRenderer<ITreeNode<CommentNode> templateData.threadMetadata.commentPreview.title = renderedComment.element.textContent ?? ''; } + if (node.element.range.startLineNumber === node.element.range.endLineNumber) { + templateData.threadMetadata.range.textContent = nls.localize('commentLine', "[Ln {0}]", node.element.range.startLineNumber); + } else { + templateData.threadMetadata.range.textContent = nls.localize('commentRange', "[Ln {0}-{1}]", node.element.range.startLineNumber, node.element.range.endLineNumber); + } + if (!node.element.hasReply()) { templateData.repliesMetadata.container.style.display = 'none'; return; @@ -208,9 +226,13 @@ export class CommentNodeRenderer implements IListRenderer<ITreeNode<CommentNode> templateData.repliesMetadata.container.style.display = ''; templateData.repliesMetadata.count.textContent = this.getCountString(commentCount); - templateData.repliesMetadata.lastReplyDetail.textContent = nls.localize('lastReplyFrom', "Last reply from {0}", node.element.replies[node.element.replies.length - 1].comment.userName); - templateData.repliesMetadata.timestamp.setTimestamp(originalComment.comment.timestamp ? new Date(originalComment.comment.timestamp) : undefined); + const lastComment = node.element.replies[node.element.replies.length - 1].comment; + templateData.repliesMetadata.lastReplyDetail.textContent = nls.localize('lastReplyFrom', "Last reply from {0}", lastComment.userName); + templateData.repliesMetadata.timestamp.setTimestamp(lastComment.timestamp ? new Date(lastComment.timestamp) : undefined); + } + private getCommentThreadWidgetStateColor(state: CommentThreadState | undefined, theme: IColorTheme): Color | undefined { + return (state !== undefined) ? getCommentThreadStateColor(state, theme) : undefined; } disposeTemplate(templateData: ICommentThreadTemplateData): void { diff --git a/src/vs/workbench/contrib/comments/browser/commentsView.ts b/src/vs/workbench/contrib/comments/browser/commentsView.ts index ad852ea3b7e..7077d35338b 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsView.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsView.ts @@ -120,7 +120,7 @@ export class CommentsPanel extends ViewPane { const focusColor = theme.getColor(focusBorder); if (focusColor) { - content.push(`.comments-panel .commenst-panel-container a:focus { outline-color: ${focusColor}; }`); + content.push(`.comments-panel .comments-panel-container a:focus { outline-color: ${focusColor}; }`); } const codeTextForegroundColor = theme.getColor(textPreformatForeground); @@ -147,6 +147,10 @@ export class CommentsPanel extends ViewPane { } } + public get hasRendered(): boolean { + return !!this.tree; + } + public override layoutBody(height: number, width: number): void { super.layoutBody(height, width); this.tree.layout(height, width); diff --git a/src/vs/workbench/contrib/comments/browser/media/panel.css b/src/vs/workbench/contrib/comments/browser/media/panel.css index 8b2e685cb8b..37bc9a17a45 100644 --- a/src/vs/workbench/contrib/comments/browser/media/panel.css +++ b/src/vs/workbench/contrib/comments/browser/media/panel.css @@ -49,8 +49,7 @@ .comments-panel .comments-panel-container .tree-container .comment-thread-container .comment-snippet-container .count, .comments-panel .comments-panel-container .tree-container .comment-thread-container .comment-metadata-container .user { - overflow: hidden; - text-overflow: ellipsis; + min-width: fit-content; } .comments-panel .comments-panel-container .tree-container .comment-thread-container .comment-snippet-container .text { @@ -74,6 +73,11 @@ text-overflow: ellipsis; max-width: 500px; overflow: hidden; + padding-right: 5px; +} + +.comments-panel .comments-panel-container .tree-container .comment-thread-container .range { + opacity: 0.8; } .comments-panel .comments-panel-container .tree-container .comment-thread-container .comment-snippet-container .text code { diff --git a/src/vs/workbench/contrib/comments/browser/media/review.css b/src/vs/workbench/contrib/comments/browser/media/review.css index 2539c3e3960..f30e7d7109b 100644 --- a/src/vs/workbench/contrib/comments/browser/media/review.css +++ b/src/vs/workbench/contrib/comments/browser/media/review.css @@ -29,6 +29,11 @@ height: 21px; } +.review-widget .body .review-comment .comment-title .comment-header-info { + overflow: hidden; + text-overflow: ellipsis; +} + .review-widget .body .review-comment .comment-title { display: flex; width: 100%; @@ -393,6 +398,7 @@ div.preview.inline .monaco-editor .comment-range-glyph { .monaco-editor .margin-view-overlays > div:hover > .comment-range-glyph.comment-diff-added:before, .monaco-editor .margin-view-overlays .comment-range-glyph.comment-diff-added.line-hover:before, +.monaco-editor .margin-view-overlays .comment-range-glyph.comment-diff-added.multiline-add:before, .monaco-editor .comment-range-glyph.comment-thread:before { position: absolute; height: 100%; diff --git a/src/vs/workbench/contrib/comments/common/commentModel.ts b/src/vs/workbench/contrib/comments/common/commentModel.ts index d8f97430b4b..06ec27f01c3 100644 --- a/src/vs/workbench/contrib/comments/common/commentModel.ts +++ b/src/vs/workbench/contrib/comments/common/commentModel.ts @@ -5,7 +5,7 @@ import { URI } from 'vs/base/common/uri'; import { IRange } from 'vs/editor/common/core/range'; -import { Comment, CommentThread, CommentThreadChangedEvent } from 'vs/editor/common/languages'; +import { Comment, CommentThread, CommentThreadChangedEvent, CommentThreadState } from 'vs/editor/common/languages'; import { groupBy } from 'vs/base/common/arrays'; import { localize } from 'vs/nls'; @@ -21,14 +21,16 @@ export class CommentNode { replies: CommentNode[] = []; resource: URI; isRoot: boolean; + threadState?: CommentThreadState; - constructor(owner: string, threadId: string, resource: URI, comment: Comment, range: IRange) { + constructor(owner: string, threadId: string, resource: URI, comment: Comment, range: IRange, threadState: CommentThreadState | undefined) { this.owner = owner; this.threadId = threadId; this.comment = comment; this.resource = resource; this.range = range; this.isRoot = false; + this.threadState = threadState; } hasReply(): boolean { @@ -51,7 +53,7 @@ export class ResourceWithCommentThreads { public static createCommentNode(owner: string, resource: URI, commentThread: CommentThread): CommentNode { const { threadId, comments, range } = commentThread; - const commentNodes: CommentNode[] = comments!.map(comment => new CommentNode(owner, threadId!, resource, comment, range)); + const commentNodes: CommentNode[] = comments!.map(comment => new CommentNode(owner, threadId!, resource, comment, range, commentThread.state)); if (commentNodes.length > 1) { commentNodes[0].replies = commentNodes.slice(1, commentNodes.length); } diff --git a/src/vs/workbench/contrib/comments/common/commentsConfiguration.ts b/src/vs/workbench/contrib/comments/common/commentsConfiguration.ts index 1ef2ba67cb7..d3c180e69a4 100644 --- a/src/vs/workbench/contrib/comments/common/commentsConfiguration.ts +++ b/src/vs/workbench/contrib/comments/common/commentsConfiguration.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ export interface ICommentsConfiguration { - openView: 'never' | 'file'; + openView: 'never' | 'file' | 'firstFile'; useRelativeTime: boolean; } diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts index 119a41914f1..81dfae861af 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts @@ -124,6 +124,8 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { public override get capabilities(): EditorInputCapabilities { let capabilities = EditorInputCapabilities.None; + capabilities |= EditorInputCapabilities.CanDropIntoEditor; + if (!this.customEditorService.getCustomEditorCapabilities(this.viewType)?.supportsMultipleEditorsPerDocument) { capabilities |= EditorInputCapabilities.Singleton; } diff --git a/src/vs/workbench/contrib/debug/browser/baseDebugView.ts b/src/vs/workbench/contrib/debug/browser/baseDebugView.ts index acdf0b9005d..19b1e114ac0 100644 --- a/src/vs/workbench/contrib/debug/browser/baseDebugView.ts +++ b/src/vs/workbench/contrib/debug/browser/baseDebugView.ts @@ -9,10 +9,12 @@ import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { HighlightedLabel, IHighlight } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; import { IInputValidationOptions, InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; import { ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; +import { Codicon } from 'vs/base/common/codicons'; import { createMatches, FuzzyScore } from 'vs/base/common/filters'; import { once } from 'vs/base/common/functional'; import { KeyCode } from 'vs/base/common/keyCodes'; import { DisposableStore, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { localize } from 'vs/nls'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { attachInputBoxStyler } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -112,7 +114,6 @@ export function renderVariable(variable: Variable, data: IVariableTemplateData, } data.expression.classList.toggle('lazy', !!variable.presentationHint?.lazy); - data.lazyButton.title = variable.presentationHint?.lazy ? variable.value : ''; renderExpressionValue(variable, data.value, { showChanged, maxValueLength: MAX_VALUE_RENDER_LENGTH_IN_VIEWLET, @@ -156,9 +157,10 @@ export abstract class AbstractExpressionsRenderer implements ITreeRenderer<IExpr renderTemplate(container: HTMLElement): IExpressionTemplateData { const expression = dom.append(container, $('.expression')); const name = dom.append(expression, $('span.name')); - const value = dom.append(expression, $('span.value')); const lazyButton = dom.append(expression, $('span.lazy-button')); - lazyButton.textContent = `(...)`; + lazyButton.classList.add(...Codicon.eye.classNamesArray); + lazyButton.title = localize('debug.lazyButton.tooltip', "Click to expand"); + const value = dom.append(expression, $('span.value')); const label = new HighlightedLabel(name); diff --git a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts index 762b74c8201..4eab6791a7f 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts @@ -235,38 +235,48 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi const breakpoints = this.debugService.getModel().getBreakpoints({ uri, lineNumber }); if (breakpoints.length) { - // Show the dialog if there is a potential condition to be accidently lost. - // Do not show dialog on linux due to electron issue freezing the mouse #50026 - if (!env.isLinux && breakpoints.some(bp => !!bp.condition || !!bp.logMessage || !!bp.hitCondition)) { + const isShiftPressed = e.event.shiftKey; + const enabled = breakpoints.some(bp => bp.enabled); + + if (isShiftPressed) { + breakpoints.forEach(bp => this.debugService.enableOrDisableBreakpoints(!enabled, bp)); + } else if (!env.isLinux && breakpoints.some(bp => !!bp.condition || !!bp.logMessage || !!bp.hitCondition)) { + // Show the dialog if there is a potential condition to be accidently lost. + // Do not show dialog on linux due to electron issue freezing the mouse #50026 const logPoint = breakpoints.every(bp => !!bp.logMessage); const breakpointType = logPoint ? nls.localize('logPoint', "Logpoint") : nls.localize('breakpoint', "Breakpoint"); - const disable = breakpoints.some(bp => bp.enabled); - const enabling = nls.localize('breakpointHasConditionDisabled', + const disabledBreakpointDialogMessage = nls.localize( + 'breakpointHasConditionDisabled', "This {0} has a {1} that will get lost on remove. Consider enabling the {0} instead.", breakpointType.toLowerCase(), logPoint ? nls.localize('message', "message") : nls.localize('condition', "condition") ); - const disabling = nls.localize('breakpointHasConditionEnabled', + const enabledBreakpointDialogMessage = nls.localize( + 'breakpointHasConditionEnabled', "This {0} has a {1} that will get lost on remove. Consider disabling the {0} instead.", breakpointType.toLowerCase(), logPoint ? nls.localize('message', "message") : nls.localize('condition', "condition") ); - const { choice } = await this.dialogService.show(severity.Info, disable ? disabling : enabling, [ - nls.localize('removeLogPoint', "Remove {0}", breakpointType), - nls.localize('disableLogPoint', "{0} {1}", disable ? nls.localize('disable', "Disable") : nls.localize('enable', "Enable"), breakpointType), - nls.localize('cancel', "Cancel") - ], { cancelId: 2 }); + const { choice } = await this.dialogService.show( + severity.Info, + enabled ? enabledBreakpointDialogMessage : disabledBreakpointDialogMessage, + [ + nls.localize('removeLogPoint', "Remove {0}", breakpointType), + nls.localize('disableLogPoint', "{0} {1}", enabled ? nls.localize('disable', "Disable") : nls.localize('enable', "Enable"), breakpointType), + nls.localize('cancel', "Cancel") + ], + { cancelId: 2 }, + ); if (choice === 0) { breakpoints.forEach(bp => this.debugService.removeBreakpoints(bp.getId())); } if (choice === 1) { - breakpoints.forEach(bp => this.debugService.enableOrDisableBreakpoints(!disable, bp)); + breakpoints.forEach(bp => this.debugService.enableOrDisableBreakpoints(!enabled, bp)); } } else { - const enabled = breakpoints.some(bp => bp.enabled); if (!enabled) { breakpoints.forEach(bp => this.debugService.enableOrDisableBreakpoints(!enabled, bp)); } else { @@ -485,7 +495,6 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi inlineWidget }; }); - } finally { this.ignoreDecorationsChangedEvent = false; } @@ -511,6 +520,12 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi inlineWidget }; }); + + for (const d of this.breakpointDecorations) { + if (d.inlineWidget) { + this.editor.layoutContentWidget(d.inlineWidget); + } + } } private async onModelDecorationsChanged(): Promise<void> { diff --git a/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts b/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts index 4be819c4cb4..3df0cf71a74 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts @@ -48,14 +48,10 @@ export interface IPrivateBreakpointWidgetService { } const DECORATION_KEY = 'breakpointwidgetdecoration'; -function isCurlyBracketOpen(input: IActiveCodeEditor): boolean { +function isPositionInCurlyBracketBlock(input: IActiveCodeEditor): boolean { const model = input.getModel(); - const prevBracket = model.bracketPairs.findPrevBracket(input.getPosition()); - if (prevBracket && prevBracket.isOpen) { - return true; - } - - return false; + const bracketPairs = model.bracketPairs.getBracketPairsInRange(Range.fromPositions(input.getPosition())); + return bracketPairs.some(p => p.openingBracketInfo.bracketText === '{'); } function createDecorations(theme: IColorTheme, placeHolder: string): IDecorationOptions[] { @@ -251,7 +247,7 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi provideCompletionItems: (model: ITextModel, position: Position, _context: CompletionContext, token: CancellationToken): Promise<CompletionList> => { let suggestionsPromise: Promise<CompletionList>; const underlyingModel = this.editor.getModel(); - if (underlyingModel && (this.context === Context.CONDITION || (this.context === Context.LOG_MESSAGE && isCurlyBracketOpen(this.input)))) { + if (underlyingModel && (this.context === Context.CONDITION || (this.context === Context.LOG_MESSAGE && isPositionInCurlyBracketBlock(this.input)))) { suggestionsPromise = provideSuggestionItems(this.languageFeaturesService.completionProvider, underlyingModel, new Position(this.lineNumber, 1), new CompletionOptions(undefined, new Set<CompletionItemKind>().add(CompletionItemKind.Snippet)), _context, token).then(suggestions => { let overwriteBefore = 0; diff --git a/src/vs/workbench/contrib/debug/browser/callStackView.ts b/src/vs/workbench/contrib/debug/browser/callStackView.ts index 687732063d7..6a045d3ce23 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackView.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackView.ts @@ -3,50 +3,50 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { RunOnceScheduler } from 'vs/base/common/async'; import * as dom from 'vs/base/browser/dom'; -import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; -import { IDebugService, State, IStackFrame, IDebugSession, IThread, CONTEXT_CALLSTACK_ITEM_TYPE, IDebugModel, CALLSTACK_VIEW_ID, CONTEXT_DEBUG_STATE, getStateLabel, CONTEXT_STACK_FRAME_SUPPORTS_RESTART, CONTEXT_CALLSTACK_SESSION_IS_ATTACH, CONTEXT_CALLSTACK_ITEM_STOPPED, CONTEXT_CALLSTACK_SESSION_HAS_ONE_THREAD, IRawStoppedDetails } from 'vs/workbench/contrib/debug/common/debug'; -import { Thread, StackFrame, ThreadAndSessionIds } from 'vs/workbench/contrib/debug/common/debugModel'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; +import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; +import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; +import { ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree'; +import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; +import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; +import { IAsyncDataSource, ITreeContextMenuEvent, ITreeNode } from 'vs/base/browser/ui/tree/tree'; +import { Action, IAction } from 'vs/base/common/actions'; +import { RunOnceScheduler } from 'vs/base/common/async'; +import { Codicon } from 'vs/base/common/codicons'; +import { Event } from 'vs/base/common/event'; +import { createMatches, FuzzyScore, IMatch } from 'vs/base/common/filters'; +import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { posix } from 'vs/base/common/path'; +import { commonSuffixLength } from 'vs/base/common/strings'; +import { localize } from 'vs/nls'; +import { Icon } from 'vs/platform/action/common/action'; +import { createAndFillInActionBarActions, createAndFillInContextMenuActions, MenuEntryActionViewItem, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IMenuService, MenuId, MenuItemAction, MenuRegistry, registerAction2, SubmenuItemAction } from 'vs/platform/actions/common/actions'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ContextKeyExpr, ContextKeyExpression, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { MenuId, IMenu, IMenuService, MenuItemAction, SubmenuItemAction, registerAction2, MenuRegistry } from 'vs/platform/actions/common/actions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { renderViewTree } from 'vs/workbench/contrib/debug/browser/baseDebugView'; -import { IAction, Action } from 'vs/base/common/actions'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IContextKey, IContextKeyService, ContextKeyExpr, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; -import { ViewPane, ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { ILabelService } from 'vs/platform/label/common/label'; -import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; -import { createAndFillInContextMenuActions, createAndFillInActionBarActions, MenuEntryActionViewItem, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; -import { ITreeNode, ITreeContextMenuEvent, IAsyncDataSource } from 'vs/base/browser/ui/tree/tree'; import { WorkbenchCompressibleAsyncDataTree } from 'vs/platform/list/browser/listService'; -import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; -import { createMatches, FuzzyScore, IMatch } from 'vs/base/common/filters'; -import { Event } from 'vs/base/common/event'; -import { dispose, IDisposable } from 'vs/base/common/lifecycle'; -import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; -import { isSessionAttach } from 'vs/workbench/contrib/debug/common/debugUtils'; -import { STOP_ID, STOP_LABEL, DISCONNECT_ID, DISCONNECT_LABEL, RESTART_SESSION_ID, RESTART_LABEL, STEP_OVER_ID, STEP_OVER_LABEL, STEP_INTO_LABEL, STEP_INTO_ID, STEP_OUT_LABEL, STEP_OUT_ID, PAUSE_ID, PAUSE_LABEL, CONTINUE_ID, CONTINUE_LABEL } from 'vs/workbench/contrib/debug/browser/debugCommands'; -import { IViewDescriptorService } from 'vs/workbench/common/views'; -import { textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; -import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { INotificationService } from 'vs/platform/notification/common/notification'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; import { attachStylerCallback } from 'vs/platform/theme/common/styler'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { commonSuffixLength } from 'vs/base/common/strings'; -import { posix } from 'vs/base/common/path'; -import { ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree'; -import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; -import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; +import { 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 { IViewDescriptorService } from 'vs/workbench/common/views'; +import { renderViewTree } from 'vs/workbench/contrib/debug/browser/baseDebugView'; +import { CONTINUE_ID, CONTINUE_LABEL, DISCONNECT_ID, DISCONNECT_LABEL, PAUSE_ID, PAUSE_LABEL, RESTART_LABEL, RESTART_SESSION_ID, STEP_INTO_ID, STEP_INTO_LABEL, STEP_OUT_ID, STEP_OUT_LABEL, STEP_OVER_ID, STEP_OVER_LABEL, STOP_ID, STOP_LABEL } from 'vs/workbench/contrib/debug/browser/debugCommands'; import * as icons from 'vs/workbench/contrib/debug/browser/debugIcons'; -import { localize } from 'vs/nls'; -import { Codicon } from 'vs/base/common/codicons'; -import { Icon } from 'vs/platform/action/common/action'; +import { createDisconnectMenuItemAction } from 'vs/workbench/contrib/debug/browser/debugToolBar'; +import { CALLSTACK_VIEW_ID, CONTEXT_CALLSTACK_ITEM_STOPPED, CONTEXT_CALLSTACK_ITEM_TYPE, CONTEXT_CALLSTACK_SESSION_HAS_ONE_THREAD, CONTEXT_CALLSTACK_SESSION_IS_ATTACH, CONTEXT_DEBUG_STATE, CONTEXT_STACK_FRAME_SUPPORTS_RESTART, getStateLabel, IDebugModel, IDebugService, IDebugSession, IRawStoppedDetails, IStackFrame, IThread, State } from 'vs/workbench/contrib/debug/common/debug'; +import { StackFrame, Thread, ThreadAndSessionIds } from 'vs/workbench/contrib/debug/common/debugModel'; +import { isSessionAttach } from 'vs/workbench/contrib/debug/common/debugUtils'; const $ = dom.$; @@ -136,14 +136,9 @@ export class CallStackView extends ViewPane { private needsRefresh = false; private ignoreSelectionChangedEvent = false; private ignoreFocusStackFrameEvent = false; - private callStackItemType: IContextKey<string>; - private callStackSessionIsAttach: IContextKey<boolean>; - private callStackItemStopped: IContextKey<boolean>; - private stackFrameSupportsRestart: IContextKey<boolean>; - private sessionHasOneThread: IContextKey<boolean>; + private dataSource!: CallStackDataSource; private tree!: WorkbenchCompressibleAsyncDataTree<IDebugModel, CallStackItem, FuzzyScore>; - private menu: IMenu; private autoExpandedSessions = new Set<IDebugSession>(); private selectionNeedsUpdate = false; @@ -154,23 +149,14 @@ export class CallStackView extends ViewPane { @IKeybindingService keybindingService: IKeybindingService, @IInstantiationService instantiationService: IInstantiationService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, - @IEditorService private readonly editorService: IEditorService, @IConfigurationService configurationService: IConfigurationService, - @IMenuService menuService: IMenuService, @IContextKeyService contextKeyService: IContextKeyService, @IOpenerService openerService: IOpenerService, @IThemeService themeService: IThemeService, - @ITelemetryService telemetryService: ITelemetryService + @ITelemetryService telemetryService: ITelemetryService, + @IMenuService private readonly menuService: IMenuService, ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); - this.callStackItemType = CONTEXT_CALLSTACK_ITEM_TYPE.bindTo(contextKeyService); - this.callStackSessionIsAttach = CONTEXT_CALLSTACK_SESSION_IS_ATTACH.bindTo(contextKeyService); - this.stackFrameSupportsRestart = CONTEXT_STACK_FRAME_SUPPORTS_RESTART.bindTo(contextKeyService); - this.callStackItemStopped = CONTEXT_CALLSTACK_ITEM_STOPPED.bindTo(contextKeyService); - this.sessionHasOneThread = CONTEXT_CALLSTACK_SESSION_HAS_ONE_THREAD.bindTo(contextKeyService); - - this.menu = menuService.createMenu(MenuId.DebugCallStackContext, contextKeyService); - this._register(this.menu); // Create scheduler to prevent unnecessary flashing of tree when reacting to changes this.onCallStackChangeScheduler = this._register(new RunOnceScheduler(async () => { @@ -239,9 +225,9 @@ export class CallStackView extends ViewPane { this.dataSource = new CallStackDataSource(this.debugService); this.tree = <WorkbenchCompressibleAsyncDataTree<IDebugModel, CallStackItem, FuzzyScore>>this.instantiationService.createInstance(WorkbenchCompressibleAsyncDataTree, 'CallStackView', treeContainer, new CallStackDelegate(), new CallStackCompressionDelegate(this.debugService), [ - new SessionsRenderer(this.menu, this.callStackItemType, this.callStackSessionIsAttach, this.callStackItemStopped, this.sessionHasOneThread, this.instantiationService), - new ThreadsRenderer(this.menu, this.callStackItemType, this.callStackItemStopped), - this.instantiationService.createInstance(StackFramesRenderer, this.callStackItemType), + this.instantiationService.createInstance(SessionsRenderer), + this.instantiationService.createInstance(ThreadsRenderer), + this.instantiationService.createInstance(StackFramesRenderer), new ErrorsRenderer(), new LoadAllRenderer(this.themeService), new ShowMoreRenderer(this.themeService) @@ -299,10 +285,10 @@ export class CallStackView extends ViewPane { return; } - const focusStackFrame = (stackFrame: IStackFrame | undefined, thread: IThread | undefined, session: IDebugSession) => { + const focusStackFrame = (stackFrame: IStackFrame | undefined, thread: IThread | undefined, session: IDebugSession, options: { explicit?: boolean; preserveFocus?: boolean; sideBySide?: boolean; pinned?: boolean } = {}) => { this.ignoreFocusStackFrameEvent = true; try { - this.debugService.focusStackFrame(stackFrame, thread, session, true); + this.debugService.focusStackFrame(stackFrame, thread, session, { ...options, ...{ explicit: true } }); } finally { this.ignoreFocusStackFrameEvent = false; } @@ -310,8 +296,12 @@ export class CallStackView extends ViewPane { const element = e.element; if (element instanceof StackFrame) { - focusStackFrame(element, element.thread, element.thread.session); - element.openInEditor(this.editorService, e.editorOptions.preserveFocus, e.sideBySide, e.editorOptions.pinned); + const opts = { + preserveFocus: e.editorOptions.preserveFocus, + sideBySide: e.sideBySide, + pinned: e.editorOptions.pinned + }; + focusStackFrame(element, element.thread, element.thread.session, opts); } if (element instanceof Thread) { focusStackFrame(undefined, element, element.session); @@ -455,22 +445,21 @@ export class CallStackView extends ViewPane { private onContextMenu(e: ITreeContextMenuEvent<CallStackItem>): void { const element = e.element; - this.stackFrameSupportsRestart.reset(); + let overlay: [string, any][] = []; if (isDebugSession(element)) { - this.callStackItemType.set('session'); + overlay = getSessionContextOverlay(element); } else if (element instanceof Thread) { - this.callStackItemType.set('thread'); + overlay = getThreadContextOverlay(element); } else if (element instanceof StackFrame) { - this.callStackItemType.set('stackFrame'); - this.stackFrameSupportsRestart.set(element.canRestart); - } else { - this.callStackItemType.reset(); + overlay = getStackFrameContextOverlay(element); } const primary: IAction[] = []; const secondary: IAction[] = []; const result = { primary, secondary }; - const actionsDisposable = createAndFillInContextMenuActions(this.menu, { arg: getContextForContributedActions(element), shouldForwardArgs: true }, result, 'inline'); + const contextKeyService = this.contextKeyService.createOverlay(overlay); + const menu = this.menuService.createMenu(MenuId.DebugCallStackContext, contextKeyService); + const actionsDisposable = createAndFillInContextMenuActions(menu, { arg: getContextForContributedActions(element), shouldForwardArgs: true }, result, 'inline'); this.contextMenuService.showContextMenu({ getAnchor: () => e.anchor, @@ -487,7 +476,8 @@ interface IThreadTemplateData { stateLabel: HTMLSpanElement; label: HighlightedLabel; actionBar: ActionBar; - elementDisposable: IDisposable[]; + elementDisposable: DisposableStore; + templateDisposable: IDisposable; } interface ISessionTemplateData { @@ -496,7 +486,8 @@ interface ISessionTemplateData { stateLabel: HTMLSpanElement; label: HighlightedLabel; actionBar: ActionBar; - elementDisposable: IDisposable[]; + elementDisposable: DisposableStore; + templateDisposable: IDisposable; } interface IErrorTemplateData { @@ -515,18 +506,25 @@ interface IStackFrameTemplateData { lineNumber: HTMLElement; label: HighlightedLabel; actionBar: ActionBar; + templateDisposable: IDisposable; +} + +function getSessionContextOverlay(session: IDebugSession): [string, any][] { + return [ + [CONTEXT_CALLSTACK_ITEM_TYPE.key, 'session'], + [CONTEXT_CALLSTACK_SESSION_IS_ATTACH.key, isSessionAttach(session)], + [CONTEXT_CALLSTACK_ITEM_STOPPED.key, session.state === State.Stopped], + [CONTEXT_CALLSTACK_SESSION_HAS_ONE_THREAD.key, session.getAllThreads().length === 1], + ]; } class SessionsRenderer implements ICompressibleTreeRenderer<IDebugSession, FuzzyScore, ISessionTemplateData> { static readonly ID = 'session'; constructor( - private menu: IMenu, - private callStackItemType: IContextKey<string>, - private callStackSessionIsAttach: IContextKey<boolean>, - private callStackItemStopped: IContextKey<boolean>, - private sessionHasOneThread: IContextKey<boolean>, - private readonly instantiationService: IInstantiationService + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IMenuService private readonly menuService: IMenuService, ) { } get templateId(): string { @@ -539,8 +537,19 @@ class SessionsRenderer implements ICompressibleTreeRenderer<IDebugSession, Fuzzy const name = dom.append(session, $('.name')); const stateLabel = dom.append(session, $('span.state.label.monaco-count-badge.long')); const label = new HighlightedLabel(name); - const actionBar = new ActionBar(session, { + const templateDisposable = new DisposableStore(); + + const stopActionViewItemDisposables = templateDisposable.add(new DisposableStore()); + const actionBar = templateDisposable.add(new ActionBar(session, { actionViewItemProvider: action => { + if ((action.id === STOP_ID || action.id === DISCONNECT_ID) && action instanceof MenuItemAction) { + stopActionViewItemDisposables.clear(); + const item = this.instantiationService.invokeFunction(accessor => createDisconnectMenuItemAction(action as MenuItemAction, stopActionViewItemDisposables, accessor)); + if (item) { + return item; + } + } + if (action instanceof MenuItemAction) { return this.instantiationService.createInstance(MenuEntryActionViewItem, action, undefined); } else if (action instanceof SubmenuItemAction) { @@ -549,9 +558,10 @@ class SessionsRenderer implements ICompressibleTreeRenderer<IDebugSession, Fuzzy return undefined; } - }); + })); - return { session, name, stateLabel, label, actionBar, elementDisposable: [] }; + const elementDisposable = templateDisposable.add(new DisposableStore()); + return { session, name, stateLabel, label, actionBar, elementDisposable, templateDisposable }; } renderElement(element: ITreeNode<IDebugSession, FuzzyScore>, _: number, data: ISessionTemplateData): void { @@ -569,20 +579,28 @@ class SessionsRenderer implements ICompressibleTreeRenderer<IDebugSession, Fuzzy data.label.set(session.getLabel(), matches); const stoppedDetails = session.getStoppedDetails(); const thread = session.getAllThreads().find(t => t.stopped); - const primary: IAction[] = []; - const secondary: IAction[] = []; - const result = { primary, secondary }; - this.callStackItemType.set('session'); - this.callStackItemStopped.set(session.state === State.Stopped); - this.sessionHasOneThread.set(session.getAllThreads().length === 1); - this.callStackSessionIsAttach.set(isSessionAttach(session)); - data.elementDisposable.push(createAndFillInActionBarActions(this.menu, { arg: getContextForContributedActions(session), shouldForwardArgs: true }, result, 'inline')); - data.actionBar.clear(); - data.actionBar.push(primary, { icon: true, label: false }); - // We need to set our internal context on the action bar, since our commands depend on that one - // While the external context our extensions rely on - data.actionBar.context = getContext(session); + const contextKeyService = this.contextKeyService.createOverlay(getSessionContextOverlay(session)); + const menu = data.elementDisposable.add(this.menuService.createMenu(MenuId.DebugCallStackContext, contextKeyService)); + + const menuDisposables = data.elementDisposable.add(new DisposableStore()); + const setupActionBar = () => { + menuDisposables.clear(); + data.actionBar.clear(); + + const primary: IAction[] = []; + const secondary: IAction[] = []; + const result = { primary, secondary }; + + menuDisposables.add(createAndFillInActionBarActions(menu, { arg: getContextForContributedActions(session), shouldForwardArgs: true }, result, 'inline')); + data.actionBar.push(primary, { icon: true, label: false }); + // We need to set our internal context on the action bar, since our commands depend on that one + // While the external context our extensions rely on + data.actionBar.context = getContext(session); + }; + data.elementDisposable.add(menu.onDidChange(() => setupActionBar())); + setupActionBar(); + data.stateLabel.style.display = ''; if (stoppedDetails) { @@ -600,21 +618,27 @@ class SessionsRenderer implements ICompressibleTreeRenderer<IDebugSession, Fuzzy } disposeTemplate(templateData: ISessionTemplateData): void { - templateData.actionBar.dispose(); + templateData.templateDisposable.dispose(); } disposeElement(_element: ITreeNode<IDebugSession, FuzzyScore>, _: number, templateData: ISessionTemplateData): void { - dispose(templateData.elementDisposable); + templateData.elementDisposable.clear(); } } +function getThreadContextOverlay(thread: IThread): [string, any][] { + return [ + [CONTEXT_CALLSTACK_ITEM_TYPE.key, 'thread'], + [CONTEXT_CALLSTACK_ITEM_STOPPED.key, thread.stopped] + ]; +} + class ThreadsRenderer implements ICompressibleTreeRenderer<IThread, FuzzyScore, IThreadTemplateData> { static readonly ID = 'thread'; constructor( - private menu: IMenu, - private callStackItemType: IContextKey<string>, - private callStackItemStopped: IContextKey<boolean> + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IMenuService private readonly menuService: IMenuService, ) { } get templateId(): string { @@ -626,10 +650,13 @@ class ThreadsRenderer implements ICompressibleTreeRenderer<IThread, FuzzyScore, const name = dom.append(thread, $('.name')); const stateLabel = dom.append(thread, $('span.state.label.monaco-count-badge.long')); const label = new HighlightedLabel(name); - const actionBar = new ActionBar(thread); - const elementDisposable: IDisposable[] = []; - return { thread, name, stateLabel, label, actionBar, elementDisposable }; + const templateDisposable = new DisposableStore(); + + const actionBar = templateDisposable.add(new ActionBar(thread)); + const elementDisposable = templateDisposable.add(new DisposableStore()); + + return { thread, name, stateLabel, label, actionBar, elementDisposable, templateDisposable }; } renderElement(element: ITreeNode<IThread, FuzzyScore>, _index: number, data: IThreadTemplateData): void { @@ -639,13 +666,26 @@ class ThreadsRenderer implements ICompressibleTreeRenderer<IThread, FuzzyScore, data.stateLabel.textContent = thread.stateLabel; data.stateLabel.classList.toggle('exception', thread.stoppedDetails?.reason === 'exception'); - data.actionBar.clear(); - this.callStackItemType.set('thread'); - this.callStackItemStopped.set(thread.stopped); - const primary: IAction[] = []; - const result = { primary, secondary: [] }; - data.elementDisposable.push(createAndFillInActionBarActions(this.menu, { arg: getContextForContributedActions(thread), shouldForwardArgs: true }, result, 'inline')); - data.actionBar.push(primary, { icon: true, label: false }); + const contextKeyService = this.contextKeyService.createOverlay(getThreadContextOverlay(thread)); + const menu = data.elementDisposable.add(this.menuService.createMenu(MenuId.DebugCallStackContext, contextKeyService)); + + const menuDisposables = data.elementDisposable.add(new DisposableStore()); + const setupActionBar = () => { + menuDisposables.clear(); + data.actionBar.clear(); + + const primary: IAction[] = []; + const secondary: IAction[] = []; + const result = { primary, secondary }; + + menuDisposables.add(createAndFillInActionBarActions(menu, { arg: getContextForContributedActions(thread), shouldForwardArgs: true }, result, 'inline')); + data.actionBar.push(primary, { icon: true, label: false }); + // We need to set our internal context on the action bar, since our commands depend on that one + // While the external context our extensions rely on + data.actionBar.context = getContext(thread); + }; + data.elementDisposable.add(menu.onDidChange(() => setupActionBar())); + setupActionBar(); } renderCompressedElements(_node: ITreeNode<ICompressedTreeNode<IThread>, FuzzyScore>, _index: number, _templateData: IThreadTemplateData, _height: number | undefined): void { @@ -653,19 +693,25 @@ class ThreadsRenderer implements ICompressibleTreeRenderer<IThread, FuzzyScore, } disposeElement(_element: any, _index: number, templateData: IThreadTemplateData): void { - dispose(templateData.elementDisposable); + templateData.elementDisposable.clear(); } disposeTemplate(templateData: IThreadTemplateData): void { - templateData.actionBar.dispose(); + templateData.templateDisposable.dispose(); } } +function getStackFrameContextOverlay(stackFrame: IStackFrame): [string, any][] { + return [ + [CONTEXT_CALLSTACK_ITEM_TYPE.key, 'stackFrame'], + [CONTEXT_STACK_FRAME_SUPPORTS_RESTART.key, stackFrame.canRestart] + ]; +} + class StackFramesRenderer implements ICompressibleTreeRenderer<IStackFrame, FuzzyScore, IStackFrameTemplateData> { static readonly ID = 'stackFrame'; constructor( - private callStackItemType: IContextKey<string>, @ILabelService private readonly labelService: ILabelService, @INotificationService private readonly notificationService: INotificationService, ) { } @@ -682,9 +728,11 @@ class StackFramesRenderer implements ICompressibleTreeRenderer<IStackFrame, Fuzz const wrapper = dom.append(file, $('span.line-number-wrapper')); const lineNumber = dom.append(wrapper, $('span.line-number.monaco-count-badge')); const label = new HighlightedLabel(labelDiv); - const actionBar = new ActionBar(stackFrame); - return { file, fileName, label, lineNumber, stackFrame, actionBar }; + const templateDisposable = new DisposableStore(); + const actionBar = templateDisposable.add(new ActionBar(stackFrame)); + + return { file, fileName, label, lineNumber, stackFrame, actionBar, templateDisposable }; } renderElement(element: ITreeNode<IStackFrame, FuzzyScore>, index: number, data: IStackFrameTemplateData): void { @@ -712,7 +760,6 @@ class StackFramesRenderer implements ICompressibleTreeRenderer<IStackFrame, Fuzz } data.actionBar.clear(); - this.callStackItemType.set('stackFrame'); if (hasActions) { const action = new Action('debug.callStack.restartFrame', localize('restartFrame', "Restart Frame"), ThemeIcon.asClassName(icons.debugRestartFrame), true, async () => { try { diff --git a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts index 11f4ef0ce63..d595b7768d1 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_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, } 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 } 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 } 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'; @@ -30,7 +30,7 @@ import { DebugStatusContribution } from 'vs/workbench/contrib/debug/browser/debu import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { launchSchemaId } from 'vs/workbench/services/configuration/common/configuration'; import { LoadedScriptsView } from 'vs/workbench/contrib/debug/browser/loadedScriptsView'; -import { RunToCursorAction } from 'vs/workbench/contrib/debug/browser/debugEditorActions'; +import { RunToCursorAction, SelectionToReplAction, SelectionToWatchExpressionsAction } from 'vs/workbench/contrib/debug/browser/debugEditorActions'; import { WatchExpressionsView, ADD_WATCH_LABEL, REMOVE_WATCH_EXPRESSIONS_COMMAND_ID, REMOVE_WATCH_EXPRESSIONS_LABEL, ADD_WATCH_ID } from 'vs/workbench/contrib/debug/browser/watchExpressionsView'; import { VariablesView, SET_VARIABLE_ID, COPY_VALUE_ID, BREAK_WHEN_VALUE_CHANGES_ID, COPY_EVALUATE_PATH_ID, ADD_TO_WATCH_ID, BREAK_WHEN_VALUE_IS_ACCESSED_ID, BREAK_WHEN_VALUE_IS_READ_ID, VIEW_MEMORY_ID } from 'vs/workbench/contrib/debug/browser/variablesView'; import { Repl } from 'vs/workbench/contrib/debug/browser/repl'; @@ -56,6 +56,7 @@ import { EditorPaneDescriptor, IEditorPaneRegistry } from 'vs/workbench/browser/ import { DisassemblyViewInput } from 'vs/workbench/contrib/debug/common/disassemblyViewInput'; import { DebugLifecycle } from 'vs/workbench/contrib/debug/common/debugLifecycle'; import { Icon } from 'vs/platform/action/common/action'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; const debugCategory = nls.localize('debugCategory', "Debug"); registerColors(); @@ -106,12 +107,15 @@ registerDebugCommandPaletteItem(STEP_INTO_ID, STEP_INTO_LABEL, CONTEXT_IN_DEBUG_ 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)); +registerDebugCommandPaletteItem(DISCONNECT_AND_SUSPEND_ID, DISCONNECT_AND_SUSPEND_LABEL, CONTEXT_IN_DEBUG_MODE, ContextKeyExpr.or(CONTEXT_FOCUSED_SESSION_IS_ATTACH, ContextKeyExpr.and(CONTEXT_SUSPEND_DEBUGGEE_SUPPORTED, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED))); registerDebugCommandPaletteItem(STOP_ID, STOP_LABEL, CONTEXT_IN_DEBUG_MODE, ContextKeyExpr.or(CONTEXT_FOCUSED_SESSION_IS_ATTACH.toNegated(), CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED)); registerDebugCommandPaletteItem(CONTINUE_ID, CONTINUE_LABEL, CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); registerDebugCommandPaletteItem(FOCUS_REPL_ID, nls.localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'debugFocusConsole' }, 'Focus on Debug Console View')); registerDebugCommandPaletteItem(JUMP_TO_CURSOR_ID, nls.localize('jumpToCursor', "Jump to Cursor"), CONTEXT_JUMP_TO_CURSOR_SUPPORTED); registerDebugCommandPaletteItem(JUMP_TO_CURSOR_ID, nls.localize('SetNextStatement', "Set Next Statement"), CONTEXT_JUMP_TO_CURSOR_SUPPORTED); registerDebugCommandPaletteItem(RunToCursorAction.ID, RunToCursorAction.LABEL, ContextKeyExpr.and(CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE.isEqualTo('stopped'))); +registerDebugCommandPaletteItem(SelectionToReplAction.ID, SelectionToReplAction.LABEL, ContextKeyExpr.and(EditorContextKeys.hasNonEmptySelection, CONTEXT_IN_DEBUG_MODE)); +registerDebugCommandPaletteItem(SelectionToWatchExpressionsAction.ID, SelectionToWatchExpressionsAction.LABEL, ContextKeyExpr.and(EditorContextKeys.hasNonEmptySelection, CONTEXT_IN_DEBUG_MODE)); registerDebugCommandPaletteItem(TOGGLE_INLINE_BREAKPOINT_ID, nls.localize('inlineBreakpoint', "Inline Breakpoint")); 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)))); @@ -135,6 +139,7 @@ const registerDebugViewMenuItem = (menuId: MenuId, id: string, title: string, or }; registerDebugViewMenuItem(MenuId.DebugCallStackContext, RESTART_SESSION_ID, RESTART_LABEL, 10, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('session'), undefined, '3_modification'); registerDebugViewMenuItem(MenuId.DebugCallStackContext, DISCONNECT_ID, DISCONNECT_LABEL, 20, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('session'), undefined, '3_modification'); +registerDebugViewMenuItem(MenuId.DebugCallStackContext, DISCONNECT_AND_SUSPEND_ID, DISCONNECT_AND_SUSPEND_LABEL, 21, ContextKeyExpr.and(CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('session'), CONTEXT_SUSPEND_DEBUGGEE_SUPPORTED, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED), undefined, '3_modification'); registerDebugViewMenuItem(MenuId.DebugCallStackContext, STOP_ID, STOP_LABEL, 30, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('session'), undefined, '3_modification'); registerDebugViewMenuItem(MenuId.DebugCallStackContext, PAUSE_ID, PAUSE_LABEL, 10, ContextKeyExpr.and(CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('thread'), CONTEXT_DEBUG_STATE.isEqualTo('running'))); registerDebugViewMenuItem(MenuId.DebugCallStackContext, CONTINUE_ID, CONTINUE_LABEL, 10, ContextKeyExpr.and(CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('thread'), CONTEXT_DEBUG_STATE.isEqualTo('stopped'))); @@ -418,8 +423,8 @@ configurationRegistry.registerConfiguration({ default: false }, 'debug.inlineValues': { - type: ['boolean', 'string'], - 'enum': [true, false, 'auto'], + type: 'string', + 'enum': ['on', 'off', 'auto'], description: nls.localize({ comment: ['This is the description for a setting'], key: 'inlineValues' }, "Show variable values inline in editor while debugging."), 'enumDescriptions': [ nls.localize('inlineValues.on', 'Always show variable values inline in editor while debugging.'), @@ -506,6 +511,11 @@ configurationRegistry.registerConfiguration({ description: nls.localize('debug.focusWindowOnBreak', "Controls whether the workbench window should be focused when the debugger breaks."), default: true }, + 'debug.focusEditorOnBreak': { + type: 'boolean', + description: nls.localize('debug.focusEditorOnBreak', "Controls whether the editor should be focused when the debugger breaks."), + default: true + }, 'debug.onTaskErrors': { enum: ['debugAnyway', 'showErrors', 'prompt', 'abort'], enumDescriptions: [nls.localize('debugAnyway', "Ignore task errors and start debugging."), nls.localize('showErrors', "Show the Problems view and do not start debugging."), nls.localize('prompt', "Prompt user."), nls.localize('cancel', "Cancel debugging.")], @@ -548,5 +558,10 @@ configurationRegistry.registerConfiguration({ default: true, description: nls.localize('debug.disassemblyView.showSourceCode', "Show Source Code in Disassembly View.") }, + 'debug.autoExpandLazyVariables': { + type: 'boolean', + default: false, + description: nls.localize('debug.autoExpandLazyVariables', "Automatically show values for variables that are lazily resolved by the debugger, such as getters.") + } } }); diff --git a/src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts b/src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts index 2a01435a6f5..24c64bc9f81 100644 --- a/src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts +++ b/src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts @@ -10,8 +10,8 @@ import Severity from 'vs/base/common/severity'; import * as strings from 'vs/base/common/strings'; import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorModel } from 'vs/editor/common/editorCommon'; -import { ITextModel } from 'vs/editor/common/model'; import { ILanguageService } from 'vs/editor/common/languages/language'; +import { ITextModel } from 'vs/editor/common/model'; import * as nls from 'vs/nls'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -22,6 +22,7 @@ import { Extensions as JSONExtensions, IJSONContributionRegistry } from 'vs/plat import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { Breakpoints } from 'vs/workbench/contrib/debug/common/breakpoints'; import { CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DEBUG_EXTENSION_AVAILABLE, IAdapterDescriptor, IAdapterManager, IConfig, IDebugAdapter, IDebugAdapterDescriptorFactory, IDebugAdapterFactory, IDebugConfiguration, IDebugSession, INTERNAL_CONSOLE_OPTIONS_SCHEMA } from 'vs/workbench/contrib/debug/common/debug'; import { Debugger } from 'vs/workbench/contrib/debug/common/debugger'; import { breakpointsExtPoint, debuggersExtPoint, launchSchema, presentationSchema } from 'vs/workbench/contrib/debug/common/debugSchemas'; @@ -42,7 +43,7 @@ export class AdapterManager extends Disposable implements IAdapterManager { private debugExtensionsAvailable: IContextKey<boolean>; private readonly _onDidRegisterDebugger = new Emitter<void>(); private readonly _onDidDebuggersExtPointRead = new Emitter<void>(); - private breakpointLanguageIdsSet = new Set<string>(); + private breakpointContributions: Breakpoints[] = []; private debuggerWhenKeys = new Set<string>(); constructor( @@ -116,13 +117,8 @@ export class AdapterManager extends Disposable implements IAdapterManager { this._onDidDebuggersExtPointRead.fire(); }); - breakpointsExtPoint.setHandler((extensions, delta) => { - delta.removed.forEach(removed => { - removed.value.forEach(breakpoints => this.breakpointLanguageIdsSet.delete(breakpoints.language)); - }); - delta.added.forEach(added => { - added.value.forEach(breakpoints => this.breakpointLanguageIdsSet.add(breakpoints.language)); - }); + breakpointsExtPoint.setHandler(extensions => { + this.breakpointContributions = extensions.flatMap(ext => ext.value.map(breakpoint => this.instantiationService.createInstance(Breakpoints, breakpoint))); }); } @@ -281,7 +277,7 @@ export class AdapterManager extends Disposable implements IAdapterManager { return true; } - return this.breakpointLanguageIdsSet.has(languageId); + return this.breakpointContributions.some(breakpoints => breakpoints.language === languageId && breakpoints.enabled); } getDebugger(type: string): Debugger | undefined { diff --git a/src/vs/workbench/contrib/debug/browser/debugColors.ts b/src/vs/workbench/contrib/debug/browser/debugColors.ts index affb86d4e98..3ebe8126973 100644 --- a/src/vs/workbench/contrib/debug/browser/debugColors.ts +++ b/src/vs/workbench/contrib/debug/browser/debugColors.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { registerColor, foreground, editorInfoForeground, editorWarningForeground, errorForeground, badgeBackground, badgeForeground, listDeemphasizedForeground, contrastBorder, inputBorder } from 'vs/platform/theme/common/colorRegistry'; +import { registerColor, foreground, editorInfoForeground, editorWarningForeground, errorForeground, badgeBackground, badgeForeground, listDeemphasizedForeground, contrastBorder, inputBorder, toolbarHoverBackground } from 'vs/platform/theme/common/colorRegistry'; import { registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { Color } from 'vs/base/common/color'; import { localize } from 'vs/nls'; @@ -125,6 +125,7 @@ export function registerColors() { const debugViewStateLabelForegroundColor = theme.getColor(debugViewStateLabelForeground)!; const debugViewStateLabelBackgroundColor = theme.getColor(debugViewStateLabelBackground)!; const debugViewValueChangedHighlightColor = theme.getColor(debugViewValueChangedHighlight)!; + const toolbarHoverBackgroundColor = theme.getColor(toolbarHoverBackground); collector.addRule(` /* Text colour of the call stack row's filename */ @@ -182,6 +183,10 @@ export function registerColors() { animation-duration: 1s; animation-fill-mode: forwards; } + + .monaco-list-row .expression .lazy-button:hover { + background-color: ${toolbarHoverBackgroundColor} + } `); const contrastBorderColor = theme.getColor(contrastBorder); diff --git a/src/vs/workbench/contrib/debug/browser/debugCommands.ts b/src/vs/workbench/contrib/debug/browser/debugCommands.ts index 4a022554117..3042792f8e8 100644 --- a/src/vs/workbench/contrib/debug/browser/debugCommands.ts +++ b/src/vs/workbench/contrib/debug/browser/debugCommands.ts @@ -44,6 +44,7 @@ export const STEP_INTO_ID = 'workbench.action.debug.stepInto'; 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'; +export const DISCONNECT_AND_SUSPEND_ID = 'workbench.action.debug.disconnectAndSuspend'; export const STOP_ID = 'workbench.action.debug.stop'; export const RESTART_FRAME_ID = 'workbench.action.debug.restartFrame'; export const CONTINUE_ID = 'workbench.action.debug.continue'; @@ -64,6 +65,7 @@ export const STEP_INTO_LABEL = nls.localize('stepIntoDebug', "Step Into"); 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"); +export const DISCONNECT_AND_SUSPEND_LABEL = nls.localize('disconnectSuspend', "Disconnect and Suspend"); export const STOP_LABEL = nls.localize('stop', "Stop"); export const CONTINUE_LABEL = nls.localize('continueDebug', "Continue"); export const FOCUS_SESSION_LABEL = nls.localize('focusSession', "Focus Session"); @@ -313,7 +315,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } }); -async function stopHandler(accessor: ServicesAccessor, _: string, context: CallStackContext | unknown, disconnect: boolean): Promise<void> { +async function stopHandler(accessor: ServicesAccessor, _: string, context: CallStackContext | unknown, disconnect: boolean, suspend?: boolean): Promise<void> { const debugService = accessor.get(IDebugService); let session: IDebugSession | undefined; if (isSessionContext(context)) { @@ -329,7 +331,7 @@ async function stopHandler(accessor: ServicesAccessor, _: string, context: CallS session = session.parentSession; } - await debugService.stopSession(session, disconnect); + await debugService.stopSession(session, disconnect, suspend); } KeybindingsRegistry.registerCommandAndKeybindingRule({ @@ -340,6 +342,11 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ handler: (accessor, _, context) => stopHandler(accessor, _, context, true) }); +CommandsRegistry.registerCommand({ + id: DISCONNECT_AND_SUSPEND_ID, + handler: (accessor, _, context) => stopHandler(accessor, _, context, true, true) +}); + KeybindingsRegistry.registerCommandAndKeybindingRule({ id: STOP_ID, weight: KeybindingWeight.WorkbenchContrib, @@ -399,7 +406,7 @@ CommandsRegistry.registerCommand({ if (stoppedChildSession && session.state !== State.Stopped) { session = stoppedChildSession; } - await debugService.focusStackFrame(undefined, undefined, session, true); + await debugService.focusStackFrame(undefined, undefined, session, { explicit: true }); const stackFrame = debugService.getViewModel().focusedStackFrame; if (stackFrame) { await stackFrame.openInEditor(editorService, true); diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts b/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts index bcb3748c6a1..4264520e56a 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts @@ -234,13 +234,16 @@ export class RunToCursorAction extends EditorAction { } } -class SelectionToReplAction extends EditorAction { +export class SelectionToReplAction extends EditorAction { + + public static readonly ID = 'editor.debug.action.selectionToRepl'; + public static readonly LABEL = nls.localize('evaluateInDebugConsole', "Evaluate in Debug Console"); constructor() { super({ - id: 'editor.debug.action.selectionToRepl', - label: nls.localize('evaluateInDebugConsole', "Evaluate in Debug Console"), - alias: 'Evaluate', + id: SelectionToReplAction.ID, + label: SelectionToReplAction.LABEL, + alias: 'Debug: Evaluate in Console', precondition: ContextKeyExpr.and(EditorContextKeys.hasNonEmptySelection, CONTEXT_IN_DEBUG_MODE, EditorContextKeys.editorTextFocus), contextMenuOpts: { group: 'debug', @@ -264,13 +267,16 @@ class SelectionToReplAction extends EditorAction { } } -class SelectionToWatchExpressionsAction extends EditorAction { +export class SelectionToWatchExpressionsAction extends EditorAction { + + public static readonly ID = 'editor.debug.action.selectionToWatch'; + public static readonly LABEL = nls.localize('addToWatch', "Add to Watch"); constructor() { super({ - id: 'editor.debug.action.selectionToWatch', - label: nls.localize('addToWatch', "Add to Watch"), - alias: 'Add to Watch', + id: SelectionToWatchExpressionsAction.ID, + label: SelectionToWatchExpressionsAction.LABEL, + alias: 'Debug: Add to Watch', precondition: ContextKeyExpr.and(EditorContextKeys.hasNonEmptySelection, CONTEXT_IN_DEBUG_MODE, EditorContextKeys.editorTextFocus), contextMenuOpts: { group: 'debug', diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts index 81133f8fade..848242efad6 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts @@ -179,8 +179,8 @@ function getWordToLineNumbersMap(model: ITextModel | null): Map<string, number[] continue; } - model.forceTokenization(lineNumber); - const lineTokens = model.getLineTokens(lineNumber); + model.tokenization.forceTokenization(lineNumber); + const lineTokens = model.tokenization.getLineTokens(lineNumber); for (let tokenIndex = 0, tokenCount = lineTokens.getCount(); tokenIndex < tokenCount; tokenIndex++) { const tokenType = lineTokens.getStandardTokenType(tokenIndex); @@ -627,7 +627,7 @@ export class DebugEditorContribution implements IDebugEditorContribution { const model = this.editor.getModel(); const inlineValuesSetting = this.configurationService.getValue<IDebugConfiguration>('debug').inlineValues; - const inlineValuesTurnedOn = inlineValuesSetting === true || (inlineValuesSetting === 'auto' && model && this.languageFeaturesService.inlineValuesProvider.has(model)); + const inlineValuesTurnedOn = inlineValuesSetting === true || inlineValuesSetting === 'on' || (inlineValuesSetting === 'auto' && model && this.languageFeaturesService.inlineValuesProvider.has(model)); if (!inlineValuesTurnedOn || !model || !stackFrame || model.uri.toString() !== stackFrame.source.uri.toString()) { if (!this.removeInlineValuesScheduler.isScheduled()) { this.removeInlineValuesScheduler.schedule(); diff --git a/src/vs/workbench/contrib/debug/browser/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index 39b73ddbc29..224b72fe4fa 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -801,9 +801,9 @@ export class DebugService implements IDebugService { }); } - async stopSession(session: IDebugSession | undefined, disconnect = false): Promise<any> { + async stopSession(session: IDebugSession | undefined, disconnect = false, suspend = false): Promise<any> { if (session) { - return disconnect ? session.disconnect() : session.terminate(); + return disconnect ? session.disconnect(undefined, suspend) : session.terminate(); } const sessions = this.model.getSessions(); @@ -815,7 +815,7 @@ export class DebugService implements IDebugService { this.cancelTokens(undefined); } - return Promise.all(sessions.map(s => disconnect ? s.disconnect() : s.terminate())); + return Promise.all(sessions.map(s => disconnect ? s.disconnect(undefined, suspend) : s.terminate())); } private async substituteVariables(launch: ILaunch | undefined, config: IConfig): Promise<IConfig | undefined> { @@ -852,11 +852,11 @@ export class DebugService implements IDebugService { //---- focus management - async focusStackFrame(_stackFrame: IStackFrame | undefined, _thread?: IThread, _session?: IDebugSession, explicit?: boolean): Promise<void> { + async focusStackFrame(_stackFrame: IStackFrame | undefined, _thread?: IThread, _session?: IDebugSession, options?: { explicit?: boolean; preserveFocus?: boolean; sideBySide?: boolean; pinned?: boolean }): Promise<void> { const { stackFrame, thread, session } = getStackFrameThreadAndSessionToFocus(this.model, _stackFrame, _thread, _session); if (stackFrame) { - const editor = await stackFrame.openInEditor(this.editorService, true); + const editor = await stackFrame.openInEditor(this.editorService, options?.preserveFocus ?? true, options?.sideBySide, options?.pinned); if (editor) { if (editor.input === DisassemblyViewInput.instance) { // Go to address is invoked via setFocus @@ -880,7 +880,7 @@ export class DebugService implements IDebugService { this.debugType.reset(); } - this.viewModel.setFocus(stackFrame, thread, session, !!explicit); + this.viewModel.setFocus(stackFrame, thread, session, !!options?.explicit); } //---- watches diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index 6d7ec022e42..3ff60d94d24 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -172,6 +172,11 @@ export class DebugSession implements IDebugSession { return this._options.debugUI?.simple ?? false; } + get autoExpandLazyVariables(): boolean { + // This tiny helper avoids converting the entire debug model to use service injection + return this.configurationService.getValue<IDebugConfiguration>('debug').autoExpandLazyVariables; + } + setConfiguration(configuration: { resolved: IConfig; unresolved: IConfig | undefined }) { this._configuration = configuration; } @@ -350,7 +355,7 @@ export class DebugSession implements IDebugSession { /** * end the current debug adapter session */ - async disconnect(restart = false): Promise<void> { + async disconnect(restart = false, suspend = false): Promise<void> { if (!this.raw) { // Adapter went down but it did not send a 'terminated' event, simulate like the event has been sent this.onDidExitAdapter(); @@ -358,9 +363,10 @@ export class DebugSession implements IDebugSession { this.cancelAllRequests(); if (this._options.lifecycleManagedByParent && this.parentSession) { - await this.parentSession.disconnect(restart); + await this.parentSession.disconnect(restart, suspend); } else if (this.raw) { - await this.raw.disconnect({ restart, terminateDebuggee: false }); + // TODO terminateDebuggee should be undefined by default? + await this.raw.disconnect({ restart, terminateDebuggee: false, suspendDebuggee: suspend }); } if (!restart) { @@ -957,7 +963,8 @@ export class DebugSession implements IDebugSession { const focusedStackFrame = this.debugService.getViewModel().focusedStackFrame; if (!focusedStackFrame || focusedStackFrame.thread.session === this) { // Only take focus if nothing is focused, or if the focus is already on the current session - await this.debugService.focusStackFrame(undefined, thread); + const preserveFocus = !this.configurationService.getValue<IDebugConfiguration>('debug').focusEditorOnBreak; + await this.debugService.focusStackFrame(undefined, thread, undefined, { preserveFocus }); } if (thread.stoppedDetails) { @@ -1003,7 +1010,7 @@ export class DebugSession implements IDebugSession { this.passFocusScheduler.cancel(); if (focusedThread && event.body.threadId === focusedThread.threadId) { // De-focus the thread in case it was focused - this.debugService.focusStackFrame(undefined, undefined, viewModel.focusedSession, false); + this.debugService.focusStackFrame(undefined, undefined, viewModel.focusedSession, { explicit: false }); } } })); @@ -1070,7 +1077,7 @@ export class DebugSession implements IDebugSession { // only log telemetry events from debug adapter if the debug extension provided the telemetry key // and the user opted in telemetry const telemetryEndpoint = this.raw.dbgr.getCustomTelemetryEndpoint(); - if (telemetryEndpoint && this.telemetryService.telemetryLevel !== TelemetryLevel.NONE) { + if (telemetryEndpoint && this.telemetryService.telemetryLevel.value !== TelemetryLevel.NONE) { // __GDPR__TODO__ We're sending events in the name of the debug extension and we can not ensure that those are declared correctly. let data = event.body.data; if (!telemetryEndpoint.sendErrorTelemetry && event.body.data) { diff --git a/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts b/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts index 4c0b2af2ea0..4667fc38a3e 100644 --- a/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts +++ b/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts @@ -15,9 +15,12 @@ import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { withUndefinedAsNull } from 'vs/base/common/types'; import { IMarkerService, MarkerSeverity } from 'vs/platform/markers/common/markers'; import { IDebugConfiguration } from 'vs/workbench/contrib/debug/common/debug'; -import { createErrorWithActions } from 'vs/base/common/errorMessage'; import { IViewsService } from 'vs/workbench/common/views'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { createErrorWithActions } from 'vs/base/common/errorMessage'; +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> { return (listener, thisArgs = null, disposables?) => { @@ -48,7 +51,8 @@ export class DebugTaskRunner { @IConfigurationService private readonly configurationService: IConfigurationService, @IViewsService private readonly viewsService: IViewsService, @IDialogService private readonly dialogService: IDialogService, - @IStorageService private readonly storageService: IStorageService + @IStorageService private readonly storageService: IStorageService, + @ICommandService private readonly commandService: ICommandService ) { } cancel(): void { @@ -158,7 +162,7 @@ export class DebugTaskRunner { const errorMessage = typeof taskId === 'string' ? nls.localize('DebugTaskNotFoundWithTaskId', "Could not find the task '{0}'.", taskId) : nls.localize('DebugTaskNotFound', "Could not find the specified task."); - return Promise.reject(createErrorWithActions(errorMessage)); + return Promise.reject(createErrorWithActions(errorMessage, [new Action(DEBUG_CONFIGURE_COMMAND_ID, DEBUG_CONFIGURE_LABEL, undefined, true, () => this.commandService.executeCommand(DEBUG_CONFIGURE_COMMAND_ID))])); } // If a task is missing the problem matcher the promise will never complete, so we need to have a workaround #35340 diff --git a/src/vs/workbench/contrib/debug/browser/debugToolBar.ts b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts index 8bb67ae408f..13d089466e4 100644 --- a/src/vs/workbench/contrib/debug/browser/debugToolBar.ts +++ b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts @@ -3,36 +3,39 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./media/debugToolBar'; -import * as errors from 'vs/base/common/errors'; import * as browser from 'vs/base/browser/browser'; import * as dom from 'vs/base/browser/dom'; +import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; +import { ActionBar, ActionsOrientation, IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; +import { Action, IAction, IRunEvent, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions'; import * as arrays from 'vs/base/common/arrays'; +import { RunOnceScheduler } from 'vs/base/common/async'; +import * as errors from 'vs/base/common/errors'; +import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import 'vs/css!./media/debugToolBar'; +import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { localize } from 'vs/nls'; -import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; -import { IAction, IRunEvent, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from 'vs/base/common/actions'; -import { ActionBar, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; -import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; -import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { IDebugConfiguration, IDebugService, State, CONTEXT_DEBUG_STATE, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_MULTI_SESSION_DEBUG, VIEWLET_ID } from 'vs/workbench/contrib/debug/common/debug'; -import { FocusSessionActionViewItem } from 'vs/workbench/contrib/debug/browser/debugActionViewItems'; +import { ICommandAction } from 'vs/platform/action/common/action'; +import { DropdownWithPrimaryActionViewItem } from 'vs/platform/actions/browser/dropdownWithPrimaryActionViewItem'; +import { createActionViewItem, createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IMenu, IMenuService, MenuId, MenuItemAction, MenuRegistry } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ContextKeyExpr, ContextKeyExpression, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { INotificationService } from 'vs/platform/notification/common/notification'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IThemeService, Themable, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { contrastBorder, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { RunOnceScheduler } from 'vs/base/common/async'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { createActionViewItem, createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { IMenu, IMenuService, MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; -import { IContextKeyService, ContextKeyExpression, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import * as icons from 'vs/workbench/contrib/debug/browser/debugIcons'; +import { IThemeService, Themable, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { FocusSessionActionViewItem } from 'vs/workbench/contrib/debug/browser/debugActionViewItems'; import { debugToolBarBackground, debugToolBarBorder } from 'vs/workbench/contrib/debug/browser/debugColors'; -import { URI } from 'vs/base/common/uri'; -import { CONTINUE_LABEL, CONTINUE_ID, PAUSE_ID, STOP_ID, DISCONNECT_ID, STEP_OVER_ID, STEP_INTO_ID, RESTART_SESSION_ID, STEP_OUT_ID, STEP_BACK_ID, REVERSE_CONTINUE_ID, RESTART_LABEL, STEP_OUT_LABEL, STEP_INTO_LABEL, STEP_OVER_LABEL, DISCONNECT_LABEL, STOP_LABEL, PAUSE_LABEL, FOCUS_SESSION_ID, FOCUS_SESSION_LABEL } from 'vs/workbench/contrib/debug/browser/debugCommands'; -import { ICommandAction } from 'vs/platform/action/common/action'; +import { CONTINUE_ID, CONTINUE_LABEL, DISCONNECT_AND_SUSPEND_ID, DISCONNECT_AND_SUSPEND_LABEL, DISCONNECT_ID, DISCONNECT_LABEL, FOCUS_SESSION_ID, FOCUS_SESSION_LABEL, PAUSE_ID, PAUSE_LABEL, RESTART_LABEL, RESTART_SESSION_ID, REVERSE_CONTINUE_ID, STEP_BACK_ID, STEP_INTO_ID, STEP_INTO_LABEL, STEP_OUT_ID, STEP_OUT_LABEL, STEP_OVER_ID, STEP_OVER_LABEL, STOP_ID, STOP_LABEL } from 'vs/workbench/contrib/debug/browser/debugCommands'; +import * as icons from 'vs/workbench/contrib/debug/browser/debugIcons'; +import { CONTEXT_DEBUG_STATE, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_MULTI_SESSION_DEBUG, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_SUSPEND_DEBUGGEE_SUPPORTED, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED, IDebugConfiguration, IDebugService, State, VIEWLET_ID } from 'vs/workbench/contrib/debug/common/debug'; +import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; const DEBUG_TOOLBAR_POSITION_KEY = 'debug.actionswidgetposition'; const DEBUG_TOOLBAR_Y_KEY = 'debug.actionswidgety'; @@ -51,6 +54,8 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { private isVisible = false; private isBuilt = false; + private readonly stopActionViewItemDisposables = this._register(new DisposableStore()); + constructor( @INotificationService private readonly notificationService: INotificationService, @ITelemetryService private readonly telemetryService: ITelemetryService, @@ -61,7 +66,7 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { @IThemeService themeService: IThemeService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IMenuService menuService: IMenuService, - @IContextKeyService contextKeyService: IContextKeyService + @IContextKeyService contextKeyService: IContextKeyService, ) { super(themeService); @@ -80,7 +85,14 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { actionViewItemProvider: (action: IAction) => { if (action.id === FOCUS_SESSION_ID) { return this.instantiationService.createInstance(FocusSessionActionViewItem, action, undefined); + } else if (action.id === STOP_ID || action.id === DISCONNECT_ID) { + this.stopActionViewItemDisposables.clear(); + const item = this.instantiationService.invokeFunction(accessor => createDisconnectMenuItemAction(action as MenuItemAction, this.stopActionViewItemDisposables, accessor)); + if (item) { + return item; + } } + return createActionViewItem(this.instantiationService, action); } })); @@ -256,6 +268,31 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { } } +export function createDisconnectMenuItemAction(action: MenuItemAction, disposables: DisposableStore, accessor: ServicesAccessor): IActionViewItem | undefined { + const menuService = accessor.get(IMenuService); + const contextKeyService = accessor.get(IContextKeyService); + const instantiationService = accessor.get(IInstantiationService); + const contextMenuService = accessor.get(IContextMenuService); + + const menu = menuService.createMenu(MenuId.DebugToolBarStop, contextKeyService); + const secondary: IAction[] = []; + disposables.add(createAndFillInActionBarActions(menu, { shouldForwardArgs: true }, secondary)); + + if (!secondary.length) { + return undefined; + } + + const dropdownAction = disposables.add(new Action('notebook.moreRunActions', localize('notebook.moreRunActionsLabel', "More..."), 'codicon-chevron-down', true)); + const item = instantiationService.createInstance(DropdownWithPrimaryActionViewItem, + action as MenuItemAction, + dropdownAction, + secondary, + 'debug-stop-actions', + contextMenuService, + {}); + return item; +} + // Debug toolbar const debugViewTitleItems: IDisposable[] = []; @@ -303,8 +340,8 @@ MenuRegistry.onDidChangeMenu(e => { registerDebugToolBarItem(CONTINUE_ID, CONTINUE_LABEL, 10, icons.debugContinue, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); registerDebugToolBarItem(PAUSE_ID, PAUSE_LABEL, 10, icons.debugPause, CONTEXT_DEBUG_STATE.notEqualsTo('stopped'), CONTEXT_DEBUG_STATE.isEqualTo('running')); -registerDebugToolBarItem(STOP_ID, STOP_LABEL, 70, icons.debugStop, CONTEXT_FOCUSED_SESSION_IS_ATTACH.toNegated(), undefined, { id: DISCONNECT_ID, title: DISCONNECT_LABEL, icon: icons.debugDisconnect }); -registerDebugToolBarItem(DISCONNECT_ID, DISCONNECT_LABEL, 70, icons.debugDisconnect, CONTEXT_FOCUSED_SESSION_IS_ATTACH, undefined, { id: STOP_ID, title: STOP_LABEL, icon: icons.debugStop }); +registerDebugToolBarItem(STOP_ID, STOP_LABEL, 70, icons.debugStop, CONTEXT_FOCUSED_SESSION_IS_ATTACH.toNegated(), undefined, { id: DISCONNECT_ID, title: DISCONNECT_LABEL, icon: icons.debugDisconnect, precondition: ContextKeyExpr.and(CONTEXT_FOCUSED_SESSION_IS_ATTACH.toNegated(), CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED), }); +registerDebugToolBarItem(DISCONNECT_ID, DISCONNECT_LABEL, 70, icons.debugDisconnect, CONTEXT_FOCUSED_SESSION_IS_ATTACH, undefined, { id: STOP_ID, title: STOP_LABEL, icon: icons.debugStop, precondition: ContextKeyExpr.and(CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED), }); registerDebugToolBarItem(STEP_OVER_ID, STEP_OVER_LABEL, 20, icons.debugStepOver, undefined, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); registerDebugToolBarItem(STEP_INTO_ID, STEP_INTO_LABEL, 30, icons.debugStepInto, undefined, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); registerDebugToolBarItem(STEP_OUT_ID, STEP_OUT_LABEL, 40, icons.debugStepOut, undefined, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); @@ -312,3 +349,39 @@ registerDebugToolBarItem(RESTART_SESSION_ID, RESTART_LABEL, 60, icons.debugResta registerDebugToolBarItem(STEP_BACK_ID, localize('stepBackDebug', "Step Back"), 50, icons.debugStepBack, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); registerDebugToolBarItem(REVERSE_CONTINUE_ID, localize('reverseContinue', "Reverse"), 55, icons.debugReverseContinue, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); registerDebugToolBarItem(FOCUS_SESSION_ID, FOCUS_SESSION_LABEL, 100, undefined, CONTEXT_MULTI_SESSION_DEBUG); + +MenuRegistry.appendMenuItem(MenuId.DebugToolBarStop, { + group: 'navigation', + when: ContextKeyExpr.and(CONTEXT_FOCUSED_SESSION_IS_ATTACH.toNegated(), CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED), + order: 0, + command: { + id: DISCONNECT_ID, + title: DISCONNECT_LABEL, + icon: icons.debugDisconnect + } +}); + +MenuRegistry.appendMenuItem(MenuId.DebugToolBarStop, { + group: 'navigation', + when: ContextKeyExpr.and(CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED), + order: 0, + command: { + id: STOP_ID, + title: STOP_LABEL, + icon: icons.debugStop + } +}); + +MenuRegistry.appendMenuItem(MenuId.DebugToolBarStop, { + group: 'navigation', + when: ContextKeyExpr.or( + ContextKeyExpr.and(CONTEXT_FOCUSED_SESSION_IS_ATTACH.toNegated(), CONTEXT_SUSPEND_DEBUGGEE_SUPPORTED, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED), + ContextKeyExpr.and(CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_SUSPEND_DEBUGGEE_SUPPORTED), + ), + order: 0, + command: { + id: DISCONNECT_AND_SUSPEND_ID, + title: DISCONNECT_AND_SUSPEND_LABEL, + icon: icons.debugDisconnect + } +}); diff --git a/src/vs/workbench/contrib/debug/browser/debugViewlet.ts b/src/vs/workbench/contrib/debug/browser/debugViewlet.ts index a90416994e1..ae38d059d88 100644 --- a/src/vs/workbench/contrib/debug/browser/debugViewlet.ts +++ b/src/vs/workbench/contrib/debug/browser/debugViewlet.ts @@ -3,34 +3,35 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; +import { IAction } from 'vs/base/common/actions'; +import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; import 'vs/css!./media/debugViewlet'; import * as nls from 'vs/nls'; -import { IAction } from 'vs/base/common/actions'; -import { IDebugService, VIEWLET_ID, State, BREAKPOINTS_VIEW_ID, CONTEXT_DEBUG_UX, CONTEXT_DEBUG_UX_KEY, REPL_VIEW_ID, CONTEXT_DEBUG_STATE, ILaunch, getStateLabel, CONTEXT_DEBUGGERS_AVAILABLE } from 'vs/workbench/contrib/debug/common/debug'; -import { StartDebugActionViewItem, FocusSessionActionViewItem } from 'vs/workbench/contrib/debug/browser/debugActionViewItems'; +import { createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { Action2, MenuId, MenuItemAction, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IProgressService } from 'vs/platform/progress/common/progress'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { IStorageService } from 'vs/platform/storage/common/storage'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ViewPaneContainer, ViewsSubMenu } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; -import { MenuId, registerAction2, Action2, MenuRegistry } from 'vs/platform/actions/common/actions'; -import { IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { ViewPaneContainer, ViewsSubMenu } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { WorkbenchStateContext } from 'vs/workbench/common/contextkeys'; import { IViewDescriptorService, IViewsService } from 'vs/workbench/common/views'; -import { WelcomeView } from 'vs/workbench/contrib/debug/browser/welcomeView'; +import { FocusSessionActionViewItem, StartDebugActionViewItem } from 'vs/workbench/contrib/debug/browser/debugActionViewItems'; +import { DEBUG_CONFIGURE_COMMAND_ID, DEBUG_CONFIGURE_LABEL, DEBUG_START_COMMAND_ID, DEBUG_START_LABEL, DISCONNECT_ID, FOCUS_SESSION_ID, SELECT_AND_START_ID, STOP_ID } from 'vs/workbench/contrib/debug/browser/debugCommands'; import { debugConfigure } from 'vs/workbench/contrib/debug/browser/debugIcons'; -import { WorkbenchStateContext } from 'vs/workbench/common/contextkeys'; -import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; -import { FOCUS_SESSION_ID, SELECT_AND_START_ID, DEBUG_CONFIGURE_COMMAND_ID, DEBUG_CONFIGURE_LABEL, DEBUG_START_LABEL, DEBUG_START_COMMAND_ID } from 'vs/workbench/contrib/debug/browser/debugCommands'; -import { IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; +import { createDisconnectMenuItemAction } from 'vs/workbench/contrib/debug/browser/debugToolBar'; +import { WelcomeView } from 'vs/workbench/contrib/debug/browser/welcomeView'; +import { BREAKPOINTS_VIEW_ID, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DEBUG_STATE, CONTEXT_DEBUG_UX, CONTEXT_DEBUG_UX_KEY, getStateLabel, IDebugService, ILaunch, REPL_VIEW_ID, State, VIEWLET_ID } from 'vs/workbench/contrib/debug/common/debug'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; export class DebugViewPaneContainer extends ViewPaneContainer { @@ -39,6 +40,8 @@ export class DebugViewPaneContainer extends ViewPaneContainer { private breakpointView: ViewPane | undefined; private paneListeners = new Map<string, IDisposable>(); + private readonly stopActionViewItemDisposables = this._register(new DisposableStore()); + constructor( @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, @ITelemetryService telemetryService: ITelemetryService, @@ -53,7 +56,7 @@ export class DebugViewPaneContainer extends ViewPaneContainer { @IConfigurationService configurationService: IConfigurationService, @IContextViewService private readonly contextViewService: IContextViewService, @IContextKeyService private readonly contextKeyService: IContextKeyService, - @IViewDescriptorService viewDescriptorService: IViewDescriptorService + @IViewDescriptorService viewDescriptorService: IViewDescriptorService, ) { super(VIEWLET_ID, { mergeViewWithContainerWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService, viewDescriptorService); @@ -97,6 +100,15 @@ export class DebugViewPaneContainer extends ViewPaneContainer { if (action.id === FOCUS_SESSION_ID) { return new FocusSessionActionViewItem(action, undefined, this.debugService, this.themeService, this.contextViewService, this.configurationService); } + + if (action.id === STOP_ID || action.id === DISCONNECT_ID) { + this.stopActionViewItemDisposables.clear(); + const item = this.instantiationService.invokeFunction(accessor => createDisconnectMenuItemAction(action as MenuItemAction, this.stopActionViewItemDisposables, accessor)); + if (item) { + return item; + } + } + return createActionViewItem(this.instantiationService, action); } diff --git a/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css b/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css index 43865553172..577a80c532e 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css +++ b/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css @@ -84,11 +84,14 @@ font-size: 11px; } -.monaco-workbench .monaco-list-row .expression .value, -.monaco-workbench .monaco-list-row .expression .lazy-button { +.monaco-workbench .monaco-list-row .expression .value { margin-left: 6px; } +.monaco-workbench .monaco-list-row .expression .lazy-button { + margin-left: 3px; +} + /* Links */ .monaco-workbench .monaco-list-row .expression .value a.link:hover { @@ -119,20 +122,14 @@ .monaco-workbench .monaco-list-row .expression .lazy-button { display: none; -} - -.monaco-workbench .monaco-list-row .expression .lazy-button:hover { - text-decoration: underline; + border-radius: 5px; + padding: 3px; } .monaco-workbench .monaco-list-row .expression.lazy .lazy-button { display: inline; } -.monaco-workbench .monaco-list-row .expression.lazy .value { - display: none; -} - .monaco-workbench .debug-inline-value { background-color: var(--vscode-editor-inlineValuesBackground); color: var(--vscode-editor-inlineValuesForeground); diff --git a/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts index d1b3b798d9f..848245e3978 100644 --- a/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts @@ -6,7 +6,7 @@ import * as nls from 'vs/nls'; import { Event, Emitter } from 'vs/base/common/event'; import * as objects from 'vs/base/common/objects'; -import { Action } from 'vs/base/common/actions'; +import { toAction } from 'vs/base/common/actions'; import * as errors from 'vs/base/common/errors'; import { createErrorWithActions } from 'vs/base/common/errorMessage'; import { formatPII, isUri } from 'vs/workbench/contrib/debug/common/debugUtils'; @@ -284,7 +284,8 @@ export class RawDebugSession implements IDisposable { */ disconnect(args: DebugProtocol.DisconnectArguments): Promise<any> { const terminateDebuggee = this.capabilities.supportTerminateDebuggee ? args.terminateDebuggee : undefined; - return this.shutdown(undefined, args.restart, terminateDebuggee); + const suspendDebuggee = this.capabilities.supportTerminateDebuggee && this.capabilities.supportSuspendDebuggee ? args.suspendDebuggee : undefined; + return this.shutdown(undefined, args.restart, terminateDebuggee, suspendDebuggee); } //---- DAP requests @@ -553,12 +554,20 @@ export class RawDebugSession implements IDisposable { //---- private - private async shutdown(error?: Error, restart = false, terminateDebuggee: boolean | undefined = undefined): Promise<any> { + private async shutdown(error?: Error, restart = false, terminateDebuggee: boolean | undefined = undefined, suspendDebuggee: boolean | undefined = undefined): Promise<any> { if (!this.inShutdown) { this.inShutdown = true; if (this.debugAdapter) { try { - const args = typeof terminateDebuggee === 'boolean' ? { restart, terminateDebuggee } : { restart }; + const args: DebugProtocol.DisconnectArguments = { restart }; + if (typeof terminateDebuggee === 'boolean') { + args.terminateDebuggee = terminateDebuggee; + } + + if (typeof suspendDebuggee === 'boolean') { + args.suspendDebuggee = suspendDebuggee; + } + this.send('disconnect', args, undefined, 2000); } catch (e) { // Catch the potential 'disconnect' error - no need to show it to the user since the adapter is shutting down @@ -738,11 +747,7 @@ export class RawDebugSession implements IDisposable { const uri = URI.parse(url); // Use a suffixed id if uri invokes a command, so default 'Open launch.json' command is suppressed on dialog const actionId = uri.scheme === Schemas.command ? 'debug.moreInfo.command' : 'debug.moreInfo'; - return createErrorWithActions(userMessage, { - actions: [new Action(actionId, label, undefined, true, async () => { - this.openerService.open(uri, { allowCommands: true }); - })] - }); + return createErrorWithActions(userMessage, [toAction({ id: actionId, label, run: () => this.openerService.open(uri, { allowCommands: true }) })]); } if (showErrors && error && error.format && error.showUser) { this.notificationService.error(userMessage); diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index cc2a06f698e..d171c967d4f 100644 --- a/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/src/vs/workbench/contrib/debug/browser/repl.ts @@ -895,7 +895,7 @@ registerAction2(class extends ViewAction<Repl> { session = stopppedChildSession; } } - await debugService.focusStackFrame(undefined, undefined, session, true); + await debugService.focusStackFrame(undefined, undefined, session, { explicit: true }); } // Need to select the session in the view since the focussed session might not have changed await view.selectSession(session); diff --git a/src/vs/workbench/contrib/debug/common/breakpoints.ts b/src/vs/workbench/contrib/debug/common/breakpoints.ts new file mode 100644 index 00000000000..38bc0d5114d --- /dev/null +++ b/src/vs/workbench/contrib/debug/common/breakpoints.ts @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ContextKeyExpr, ContextKeyExpression, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IBreakpointContribution } from 'vs/workbench/contrib/debug/common/debug'; + +export class Breakpoints { + + private breakpointsWhen: ContextKeyExpression | undefined; + + constructor( + private readonly breakpointContribution: IBreakpointContribution, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + ) { + this.breakpointsWhen = typeof breakpointContribution.when === 'string' ? ContextKeyExpr.deserialize(breakpointContribution.when) : undefined; + } + + get language(): string { + return this.breakpointContribution.language; + } + + get enabled(): boolean { + return !this.breakpointsWhen || this.contextKeyService.contextMatchesRules(this.breakpointsWhen); + } +} diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 7fd322d2d11..9457ae8c1b4 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -80,6 +80,7 @@ export const CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED = new RawContextKey<bool export const CONTEXT_BREAK_WHEN_VALUE_IS_ACCESSED_SUPPORTED = new RawContextKey<boolean>('breakWhenValueIsAccessedSupported', false, { type: 'boolean', description: nls.localize('breakWhenValueIsAccessedSupported', "True when the focused breakpoint supports to break when value is accessed.") }); export const CONTEXT_BREAK_WHEN_VALUE_IS_READ_SUPPORTED = new RawContextKey<boolean>('breakWhenValueIsReadSupported', false, { type: 'boolean', description: nls.localize('breakWhenValueIsReadSupported', "True when the focused breakpoint supports to break when value is read.") }); export const CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED = new RawContextKey<boolean>('terminateDebuggeeSupported', false, { type: 'boolean', description: nls.localize('terminateDebuggeeSupported', "True when the focused session supports the terminate debuggee capability.") }); +export const CONTEXT_SUSPEND_DEBUGGEE_SUPPORTED = new RawContextKey<boolean>('suspendDebuggeeSupported', false, { type: 'boolean', description: nls.localize('suspendDebuggeeSupported', "True when the focused session supports the suspend debuggee capability.") }); export const CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT = new RawContextKey<boolean>('variableEvaluateNamePresent', false, { type: 'boolean', description: nls.localize('variableEvaluateNamePresent', "True when the focused variable has an 'evalauteName' field set.") }); export const CONTEXT_VARIABLE_IS_READONLY = new RawContextKey<boolean>('variableIsReadonly', false, { type: 'boolean', description: nls.localize('variableIsReadonly', "True when the focused variable is readonly.") }); export const CONTEXT_EXCEPTION_WIDGET_VISIBLE = new RawContextKey<boolean>('exceptionWidgetVisible', false, { type: 'boolean', description: nls.localize('exceptionWidgetVisible', "True when the exception widget is visible.") }); @@ -290,6 +291,7 @@ export interface IDebugSession extends ITreeElement { readonly compoundRoot: DebugCompoundRoot | undefined; readonly name: string; readonly isSimpleUI: boolean; + readonly autoExpandLazyVariables: boolean; setSubId(subId: string | undefined): void; @@ -339,7 +341,7 @@ export interface IDebugSession extends ITreeElement { launchOrAttach(config: IConfig): Promise<void>; restart(): Promise<void>; terminate(restart?: boolean /* false */): Promise<void>; - disconnect(restart?: boolean /* false */): Promise<void>; + disconnect(restart?: boolean /* false */, suspend?: boolean): Promise<void>; sendBreakpoints(modelUri: uri, bpts: IBreakpoint[], sourceModified: boolean): Promise<void>; sendFunctionBreakpoints(fbps: IFunctionBreakpoint[]): Promise<void>; @@ -611,7 +613,7 @@ export interface IDebugConfiguration { allowBreakpointsEverywhere: boolean; openDebug: 'neverOpen' | 'openOnSessionStart' | 'openOnFirstSessionStart' | 'openOnDebugBreak'; openExplorerOnEnd: boolean; - inlineValues: boolean | 'auto'; + inlineValues: boolean | 'auto' | 'on' | 'off'; // boolean for back-compat toolBarLocation: 'floating' | 'docked' | 'hidden'; showInStatusBar: 'never' | 'always' | 'onFirstSessionStart'; internalConsoleOptions: 'neverOpen' | 'openOnSessionStart' | 'openOnFirstSessionStart'; @@ -629,6 +631,7 @@ export interface IDebugConfiguration { acceptSuggestionOnEnter: 'off' | 'on'; }; focusWindowOnBreak: boolean; + focusEditorOnBreak: boolean; onTaskErrors: 'debugAnyway' | 'showErrors' | 'prompt' | 'abort'; showBreakpointsInOverviewRuler: boolean; showInlineBreakpointCandidates: boolean; @@ -636,6 +639,7 @@ export interface IDebugConfiguration { disassemblyView: { showSourceCode: boolean; }; + autoExpandLazyVariables: boolean; } export interface IGlobalConfig { @@ -770,6 +774,11 @@ export interface IDebuggerContribution extends IPlatformSpecificAdapterContribut when?: string; } +export interface IBreakpointContribution { + language: string; + when?: string; +} + export enum DebugConfigurationProviderTriggerKind { /** * `DebugConfigurationProvider.provideDebugConfigurations` is called to provide the initial debug configurations for a newly created launch.json. @@ -950,7 +959,7 @@ export interface IDebugService { /** * Sets the focused stack frame and evaluates all expressions against the newly focused stack frame, */ - focusStackFrame(focusedStackFrame: IStackFrame | undefined, thread?: IThread, session?: IDebugSession, explicit?: boolean): Promise<void>; + focusStackFrame(focusedStackFrame: IStackFrame | undefined, thread?: IThread, session?: IDebugSession, options?: { explicit?: boolean; preserveFocus?: boolean; sideBySide?: boolean; pinned?: boolean }): Promise<void>; /** * Returns true if breakpoints can be set for a given editor model. Depends on mode. @@ -1073,7 +1082,7 @@ export interface IDebugService { /** * Stops the session. If no session is specified then all sessions are stopped. */ - stopSession(session: IDebugSession | undefined, disconnect?: boolean): Promise<any>; + stopSession(session: IDebugSession | undefined, disconnect?: boolean, suspend?: boolean): Promise<any>; /** * Makes unavailable all sources with the passed uri. Source will appear as grayed out in callstack view. diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index 52994951b34..c05fc04c956 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -42,9 +42,9 @@ export class ExpressionContainer implements IExpressionContainer { constructor( protected session: IDebugSession | undefined, - protected threadId: number | undefined, + protected readonly threadId: number | undefined, private _reference: number | undefined, - private id: string, + private readonly id: string, public namedVariables: number | undefined = 0, public indexedVariables: number | undefined = 0, public memoryReference: string | undefined = undefined, @@ -152,7 +152,7 @@ export class ExpressionContainer implements IExpressionContainer { } const nameCount = new Map<string, number>(); - return response.body.variables.filter(v => !!v).map((v: IDebugProtocolVariableWithContext) => { + const vars = response.body.variables.filter(v => !!v).map((v: IDebugProtocolVariableWithContext) => { if (isString(v.value) && isString(v.name) && typeof v.variablesReference === 'number') { const count = nameCount.get(v.name) || 0; const idDuplicationIndex = count > 0 ? count.toString() : ''; @@ -161,6 +161,12 @@ export class ExpressionContainer implements IExpressionContainer { } return new Variable(this.session, this.threadId, this, 0, '', undefined, nls.localize('invalidVariableAttributes', "Invalid variable attributes"), 0, 0, undefined, { kind: 'virtual' }, undefined, undefined, false); }); + + if (this.session!.autoExpandLazyVariables) { + await Promise.all(vars.map(v => v.presentationHint?.lazy && v.evaluateLazy())); + } + + return vars; } catch (e) { return [new Variable(this.session, this.threadId, this, 0, '', undefined, e.message, 0, 0, undefined, { kind: 'virtual' }, undefined, undefined, false)]; } @@ -275,9 +281,9 @@ export class Variable extends ExpressionContainer implements IExpression { constructor( session: IDebugSession | undefined, threadId: number | undefined, - public parent: IExpressionContainer, + public readonly parent: IExpressionContainer, reference: number | undefined, - public name: string, + public readonly name: string, public evaluateName: string | undefined, value: string | undefined, namedVariables: number | undefined, @@ -285,8 +291,8 @@ export class Variable extends ExpressionContainer implements IExpression { memoryReference: string | undefined, presentationHint: DebugProtocol.VariablePresentationHint | undefined, type: string | undefined = undefined, - public variableMenuContext: string | undefined = undefined, - public available = true, + public readonly variableMenuContext: string | undefined = undefined, + public readonly available = true, startOfVariables = 0, idDuplicationIndex = '', ) { @@ -346,12 +352,12 @@ export class Scope extends ExpressionContainer implements IScope { constructor( stackFrame: IStackFrame, index: number, - public name: string, + public readonly name: string, reference: number, public expensive: boolean, namedVariables?: number, indexedVariables?: number, - public range?: IRange + public readonly range?: IRange ) { super(stackFrame.thread.session, stackFrame.thread.threadId, reference, `scope:${name}:${index}`, namedVariables, indexedVariables); } @@ -389,15 +395,15 @@ export class StackFrame implements IStackFrame { private scopes: Promise<Scope[]> | undefined; constructor( - public thread: Thread, - public frameId: number, - public source: Source, - public name: string, - public presentationHint: string | undefined, - public range: IRange, - private index: number, - public canRestart: boolean, - public instructionPointerReference?: string + public readonly thread: Thread, + public readonly frameId: number, + public readonly source: Source, + public readonly name: string, + public readonly presentationHint: string | undefined, + public readonly range: IRange, + private readonly index: number, + public readonly canRestart: boolean, + public readonly instructionPointerReference?: string ) { } getId(): string { @@ -482,7 +488,7 @@ export class Thread implements IThread { public reachedEndOfCallStack = false; public lastSteppingGranularity: DebugProtocol.SteppingGranularity | undefined; - constructor(public session: IDebugSession, public name: string, public threadId: number) { + constructor(public readonly session: IDebugSession, public name: string, public readonly threadId: number) { this.callStack = []; this.staleCallStack = []; this.stopped = false; @@ -721,7 +727,7 @@ export class MemoryRegion extends Disposable implements IMemoryRegion { export class Enablement implements IEnablement { constructor( public enabled: boolean, - private id: string + private readonly id: string ) { } getId(): string { @@ -851,7 +857,7 @@ export abstract class BaseBreakpoint extends Enablement implements IBaseBreakpoi export class Breakpoint extends BaseBreakpoint implements IBreakpoint { constructor( - private _uri: uri, + private readonly _uri: uri, private _lineNumber: number, private _column: number | undefined, enabled: boolean, @@ -1006,15 +1012,15 @@ export class FunctionBreakpoint extends BaseBreakpoint implements IFunctionBreak export class DataBreakpoint extends BaseBreakpoint implements IDataBreakpoint { constructor( - public description: string, - public dataId: string, - public canPersist: boolean, + public readonly description: string, + public readonly dataId: string, + public readonly canPersist: boolean, enabled: boolean, hitCondition: string | undefined, condition: string | undefined, logMessage: string | undefined, - public accessTypes: DebugProtocol.DataBreakpointAccessType[] | undefined, - public accessType: DebugProtocol.DataBreakpointAccessType, + public readonly accessTypes: DebugProtocol.DataBreakpointAccessType[] | undefined, + public readonly accessType: DebugProtocol.DataBreakpointAccessType, id = generateUuid() ) { super(enabled, hitCondition, condition, logMessage, id); @@ -1045,13 +1051,13 @@ export class DataBreakpoint extends BaseBreakpoint implements IDataBreakpoint { export class ExceptionBreakpoint extends BaseBreakpoint implements IExceptionBreakpoint { constructor( - public filter: string, - public label: string, + public readonly filter: string, + public readonly label: string, enabled: boolean, - public supportsCondition: boolean, + public readonly supportsCondition: boolean, condition: string | undefined, - public description: string | undefined, - public conditionDescription: string | undefined + public readonly description: string | undefined, + public readonly conditionDescription: string | undefined ) { super(enabled, undefined, condition, undefined, generateUuid()); } @@ -1079,9 +1085,9 @@ export class ExceptionBreakpoint extends BaseBreakpoint implements IExceptionBre export class InstructionBreakpoint extends BaseBreakpoint implements IInstructionBreakpoint { constructor( - public instructionReference: string, - public offset: number, - public canPersist: boolean, + public readonly instructionReference: string, + public readonly offset: number, + public readonly canPersist: boolean, enabled: boolean, hitCondition: string | undefined, condition: string | undefined, diff --git a/src/vs/workbench/contrib/debug/common/debugSchemas.ts b/src/vs/workbench/contrib/debug/common/debugSchemas.ts index 3613a1b44a4..a2a9c7f5480 100644 --- a/src/vs/workbench/contrib/debug/common/debugSchemas.ts +++ b/src/vs/workbench/contrib/debug/common/debugSchemas.ts @@ -5,7 +5,7 @@ import * as extensionsRegistry from 'vs/workbench/services/extensions/common/extensionsRegistry'; import * as nls from 'vs/nls'; -import { IDebuggerContribution, ICompound } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebuggerContribution, ICompound, IBreakpointContribution } from 'vs/workbench/contrib/debug/common/debug'; import { launchSchemaId } from 'vs/workbench/services/configuration/common/configuration'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { inputsSchema } from 'vs/workbench/services/configurationResolver/common/configurationResolverSchema'; @@ -68,7 +68,7 @@ export const debuggersExtPoint = extensionsRegistry.ExtensionsRegistry.registerE type: 'object' }, when: { - description: nls.localize('vscode.extension.contributes.debuggers.when', "Condition which must be true to enable this type of debugger. Consider using 'shellExecutionSupported', 'virtualWorkspace', 'resourceScheme' or an extension defined context key as appropriate for this."), + description: nls.localize('vscode.extension.contributes.debuggers.when', "Condition which must be true to enable this type of debugger. Consider using 'shellExecutionSupported', 'virtualWorkspace', 'resourceScheme' or an extension-defined context key as appropriate for this."), type: 'string', default: '' }, @@ -107,12 +107,8 @@ export const debuggersExtPoint = extensionsRegistry.ExtensionsRegistry.registerE } }); -export interface IRawBreakpointContribution { - language: string; -} - // breakpoints extension point #9037 -export const breakpointsExtPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint<IRawBreakpointContribution[]>({ +export const breakpointsExtPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint<IBreakpointContribution[]>({ extensionPoint: 'breakpoints', jsonSchema: { description: nls.localize('vscode.extension.contributes.breakpoints', 'Contributes breakpoints.'), @@ -127,6 +123,11 @@ export const breakpointsExtPoint = extensionsRegistry.ExtensionsRegistry.registe description: nls.localize('vscode.extension.contributes.breakpoints.language', "Allow breakpoints for this language."), type: 'string' }, + when: { + description: nls.localize('vscode.extension.contributes.breakpoints.when', "Condition which must be true to enable breakpoints in this language. Consider matching this to the debugger when clause as appropriate."), + type: 'string', + default: '' + } } } } diff --git a/src/vs/workbench/contrib/debug/common/debugViewModel.ts b/src/vs/workbench/contrib/debug/common/debugViewModel.ts index a6444a1f5fa..46468bb81d7 100644 --- a/src/vs/workbench/contrib/debug/common/debugViewModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugViewModel.ts @@ -5,7 +5,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { CONTEXT_DISASSEMBLE_REQUEST_SUPPORTED, CONTEXT_EXPRESSION_SELECTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_FOCUSED_STACK_FRAME_HAS_INSTRUCTION_POINTER_REFERENCE, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_MULTI_SESSION_DEBUG, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_SET_EXPRESSION_SUPPORTED, CONTEXT_SET_VARIABLE_SUPPORTED, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_STEP_INTO_TARGETS_SUPPORTED, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED, IDebugSession, IExpression, IExpressionContainer, IStackFrame, IThread, IViewModel } from 'vs/workbench/contrib/debug/common/debug'; +import { CONTEXT_DISASSEMBLE_REQUEST_SUPPORTED, CONTEXT_EXPRESSION_SELECTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_FOCUSED_STACK_FRAME_HAS_INSTRUCTION_POINTER_REFERENCE, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_MULTI_SESSION_DEBUG, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_SET_EXPRESSION_SUPPORTED, CONTEXT_SET_VARIABLE_SUPPORTED, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_STEP_INTO_TARGETS_SUPPORTED, CONTEXT_SUSPEND_DEBUGGEE_SUPPORTED, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED, IDebugSession, IExpression, IExpressionContainer, IStackFrame, IThread, IViewModel } from 'vs/workbench/contrib/debug/common/debug'; import { isSessionAttach } from 'vs/workbench/contrib/debug/common/debugUtils'; export class ViewModel implements IViewModel { @@ -31,7 +31,8 @@ export class ViewModel implements IViewModel { private setVariableSupported!: IContextKey<boolean>; private setExpressionSupported!: IContextKey<boolean>; private multiSessionDebug!: IContextKey<boolean>; - private terminateDebuggeeSuported!: IContextKey<boolean>; + private terminateDebuggeeSupported!: IContextKey<boolean>; + private suspendDebuggeeSupported!: IContextKey<boolean>; private disassembleRequestSupported!: IContextKey<boolean>; private focusedStackFrameHasInstructionPointerReference!: IContextKey<Boolean>; @@ -47,7 +48,8 @@ export class ViewModel implements IViewModel { this.setVariableSupported = CONTEXT_SET_VARIABLE_SUPPORTED.bindTo(contextKeyService); this.setExpressionSupported = CONTEXT_SET_EXPRESSION_SUPPORTED.bindTo(contextKeyService); this.multiSessionDebug = CONTEXT_MULTI_SESSION_DEBUG.bindTo(contextKeyService); - this.terminateDebuggeeSuported = CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED.bindTo(contextKeyService); + this.terminateDebuggeeSupported = CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED.bindTo(contextKeyService); + this.suspendDebuggeeSupported = CONTEXT_SUSPEND_DEBUGGEE_SUPPORTED.bindTo(contextKeyService); this.disassembleRequestSupported = CONTEXT_DISASSEMBLE_REQUEST_SUPPORTED.bindTo(contextKeyService); this.focusedStackFrameHasInstructionPointerReference = CONTEXT_FOCUSED_STACK_FRAME_HAS_INSTRUCTION_POINTER_REFERENCE.bindTo(contextKeyService); }); @@ -85,7 +87,8 @@ export class ViewModel implements IViewModel { this.jumpToCursorSupported.set(session ? !!session.capabilities.supportsGotoTargetsRequest : false); this.setVariableSupported.set(session ? !!session.capabilities.supportsSetVariable : false); this.setExpressionSupported.set(session ? !!session.capabilities.supportsSetExpression : false); - this.terminateDebuggeeSuported.set(session ? !!session.capabilities.supportTerminateDebuggee : false); + this.terminateDebuggeeSupported.set(session ? !!session.capabilities.supportTerminateDebuggee : false); + this.suspendDebuggeeSupported.set(session ? !!session.capabilities.supportSuspendDebuggee : false); this.disassembleRequestSupported.set(!!session?.capabilities.supportsDisassembleRequest); this.focusedStackFrameHasInstructionPointerReference.set(!!stackFrame?.instructionPointerReference); const attach = !!session && isSessionAttach(session); diff --git a/src/vs/workbench/contrib/debug/common/debugger.ts b/src/vs/workbench/contrib/debug/common/debugger.ts index c1c0166702d..8a41ba6c603 100644 --- a/src/vs/workbench/contrib/debug/common/debugger.ts +++ b/src/vs/workbench/contrib/debug/common/debugger.ts @@ -217,6 +217,7 @@ export class Debugger implements IDebugger { const attributes: IJSONSchema = this.debuggerContribution.configurationAttributes[request]; const defaultRequired = ['name', 'type', 'request']; attributes.required = attributes.required && attributes.required.length ? defaultRequired.concat(attributes.required) : defaultRequired; + attributes.additionalProperties = false; attributes.type = 'object'; if (!attributes.properties) { attributes.properties = {}; @@ -239,38 +240,37 @@ export class Debugger implements IDebugger { $ref: `#/definitions/common/properties/${prop}` }; } - definitions[definitionId] = attributes; - Object.keys(properties).forEach(name => { // Use schema allOf property to get independent error reporting #21113 ConfigurationResolverUtils.applyDeprecatedVariableMessage(properties[name]); }); - const result = { - allOf: [{ - $ref: `#/definitions/${definitionId}` - }, { - properties: { - windows: { - $ref: `#/definitions/${definitionId}`, - description: nls.localize('debugWindowsConfiguration', "Windows specific launch configuration attributes."), - required: [], - }, - osx: { - $ref: `#/definitions/${definitionId}`, - description: nls.localize('debugOSXConfiguration', "OS X specific launch configuration attributes."), - required: [], - }, - linux: { - $ref: `#/definitions/${definitionId}`, - description: nls.localize('debugLinuxConfiguration', "Linux specific launch configuration attributes."), - required: [], - } + definitions[definitionId] = { ...attributes }; + + // Don't add the OS props to the real attributes object so they don't show up in 'definitions' + const attributesCopy = { ...attributes }; + attributesCopy.properties = { + ...properties, + ...{ + windows: { + $ref: `#/definitions/${definitionId}`, + description: nls.localize('debugWindowsConfiguration', "Windows specific launch configuration attributes."), + required: [], + }, + osx: { + $ref: `#/definitions/${definitionId}`, + description: nls.localize('debugOSXConfiguration', "OS X specific launch configuration attributes."), + required: [], + }, + linux: { + $ref: `#/definitions/${definitionId}`, + description: nls.localize('debugLinuxConfiguration', "Linux specific launch configuration attributes."), + required: [], } - }] + } }; - return result; + return attributesCopy; }); } } diff --git a/src/vs/workbench/contrib/debug/node/debugAdapter.ts b/src/vs/workbench/contrib/debug/node/debugAdapter.ts index 6347d7ba2a5..4f0bf897787 100644 --- a/src/vs/workbench/contrib/debug/node/debugAdapter.ts +++ b/src/vs/workbench/contrib/debug/node/debugAdapter.ts @@ -13,7 +13,7 @@ import * as strings from 'vs/base/common/strings'; import * as objects from 'vs/base/common/objects'; import * as platform from 'vs/base/common/platform'; import { ExtensionsChannelId } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { IOutputService } from 'vs/workbench/contrib/output/common/output'; +import { IOutputService } from 'vs/workbench/services/output/common/output'; import { IDebugAdapterExecutable, IDebuggerContribution, IPlatformSpecificAdapterContribution, IDebugAdapterServer, IDebugAdapterNamedPipeServer } from 'vs/workbench/contrib/debug/common/debug'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { AbstractDebugAdapter } from '../common/abstractDebugAdapter'; diff --git a/src/vs/workbench/contrib/debug/node/terminals.ts b/src/vs/workbench/contrib/debug/node/terminals.ts index c27c88f27a6..5418d9e8ffc 100644 --- a/src/vs/workbench/contrib/debug/node/terminals.ts +++ b/src/vs/workbench/contrib/debug/node/terminals.ts @@ -121,7 +121,8 @@ export function prepareCommand(shell: string, args: string[], cwd?: string, env? quote = (s: string) => { s = s.replace(/\"/g, '""'); - return (s.indexOf(' ') >= 0 || s.indexOf('"') >= 0 || s.length === 0) ? `"${s}"` : s; + s = s.replace(/([><!^&|])/g, '^$1'); + return (' "'.split('').some(char => s.includes(char)) || s.length === 0) ? `"${s}"` : s; }; if (cwd) { @@ -138,7 +139,7 @@ export function prepareCommand(shell: string, args: string[], cwd?: string, env? if (value === null) { command += `set "${key}=" && `; } else { - value = value.replace(/[\^\&\|\<\>]/g, s => `^${s}`); + value = value.replace(/[&^|<>]/g, s => `^${s}`); command += `set "${key}=${value}" && `; } } @@ -154,8 +155,8 @@ export function prepareCommand(shell: string, args: string[], cwd?: string, env? case ShellType.bash: { quote = (s: string) => { - s = s.replace(/(["'\\\$])/g, '\\$1'); - return (s.indexOf(' ') >= 0 || s.indexOf(';') >= 0 || s.length === 0) ? `"${s}"` : s; + s = s.replace(/(["'\\\$!><#()\[\]*&^|])/g, '\\$1'); + return (' ;'.split('').some(char => s.includes(char)) || s.length === 0) ? `"${s}"` : s; }; const hardQuote = (s: string) => { diff --git a/src/vs/workbench/contrib/debug/test/browser/mockDebug.ts b/src/vs/workbench/contrib/debug/test/browser/mockDebug.ts index 81c394466ec..1bebaa31adb 100644 --- a/src/vs/workbench/contrib/debug/test/browser/mockDebug.ts +++ b/src/vs/workbench/contrib/debug/test/browser/mockDebug.ts @@ -168,6 +168,8 @@ export class MockDebugService implements IDebugService { } export class MockSession implements IDebugSession { + readonly autoExpandLazyVariables = false; + getMemory(memoryReference: string): IMemoryRegion { throw new Error('Method not implemented.'); } diff --git a/src/vs/workbench/contrib/dropIntoEditor/browser/dropIntoEditor.contibution.ts b/src/vs/workbench/contrib/dropIntoEditor/browser/dropIntoEditor.contibution.ts new file mode 100644 index 00000000000..7e1380f1d1f --- /dev/null +++ b/src/vs/workbench/contrib/dropIntoEditor/browser/dropIntoEditor.contibution.ts @@ -0,0 +1,151 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { distinct } from 'vs/base/common/arrays'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { Mimes } from 'vs/base/common/mime'; +import { relativePath } from 'vs/base/common/resources'; +import { URI } from 'vs/base/common/uri'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; +import { IPosition } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; +import { IEditorContribution } from 'vs/editor/common/editorCommon'; +import { IDataTransferItem } from 'vs/editor/common/languages'; +import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; +import { performSnippetEdit } from 'vs/editor/contrib/snippet/browser/snippetController2'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { extractEditorsDropData } from 'vs/workbench/browser/dnd'; +import { IDataTransfer } from 'vs/workbench/common/dnd'; + + +export class DropIntoEditorController extends Disposable implements IEditorContribution { + + public static readonly ID = 'editor.contrib.dropIntoEditorController'; + + constructor( + editor: ICodeEditor, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, + @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, + ) { + super(); + + editor.onDropIntoEditor(e => this.onDropIntoEditor(editor, e.position, e.event)); + } + + private async onDropIntoEditor(editor: ICodeEditor, position: IPosition, dragEvent: DragEvent) { + if (!dragEvent.dataTransfer || !editor.hasModel()) { + return; + } + + const model = editor.getModel(); + const modelVersionNow = model.getVersionId(); + + const textEditorDataTransfer: IDataTransfer = new Map<string, IDataTransferItem>(); + for (const item of dragEvent.dataTransfer.items) { + if (item.kind === 'string') { + const type = item.type; + const asStringValue = new Promise<string>(resolve => item.getAsString(resolve)); + textEditorDataTransfer.set(type, { + asString: () => asStringValue, + value: undefined + }); + } + } + + if (!textEditorDataTransfer.has(Mimes.uriList.toLowerCase())) { + const editorData = (await this._instantiationService.invokeFunction(extractEditorsDropData, dragEvent)) + .filter(input => input.resource) + .map(input => input.resource!.toString()); + + if (editorData.length) { + const str = distinct(editorData).join('\n'); + textEditorDataTransfer.set(Mimes.uriList.toLowerCase(), { + asString: () => Promise.resolve(str), + value: undefined + }); + } + } + + if (textEditorDataTransfer.size === 0) { + return; + } + + if (editor.getModel().getVersionId() !== modelVersionNow) { + return; + } + + const cts = new CancellationTokenSource(); + editor.onDidDispose(() => cts.cancel()); + model.onDidChangeContent(() => cts.cancel()); + + const ordered = this._languageFeaturesService.documentOnDropEditProvider.ordered(model); + for (const provider of ordered) { + const edit = await provider.provideDocumentOnDropEdits(model, position, textEditorDataTransfer, cts.token); + if (cts.token.isCancellationRequested || editor.getModel().getVersionId() !== modelVersionNow) { + return; + } + + if (edit) { + performSnippetEdit(editor, edit); + return; + } + } + + return this.doDefaultDrop(editor, position, textEditorDataTransfer, cts.token); + } + + private async doDefaultDrop(editor: ICodeEditor, position: IPosition, textEditorDataTransfer: IDataTransfer, token: CancellationToken): Promise<void> { + const range = new Range(position.lineNumber, position.column, position.lineNumber, position.column); + + const urlListEntry = textEditorDataTransfer.get('text/uri-list'); + if (urlListEntry) { + const urlList = await urlListEntry.asString(); + return this.doUriListDrop(editor, range, urlList, token); + } + + const textEntry = textEditorDataTransfer.get('text') ?? textEditorDataTransfer.get(Mimes.text); + if (textEntry) { + const text = await textEntry.asString(); + performSnippetEdit(editor, { range, snippet: text }); + } + } + + private async doUriListDrop(editor: ICodeEditor, range: Range, urlList: string, token: CancellationToken): Promise<void> { + const uris: URI[] = []; + for (const resource of urlList.split('\n')) { + try { + uris.push(URI.parse(resource)); + } catch { + // noop + } + } + + if (!uris.length) { + return; + } + + const snippet = uris + .map(uri => { + const root = this._workspaceContextService.getWorkspaceFolder(uri); + if (root) { + const rel = relativePath(root.uri, uri); + if (rel) { + return rel; + } + } + return uri.fsPath; + }) + .join(' '); + + performSnippetEdit(editor, { range, snippet }); + } +} + + +registerEditorContribution(DropIntoEditorController.ID, DropIntoEditorController); diff --git a/src/vs/workbench/contrib/emmet/browser/emmetActions.ts b/src/vs/workbench/contrib/emmet/browser/emmetActions.ts index 21292c80a47..08b8e4d9c32 100644 --- a/src/vs/workbench/contrib/emmet/browser/emmetActions.ts +++ b/src/vs/workbench/contrib/emmet/browser/emmetActions.ts @@ -93,7 +93,7 @@ export abstract class EmmetEditorAction extends EditorAction { } const position = selection.getStartPosition(); - model.tokenizeIfCheap(position.lineNumber); + model.tokenization.tokenizeIfCheap(position.lineNumber); const languageId = model.getLanguageIdAtPosition(position.lineNumber, position.column); const syntax = languageId.split('.').pop(); diff --git a/src/vs/workbench/contrib/experiments/common/experimentService.ts b/src/vs/workbench/contrib/experiments/common/experimentService.ts index a0d52ac4b52..0770dbb143e 100644 --- a/src/vs/workbench/contrib/experiments/common/experimentService.ts +++ b/src/vs/workbench/contrib/experiments/common/experimentService.ts @@ -21,6 +21,7 @@ import { asJson, IRequestService } from 'vs/platform/request/common/request'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { ITelemetryService, lastSessionDateStorageKey } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceTagsService } from 'vs/workbench/contrib/tags/common/workspaceTags'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { ITextFileEditorModel, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; @@ -182,7 +183,8 @@ export class ExperimentService extends Disposable implements IExperimentService @IConfigurationService private readonly configurationService: IConfigurationService, @IProductService private readonly productService: IProductService, @IWorkspaceTagsService private readonly workspaceTagsService: IWorkspaceTagsService, - @IExtensionService private readonly extensionService: IExtensionService + @IExtensionService private readonly extensionService: IExtensionService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService ) { super(); @@ -227,6 +229,10 @@ export class ExperimentService extends Disposable implements IExperimentService } protected async getExperiments(): Promise<IRawExperiment[] | null> { + if (this.environmentService.enableSmokeTestDriver || this.environmentService.extensionTestsLocationURI) { + return []; // TODO@sbatten add CLI argument (https://github.com/microsoft/vscode-internalbacklog/issues/2855) + } + const experimentsUrl = this.configurationService.getValue<string>('_workbench.experimentsUrl') || this.productService.experimentsUrl; if (!experimentsUrl || this.configurationService.getValue('workbench.enableExperiments') === false) { return []; diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index cb560efaaa3..8e2d1a5badd 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -71,6 +71,7 @@ import { MarkdownString } from 'vs/base/common/htmlContent'; 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'); class NavBar extends Disposable { @@ -183,10 +184,10 @@ class VersionWidget extends ExtensionWithDifferentGalleryVersionWidget { this.render(); } render(): void { - if (!this.extension) { + if (!this.extension || !semver.valid(this.extension.version)) { return; } - this.element.textContent = `v${this.gallery ? this.gallery.version : this.extension.version}`; + this.element.textContent = `v${this.gallery?.version ?? this.extension.version}`; } } diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index 41515d1c2e2..ebbd930216f 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -200,9 +200,12 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration) } }, additionalProperties: false, - default: { - 'pub.name': false - } + default: {}, + defaultSnippets: [{ + 'body': { + 'pub.name': false + } + }] }, 'extensions.experimental.affinity': { type: 'object', @@ -214,9 +217,12 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration) } }, additionalProperties: false, - default: { - 'pub.name': 1 - } + default: {}, + defaultSnippets: [{ + 'body': { + 'pub.name': 1 + } + }] }, [WORKSPACE_TRUST_EXTENSION_SUPPORT]: { type: 'object', diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 2de5c8e1411..dd69b73511d 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -601,7 +601,7 @@ export abstract class InstallInOtherServerAction extends ExtensionAction { this.extensionsWorkbenchService.open(this.extension); alert(localize('installExtensionStart', "Installing extension {0} started. An editor is now open with more details on this extension", this.extension.displayName)); if (this.extension.gallery) { - await this.server.extensionManagementService.installFromGallery(this.extension.gallery); + await this.server.extensionManagementService.installFromGallery(this.extension.gallery, { installPreReleaseVersion: this.extension.local?.preRelease }); } else { const vsix = await this.extension.server!.extensionManagementService.zip(this.extension.local!); await this.server.extensionManagementService.install(vsix); @@ -2348,6 +2348,11 @@ export class ExtensionStatusAction extends ExtensionAction { } } + if (isEnabled && !isRunning && !this.extension.local.isValid) { + const errors = this.extension.local.validations.filter(([severity]) => severity === Severity.Error).map(([, message]) => message); + this.updateStatus({ icon: errorIcon, message: new MarkdownString(errors.join(' ').trim()) }, true); + } + } private updateStatus(status: ExtensionStatus | undefined, updateClass: boolean): void { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts index f51545869aa..cf3f3473212 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts @@ -719,11 +719,9 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE const message = err && err.message || ''; if (/ECONNREFUSED/.test(message)) { - const error = createErrorWithActions(localize('suggestProxyError', "Marketplace returned 'ECONNREFUSED'. Please check the 'http.proxy' setting."), { - actions: [ - new Action('open user settings', localize('open user settings', "Open User Settings"), undefined, true, () => this.preferencesService.openUserSettings()) - ] - }); + const error = createErrorWithActions(localize('suggestProxyError', "Marketplace returned 'ECONNREFUSED'. Please check the 'http.proxy' setting."), [ + new Action('open user settings', localize('open user settings', "Open User Settings"), undefined, true, () => this.preferencesService.openUserSettings()) + ]); this.notificationService.error(error); return; diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index d7455895d9e..de628c54a8d 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -972,11 +972,9 @@ export class ExtensionsListView extends ViewPane { const message = err && err.message || ''; if (/ECONNREFUSED/.test(message)) { - const error = createErrorWithActions(localize('suggestProxyError', "Marketplace returned 'ECONNREFUSED'. Please check the 'http.proxy' setting."), { - actions: [ - new Action('open user settings', localize('open user settings', "Open User Settings"), undefined, true, () => this.preferencesService.openUserSettings()) - ] - }); + const error = createErrorWithActions(localize('suggestProxyError', "Marketplace returned 'ECONNREFUSED'. Please check the 'http.proxy' setting."), [ + new Action('open user settings', localize('open user settings', "Open User Settings"), undefined, true, () => this.preferencesService.openUserSettings()) + ]); this.notificationService.error(error); return; diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts index 522cc6346be..76d062f4c5a 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 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'; @@ -480,7 +481,10 @@ export class ExtensionHoverWidget extends ExtensionWidget { } const markdown = new MarkdownString('', { isTrusted: true, supportThemeIcons: true }); - markdown.appendMarkdown(`**${this.extension.displayName}** <span style="background-color:#8080802B;">** _v${this.extension.version}_** </span>`); + markdown.appendMarkdown(`**${this.extension.displayName}**`); + if (semver.valid(this.extension.version)) { + markdown.appendMarkdown(` <span style="background-color:#8080802B;">** _v${this.extension.version}_** </span>`); + } if (this.extension.state === ExtensionState.Installed ? this.extension.local?.isPreReleaseVersion : this.extension.gallery?.properties.isPreReleaseVersion) { const extensionPreReleaseIcon = this.themeService.getColorTheme().getColor(extensionPreReleaseIconColor); markdown.appendMarkdown(`** ** <span style="color:#ffffff;background-color:${extensionPreReleaseIcon ? Color.Format.CSS.formatHex(extensionPreReleaseIcon) : '#ffffff'};"> $(${preReleaseIcon.id}) ${localize('pre-release-label', "Pre-Release")} </span>`); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 2b0a0fad05b..57acb10a415 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -8,7 +8,7 @@ import * as semver from 'vs/base/common/semver/semver'; import { Event, Emitter } from 'vs/base/common/event'; import { index, distinct } from 'vs/base/common/arrays'; import { Promises, ThrottledDelayer } from 'vs/base/common/async'; -import { canceled, isCancellationError } from 'vs/base/common/errors'; +import { CancellationError, isCancellationError } from 'vs/base/common/errors'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { IPager, singlePagePager } from 'vs/base/common/paging'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -203,17 +203,25 @@ export class Extension implements IExtension { } get outdated(): boolean { - if (!this.gallery || !this.local) { - return false; - } - if (!this.local.preRelease && this.gallery.properties.isPreReleaseVersion) { - return false; - } - if (semver.gt(this.latestVersion, this.version)) { - return true; - } - if (this.outdatedTargetPlatform) { - return true; + try { + if (!this.gallery || !this.local) { + return false; + } + // Do not allow updating system extensions in stable + if (this.type === ExtensionType.System && this.productService.quality === 'stable') { + return false; + } + if (!this.local.preRelease && this.gallery.properties.isPreReleaseVersion) { + return false; + } + if (semver.gt(this.latestVersion, this.version)) { + return true; + } + if (this.outdatedTargetPlatform) { + return true; + } + } catch (error) { + /* Ignore */ } return false; } @@ -1057,8 +1065,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) { - // Skip if the builtin extension does not have Marketplace id + 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. continue; } infos.push({ ...installed.identifier, preRelease: !!installed.local?.preRelease }); @@ -1353,7 +1361,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } } ], { - onCancel: () => reject(canceled()) + onCancel: () => reject(new CancellationError()) }); }); } diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extension.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extension.test.ts index 335679af2a9..f07341fe991 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extension.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extension.test.ts @@ -12,6 +12,7 @@ import { URI } from 'vs/base/common/uri'; import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { generateUuid } from 'vs/base/common/uuid'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { IProductService } from 'vs/platform/product/common/productService'; suite('Extension Test', () => { @@ -19,6 +20,7 @@ suite('Extension Test', () => { setup(() => { instantiationService = new TestInstantiationService(); + instantiationService.stub(IProductService, <Partial<IProductService>>{ quality: 'insiders' }); }); test('extension is not outdated when there is no local and gallery', () => { @@ -51,6 +53,12 @@ suite('Extension Test', () => { assert.strictEqual(extension.outdated, true); }); + test('extension is not outdated when local is built in and older than gallery but product quality is stable', () => { + instantiationService.stub(IProductService, <Partial<IProductService>>{ quality: 'stable' }); + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, undefined, aLocalExtension('somext', { version: '1.0.0' }, { type: ExtensionType.System }), aGalleryExtension('somext', { version: '1.0.1' })); + assert.strictEqual(extension.outdated, false); + }); + test('extension is outdated when local and gallery are on same version but on different target platforms', () => { const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, undefined, aLocalExtension('somext', {}, { targetPlatform: TargetPlatform.WIN32_IA32 }), aGalleryExtension('somext', {}, { targetPlatform: TargetPlatform.WIN32_X64 })); assert.strictEqual(extension.outdated, true); diff --git a/src/vs/workbench/contrib/feedback/browser/feedback.ts b/src/vs/workbench/contrib/feedback/browser/feedback.ts index 46fc36b304e..77be54e32c4 100644 --- a/src/vs/workbench/contrib/feedback/browser/feedback.ts +++ b/src/vs/workbench/contrib/feedback/browser/feedback.ts @@ -4,15 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/feedback'; -import * as nls from 'vs/nls'; +import { localize } from 'vs/nls'; import { IDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; -import * as dom from 'vs/base/browser/dom'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IIntegrityService } from 'vs/workbench/services/integrity/common/integrity'; import { IThemeService, registerThemingParticipant, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; import { attachButtonStyler, attachStylerCallback } from 'vs/platform/theme/common/styler'; import { editorWidgetBackground, editorWidgetForeground, widgetShadow, inputBorder, inputForeground, inputBackground, inputActiveOptionBorder, editorBackground, textLinkForeground, contrastBorder } from 'vs/platform/theme/common/colorRegistry'; +import { append, $, addDisposableListener, EventType, EventHelper, prepend } from 'vs/base/browser/dom'; import { IAnchor } from 'vs/base/browser/ui/contextview/contextview'; import { Button } from 'vs/base/browser/ui/button/button'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -114,25 +114,26 @@ export class FeedbackWidget extends Disposable { container.classList.add('monaco-menu-container'); // Form - this.feedbackForm = dom.append<HTMLFormElement>(container, dom.$('form.feedback-form')); + this.feedbackForm = append<HTMLFormElement>(container, $('form.feedback-form')); this.feedbackForm.setAttribute('action', 'javascript:void(0);'); // Title - dom.append(this.feedbackForm, dom.$('h2.title')).textContent = nls.localize("label.sendASmile", "Tweet us your feedback."); + append(this.feedbackForm, $('h2.title')).textContent = localize("label.sendASmile", "Tweet us your feedback."); // Close Button (top right) - const closeBtn = dom.append(this.feedbackForm, dom.$('div.cancel' + Codicon.close.cssSelector)); + const closeBtn = append(this.feedbackForm, $(`div.cancel${Codicon.close.cssSelector}`)); closeBtn.tabIndex = 0; closeBtn.setAttribute('role', 'button'); - closeBtn.title = nls.localize('close', "Close"); + closeBtn.title = localize('close', "Close"); - disposables.add(dom.addDisposableListener(container, dom.EventType.KEY_DOWN, keyboardEvent => { + disposables.add(addDisposableListener(container, EventType.KEY_DOWN, keyboardEvent => { const standardKeyboardEvent = new StandardKeyboardEvent(keyboardEvent); if (standardKeyboardEvent.keyCode === KeyCode.Escape) { this.hide(); } })); - disposables.add(dom.addDisposableListener(closeBtn, dom.EventType.MOUSE_OVER, () => { + + disposables.add(addDisposableListener(closeBtn, EventType.MOUSE_OVER, () => { const theme = this.themeService.getColorTheme(); let darkenFactor: number | undefined; switch (theme.type) { @@ -155,47 +156,47 @@ export class FeedbackWidget extends Disposable { } })); - disposables.add(dom.addDisposableListener(closeBtn, dom.EventType.MOUSE_OUT, () => { + disposables.add(addDisposableListener(closeBtn, EventType.MOUSE_OUT, () => { closeBtn.style.backgroundColor = ''; })); this.invoke(closeBtn, disposables, () => this.hide()); // Content - const content = dom.append(this.feedbackForm, dom.$('div.content')); + const content = append(this.feedbackForm, $('div.content')); // Sentiment Buttons - const sentimentContainer = dom.append(content, dom.$('div')); + const sentimentContainer = append(content, $('div')); if (!this.isPure) { - dom.append(sentimentContainer, dom.$('span')).textContent = nls.localize("patchedVersion1", "Your installation is corrupt."); + append(sentimentContainer, $('span')).textContent = localize("patchedVersion1", "Your installation is corrupt."); sentimentContainer.appendChild(document.createElement('br')); - dom.append(sentimentContainer, dom.$('span')).textContent = nls.localize("patchedVersion2", "Please specify this if you submit a bug."); + append(sentimentContainer, $('span')).textContent = localize("patchedVersion2", "Please specify this if you submit a bug."); sentimentContainer.appendChild(document.createElement('br')); } - dom.append(sentimentContainer, dom.$('span')).textContent = nls.localize("sentiment", "How was your experience?"); + append(sentimentContainer, $('span')).textContent = localize("sentiment", "How was your experience?"); - const feedbackSentiment = dom.append(sentimentContainer, dom.$('div.feedback-sentiment')); + const feedbackSentiment = append(sentimentContainer, $('div.feedback-sentiment')); // Sentiment: Smiley - this.smileyInput = dom.append(feedbackSentiment, dom.$('div.sentiment')); + this.smileyInput = append(feedbackSentiment, $('div.sentiment')); this.smileyInput.classList.add('smile'); this.smileyInput.setAttribute('aria-checked', 'false'); - this.smileyInput.setAttribute('aria-label', nls.localize('smileCaption', "Happy Feedback Sentiment")); + this.smileyInput.setAttribute('aria-label', localize('smileCaption', "Happy Feedback Sentiment")); this.smileyInput.setAttribute('role', 'checkbox'); - this.smileyInput.title = nls.localize('smileCaption', "Happy Feedback Sentiment"); + this.smileyInput.title = localize('smileCaption', "Happy Feedback Sentiment"); this.smileyInput.tabIndex = 0; this.invoke(this.smileyInput, disposables, () => this.setSentiment(true)); // Sentiment: Frowny - this.frownyInput = dom.append(feedbackSentiment, dom.$('div.sentiment')); + this.frownyInput = append(feedbackSentiment, $('div.sentiment')); this.frownyInput.classList.add('frown'); this.frownyInput.setAttribute('aria-checked', 'false'); - this.frownyInput.setAttribute('aria-label', nls.localize('frownCaption', "Sad Feedback Sentiment")); + this.frownyInput.setAttribute('aria-label', localize('frownCaption', "Sad Feedback Sentiment")); this.frownyInput.setAttribute('role', 'checkbox'); - this.frownyInput.title = nls.localize('frownCaption', "Sad Feedback Sentiment"); + this.frownyInput.title = localize('frownCaption', "Sad Feedback Sentiment"); this.frownyInput.tabIndex = 0; this.invoke(this.frownyInput, disposables, () => this.setSentiment(false)); @@ -209,23 +210,23 @@ export class FeedbackWidget extends Disposable { } // Contact Us Box - const contactUsContainer = dom.append(content, dom.$('div.contactus')); + const contactUsContainer = append(content, $('div.contactus')); - dom.append(contactUsContainer, dom.$('span')).textContent = nls.localize("other ways to contact us", "Other ways to contact us"); + append(contactUsContainer, $('span')).textContent = localize("other ways to contact us", "Other ways to contact us"); - const channelsContainer = dom.append(contactUsContainer, dom.$('div.channels')); + const channelsContainer = append(contactUsContainer, $('div.channels')); // Contact: Submit a Bug - const submitBugLinkContainer = dom.append(channelsContainer, dom.$('div')); + const submitBugLinkContainer = append(channelsContainer, $('div')); - const submitBugLink = dom.append(submitBugLinkContainer, dom.$('a')); + const submitBugLink = append(submitBugLinkContainer, $('a')); submitBugLink.setAttribute('target', '_blank'); submitBugLink.setAttribute('href', '#'); - submitBugLink.textContent = nls.localize("submit a bug", "Submit a bug"); + submitBugLink.textContent = localize("submit a bug", "Submit a bug"); submitBugLink.tabIndex = 0; - disposables.add(dom.addDisposableListener(submitBugLink, 'click', e => { - dom.EventHelper.stop(e); + disposables.add(addDisposableListener(submitBugLink, 'click', e => { + EventHelper.stop(e); const actionId = 'workbench.action.openIssueReporter'; this.commandService.executeCommand(actionId); this.hide(); @@ -234,57 +235,57 @@ export class FeedbackWidget extends Disposable { // Contact: Request a Feature if (!!this.requestFeatureLink) { - const requestFeatureLinkContainer = dom.append(channelsContainer, dom.$('div')); + const requestFeatureLinkContainer = append(channelsContainer, $('div')); - const requestFeatureLink = dom.append(requestFeatureLinkContainer, dom.$('a')); + const requestFeatureLink = append(requestFeatureLinkContainer, $('a')); requestFeatureLink.setAttribute('target', '_blank'); requestFeatureLink.setAttribute('href', this.requestFeatureLink); - requestFeatureLink.textContent = nls.localize("request a missing feature", "Request a missing feature"); + requestFeatureLink.textContent = localize("request a missing feature", "Request a missing feature"); requestFeatureLink.tabIndex = 0; - disposables.add(dom.addDisposableListener(requestFeatureLink, 'click', e => this.hide())); + disposables.add(addDisposableListener(requestFeatureLink, 'click', e => this.hide())); } // Remaining Characters - const remainingCharacterCountContainer = dom.append(this.feedbackForm, dom.$('h3')); - remainingCharacterCountContainer.textContent = nls.localize("tell us why", "Tell us why?"); + const remainingCharacterCountContainer = append(this.feedbackForm, $('h3')); + remainingCharacterCountContainer.textContent = localize("tell us why", "Tell us why?"); - this.remainingCharacterCount = dom.append(remainingCharacterCountContainer, dom.$('span.char-counter')); + this.remainingCharacterCount = append(remainingCharacterCountContainer, $('span.char-counter')); this.remainingCharacterCount.textContent = this.getCharCountText(0); // Feedback Input Form - this.feedbackDescriptionInput = dom.append<HTMLTextAreaElement>(this.feedbackForm, dom.$('textarea.feedback-description')); + this.feedbackDescriptionInput = append<HTMLTextAreaElement>(this.feedbackForm, $('textarea.feedback-description')); this.feedbackDescriptionInput.rows = 3; this.feedbackDescriptionInput.maxLength = this.maxFeedbackCharacters; this.feedbackDescriptionInput.textContent = this.feedback; this.feedbackDescriptionInput.required = true; - this.feedbackDescriptionInput.setAttribute('aria-label', nls.localize("feedbackTextInput", "Tell us your feedback")); + this.feedbackDescriptionInput.setAttribute('aria-label', localize("feedbackTextInput", "Tell us your feedback")); this.feedbackDescriptionInput.focus(); - disposables.add(dom.addDisposableListener(this.feedbackDescriptionInput, 'keyup', () => this.updateCharCountText())); + disposables.add(addDisposableListener(this.feedbackDescriptionInput, 'keyup', () => this.updateCharCountText())); // Feedback Input Form Buttons Container - const buttonsContainer = dom.append(this.feedbackForm, dom.$('div.form-buttons')); + const buttonsContainer = append(this.feedbackForm, $('div.form-buttons')); // Checkbox: Hide Feedback Smiley - const hideButtonContainer = dom.append(buttonsContainer, dom.$('div.hide-button-container')); + const hideButtonContainer = append(buttonsContainer, $('div.hide-button-container')); - this.hideButton = dom.append(hideButtonContainer, dom.$('input.hide-button')) as HTMLInputElement; + this.hideButton = append(hideButtonContainer, $('input.hide-button')) as HTMLInputElement; this.hideButton.type = 'checkbox'; this.hideButton.checked = true; this.hideButton.id = 'hide-button'; - const hideButtonLabel = dom.append(hideButtonContainer, dom.$('label')); + const hideButtonLabel = append(hideButtonContainer, $('label')); hideButtonLabel.setAttribute('for', 'hide-button'); - hideButtonLabel.textContent = nls.localize('showFeedback', "Show Feedback Icon in Status Bar"); + hideButtonLabel.textContent = localize('showFeedback', "Show Feedback Icon in Status Bar"); // Button: Send Feedback this.sendButton = new Button(buttonsContainer); this.sendButton.enabled = false; - this.sendButton.label = nls.localize('tweet', "Tweet"); - dom.prepend(this.sendButton.element, dom.$('span' + Codicon.twitter.cssSelector)); + this.sendButton.label = localize('tweet', "Tweet"); + prepend(this.sendButton.element, $(`span${Codicon.twitter.cssSelector}`)); this.sendButton.element.classList.add('send'); - this.sendButton.element.title = nls.localize('tweetFeedback', "Tweet Feedback"); + this.sendButton.element.title = localize('tweetFeedback', "Tweet Feedback"); disposables.add(attachButtonStyler(this.sendButton, this.themeService)); this.sendButton.onDidClick(() => this.onSubmit()); @@ -326,8 +327,8 @@ export class FeedbackWidget extends Disposable { private getCharCountText(charCount: number): string { const remaining = this.maxFeedbackCharacters - charCount; const text = (remaining === 1) - ? nls.localize("character left", "character left") - : nls.localize("characters left", "characters left"); + ? localize("character left", "character left") + : localize("characters left", "characters left"); return `(${remaining} ${text})`; } @@ -370,9 +371,9 @@ export class FeedbackWidget extends Disposable { } private invoke(element: HTMLElement, disposables: DisposableStore, callback: () => void): HTMLElement { - disposables.add(dom.addDisposableListener(element, 'click', callback)); + disposables.add(addDisposableListener(element, 'click', callback)); - disposables.add(dom.addDisposableListener(element, 'keypress', e => { + disposables.add(addDisposableListener(element, 'keypress', e => { if (e instanceof KeyboardEvent) { const keyboardEvent = <KeyboardEvent>e; if (keyboardEvent.keyCode === 13 || keyboardEvent.keyCode === 32) { // Enter or Spacebar diff --git a/src/vs/workbench/contrib/files/browser/editors/fileEditorInput.ts b/src/vs/workbench/contrib/files/browser/editors/fileEditorInput.ts index 630cd3a523a..b6317eb2dd7 100644 --- a/src/vs/workbench/contrib/files/browser/editors/fileEditorInput.ts +++ b/src/vs/workbench/contrib/files/browser/editors/fileEditorInput.ts @@ -60,6 +60,10 @@ export class FileEditorInput extends AbstractTextResourceEditorInput implements } } + if (!(capabilities & EditorInputCapabilities.Readonly)) { + capabilities |= EditorInputCapabilities.CanDropIntoEditor; + } + return capabilities; } diff --git a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts index f7eca9c59df..e77d66da61a 100644 --- a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts +++ b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts @@ -6,7 +6,7 @@ import { localize } from 'vs/nls'; import { assertIsDefined } from 'vs/base/common/types'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; -import { toAction } from 'vs/base/common/actions'; +import { 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'; @@ -26,7 +26,7 @@ import { ICodeEditorViewState, ScrollType } from 'vs/editor/common/editorCommon' import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { IErrorWithActions } from 'vs/base/common/errorMessage'; +import { 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'; @@ -180,15 +180,29 @@ export class TextFileEditor extends BaseTextEditor<ICodeEditorViewState> { // Similar, handle case where we were asked to open a folder in the text editor. if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_IS_DIRECTORY) { - this.openAsFolder(input); + let action: IAction; + if (this.contextService.isInsideWorkspace(input.preferredResource)) { + action = toAction({ + id: 'workbench.files.action.reveal', label: localize('reveal', "Reveal in Explorer View"), run: async () => { + await this.paneCompositeService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar, true); - throw new Error(localize('openFolderError', "File is a directory")); + return this.explorerService.select(input.preferredResource, true); + } + }); + } else { + action = toAction({ + id: 'workbench.files.action.ok', label: localize('ok', "OK"), run: async () => { + // No operation possible, but clicking OK will close the editor + } + }); + } + + throw createErrorWithActions(new FileOperationError(localize('fileIsDirectoryError', "File is a directory"), FileOperationResult.FILE_IS_DIRECTORY), [action]); } // Offer to create a file from the error if we have a file not found and the name is valid if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND && await this.pathService.hasValidBasename(input.preferredResource)) { - const fileNotFoundError: FileOperationError & IErrorWithActions = new FileOperationError(localize('fileNotFoundError', "File not found"), FileOperationResult.FILE_NOT_FOUND); - fileNotFoundError.actions = [ + const fileNotFoundError = createErrorWithActions(new FileOperationError(localize('fileNotFoundError', "File not found"), FileOperationResult.FILE_NOT_FOUND), [ toAction({ id: 'workbench.files.action.createMissingFile', label: localize('createFile', "Create File"), run: async () => { await this.textFileService.create([{ resource: input.preferredResource }]); @@ -201,7 +215,7 @@ export class TextFileEditor extends BaseTextEditor<ICodeEditorViewState> { }); } }) - ]; + ]); throw fileNotFoundError; } @@ -262,22 +276,6 @@ export class TextFileEditor extends BaseTextEditor<ICodeEditorViewState> { group.openEditor(editor, editorOptions); } - private async openAsFolder(input: FileEditorInput): Promise<void> { - if (!this.group) { - return; - } - - // Since we cannot open a folder, we have to restore the previous input if any and close the editor - await this.group.closeEditor(this.input); - - // Best we can do is to reveal the folder in the explorer - if (this.contextService.isInsideWorkspace(input.preferredResource)) { - await this.paneCompositeService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar); - - this.explorerService.select(input.preferredResource, true); - } - } - override clearInput(): void { super.clearInput(); diff --git a/src/vs/workbench/contrib/files/browser/explorerService.ts b/src/vs/workbench/contrib/files/browser/explorerService.ts index 5293c2640ca..a1d5910f284 100644 --- a/src/vs/workbench/contrib/files/browser/explorerService.ts +++ b/src/vs/workbench/contrib/files/browser/explorerService.ts @@ -33,8 +33,7 @@ export class ExplorerService implements IExplorerService { private readonly disposables = new DisposableStore(); private editable: { stat: ExplorerItem; data: IEditableData } | undefined; - private _sortOrder: SortOrder; - private _lexicographicOptions: LexicographicOptions; + private config: IFilesConfiguration['explorer']; private cutItems: ExplorerItem[] | undefined; private view: IExplorerView | undefined; private model: ExplorerModel; @@ -52,8 +51,7 @@ export class ExplorerService implements IExplorerService { @IProgressService private readonly progressService: IProgressService, @IHostService hostService: IHostService ) { - this._sortOrder = this.configurationService.getValue('explorer.sortOrder'); - this._lexicographicOptions = this.configurationService.getValue('explorer.sortOrderLexicographicOptions'); + this.config = this.configurationService.getValue('explorer'); this.model = new ExplorerModel(this.contextService, this.uriIdentityService, this.fileService, this.configurationService); this.disposables.add(this.model); @@ -65,7 +63,7 @@ export class ExplorerService implements IExplorerService { // Filter to the ones we care const types = [FileChangeType.DELETED]; - if (this._sortOrder === SortOrder.Modified) { + if (this.config.sortOrder === SortOrder.Modified) { types.push(FileChangeType.UPDATED); } @@ -142,8 +140,8 @@ export class ExplorerService implements IExplorerService { get sortOrderConfiguration(): ISortOrderConfiguration { return { - sortOrder: this._sortOrder, - lexicographicOptions: this._lexicographicOptions, + sortOrder: this.config.sortOrder, + lexicographicOptions: this.config.sortOrderLexicographicOptions, }; } @@ -151,21 +149,19 @@ export class ExplorerService implements IExplorerService { this.view = contextProvider; } - getContext(respectMultiSelection: boolean, includeNestedChildren = false): ExplorerItem[] { + getContext(respectMultiSelection: boolean): ExplorerItem[] { if (!this.view) { return []; } const items = new Set<ExplorerItem>(this.view.getContext(respectMultiSelection)); - if (includeNestedChildren) { - items.forEach(item => { - if (item.nestedChildren) { - for (const child of item.nestedChildren) { - items.add(child); - } + items.forEach(item => { + if (this.view?.isItemCollapsed(item) && item.nestedChildren) { + for (const child of item.nestedChildren) { + items.add(child); } - }); - } + } + }); return [...items]; } @@ -261,7 +257,7 @@ export class ExplorerService implements IExplorerService { } // Stat needs to be resolved first and then revealed - const options: IResolveFileOptions = { resolveTo: [resource], resolveMetadata: this._sortOrder === SortOrder.Modified }; + const options: IResolveFileOptions = { resolveTo: [resource], resolveMetadata: this.config.sortOrder === SortOrder.Modified }; const root = this.findClosestRoot(resource); if (!root) { return undefined; @@ -302,6 +298,11 @@ export class ExplorerService implements IExplorerService { // File events private async onDidRunOperation(e: FileOperationEvent): Promise<void> { + // When nesting, changes to one file in a folder may impact the rendered structure + // of all the folder's immediate children, thus a recursive refresh is needed. + // Ideally the tree would be able to recusively refresh just one level but that does not yet exist. + const shouldDeepRefresh = this.config.fileNesting.enabled; + // Add if (e.isOperation(FileOperation.CREATE) || e.isOperation(FileOperation.COPY)) { const addedElement = e.target; @@ -313,7 +314,7 @@ export class ExplorerService implements IExplorerService { // Add the new file to its parent (Model) await Promise.all(parents.map(async p => { // We have to check if the parent is resolved #29177 - const resolveMetadata = this._sortOrder === `modified`; + const resolveMetadata = this.config.sortOrder === `modified`; if (!p.isDirectoryResolved) { const stat = await this.fileService.resolve(p.resource, { resolveMetadata }); if (stat) { @@ -327,7 +328,7 @@ export class ExplorerService implements IExplorerService { p.removeChild(childElement); p.addChild(childElement); // Refresh the Parent (View) - await this.view?.refresh(false, p); + await this.view?.refresh(shouldDeepRefresh, p); })); } } @@ -346,7 +347,7 @@ export class ExplorerService implements IExplorerService { await Promise.all(modelElements.map(async modelElement => { // Rename File (Model) modelElement.rename(newElement); - await this.view?.refresh(false, modelElement.parent); + await this.view?.refresh(shouldDeepRefresh, modelElement.parent); })); } @@ -363,7 +364,7 @@ export class ExplorerService implements IExplorerService { await this.view?.refresh(false, oldNestedParent); } await this.view?.refresh(false, oldParent); - await this.view?.refresh(false, newParents[index]); + await this.view?.refresh(shouldDeepRefresh, newParents[index]); })); } } @@ -384,7 +385,7 @@ export class ExplorerService implements IExplorerService { await this.view?.refresh(false, oldNestedParent); } // Refresh Parent (View) - await this.view?.refresh(false, parent); + await this.view?.refresh(shouldDeepRefresh, parent); } })); } @@ -393,22 +394,22 @@ export class ExplorerService implements IExplorerService { private async onConfigurationUpdated(configuration: IFilesConfiguration, event?: IConfigurationChangeEvent): Promise<void> { let shouldRefresh = false; - if (event?.affectedKeys.some(x => x.startsWith('explorer.experimental.fileNesting.'))) { + if (event?.affectedKeys.some(x => x.startsWith('explorer.fileNesting.'))) { shouldRefresh = true; } const configSortOrder = configuration?.explorer?.sortOrder || SortOrder.Default; - if (this._sortOrder !== configSortOrder) { - shouldRefresh = this._sortOrder !== undefined; - this._sortOrder = configSortOrder; + if (this.config.sortOrder !== configSortOrder) { + shouldRefresh = this.config.sortOrder !== undefined; } const configLexicographicOptions = configuration?.explorer?.sortOrderLexicographicOptions || LexicographicOptions.Default; - if (this._lexicographicOptions !== configLexicographicOptions) { - shouldRefresh = shouldRefresh || this._lexicographicOptions !== undefined; - this._lexicographicOptions = configLexicographicOptions; + if (this.config.sortOrderLexicographicOptions !== configLexicographicOptions) { + shouldRefresh = shouldRefresh || this.config.sortOrderLexicographicOptions !== undefined; } + this.config = configuration.explorer; + if (shouldRefresh) { await this.refresh(); } diff --git a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts index d88bd1c2d2b..57bb796dfc4 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts @@ -154,15 +154,16 @@ const copyRelativePathCommand = { // Editor Title Context Menu appendEditorTitleContextMenuItem(COPY_PATH_COMMAND_ID, copyPathCommand.title, ResourceContextKey.IsFileSystemResource, '1_cutcopypaste'); appendEditorTitleContextMenuItem(COPY_RELATIVE_PATH_COMMAND_ID, copyRelativePathCommand.title, ResourceContextKey.IsFileSystemResource, '1_cutcopypaste'); -appendEditorTitleContextMenuItem(REVEAL_IN_EXPLORER_COMMAND_ID, nls.localize('revealInSideBar', "Reveal in Explorer View"), ResourceContextKey.IsFileSystemResource); +appendEditorTitleContextMenuItem(REVEAL_IN_EXPLORER_COMMAND_ID, nls.localize('revealInSideBar', "Reveal in Explorer View"), ResourceContextKey.IsFileSystemResource, '2_files', 1); -export function appendEditorTitleContextMenuItem(id: string, title: string, when: ContextKeyExpression | undefined, group?: string): void { +export function appendEditorTitleContextMenuItem(id: string, title: string, when: ContextKeyExpression | undefined, group: string, order?: number): void { // Menu MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id, title }, when, - group: group || '2_files' + group, + order }); } @@ -576,7 +577,7 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { }); // Empty Editor Group Context Menu -MenuRegistry.appendMenuItem(MenuId.EmptyEditorGroupContext, { command: { id: NEW_UNTITLED_FILE_COMMAND_ID, title: nls.localize('newFile', "New File") }, group: '1_file', order: 10 }); +MenuRegistry.appendMenuItem(MenuId.EmptyEditorGroupContext, { command: { id: NEW_UNTITLED_FILE_COMMAND_ID, title: nls.localize('newFile', "New Text File") }, group: '1_file', order: 10 }); MenuRegistry.appendMenuItem(MenuId.EmptyEditorGroupContext, { command: { id: 'workbench.action.quickOpen', title: nls.localize('openFile', "Open File...") }, group: '1_file', order: 20 }); // File menu @@ -585,7 +586,7 @@ MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { group: '1_new', command: { id: NEW_UNTITLED_FILE_COMMAND_ID, - title: nls.localize({ key: 'miNewFile', comment: ['&& denotes a mnemonic'] }, "&&New File") + title: nls.localize({ key: 'miNewFile', comment: ['&& denotes a mnemonic'] }, "&&New Text File") }, order: 1 }); diff --git a/src/vs/workbench/contrib/files/browser/fileActions.ts b/src/vs/workbench/contrib/files/browser/fileActions.ts index d3100d24946..56160c6d47d 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.ts @@ -906,9 +906,7 @@ export const renameHandler = async (accessor: ServicesAccessor) => { export const moveFileToTrashHandler = async (accessor: ServicesAccessor) => { const explorerService = accessor.get(IExplorerService); - const configurationService = accessor.get(IConfigurationService); - const groupNests = configurationService.getValue<IFilesConfiguration>().explorer.experimental.fileNesting.operateAsGroup; - const stats = explorerService.getContext(true, groupNests).filter(s => !s.isRoot); + const stats = explorerService.getContext(true).filter(s => !s.isRoot); if (stats.length) { await deleteFiles(accessor.get(IExplorerService), accessor.get(IWorkingCopyFileService), accessor.get(IDialogService), accessor.get(IConfigurationService), stats, true); } @@ -916,9 +914,7 @@ export const moveFileToTrashHandler = async (accessor: ServicesAccessor) => { export const deleteFileHandler = async (accessor: ServicesAccessor) => { const explorerService = accessor.get(IExplorerService); - const configurationService = accessor.get(IConfigurationService); - const groupNests = configurationService.getValue<IFilesConfiguration>().explorer.experimental.fileNesting.operateAsGroup; - const stats = explorerService.getContext(true, groupNests).filter(s => !s.isRoot); + const stats = explorerService.getContext(true).filter(s => !s.isRoot); if (stats.length) { await deleteFiles(accessor.get(IExplorerService), accessor.get(IWorkingCopyFileService), accessor.get(IDialogService), accessor.get(IConfigurationService), stats, false); @@ -928,9 +924,7 @@ export const deleteFileHandler = async (accessor: ServicesAccessor) => { let pasteShouldMove = false; export const copyFileHandler = async (accessor: ServicesAccessor) => { const explorerService = accessor.get(IExplorerService); - const configurationService = accessor.get(IConfigurationService); - const groupNests = configurationService.getValue<IFilesConfiguration>().explorer.experimental.fileNesting.operateAsGroup; - const stats = explorerService.getContext(true, groupNests); + const stats = explorerService.getContext(true); if (stats.length > 0) { await explorerService.setToCopy(stats, false); pasteShouldMove = false; @@ -939,9 +933,7 @@ export const copyFileHandler = async (accessor: ServicesAccessor) => { export const cutFileHandler = async (accessor: ServicesAccessor) => { const explorerService = accessor.get(IExplorerService); - const configurationService = accessor.get(IConfigurationService); - const groupNests = configurationService.getValue<IFilesConfiguration>().explorer.experimental.fileNesting.operateAsGroup; - const stats = explorerService.getContext(true, groupNests); + const stats = explorerService.getContext(true); if (stats.length > 0) { await explorerService.setToCopy(stats, true); pasteShouldMove = true; diff --git a/src/vs/workbench/contrib/files/browser/fileImportExport.ts b/src/vs/workbench/contrib/files/browser/fileImportExport.ts index 08fdd59d803..71eda39201c 100644 --- a/src/vs/workbench/contrib/files/browser/fileImportExport.ts +++ b/src/vs/workbench/contrib/files/browser/fileImportExport.ts @@ -14,7 +14,7 @@ import { IFilesConfiguration, UndoConfirmLevel, VIEW_ID } from 'vs/workbench/con import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { Limiter, Promises, RunOnceWorker } from 'vs/base/common/async'; import { newWriteableBufferStream, VSBuffer } from 'vs/base/common/buffer'; -import { basename, joinPath } from 'vs/base/common/resources'; +import { basename, dirname, joinPath } from 'vs/base/common/resources'; import { ResourceFileEdit } from 'vs/editor/browser/services/bulkEditService'; import { ExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel'; import { URI } from 'vs/base/common/uri'; @@ -35,6 +35,7 @@ import { canceled } from 'vs/base/common/errors'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { WebFileSystemAccess } from 'vs/platform/files/browser/webFileSystemAccess'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; //#region Browser File Upload (drag and drop, input element) @@ -574,12 +575,15 @@ interface IDownloadOperation { export class FileDownload { + private static readonly LAST_USED_DOWNLOAD_PATH_STORAGE_KEY = 'workbench.explorer.downloadPath'; + constructor( @IFileService private readonly fileService: IFileService, @IExplorerService private readonly explorerService: IExplorerService, @IProgressService private readonly progressService: IProgressService, @ILogService private readonly logService: ILogService, - @IFileDialogService private readonly fileDialogService: IFileDialogService + @IFileDialogService private readonly fileDialogService: IFileDialogService, + @IStorageService private readonly storageService: IStorageService ) { } @@ -791,12 +795,18 @@ export class FileDownload { private async doDownloadNative(explorerItem: ExplorerItem, progress: IProgress<IProgressStep>, cts: CancellationTokenSource): Promise<void> { progress.report({ message: explorerItem.name }); - const defaultUri = joinPath( - explorerItem.isDirectory ? - await this.fileDialogService.defaultFolderPath(Schemas.file) : - await this.fileDialogService.defaultFilePath(Schemas.file), - explorerItem.name - ); + let defaultUri: URI; + const lastUsedDownloadPath = this.storageService.get(FileDownload.LAST_USED_DOWNLOAD_PATH_STORAGE_KEY, StorageScope.GLOBAL); + if (lastUsedDownloadPath) { + defaultUri = joinPath(URI.file(lastUsedDownloadPath), explorerItem.name); + } else { + defaultUri = joinPath( + explorerItem.isDirectory ? + await this.fileDialogService.defaultFolderPath(Schemas.file) : + await this.fileDialogService.defaultFilePath(Schemas.file), + explorerItem.name + ); + } const destination = await this.fileDialogService.showSaveDialog({ availableFileSystems: [Schemas.file], @@ -806,6 +816,11 @@ 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); + + // Perform download await this.explorerService.applyBulkEdit([new ResourceFileEdit(explorerItem.resource, destination, { overwrite: true, copy: true })], { undoLabel: localize('downloadBulkEdit', "Download {0}", explorerItem.name), progressLabel: localize('downloadingBulkEdit', "Downloading {0}", explorerItem.name), diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index 1a6f2c8f672..b162ad2ead5 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -246,7 +246,7 @@ configurationRegistry.registerConfiguration({ 'files.watcherExclude': { 'type': 'object', 'default': { '**/.git/objects/**': true, '**/.git/subtree-cache/**': true, '**/node_modules/*/**': true, '**/.hg/store/**': true }, - 'markdownDescription': nls.localize('watcherExclude', "Configure paths or glob patterns to exclude from file watching. Paths that are relative (for example `build/output`) will be resolved to an absolute path using the currently opened workspace. Glob patterns must match on absolute paths (i.e. prefix with `**/` or the full path and suffix with `/**` to match files within a path) to match properly (for example `**/build/output/**` or `/Users/name/workspaces/project/build/output/**`). When you experience the file watcher process consuming a lot of CPU, make sure to exclude large folders that are of less interest (such as build output folders)."), + 'markdownDescription': nls.localize('watcherExclude', "Configure paths or glob patterns to exclude from file watching. Paths or basic glob patterns that are relative (for example `build/output` or `*.js`) will be resolved to an absolute path using the currently opened workspace. Complex glob patterns must match on absolute paths (i.e. prefix with `**/` or the full path and suffix with `/**` to match files within a path) to match properly (for example `**/build/output/**` or `/Users/name/workspaces/project/build/output/**`). When you experience the file watcher process consuming a lot of CPU, make sure to exclude large folders that are of less interest (such as build output folders)."), 'scope': ConfigurationScope.RESOURCE }, 'files.watcherInclude': { @@ -337,10 +337,16 @@ configurationRegistry.registerConfiguration({ 'properties': { 'explorer.openEditors.visible': { 'type': 'number', - 'description': nls.localize({ key: 'openEditorsVisible', comment: ['Open is an adjective'] }, "Number of editors shown in the Open Editors pane. Setting this to 0 hides the Open Editors pane."), + 'description': nls.localize({ key: 'openEditorsVisible', comment: ['Open is an adjective'] }, "The maximum number of editors shown in the Open Editors pane. Setting this to 0 hides the Open Editors pane."), 'default': 9, 'minimum': 0 }, + 'explorer.openEditors.minVisible': { + 'type': 'number', + 'description': nls.localize({ key: 'openEditorsVisibleMin', comment: ['Open is an adjective'] }, "The minimum number of editor slots shown in the Open Editors pane. If set to 0 the Open Editors pane will dynamically resize based on the number of editors."), + 'default': 0, + 'minimum': 0 + }, 'explorer.openEditors.sortOrder': { 'type': 'string', 'enum': ['editorOrder', 'alphabetical', 'fullPath'], @@ -411,7 +417,7 @@ configurationRegistry.registerConfiguration({ nls.localize('sortOrder.modified', 'Files and folders are sorted by last modified date in descending order. Folders are displayed before files.'), nls.localize('sortOrder.foldersNestsFiles', 'Files and folders are sorted by their names. Folders are displayed before files. Files with nested children are displayed before other files.') ], - 'markdownDescription': nls.localize('sortOrder', "Controls the property-based sorting of files and folders in the explorer. When `#explorer.experimental.fileNesting.enabled#` is enabled, also controls sorting of nested files.") + 'markdownDescription': nls.localize('sortOrder', "Controls the property-based sorting of files and folders in the explorer. When `#explorer.fileNesting.enabled#` is enabled, also controls sorting of nested files.") }, 'explorer.sortOrderLexicographicOptions': { 'type': 'string', @@ -465,23 +471,18 @@ configurationRegistry.registerConfiguration({ 'description': nls.localize('copyRelativePathSeparator', "The path separation character used when copying relative file paths."), 'default': 'auto' }, - 'explorer.experimental.fileNesting.enabled': { + 'explorer.fileNesting.enabled': { 'type': 'boolean', scope: ConfigurationScope.RESOURCE, - 'markdownDescription': nls.localize('fileNestingEnabled', "Experimental. Controls whether file nesting is enabled in the explorer. File nesting allows for related files in a directory to be visually grouped together under a single parent file."), + 'markdownDescription': nls.localize('fileNestingEnabled', "Controls whether file nesting is enabled in the explorer. File nesting allows for related files in a directory to be visually grouped together under a single parent file."), 'default': false, }, - 'explorer.experimental.fileNesting.expand': { - 'type': 'boolean', - 'markdownDescription': nls.localize('fileNestingExpand', "Experimental. Controls whether file nests are automatically expanded. `#explorer.experimental.fileNesting.enabled#` must be set for this to take effect."), - 'default': true, - }, - 'explorer.experimental.fileNesting.operateAsGroup': { + 'explorer.fileNesting.expand': { 'type': 'boolean', - 'markdownDescription': nls.localize('operateAsGroup', "Controls whether file nests are treated as a group for clipboard operations, file deletions, and during drag and drop."), + 'markdownDescription': nls.localize('fileNestingExpand', "Controls whether file nests are automatically expanded. `#explorer.fileNesting.enabled#` must be set for this to take effect."), 'default': true, }, - 'explorer.experimental.fileNesting.patterns': { + 'explorer.fileNesting.patterns': { 'type': 'object', scope: ConfigurationScope.RESOURCE, 'markdownDescription': nls.localize('fileNestingPatterns', "Controls nesting of files in the explorer. Each __Item__ represents a parent pattern and may contain a single `*` character that matches any string. Each __Value__ represents a comma separated list of the child patterns that should be shown nested under a given parent. Child patterns may contain several special tokens:\n- `${capture}`: Matches the resolved value of the `*` from the parent pattern\n- `${basename}`: Matches the parent file's basename, the `file` in `file.ts`\n- `${extname}`: Matches the parent file's extension, the `ts` in `file.ts`\n- `${dirname}`: Matches the parent file's directory name, the `src` in `src/file.ts`\n- `*`: Matches any string, may only be used once per child pattern"), diff --git a/src/vs/workbench/contrib/files/browser/files.ts b/src/vs/workbench/contrib/files/browser/files.ts index 3c034c4e388..b4b01687527 100644 --- a/src/vs/workbench/contrib/files/browser/files.ts +++ b/src/vs/workbench/contrib/files/browser/files.ts @@ -56,6 +56,7 @@ export interface IExplorerView { itemsCopied(tats: ExplorerItem[], cut: boolean, previousCut: ExplorerItem[] | undefined): void; setEditable(stat: ExplorerItem, isEditing: boolean): Promise<void>; isItemVisible(item: ExplorerItem): boolean; + isItemCollapsed(item: ExplorerItem): boolean; hasFocus(): boolean; } diff --git a/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/src/vs/workbench/contrib/files/browser/views/explorerView.ts index c82cc35150a..1ba98e190c7 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerView.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerView.ts @@ -56,7 +56,6 @@ import { Codicon } from 'vs/base/common/codicons'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IEditorResolverService } from 'vs/workbench/services/editor/common/editorResolverService'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; -import { INotificationService } from 'vs/platform/notification/common/notification'; import { EditorOpenSource } from 'vs/platform/editor/common/editor'; import { ResourceMap } from 'vs/base/common/map'; @@ -188,7 +187,6 @@ export class ExplorerView extends ViewPane implements IExplorerView { @IMenuService private readonly menuService: IMenuService, @ITelemetryService telemetryService: ITelemetryService, @IExplorerService private readonly explorerService: IExplorerService, - @INotificationService private readonly notificationService: INotificationService, @IStorageService private readonly storageService: IStorageService, @IClipboardService private clipboardService: IClipboardService, @IFileService private readonly fileService: IFileService, @@ -325,6 +323,10 @@ export class ExplorerView extends ViewPane implements IExplorerView { return this.filter.filter(item, TreeVisibility.Visible); } + isItemCollapsed(item: ExplorerItem): boolean { + return this.tree.isCollapsed(item); + } + async setEditable(stat: ExplorerItem, isEditing: boolean): Promise<void> { if (isEditing) { this.horizontalScrolling = this.tree.options.horizontalScrolling; @@ -384,10 +386,10 @@ export class ExplorerView extends ViewPane implements IExplorerView { const isCompressionEnabled = () => this.configurationService.getValue<boolean>('explorer.compactFolders'); - const getFileNestingSettings = (item?: ExplorerItem) => this.configurationService.getValue<IFilesConfiguration>({ resource: item?.root.resource }).explorer.experimental.fileNesting; + const getFileNestingSettings = (item?: ExplorerItem) => this.configurationService.getValue<IFilesConfiguration>({ resource: item?.root.resource }).explorer.fileNesting; this.tree = <WorkbenchCompressibleAsyncDataTree<ExplorerItem | ExplorerItem[], ExplorerItem, FuzzyScore>>this.instantiationService.createInstance(WorkbenchCompressibleAsyncDataTree, 'FileExplorer', container, new ExplorerDelegate(), new ExplorerCompressionDelegate(), [this.renderer], - this.instantiationService.createInstance(ExplorerDataSource), { + this.instantiationService.createInstance(ExplorerDataSource, this.filter), { compressionEnabled: isCompressionEnabled(), accessibilityProvider: this.renderer, identityProvider, @@ -410,7 +412,7 @@ export class ExplorerView extends ViewPane implements IExplorerView { multipleSelectionSupport: true, filter: this.filter, sorter: this.instantiationService.createInstance(FileSorter), - dnd: this.instantiationService.createInstance(FileDragAndDrop), + dnd: this.instantiationService.createInstance(FileDragAndDrop, (item) => this.isItemCollapsed(item)), collapseByDefault: (e) => { if (e instanceof ExplorerItem) { if (e.hasNests && getFileNestingSettings(e).expand) { @@ -625,23 +627,9 @@ export class ExplorerView extends ViewPane implements IExplorerView { } const toRefresh = item || this.tree.getInput(); - if (this.configurationService.getValue<IFilesConfiguration>({ resource: item?.root.resource }).explorer.experimental.fileNesting.enabled) { - return (async () => { - try { - await this.tree.updateChildren(toRefresh, recursive, false, { - diffIdentityProvider: identityProvider - }); - } catch (e) { - this.notificationService.error('Internal error in file explorer. This may be due to experimental file nesting.'); - console.error('Unepxected error', e, 'in refreshing explorer. This may be due to experimental file nesting.'); - return; - } - })(); - } else { - return this.tree.updateChildren(toRefresh, recursive, false, { - diffIdentityProvider: identityProvider - }); - } + return this.tree.updateChildren(toRefresh, recursive, false, { + diffIdentityProvider: identityProvider + }); } override getOptimalWidth(): number { diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index f445df889f1..1181f9867c4 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -75,6 +75,7 @@ export const explorerRootErrorEmitter = new Emitter<URI>(); export class ExplorerDataSource implements IAsyncDataSource<ExplorerItem | ExplorerItem[], ExplorerItem> { constructor( + private fileFilter: FilesFilter, @IProgressService private readonly progressService: IProgressService, @IConfigurationService private readonly configService: IConfigurationService, @INotificationService private readonly notificationService: INotificationService, @@ -85,7 +86,8 @@ export class ExplorerDataSource implements IAsyncDataSource<ExplorerItem | Explo ) { } hasChildren(element: ExplorerItem | ExplorerItem[]): boolean { - return Array.isArray(element) || element.hasChildren; + // don't render nest parents as containing children when all the children are filtered out + return Array.isArray(element) || element.hasChildren((stat) => this.fileFilter.filter(stat, TreeVisibility.Visible)); } getChildren(element: ExplorerItem | ExplorerItem[]): ExplorerItem[] | Promise<ExplorerItem[]> { @@ -849,6 +851,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> { private dropEnabled = false; constructor( + private isCollapsed: (item: ExplorerItem) => boolean, @IExplorerService private explorerService: IExplorerService, @IEditorService private editorService: IEditorService, @IDialogService private dialogService: IDialogService, @@ -1082,8 +1085,8 @@ export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> { const elementsData = FileDragAndDrop.getStatsFromDragAndDropData(data); const distinctItems = new Set(elementsData); - if (this.configurationService.getValue<IFilesConfiguration>().explorer.experimental.fileNesting.operateAsGroup) { - for (const item of distinctItems) { + for (const item of distinctItems) { + if (this.isCollapsed(item)) { const nestedChildren = item.nestedChildren; if (nestedChildren) { for (const child of nestedChildren) { @@ -1092,6 +1095,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> { } } } + const items = distinctParents([...distinctItems], s => s.resource); const isCopy = (originalEvent.ctrlKey && !isMacintosh) || (originalEvent.altKey && isMacintosh); diff --git a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index b7bc23b3257..06ba5acdbde 100644 --- a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -59,6 +59,7 @@ const $ = dom.$; export class OpenEditorsView extends ViewPane { private static readonly DEFAULT_VISIBLE_OPEN_EDITORS = 9; + private static readonly DEFAULT_MIN_VISIBLE_OPEN_EDITORS = 0; static readonly ID = 'workbench.explorer.openEditorsView'; static readonly NAME = nls.localize({ key: 'openEditors', comment: ['Open is an adjective'] }, "Open Editors"); @@ -466,12 +467,17 @@ export class OpenEditorsView extends ViewPane { } private getMaxExpandedBodySize(): number { + let minVisibleOpenEditors = this.configurationService.getValue<number>('explorer.openEditors.minVisible'); + // If it's not a number setting it to 0 will result in dynamic resizing. + if (typeof minVisibleOpenEditors !== 'number') { + minVisibleOpenEditors = OpenEditorsView.DEFAULT_MIN_VISIBLE_OPEN_EDITORS; + } const containerModel = this.viewDescriptorService.getViewContainerModel(this.viewDescriptorService.getViewContainerByViewId(this.id)!)!; if (containerModel.visibleViewDescriptors.length <= 1) { return Number.POSITIVE_INFINITY; } - return this.elementCount * OpenEditorsDelegate.ITEM_HEIGHT; + return (Math.max(this.elementCount, minVisibleOpenEditors)) * OpenEditorsDelegate.ITEM_HEIGHT; } private getMinExpandedBodySize(): number { diff --git a/src/vs/workbench/contrib/files/common/explorerModel.ts b/src/vs/workbench/contrib/files/common/explorerModel.ts index dfc1c9effc5..4b7b2c6eace 100644 --- a/src/vs/workbench/contrib/files/common/explorerModel.ts +++ b/src/vs/workbench/contrib/files/common/explorerModel.ts @@ -120,8 +120,12 @@ export class ExplorerItem { this._isExcluded = value; } - get hasChildren() { - return this.isDirectory || this.hasNests; + hasChildren(filter: (stat: ExplorerItem) => boolean): boolean { + if (this.hasNests) { + return this.nestedChildren?.some(c => filter(c)) ?? false; + } else { + return this.isDirectory; + } } get hasNests() { @@ -298,7 +302,7 @@ export class ExplorerItem { } fetchChildren(sortOrder: SortOrder): ExplorerItem[] | Promise<ExplorerItem[]> { - const nestingConfig = this.configService.getValue<IFilesConfiguration>({ resource: this.root.resource }).explorer.experimental.fileNesting; + const nestingConfig = this.configService.getValue<IFilesConfiguration>({ resource: this.root.resource }).explorer.fileNesting; // fast path when the children can be resolved sync if (nestingConfig.enabled && this.nestedChildren) { @@ -369,12 +373,15 @@ export class ExplorerItem { private _fileNester: ExplorerFileNestingTrie | undefined; private get fileNester(): ExplorerFileNestingTrie { if (!this.root._fileNester) { - const nestingConfig = this.configService.getValue<IFilesConfiguration>({ resource: this.root.resource }).explorer.experimental.fileNesting; + const nestingConfig = this.configService.getValue<IFilesConfiguration>({ resource: this.root.resource }).explorer.fileNesting; const patterns = Object.entries(nestingConfig.patterns) .filter(entry => typeof (entry[0]) === 'string' && typeof (entry[1]) === 'string' && entry[0] && entry[1]) .map(([parentPattern, childrenPatterns]) => - [parentPattern.trim(), childrenPatterns.split(',').map(p => this.getPlatformAwareName(p.trim().replace(/\u200b/g, '')))] as [string, string[]]); + [ + this.getPlatformAwareName(parentPattern.trim()), + childrenPatterns.split(',').map(p => this.getPlatformAwareName(p.trim().replace(/\u200b/g, ''))) + ] as [string, string[]]); this.root._fileNester = new ExplorerFileNestingTrie(patterns); } diff --git a/src/vs/workbench/contrib/files/common/files.ts b/src/vs/workbench/contrib/files/common/files.ts index aff70bc0e8b..5f10b350de4 100644 --- a/src/vs/workbench/contrib/files/common/files.ts +++ b/src/vs/workbench/contrib/files/common/files.ts @@ -98,13 +98,10 @@ export interface IFilesConfiguration extends PlatformIFilesConfiguration, IWorkb badges: boolean; }; incrementalNaming: 'simple' | 'smart'; - experimental: { - fileNesting: { - enabled: boolean; - operateAsGroup: boolean; - expand: boolean; - patterns: { [parent: string]: string }; - }; + fileNesting: { + enabled: boolean; + expand: boolean; + patterns: { [parent: string]: string }; }; }; editor: IEditorOptions; diff --git a/src/vs/workbench/contrib/files/electron-sandbox/fileActions.contribution.ts b/src/vs/workbench/contrib/files/electron-sandbox/fileActions.contribution.ts index 7e5450675c1..6473ad59136 100644 --- a/src/vs/workbench/contrib/files/electron-sandbox/fileActions.contribution.ts +++ b/src/vs/workbench/contrib/files/electron-sandbox/fileActions.contribution.ts @@ -57,7 +57,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } }); -appendEditorTitleContextMenuItem(REVEAL_IN_OS_COMMAND_ID, REVEAL_IN_OS_LABEL, REVEAL_IN_OS_WHEN_CONTEXT); +appendEditorTitleContextMenuItem(REVEAL_IN_OS_COMMAND_ID, REVEAL_IN_OS_LABEL, REVEAL_IN_OS_WHEN_CONTEXT, '2_files', 0); // Menu registration - open editors diff --git a/src/vs/workbench/contrib/files/electron-sandbox/textFileEditor.ts b/src/vs/workbench/contrib/files/electron-sandbox/textFileEditor.ts index e4d1eee0b33..64978ce1742 100644 --- a/src/vs/workbench/contrib/files/electron-sandbox/textFileEditor.ts +++ b/src/vs/workbench/contrib/files/electron-sandbox/textFileEditor.ts @@ -62,24 +62,22 @@ export class NativeTextFileEditor extends TextFileEditor { if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_EXCEEDS_MEMORY_LIMIT) { const memoryLimit = Math.max(MIN_MAX_MEMORY_SIZE_MB, +this.textResourceConfigurationService.getValue<number>(undefined, 'files.maxMemoryForLargeFilesMB') || FALLBACK_MAX_MEMORY_SIZE_MB); - throw createErrorWithActions(localize('fileTooLargeForHeapError', "To open a file of this size, you need to restart and allow {0} to use more memory", this.productService.nameShort), { - actions: [ - toAction({ - id: 'workbench.window.action.relaunchWithIncreasedMemoryLimit', label: localize('relaunchWithIncreasedMemoryLimit', "Restart with {0} MB", memoryLimit), run: () => { - return this.nativeHostService.relaunch({ - addArgs: [ - `--max-memory=${memoryLimit}` - ] - }); - } - }), - toAction({ - id: 'workbench.window.action.configureMemoryLimit', label: localize('configureMemoryLimit', 'Configure Memory Limit'), run: () => { - return this.preferencesService.openUserSettings({ query: 'files.maxMemoryForLargeFilesMB' }); - } - }), - ] - }); + throw createErrorWithActions(localize('fileTooLargeForHeapError', "To open a file of this size, you need to restart and allow {0} to use more memory", this.productService.nameShort), [ + toAction({ + id: 'workbench.window.action.relaunchWithIncreasedMemoryLimit', label: localize('relaunchWithIncreasedMemoryLimit', "Restart with {0} MB", memoryLimit), run: () => { + return this.nativeHostService.relaunch({ + addArgs: [ + `--max-memory=${memoryLimit}` + ] + }); + } + }), + toAction({ + id: 'workbench.window.action.configureMemoryLimit', label: localize('configureMemoryLimit', 'Configure Memory Limit'), run: () => { + return this.preferencesService.openUserSettings({ query: 'files.maxMemoryForLargeFilesMB' }); + } + }), + ]); } // Fallback to handling in super type diff --git a/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts b/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts index 3481452d194..aba0c2eed69 100644 --- a/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts +++ b/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts @@ -3,15 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { EditorAction, registerEditorAction, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { getCodeEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorAction, registerEditorAction } from 'vs/editor/browser/editorExtensions'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { DocumentFormattingEditProvider, DocumentRangeFormattingEditProvider } from 'vs/editor/common/languages'; import * as nls from 'vs/nls'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { formatDocumentRangesWithProvider, formatDocumentWithProvider, getRealAndSyntheticDocumentFormattersOrdered, FormattingConflicts, FormattingMode } from 'vs/editor/contrib/format/browser/format'; import { Range } from 'vs/editor/common/core/range'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -21,7 +21,7 @@ import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'v import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry, IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IExtensionService, toExtension } from 'vs/workbench/services/extensions/common/extensions'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ITextModel } from 'vs/editor/common/model'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; @@ -30,6 +30,10 @@ import { IWorkbenchExtensionEnablementService } from 'vs/workbench/services/exte import { editorConfigurationBaseNode } from 'vs/editor/common/config/editorConfigurationSchema'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; +import { ILanguageStatusService } from 'vs/workbench/services/languageStatus/common/languageStatusService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { generateUuid } from 'vs/base/common/uuid'; type FormattingEditProvider = DocumentFormattingEditProvider | DocumentRangeFormattingEditProvider; @@ -41,6 +45,8 @@ class DefaultFormatter extends Disposable implements IWorkbenchContribution { static extensionItemLabels: string[] = []; static extensionDescriptions: string[] = []; + private readonly _languageStatusStore = this._store.add(new DisposableStore()); + constructor( @IExtensionService private readonly _extensionService: IExtensionService, @IWorkbenchExtensionEnablementService private readonly _extensionEnablementService: IWorkbenchExtensionEnablementService, @@ -49,10 +55,17 @@ class DefaultFormatter extends Disposable implements IWorkbenchContribution { @IDialogService private readonly _dialogService: IDialogService, @IQuickInputService private readonly _quickInputService: IQuickInputService, @ILanguageService private readonly _languageService: ILanguageService, + @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, + @ILanguageStatusService private readonly _languageStatusService: ILanguageStatusService, + @IEditorService private readonly _editorService: IEditorService, ) { super(); - this._register(this._extensionService.onDidChangeExtensions(this._updateConfigValues, this)); - this._register(FormattingConflicts.setFormatterSelector((formatter, document, mode) => this._selectFormatter(formatter, document, mode))); + this._store.add(this._extensionService.onDidChangeExtensions(this._updateConfigValues, this)); + this._store.add(FormattingConflicts.setFormatterSelector((formatter, document, mode) => this._selectFormatter(formatter, document, mode))); + this._store.add(_editorService.onDidActiveEditorChange(this._updateStatus, this)); + this._store.add(_languageFeaturesService.documentFormattingEditProvider.onDidChange(this._updateStatus, this)); + this._store.add(_languageFeaturesService.documentRangeFormattingEditProvider.onDidChange(this._updateStatus, this)); + this._store.add(_configService.onDidChangeConfiguration(e => e.affectsConfiguration(DefaultFormatter.configName) && this._updateStatus())); this._updateConfigValues(); } @@ -93,8 +106,7 @@ class DefaultFormatter extends Disposable implements IWorkbenchContribution { return s.match(/\s/) ? `'${s}'` : s; } - private async _selectFormatter<T extends FormattingEditProvider>(formatter: T[], document: ITextModel, mode: FormattingMode): Promise<T | undefined> { - + private async _analyzeFormatter<T extends FormattingEditProvider>(formatter: T[], document: ITextModel): Promise<T | string> { const defaultFormatterId = this._configService.getValue<string>(DefaultFormatter.configName, { resource: document.uri, overrideIdentifier: document.getLanguageId() @@ -114,23 +126,9 @@ class DefaultFormatter extends Disposable implements IWorkbenchContribution { // formatter does not target this file const langName = this._languageService.getLanguageName(document.getLanguageId()) || document.getLanguageId(); const detail = nls.localize('miss', "Extension '{0}' is configured as formatter but it cannot format '{1}'-files", extension.displayName || extension.name, langName); - if (mode === FormattingMode.Silent) { - this._notificationService.status(detail, { hideAfter: 4000 }); - return undefined; - } else { - const result = await this._dialogService.confirm({ - message: nls.localize('miss.1', "Change Default Formatter"), - detail, - primaryButton: nls.localize('do.config', "Configure..."), - secondaryButton: nls.localize('cancel', "Cancel") - }); - if (result.confirmed) { - return this._pickAndPersistDefaultFormatter(formatter, document); - } else { - return undefined; - } - } + return detail; } + } else if (formatter.length === 1) { // ok -> nothing configured but only one formatter available return formatter[0]; @@ -138,26 +136,35 @@ class DefaultFormatter extends Disposable implements IWorkbenchContribution { const langName = this._languageService.getLanguageName(document.getLanguageId()) || document.getLanguageId(); const message = !defaultFormatterId - ? nls.localize('config.needed', "There are multiple formatters for '{0}' files. Select a default formatter to continue.", DefaultFormatter._maybeQuotes(langName)) + ? nls.localize('config.needed', "There are multiple formatters for '{0}' files. One of them should be configured as default formatter.", DefaultFormatter._maybeQuotes(langName)) : nls.localize('config.bad', "Extension '{0}' is configured as formatter but not available. Select a different default formatter to continue.", defaultFormatterId); + return message; + } + + private async _selectFormatter<T extends FormattingEditProvider>(formatter: T[], document: ITextModel, mode: FormattingMode): Promise<T | undefined> { + const formatterOrMessage = await this._analyzeFormatter(formatter, document); + if (typeof formatterOrMessage !== 'string') { + return formatterOrMessage; + } + if (mode !== FormattingMode.Silent) { // running from a user action -> show modal dialog so that users configure // a default formatter const result = await this._dialogService.confirm({ - message, + message: nls.localize('miss.1', "Configure Default Formatter"), + detail: formatterOrMessage, primaryButton: nls.localize('do.config', "Configure..."), secondaryButton: nls.localize('cancel', "Cancel") }); if (result.confirmed) { return this._pickAndPersistDefaultFormatter(formatter, document); } - } else { // no user action -> show a silent notification and proceed this._notificationService.prompt( Severity.Info, - message, + formatterOrMessage, [{ label: nls.localize('do.config', "Configure..."), run: () => this._pickAndPersistDefaultFormatter(formatter, document) }], { silent: true } ); @@ -184,6 +191,51 @@ class DefaultFormatter extends Disposable implements IWorkbenchContribution { }); return formatter[pick.index]; } + + // --- status item + + private _updateStatus() { + this._languageStatusStore.clear(); + + const editor = getCodeEditor(this._editorService.activeTextEditorControl); + if (!editor || !editor.hasModel()) { + return; + } + + + const document = editor.getModel(); + const formatter = getRealAndSyntheticDocumentFormattersOrdered(this._languageFeaturesService.documentFormattingEditProvider, this._languageFeaturesService.documentRangeFormattingEditProvider, document); + + if (formatter.length === 0) { + return; + } + + const cts = new CancellationTokenSource(); + this._languageStatusStore.add(toDisposable(() => cts.dispose(true))); + + this._analyzeFormatter(formatter, document).then(result => { + if (cts.token.isCancellationRequested) { + return; + } + if (typeof result !== 'string') { + return; + } + const command = { id: `formatter/configure/dfl/${generateUuid()}`, title: nls.localize('do.config', "Configure...") }; + this._languageStatusStore.add(CommandsRegistry.registerCommand(command.id, () => this._pickAndPersistDefaultFormatter(formatter, document))); + this._languageStatusStore.add(this._languageStatusService.addStatus({ + id: 'formatter.conflict', + name: nls.localize('summary', "Formatter Conflicts"), + selector: { language: document.getLanguageId(), pattern: document.uri.fsPath }, + severity: Severity.Error, + label: nls.localize('formatter', "Formatting"), + detail: result, + busy: false, + source: '', + command, + accessibilityInfo: undefined + })); + }); + } } Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution( diff --git a/src/vs/workbench/contrib/keybindings/browser/keybindings.contribution.ts b/src/vs/workbench/contrib/keybindings/browser/keybindings.contribution.ts index 6fc249b03be..5f6cdfb3298 100644 --- a/src/vs/workbench/contrib/keybindings/browser/keybindings.contribution.ts +++ b/src/vs/workbench/contrib/keybindings/browser/keybindings.contribution.ts @@ -9,7 +9,7 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { CATEGORIES } from 'vs/workbench/common/actions'; import { rendererLogChannelId } from 'vs/workbench/contrib/logs/common/logConstants'; -import { IOutputService } from 'vs/workbench/contrib/output/common/output'; +import { IOutputService } from 'vs/workbench/services/output/common/output'; class ToggleKeybindingsLogAction extends Action2 { diff --git a/src/vs/workbench/contrib/languageDetection/browser/languageDetection.contribution.ts b/src/vs/workbench/contrib/languageDetection/browser/languageDetection.contribution.ts new file mode 100644 index 00000000000..94f1e9af9bd --- /dev/null +++ b/src/vs/workbench/contrib/languageDetection/browser/languageDetection.contribution.ts @@ -0,0 +1,148 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { getCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { localize } from 'vs/nls'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from 'vs/workbench/services/statusbar/browser/statusbar'; +import { ILanguageDetectionService } from 'vs/workbench/services/languageDetection/common/languageDetectionWorkerService'; +import { ThrottledDelayer } from 'vs/base/common/async'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { registerAction2, Action2 } from 'vs/platform/actions/common/actions'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { NOTEBOOK_EDITOR_EDITABLE } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import { Schemas } from 'vs/base/common/network'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; + +const detectLanguageCommandId = 'editor.detectLanguage'; + +class LanguageDetectionStatusContribution implements IWorkbenchContribution { + + private static readonly _id = 'status.languageDetectionStatus'; + + private readonly _disposables = new DisposableStore(); + private _combinedEntry?: IStatusbarEntryAccessor; + private _delayer = new ThrottledDelayer(1000); + private _renderDisposables = new DisposableStore(); + + constructor( + @ILanguageDetectionService private readonly _languageDetectionService: ILanguageDetectionService, + @IStatusbarService private readonly _statusBarService: IStatusbarService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IEditorService private readonly _editorService: IEditorService, + @ILanguageService private readonly _languageService: ILanguageService, + @IKeybindingService private readonly _keybindingService: IKeybindingService, + ) { + _editorService.onDidActiveEditorChange(() => this._update(true), this, this._disposables); + this._update(false); + } + + dispose(): void { + this._disposables.dispose(); + this._delayer.dispose(); + this._combinedEntry?.dispose(); + this._renderDisposables.dispose(); + } + + private _update(clear: boolean): void { + if (clear) { + this._combinedEntry?.dispose(); + this._combinedEntry = undefined; + } + this._delayer.trigger(() => this._doUpdate()); + } + + private async _doUpdate(): Promise<void> { + const editor = getCodeEditor(this._editorService.activeTextEditorControl); + + this._renderDisposables.clear(); + + // update when editor language changes + editor?.onDidChangeModelLanguage(() => this._update(true), this, this._renderDisposables); + editor?.onDidChangeModelContent(() => this._update(false), this, this._renderDisposables); + const editorModel = editor?.getModel(); + const editorUri = editorModel?.uri; + const existingId = editorModel?.getLanguageId(); + const enablementConfig = this._configurationService.getValue('workbench.editor.languageDetectionHints'); + const enabled = enablementConfig === 'always' || enablementConfig === 'textEditors'; + const disableLightbulb = !enabled || editorUri?.scheme !== Schemas.untitled || !existingId; + + if (disableLightbulb || !editorUri) { + this._combinedEntry?.dispose(); + this._combinedEntry = undefined; + } else { + const lang = await this._languageDetectionService.detectLanguage(editorUri); + const skip: Record<string, string | undefined> = { 'jsonc': 'json' }; + const existing = editorModel.getLanguageId(); + if (lang && lang !== existing && skip[existing] !== lang) { + const detectedName = this._languageService.getLanguageName(lang) || lang; + let tooltip = localize('status.autoDetectLanguage', "Accept Detected Language: {0}", detectedName); + const keybinding = this._keybindingService.lookupKeybinding(detectLanguageCommandId); + const label = keybinding?.getLabel(); + if (label) { + tooltip += ` (${label})`; + } + + const props: IStatusbarEntry = { + name: localize('langDetection.name', "Language Detection"), + ariaLabel: localize('langDetection.aria', "Change to Detected Language: {0}", lang), + tooltip, + command: detectLanguageCommandId, + text: '$(lightbulb-autofix)', + }; + if (!this._combinedEntry) { + this._combinedEntry = this._statusBarService.addEntry(props, LanguageDetectionStatusContribution._id, StatusbarAlignment.RIGHT, { id: 'status.editor.mode', alignment: StatusbarAlignment.RIGHT, compact: true }); + } else { + this._combinedEntry.update(props); + } + } else { + this._combinedEntry?.dispose(); + this._combinedEntry = undefined; + } + } + } +} + +Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(LanguageDetectionStatusContribution, LifecyclePhase.Restored); + + +registerAction2(class extends Action2 { + + constructor() { + super({ + id: detectLanguageCommandId, + title: localize('detectlang', 'Detect Language from Content'), + f1: true, + precondition: ContextKeyExpr.and(NOTEBOOK_EDITOR_EDITABLE.toNegated(), EditorContextKeys.editorTextFocus), + keybinding: { primary: KeyCode.KeyD | KeyMod.Alt | KeyMod.Shift, weight: KeybindingWeight.WorkbenchContrib } + }); + } + + async run(accessor: ServicesAccessor): Promise<void> { + const editorService = accessor.get(IEditorService); + const languageDetectionService = accessor.get(ILanguageDetectionService); + const editor = getCodeEditor(editorService.activeTextEditorControl); + const notificationService = accessor.get(INotificationService); + const editorUri = editor?.getModel()?.uri; + if (editorUri) { + const lang = await languageDetectionService.detectLanguage(editorUri); + if (lang) { + editor.getModel()?.setMode(lang); + } else { + notificationService.warn(localize('noDetection', "Unable to detect editor language")); + } + } + } +}); diff --git a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts index d6d4689d90e..0372c278593 100644 --- a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts +++ b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/languageStatus'; import * as dom from 'vs/base/browser/dom'; import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; -import { DisposableStore, dispose } from 'vs/base/common/lifecycle'; +import { DisposableStore, dispose, toDisposable } from 'vs/base/common/lifecycle'; import Severity from 'vs/base/common/severity'; import { getCodeEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { localize } from 'vs/nls'; @@ -28,6 +28,8 @@ import { Codicon } from 'vs/base/common/codicons'; import { IStorageService, IStorageValueChangeEvent, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { equals } from 'vs/base/common/arrays'; import { URI } from 'vs/base/common/uri'; +import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; class LanguageStatusViewModel { @@ -202,16 +204,21 @@ class EditorStatusContribution implements IWorkbenchContribution { // animate the status bar icon whenever language status changes, repeat animation // when severity is warning or error, don't show animation when showing progress/busy const userHasInteractedWithStatus = this._interactionCounter.value >= 3; - const node = document.querySelector('.monaco-workbench .statusbar DIV#status\\.languageStatus span.codicon'); - if (node instanceof HTMLElement) { + const node = document.querySelector('.monaco-workbench .statusbar DIV#status\\.languageStatus A>SPAN.codicon'); + const container = document.querySelector('.monaco-workbench .statusbar DIV#status\\.languageStatus'); + if (node instanceof HTMLElement && container) { const _wiggle = 'wiggle'; - const _repeat = 'repeat'; + const _flash = 'flash'; if (!isOneBusy) { + // wiggle icon when severe or "new" node.classList.toggle(_wiggle, showSeverity || !userHasInteractedWithStatus); - node.classList.toggle(_repeat, showSeverity); - this._renderDisposables.add(dom.addDisposableListener(node, 'animationend', _e => node.classList.remove(_wiggle, _repeat))); + this._renderDisposables.add(dom.addDisposableListener(node, 'animationend', _e => node.classList.remove(_wiggle))); + // flash background when severe + container.classList.toggle(_flash, showSeverity); + this._renderDisposables.add(dom.addDisposableListener(container, 'animationend', _e => container.classList.remove(_flash))); } else { - node.classList.remove(_wiggle, _repeat); + node.classList.remove(_wiggle); + container.classList.remove(_flash); } } @@ -226,7 +233,8 @@ class EditorStatusContribution implements IWorkbenchContribution { observer.disconnect(); } }); - observer.observe(document.body, { childList: true, subtree: true }); + observer.observe(hoverTarget, { childList: true, subtree: true }); + this._renderDisposables.add(toDisposable(() => observer.disconnect())); } } } @@ -384,3 +392,19 @@ class EditorStatusContribution implements IWorkbenchContribution { } Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(EditorStatusContribution, LifecyclePhase.Restored); + +registerAction2(class extends Action2 { + + constructor() { + super({ + id: 'editor.inlayHints.Reset', + title: localize('reset', 'Reset Language Status Interaction Counter'), + category: localize('cat', 'View'), + f1: true + }); + } + + run(accessor: ServicesAccessor): void { + accessor.get(IStorageService).remove('languageStatus.interactCount', StorageScope.GLOBAL); + } +}); diff --git a/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css b/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css index 131695f460a..1fc68790f97 100644 --- a/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css +++ b/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css @@ -3,37 +3,56 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +/* status bar animation */ @keyframes wiggle { 0% { - transform: rotate(0) scale(1) + transform: rotate(0) scale(1); } 15%, 45% { - transform: rotate(.04turn) scale(1.1) + transform: rotate(.04turn) scale(1.1); } 30%, 60% { - transform: rotate(-.04turn) scale(1.2) + transform: rotate(-.04turn) scale(1.2); } 100% { - transform: rotate(0) scale(1) + transform: rotate(0) scale(1); } } -.monaco-workbench.enable-motion .statusbar DIV#status\.languageStatus span.codicon.wiggle { +.monaco-workbench .statusbar DIV#status\.languageStatus A>SPAN.codicon.wiggle { animation-duration: .8s; animation-iteration-count: 1; animation-name: wiggle; } -.monaco-workbench.enable-motion .statusbar DIV#status\.languageStatus span.codicon.wiggle.repeat { - animation-iteration-count: 3; +@keyframes flash { + 0% { + background-color: initial; + } + + 50% { + background-color: var(--vscode-statusBarItem-prominentBackground); + } + + 100% { + background-color: initial; + } +} + +.monaco-workbench .statusbar DIV#status\.languageStatus.flash A { + animation-duration: .8s; + animation-iteration-count: 1; + animation-name: flash; } +/* --- hover */ + .monaco-workbench .hover-language-status { display: flex; padding: 4px 8px; diff --git a/src/vs/workbench/contrib/localHistory/browser/localHistory.ts b/src/vs/workbench/contrib/localHistory/browser/localHistory.ts index c182f0ccfd1..20bd201237b 100644 --- a/src/vs/workbench/contrib/localHistory/browser/localHistory.ts +++ b/src/vs/workbench/contrib/localHistory/browser/localHistory.ts @@ -8,8 +8,22 @@ import { Codicon } from 'vs/base/common/codicons'; import { language } from 'vs/base/common/platform'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; +import { IdleValue } from 'vs/base/common/async'; -export const LOCAL_HISTORY_DATE_FORMATTER = new Intl.DateTimeFormat(language, { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' }); +export const LOCAL_HISTORY_DATE_FORMATTER: IdleValue<{ format: (timestamp: number) => string }> = new IdleValue(() => { + const options: Intl.DateTimeFormatOptions = { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' }; + + let formatter: Intl.DateTimeFormat; + try { + formatter = new Intl.DateTimeFormat(language, options); + } catch (error) { + formatter = new Intl.DateTimeFormat(undefined, options); // error can happen when language is invalid (https://github.com/microsoft/vscode/issues/147086) + } + + return { + format: date => formatter.format(date) + }; +}); export const LOCAL_HISTORY_MENU_CONTEXT_VALUE = 'localHistory:item'; export const LOCAL_HISTORY_MENU_CONTEXT_KEY = ContextKeyExpr.equals('timelineItem', LOCAL_HISTORY_MENU_CONTEXT_VALUE); diff --git a/src/vs/workbench/contrib/localHistory/browser/localHistoryCommands.ts b/src/vs/workbench/contrib/localHistory/browser/localHistoryCommands.ts index 0b2a709a069..49a5a67e8e7 100644 --- a/src/vs/workbench/contrib/localHistory/browser/localHistoryCommands.ts +++ b/src/vs/workbench/contrib/localHistory/browser/localHistoryCommands.ts @@ -632,7 +632,7 @@ export async function findLocalHistoryEntry(workingCopyHistoryService: IWorkingC const SEP = /\//g; function toLocalHistoryEntryDateLabel(timestamp: number): string { - return `${LOCAL_HISTORY_DATE_FORMATTER.format(timestamp).replace(SEP, '-')}`; // preserving `/` will break editor labels, so replace it with a non-path symbol + return `${LOCAL_HISTORY_DATE_FORMATTER.value.format(timestamp).replace(SEP, '-')}`; // preserving `/` will break editor labels, so replace it with a non-path symbol } //#endregion diff --git a/src/vs/workbench/contrib/localHistory/browser/localHistoryTimeline.ts b/src/vs/workbench/contrib/localHistory/browser/localHistoryTimeline.ts index ca34fae943c..2dee3866cbd 100644 --- a/src/vs/workbench/contrib/localHistory/browser/localHistoryTimeline.ts +++ b/src/vs/workbench/contrib/localHistory/browser/localHistoryTimeline.ts @@ -8,7 +8,7 @@ import { Emitter } from 'vs/base/common/event'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { InternalTimelineOptions, ITimelineService, Timeline, TimelineChangeEvent, TimelineItem, TimelineOptions, TimelineProvider } from 'vs/workbench/contrib/timeline/common/timeline'; +import { ITimelineService, Timeline, TimelineChangeEvent, TimelineItem, TimelineOptions, TimelineProvider } from 'vs/workbench/contrib/timeline/common/timeline'; import { IWorkingCopyHistoryEntry, IWorkingCopyHistoryService } from 'vs/workbench/services/workingCopy/common/workingCopyHistory'; import { URI } from 'vs/base/common/uri'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; @@ -102,7 +102,7 @@ export class LocalHistoryTimeline extends Disposable implements IWorkbenchContri }); } - async provideTimeline(uri: URI, options: TimelineOptions, token: CancellationToken, internalOptions?: InternalTimelineOptions): Promise<Timeline> { + async provideTimeline(uri: URI, options: TimelineOptions, token: CancellationToken): Promise<Timeline> { const items: TimelineItem[] = []; // Try to convert the provided `uri` into a form that is likely @@ -151,7 +151,7 @@ export class LocalHistoryTimeline extends Disposable implements IWorkbenchContri return { handle: entry.id, label: SaveSourceRegistry.getSourceLabel(entry.source), - tooltip: new MarkdownString(`$(history) ${LOCAL_HISTORY_DATE_FORMATTER.format(entry.timestamp)}\n\n${SaveSourceRegistry.getSourceLabel(entry.source)}`, { supportThemeIcons: true }), + tooltip: new MarkdownString(`$(history) ${LOCAL_HISTORY_DATE_FORMATTER.value.format(entry.timestamp)}\n\n${SaveSourceRegistry.getSourceLabel(entry.source)}`, { supportThemeIcons: true }), source: LocalHistoryTimeline.ID, timestamp: entry.timestamp, themeIcon: LOCAL_HISTORY_ICON_ENTRY, diff --git a/src/vs/workbench/contrib/logs/browser/logs.contribution.ts b/src/vs/workbench/contrib/logs/browser/logs.contribution.ts new file mode 100644 index 00000000000..d78954e856e --- /dev/null +++ b/src/vs/workbench/contrib/logs/browser/logs.contribution.ts @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IWorkbenchActionRegistry, Extensions as WorkbenchActionExtensions, CATEGORIES } from 'vs/workbench/common/actions'; +import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { OpenWindowSessionLogFileAction } from 'vs/workbench/contrib/logs/common/logsActions'; +import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { LogsDataCleaner } from 'vs/workbench/contrib/logs/common/logsDataCleaner'; + +class WebLogOutputChannels extends Disposable implements IWorkbenchContribution { + + constructor( + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + super(); + this.registerWebContributions(); + } + + private registerWebContributions(): void { + this.instantiationService.createInstance(LogsDataCleaner); + + const workbenchActionsRegistry = Registry.as<IWorkbenchActionRegistry>(WorkbenchActionExtensions.WorkbenchActions); + workbenchActionsRegistry.registerWorkbenchAction(SyncActionDescriptor.from(OpenWindowSessionLogFileAction), 'Developer: Open Window Log File (Session)...', CATEGORIES.Developer.value); + } + +} + +Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(WebLogOutputChannels, LifecyclePhase.Restored); diff --git a/src/vs/workbench/contrib/logs/common/logs.contribution.ts b/src/vs/workbench/contrib/logs/common/logs.contribution.ts index 6cbfdc4cd7b..5e533149305 100644 --- a/src/vs/workbench/contrib/logs/common/logs.contribution.ts +++ b/src/vs/workbench/contrib/logs/common/logs.contribution.ts @@ -4,29 +4,22 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import { join } from 'vs/base/common/path'; import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchActionRegistry, Extensions as WorkbenchActionExtensions, CATEGORIES } from 'vs/workbench/common/actions'; import { Action2, registerAction2, SyncActionDescriptor } from 'vs/platform/actions/common/actions'; -import { SetLogLevelAction, OpenWindowSessionLogFileAction } from 'vs/workbench/contrib/logs/common/logsActions'; +import { SetLogLevelAction } from 'vs/workbench/contrib/logs/common/logsActions'; import * as Constants from 'vs/workbench/contrib/logs/common/logConstants'; import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { IFileService, whenProviderRegistered } from 'vs/platform/files/common/files'; -import { URI } from 'vs/base/common/uri'; -import { IOutputChannelRegistry, Extensions as OutputExt } from 'vs/workbench/services/output/common/output'; +import { IFileService } from 'vs/platform/files/common/files'; +import { IOutputService, registerLogChannel } from 'vs/workbench/services/output/common/output'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { ILogService, LogLevel } from 'vs/platform/log/common/log'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { isWeb } from 'vs/base/common/platform'; -import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { LogsDataCleaner } from 'vs/workbench/contrib/logs/common/logsDataCleaner'; -import { IOutputService } from 'vs/workbench/contrib/output/common/output'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { supportsTelemetry } from 'vs/platform/telemetry/common/telemetryUtils'; import { IProductService } from 'vs/platform/product/common/productService'; -import { createCancelablePromise, timeout } from 'vs/base/common/async'; -import { canceled, getErrorMessage, isCancellationError } from 'vs/base/common/errors'; -import { CancellationToken } from 'vs/base/common/cancellation'; +import { URI } from 'vs/base/common/uri'; const workbenchActionsRegistry = Registry.as<IWorkbenchActionRegistry>(WorkbenchActionExtensions.WorkbenchActions); workbenchActionsRegistry.registerWorkbenchAction(SyncActionDescriptor.from(SetLogLevelAction), 'Developer: Set Log Level...', CATEGORIES.Developer.value); @@ -38,15 +31,9 @@ class LogOutputChannels extends Disposable implements IWorkbenchContribution { @IProductService private readonly productService: IProductService, @ILogService private readonly logService: ILogService, @IFileService private readonly fileService: IFileService, - @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); this.registerCommonContributions(); - if (isWeb) { - this.registerWebContributions(); - } else { - this.registerNativeContributions(); - } } private registerCommonContributions(): void { @@ -84,47 +71,9 @@ class LogOutputChannels extends Disposable implements IWorkbenchContribution { }); } - private registerWebContributions(): void { - this.instantiationService.createInstance(LogsDataCleaner); - - const workbenchActionsRegistry = Registry.as<IWorkbenchActionRegistry>(WorkbenchActionExtensions.WorkbenchActions); - workbenchActionsRegistry.registerWorkbenchAction(SyncActionDescriptor.from(OpenWindowSessionLogFileAction), 'Developer: Open Window Log File (Session)...', CATEGORIES.Developer.value); - } - - private registerNativeContributions(): void { - this.registerLogChannel(Constants.mainLogChannelId, nls.localize('mainLog', "Main"), URI.file(join(this.environmentService.logsPath, `main.log`))); - this.registerLogChannel(Constants.sharedLogChannelId, nls.localize('sharedLog', "Shared"), URI.file(join(this.environmentService.logsPath, `sharedprocess.log`))); - } - - private async registerLogChannel(id: string, label: string, file: URI): Promise<void> { - await whenProviderRegistered(file, this.fileService); - const outputChannelRegistry = Registry.as<IOutputChannelRegistry>(OutputExt.OutputChannels); - try { - const promise = createCancelablePromise(token => this.whenFileExists(file, 1, token)); - this._register(toDisposable(() => promise.cancel())); - await promise; - outputChannelRegistry.registerChannel({ id, label, file, log: true }); - } catch (error) { - if (!isCancellationError(error)) { - this.logService.error('Error while registering log channel', file.toString(), getErrorMessage(error)); - } - } - } - - private async whenFileExists(file: URI, trial: number, token: CancellationToken): Promise<void> { - const exists = await this.fileService.exists(file); - if (exists) { - return; - } - if (token.isCancellationRequested) { - throw canceled(); - } - if (trial > 10) { - throw new Error(`Timed out while waiting for file to be created`); - } - this.logService.debug(`[Registering Log Channel] File does not exist. Waiting for 1s to retry.`, file.toString()); - await timeout(1000, token); - await this.whenFileExists(file, trial + 1, token); + 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/logs/electron-sandbox/logs.contribution.ts b/src/vs/workbench/contrib/logs/electron-sandbox/logs.contribution.ts index 34b8d2ff3fa..022c66402f2 100644 --- a/src/vs/workbench/contrib/logs/electron-sandbox/logs.contribution.ts +++ b/src/vs/workbench/contrib/logs/electron-sandbox/logs.contribution.ts @@ -3,10 +3,46 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as nls from 'vs/nls'; import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchActionRegistry, Extensions as WorkbenchActionExtensions, CATEGORIES } from 'vs/workbench/common/actions'; import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; import { OpenLogsFolderAction, OpenExtensionLogsFolderAction } from 'vs/workbench/contrib/logs/electron-sandbox/logsActions'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import * as Constants from 'vs/workbench/contrib/logs/common/logConstants'; +import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { IFileService } from 'vs/platform/files/common/files'; +import { ILogService } from 'vs/platform/log/common/log'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { join } from 'vs/base/common/path'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { registerLogChannel } from 'vs/workbench/services/output/common/output'; + +class NativeLogOutputChannels extends Disposable implements IWorkbenchContribution { + + constructor( + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @ILogService private readonly logService: ILogService, + @IFileService private readonly fileService: IFileService, + ) { + super(); + this.registerNativeContributions(); + } + + private registerNativeContributions(): void { + this.registerLogChannel(Constants.mainLogChannelId, nls.localize('mainLog', "Main"), URI.file(join(this.environmentService.logsPath, `main.log`))); + this.registerLogChannel(Constants.sharedLogChannelId, nls.localize('sharedLog', "Shared"), URI.file(join(this.environmentService.logsPath, `sharedprocess.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())); + } + +} + +Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(NativeLogOutputChannels, LifecyclePhase.Restored); const workbenchActionsRegistry = Registry.as<IWorkbenchActionRegistry>(WorkbenchActionExtensions.WorkbenchActions); workbenchActionsRegistry.registerWorkbenchAction(SyncActionDescriptor.from(OpenLogsFolderAction), 'Developer: Open Logs Folder', CATEGORIES.Developer.value); diff --git a/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts b/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts index c47ff0b58ff..68185694831 100644 --- a/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts +++ b/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts @@ -121,7 +121,7 @@ code > div { } .vscode-high-contrast code > div { - background-color: rgb(0, 0, 0); + background-color: var(--vscode-textCodeBlock-background); } .vscode-high-contrast h1 { diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/statusBarProviders.ts b/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/statusBarProviders.ts index f1834e4ee96..750af12fdf4 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/statusBarProviders.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/statusBarProviders.ts @@ -5,16 +5,21 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { Disposable } from 'vs/base/common/lifecycle'; +import { ResourceMap } from 'vs/base/common/map'; import { URI } from 'vs/base/common/uri'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { localize } from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; -import { CHANGE_CELL_LANGUAGE } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CHANGE_CELL_LANGUAGE, DETECT_CELL_LANGUAGE } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/common/notebookCellStatusBarService'; import { CellKind, CellStatusbarAlignment, INotebookCellStatusBarItem, INotebookCellStatusBarItemList, INotebookCellStatusBarItemProvider } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; +import { ILanguageDetectionService } from 'vs/workbench/services/languageDetection/common/languageDetectionWorkerService'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; class CellStatusBarLanguagePickerProvider implements INotebookCellStatusBarItemProvider { @@ -50,6 +55,80 @@ class CellStatusBarLanguagePickerProvider implements INotebookCellStatusBarItemP } } +class CellStatusBarLanguageDetectionProvider implements INotebookCellStatusBarItemProvider { + + readonly viewType = '*'; + + private cache = new ResourceMap<{ + lastUpdate: number; + lastCellLang: string; + lastGuess?: string; + }>(); + + constructor( + @INotebookService private readonly _notebookService: INotebookService, + @INotebookKernelService private readonly _notebookKernelService: INotebookKernelService, + @ILanguageService private readonly _languageService: ILanguageService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @ILanguageDetectionService private readonly _languageDetectionService: ILanguageDetectionService, + @IKeybindingService private readonly _keybindingService: IKeybindingService, + ) { } + + async provideCellStatusBarItems(uri: URI, index: number, token: CancellationToken): Promise<INotebookCellStatusBarItemList | undefined> { + const doc = this._notebookService.getNotebookTextModel(uri); + const cell = doc?.cells[index]; + if (!cell) { return; } + + const enablementConfig = this._configurationService.getValue('workbench.editor.languageDetectionHints'); + const enabled = enablementConfig === 'always' || enablementConfig === 'notebookEditors'; + if (!enabled) { + return; + } + + const currentLanguageId = cell.cellKind === CellKind.Markup ? + 'markdown' : + (this._languageService.getLanguageIdByLanguageName(cell.language) || cell.language); + + if (!this.cache.has(uri)) { + this.cache.set(uri, { lastCellLang: currentLanguageId, lastUpdate: 0 }); + } + + const cached = this.cache.get(uri)!; + if (cached.lastUpdate < Date.now() - 1000 || cached.lastCellLang !== currentLanguageId) { + cached.lastUpdate = Date.now(); + cached.lastCellLang = currentLanguageId; + + const kernel = this._notebookKernelService.getSelectedOrSuggestedKernel(doc); + + if (kernel) { + const availableLangs = []; + availableLangs.push(...kernel.supportedLanguages, 'markdown'); + cached.lastGuess = await this._languageDetectionService.detectLanguage(cell.uri, availableLangs); + } + } + + const items: INotebookCellStatusBarItem[] = []; + if (cached.lastGuess && currentLanguageId !== cached.lastGuess) { + const detectedName = this._languageService.getLanguageName(cached.lastGuess) || cached.lastGuess; + let tooltip = localize('notebook.cell.status.autoDetectLanguage', "Accept Detected Language: {0}", detectedName); + const keybinding = this._keybindingService.lookupKeybinding(DETECT_CELL_LANGUAGE); + const label = keybinding?.getLabel(); + if (label) { + tooltip += ` (${label})`; + } + items.push({ + text: '$(lightbulb-autofix)', + command: DETECT_CELL_LANGUAGE, + tooltip, + alignment: CellStatusbarAlignment.Right, + priority: -Number.MAX_SAFE_INTEGER + 1 + }); + } + + return { items }; + } +} + class BuiltinCellStatusBarProviders extends Disposable { constructor( @IInstantiationService instantiationService: IInstantiationService, @@ -58,6 +137,7 @@ class BuiltinCellStatusBarProviders extends Disposable { const builtinProviders = [ CellStatusBarLanguagePickerProvider, + CellStatusBarLanguageDetectionProvider, ]; builtinProviders.forEach(p => { this._register(notebookCellStatusBarService.registerCellStatusBarItemProvider(instantiationService.createInstance(p))); 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 10037c96943..8fbc0999419 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard.ts @@ -27,7 +27,7 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation import { RedoCommand, UndoCommand } from 'vs/editor/browser/editorExtensions'; import { IWebview } from 'vs/workbench/contrib/webview/browser/webview'; import { CATEGORIES } from 'vs/workbench/common/actions'; -import { IOutputService } from 'vs/workbench/contrib/output/common/output'; +import { IOutputService } from 'vs/workbench/services/output/common/output'; import { rendererLogChannelId } from 'vs/workbench/contrib/logs/common/logConstants'; import { ILogService } from 'vs/platform/log/common/log'; 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 cb7ab0f8586..920669194f6 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar.ts @@ -28,7 +28,7 @@ import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/note import { configureKernelIcon, 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 } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { INotebookKernel, INotebookKernelService, NotebookKernelType } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; @@ -38,7 +38,7 @@ import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeat registerAction2(class extends Action2 { constructor() { super({ - id: '_notebook.selectKernel', + id: SELECT_KERNEL_ID, category: NOTEBOOK_ACTIONS_CATEGORY, title: { value: nls.localize('notebookActions.selectKernel', "Select Notebook Kernel"), original: 'Select Notebook Kernel' }, // precondition: NOTEBOOK_IS_ACTIVE_EDITOR, @@ -182,12 +182,7 @@ registerAction2(class extends Action2 { return res; } const quickPickItems: QuickPickInput<IQuickPickItem | KernelPick>[] = []; - if (!all.length) { - quickPickItems.push({ - id: 'install', - label: nls.localize('installKernels', "Install kernels from the marketplace"), - }); - } else { + if (all.length) { // Always display suggested kernels on the top. if (suggestions.length) { quickPickItems.push({ @@ -210,6 +205,14 @@ registerAction2(class extends Action2 { }); } + if (!all.find(item => item.type === NotebookKernelType.Resolved)) { + // there is no resolved kernel, show the install from marketplace + quickPickItems.push({ + id: 'install', + label: nls.localize('installKernels', "Install kernels from the marketplace"), + }); + } + const pick = await quickInputService.pick(quickPickItems, { placeHolder: selected ? nls.localize('prompt.placeholder.change', "Change kernel for '{0}'", labelService.getUriLabel(notebook.uri, { relative: true })) @@ -381,7 +384,7 @@ export class KernelStatus extends Disposable implements IWorkbenchContribution { tooltip: isSuggested ? nls.localize('tooltop', "{0} (suggestion)", tooltip) : tooltip, command: SELECT_KERNEL_ID, }, - '_notebook.selectKernel', + SELECT_KERNEL_ID, StatusbarAlignment.RIGHT, 10 )); @@ -399,7 +402,7 @@ export class KernelStatus extends Disposable implements IWorkbenchContribution { command: SELECT_KERNEL_ID, backgroundColor: { id: 'statusBarItem.prominentBackground' } }, - '_notebook.selectKernel', + SELECT_KERNEL_ID, StatusbarAlignment.RIGHT, 10 )); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/findModel.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/findModel.ts index f1b0150286c..2f5d35ba93f 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/findModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/findModel.ts @@ -88,7 +88,7 @@ export class FindModel extends Disposable { }; } - find(previous: boolean) { + find(option: { previous: boolean } | { index: number }) { if (!this.findMatches.length) { return; } @@ -96,14 +96,20 @@ export class FindModel extends Disposable { // let currCell; if (!this._findMatchesStarts) { this.set(this._findMatches, true); + if ('index' in option) { + this._currentMatch = option.index; + } } else { // const currIndex = this._findMatchesStarts!.getIndexOf(this._currentMatch); // currCell = this._findMatches[currIndex.index].cell; const totalVal = this._findMatchesStarts.getTotalSum(); - if (this._currentMatch === -1) { - this._currentMatch = previous ? totalVal - 1 : 0; + if ('index' in option) { + this._currentMatch = option.index; + } + else if (this._currentMatch === -1) { + this._currentMatch = option.previous ? totalVal - 1 : 0; } else { - const nextVal = (this._currentMatch + (previous ? -1 : 1) + totalVal) % totalVal; + const nextVal = (this._currentMatch + (option.previous ? -1 : 1) + totalVal) % totalVal; this._currentMatch = nextVal; } } @@ -157,8 +163,8 @@ export class FindModel extends Disposable { } async research() { - this._throttledDelayer.trigger(() => { - this._research(); + return this._throttledDelayer.trigger(() => { + return this._research(); }); } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFind.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFind.ts new file mode 100644 index 00000000000..f1ff2386aa7 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFind.ts @@ -0,0 +1,135 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./media/notebookFind'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { Schemas } from 'vs/base/common/network'; +import { isEqual } from 'vs/base/common/resources'; +import { URI } from 'vs/base/common/uri'; +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 { localize } from 'vs/nls'; +import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +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 { NotebookFindWidget } from 'vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget'; +import { getNotebookEditorFromEditorPane } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { registerNotebookContribution } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; +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'; + +registerNotebookContribution(NotebookFindWidget.id, NotebookFindWidget); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'notebook.hideFind', + title: { value: localize('notebookActions.hideFind', "Hide Find in Notebook"), original: 'Hide Find in Notebook' }, + keybinding: { + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED), + primary: KeyCode.Escape, + weight: KeybindingWeight.WorkbenchContrib + } + }); + } + + async run(accessor: ServicesAccessor): Promise<void> { + const editorService = accessor.get(IEditorService); + const editor = getNotebookEditorFromEditorPane(editorService.activeEditorPane); + + if (!editor) { + return; + } + + const controller = editor.getContribution<NotebookFindWidget>(NotebookFindWidget.id); + controller.hide(); + editor.focus(); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'notebook.find', + title: { value: localize('notebookActions.findInNotebook', "Find in Notebook"), original: 'Find in Notebook' }, + keybinding: { + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR, EditorContextKeys.focus.toNegated()), + primary: KeyCode.KeyF | KeyMod.CtrlCmd, + weight: KeybindingWeight.WorkbenchContrib + } + }); + } + + async run(accessor: ServicesAccessor): Promise<void> { + const editorService = accessor.get(IEditorService); + const editor = getNotebookEditorFromEditorPane(editorService.activeEditorPane); + + if (!editor) { + return; + } + + const controller = editor.getContribution<NotebookFindWidget>(NotebookFindWidget.id); + controller.show(); + } +}); + +function notebookContainsTextModel(uri: URI, textModel: ITextModel) { + if (textModel.uri.scheme === Schemas.vscodeNotebookCell) { + const cellUri = CellUri.parse(textModel.uri); + if (cellUri && isEqual(cellUri.notebook, uri)) { + return true; + } + } + + return false; +} + + +StartFindAction.addImplementation(100, (accessor: ServicesAccessor, codeEditor: ICodeEditor, args: any) => { + const editorService = accessor.get(IEditorService); + const editor = getNotebookEditorFromEditorPane(editorService.activeEditorPane); + + if (!editor) { + return false; + } + + if (!editor.hasEditorFocus() && !editor.hasWebviewFocus()) { + const codeEditorService = accessor.get(ICodeEditorService); + // check if the active pane contains the active text editor + const textEditor = codeEditorService.getFocusedCodeEditor() || codeEditorService.getActiveCodeEditor(); + if (editor.hasModel() && textEditor && textEditor.hasModel() && notebookContainsTextModel(editor.textModel.uri, textEditor.getModel())) { + // the active text editor is in notebook editor + } else { + return false; + } + } + + const controller = editor.getContribution<NotebookFindWidget>(NotebookFindWidget.id); + controller.show(); + return true; +}); + +StartFindReplaceAction.addImplementation(100, (accessor: ServicesAccessor, codeEditor: ICodeEditor, args: any) => { + const editorService = accessor.get(IEditorService); + const editor = getNotebookEditorFromEditorPane(editorService.activeEditorPane); + + if (!editor) { + return false; + } + + const controller = editor.getContribution<NotebookFindWidget>(NotebookFindWidget.id); + if (controller) { + controller.replace(); + return true; + } + + return false; +}); + 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 9e7e660b6b0..329490a8431 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts @@ -614,7 +614,7 @@ export abstract class SimpleFindReplaceWidget extends Widget { this._findInput.focus(); } - public show(initialInput?: string): void { + public show(initialInput?: string, options?: { focus?: boolean }): void { if (initialInput && !this._isVisible) { this._findInput.setValue(initialInput); } @@ -625,7 +625,9 @@ export abstract class SimpleFindReplaceWidget extends Widget { this._domNode.classList.add('visible', 'visible-transition'); this._domNode.setAttribute('aria-hidden', 'false'); - this.focus(); + if (options?.focus ?? true) { + this.focus(); + } }, 0); } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/findController.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget.ts index 662b1929a2e..c027d8137d4 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/findController.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget.ts @@ -3,46 +3,42 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./media/notebookFind'; +import * as DOM from 'vs/base/browser/dom'; +import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { alert as alertFn } from 'vs/base/browser/ui/aria/aria'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import * as strings from 'vs/base/common/strings'; -import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; -import { IContextKeyService, IContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; -import { INotebookEditor, CellEditState, INotebookEditorContribution, getNotebookEditorFromEditorPane } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { Range } from 'vs/editor/common/core/range'; +import { FindMatch } from 'vs/editor/common/model'; import { MATCHES_LIMIT } from 'vs/editor/contrib/find/browser/findModel'; -import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; -import * as DOM from 'vs/base/browser/dom'; -import { registerNotebookContribution } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; -import { registerAction2, Action2, IMenuService } from 'vs/platform/actions/common/actions'; -import { localize } from 'vs/nls'; -import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { FindReplaceState } from 'vs/editor/contrib/find/browser/findState'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { StartFindAction, StartFindReplaceAction } from 'vs/editor/contrib/find/browser/findController'; -import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { NLS_MATCHES_LOCATION, NLS_NO_RESULTS } from 'vs/editor/contrib/find/browser/findWidget'; +import { localize } from 'vs/nls'; +import { IMenuService } from 'vs/platform/actions/common/actions'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { NotebookFindFilters } from 'vs/workbench/contrib/notebook/browser/contrib/find/findFilters'; import { FindModel } from 'vs/workbench/contrib/notebook/browser/contrib/find/findModel'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { FindMatch, ITextModel } from 'vs/editor/common/model'; import { SimpleFindReplaceWidget } from 'vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget'; -import { NotebookFindFilters } from 'vs/workbench/contrib/notebook/browser/contrib/find/findFilters'; -import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { Schemas } from 'vs/base/common/network'; -import { CellUri } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { URI } from 'vs/base/common/uri'; -import { isEqual } from 'vs/base/common/resources'; +import { CellEditState, INotebookEditor, INotebookEditorContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; const FIND_HIDE_TRANSITION = 'find-hide-transition'; const FIND_SHOW_TRANSITION = 'find-show-transition'; let MAX_MATCHES_COUNT_WIDTH = 69; const PROGRESS_BAR_DELAY = 200; // show progress for at least 200ms +export interface IShowNotebookFindWidgetOptions { + isRegex?: boolean; + wholeWord?: boolean; + matchCase?: boolean; + matchIndex?: number; + focus?: boolean; +} + export class NotebookFindWidget extends SimpleFindReplaceWidget implements INotebookEditorContribution { static id: string = 'workbench.notebook.find'; protected _findWidgetFocused: IContextKey<boolean>; @@ -104,7 +100,7 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget implements INote private _onFindInputKeyDown(e: IKeyboardEvent): void { if (e.equals(KeyCode.Enter)) { - this._findModel.find(false); + this.find(false); e.preventDefault(); return; } else if (e.equals(KeyMod.Shift | KeyCode.Enter)) { @@ -125,8 +121,12 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget implements INote return false; } + private findIndex(index: number): void { + this._findModel.find({ index }); + } + protected find(previous: boolean): void { - this._findModel.find(previous); + this._findModel.find({ previous }); } protected replaceOne() { @@ -141,7 +141,7 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget implements INote this._findModel.ensureFindMatches(); if (this._findModel.currentMatch < 0) { - this._findModel.find(false); + this._findModel.find({ previous: false }); } const currentMatch = this._findModel.getCurrentMatch(); @@ -213,10 +213,18 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget implements INote protected onFindInputFocusTrackerFocus(): void { } protected onFindInputFocusTrackerBlur(): void { } - override show(initialInput?: string): void { - super.show(initialInput); + override async show(initialInput?: string, options?: IShowNotebookFindWidgetOptions): Promise<void> { + super.show(initialInput, options); this._state.change({ searchString: initialInput ?? '', isRevealed: true }, false); - this._findInput.select(); + + if (typeof options?.matchIndex === 'number') { + if (!this._findModel.findMatches.length) { + await this._findModel.research(); + } + this.findIndex(options.matchIndex); + } else { + this._findInput.select(); + } if (this._showTimeout === null) { if (this._hideTimeout !== null) { @@ -343,110 +351,3 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget implements INote super.dispose(); } } - -registerNotebookContribution(NotebookFindWidget.id, NotebookFindWidget); - -registerAction2(class extends Action2 { - constructor() { - super({ - id: 'notebook.hideFind', - title: { value: localize('notebookActions.hideFind', "Hide Find in Notebook"), original: 'Hide Find in Notebook' }, - keybinding: { - when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED), - primary: KeyCode.Escape, - weight: KeybindingWeight.WorkbenchContrib - } - }); - } - - async run(accessor: ServicesAccessor): Promise<void> { - const editorService = accessor.get(IEditorService); - const editor = getNotebookEditorFromEditorPane(editorService.activeEditorPane); - - if (!editor) { - return; - } - - const controller = editor.getContribution<NotebookFindWidget>(NotebookFindWidget.id); - controller.hide(); - editor.focus(); - } -}); - -registerAction2(class extends Action2 { - constructor() { - super({ - id: 'notebook.find', - title: { value: localize('notebookActions.findInNotebook', "Find in Notebook"), original: 'Find in Notebook' }, - keybinding: { - when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR, EditorContextKeys.focus.toNegated()), - primary: KeyCode.KeyF | KeyMod.CtrlCmd, - weight: KeybindingWeight.WorkbenchContrib - } - }); - } - - async run(accessor: ServicesAccessor): Promise<void> { - const editorService = accessor.get(IEditorService); - const editor = getNotebookEditorFromEditorPane(editorService.activeEditorPane); - - if (!editor) { - return; - } - - const controller = editor.getContribution<NotebookFindWidget>(NotebookFindWidget.id); - controller.show(); - } -}); - -function notebookContainsTextModel(uri: URI, textModel: ITextModel) { - if (textModel.uri.scheme === Schemas.vscodeNotebookCell) { - const cellUri = CellUri.parse(textModel.uri); - if (cellUri && isEqual(cellUri.notebook, uri)) { - return true; - } - } - - return false; -} - -StartFindAction.addImplementation(100, (accessor: ServicesAccessor, codeEditor: ICodeEditor, args: any) => { - const editorService = accessor.get(IEditorService); - const editor = getNotebookEditorFromEditorPane(editorService.activeEditorPane); - - if (!editor) { - return false; - } - - if (!editor.hasEditorFocus() && !editor.hasWebviewFocus()) { - const codeEditorService = accessor.get(ICodeEditorService); - // check if the active pane contains the active text editor - const textEditor = codeEditorService.getFocusedCodeEditor() || codeEditorService.getActiveCodeEditor(); - if (editor.hasModel() && textEditor && textEditor.hasModel() && notebookContainsTextModel(editor.textModel.uri, textEditor.getModel())) { - // the active text editor is in notebook editor - } else { - return false; - } - } - - const controller = editor.getContribution<NotebookFindWidget>(NotebookFindWidget.id); - controller.show(); - return true; -}); - -StartFindReplaceAction.addImplementation(100, (accessor: ServicesAccessor, codeEditor: ICodeEditor, args: any) => { - const editorService = accessor.get(IEditorService); - const editor = getNotebookEditorFromEditorPane(editorService.activeEditorPane); - - if (!editor) { - return false; - } - - const controller = editor.getContribution<NotebookFindWidget>(NotebookFindWidget.id); - if (controller) { - controller.replace(); - return true; - } - - return false; -}); diff --git a/src/vs/workbench/contrib/notebook/browser/controller/apiActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/apiActions.ts index d0c6379a54b..68d9d0f8dcc 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 } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { INotebookKernelService, IResolvedNotebookKernel, NotebookKernelType } 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.map(provider => ({ + return kernels.all.filter(kernel => kernel.type === NotebookKernelType.Resolved).map((provider) => ({ id: provider.id, label: provider.label, kind: provider.kind, description: provider.description, detail: provider.detail, isPreferred: false, // todo@jrieken,@rebornix - preloads: provider.preloadUris, + preloads: (provider as IResolvedNotebookKernel).preloadUris, })); }); diff --git a/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts index 588675f6403..8d253828d2e 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts @@ -21,12 +21,14 @@ import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/ import { changeCellToKind, runDeleteAction } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations'; import { CellToolbarOrder, CELL_TITLE_CELL_GROUP_ID, CELL_TITLE_OUTPUT_GROUP_ID, executeNotebookCondition, INotebookActionContext, INotebookCellActionContext, NotebookAction, NotebookCellAction, NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; import { NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_TYPE, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_HAS_OUTPUTS, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_USE_CONSOLIDATED_OUTPUT_BUTTON } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; -import { CellEditState, CHANGE_CELL_LANGUAGE, QUIT_EDIT_CELL_COMMAND_ID } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellEditState, CHANGE_CELL_LANGUAGE, DETECT_CELL_LANGUAGE, QUIT_EDIT_CELL_COMMAND_ID } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import * as icons from 'vs/workbench/contrib/notebook/browser/notebookIcons'; import { CellEditType, CellKind, ICellEditOperation, NotebookCellExecutionState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { ILanguageDetectionService } from 'vs/workbench/services/languageDetection/common/languageDetectionWorkerService'; import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; const CLEAR_ALL_CELLS_OUTPUTS_COMMAND_ID = 'notebook.clearAllCellsOutputs'; const EDIT_CELL_COMMAND_ID = 'notebook.cell.edit'; @@ -437,23 +439,7 @@ registerAction2(class ChangeCellLanguageAction extends NotebookCellAction<ICellR } private async setLanguage(context: IChangeCellContext, languageId: string) { - if (languageId === 'markdown' && context.cell?.language !== 'markdown') { - const idx = context.notebookEditor.getCellIndex(context.cell); - await changeCellToKind(CellKind.Markup, { cell: context.cell, notebookEditor: context.notebookEditor, ui: true }, 'markdown', Mimes.markdown); - const newCell = context.notebookEditor.cellAt(idx); - - if (newCell) { - context.notebookEditor.focusNotebookCell(newCell, 'editor'); - } - } else if (languageId !== 'markdown' && context.cell?.cellKind === CellKind.Markup) { - await changeCellToKind(CellKind.Code, { cell: context.cell, notebookEditor: context.notebookEditor, ui: true }, languageId); - } else { - const index = context.notebookEditor.textModel.cells.indexOf(context.cell.model); - context.notebookEditor.textModel.applyEdits( - [{ editType: CellEditType.CellLanguage, index, language: languageId }], - true, undefined, () => undefined, undefined, true - ); - } + await setCellToLanguage(languageId, context); } /** @@ -478,3 +464,50 @@ registerAction2(class ChangeCellLanguageAction extends NotebookCellAction<ICellR return fakeResource; } }); + +registerAction2(class DetectCellLanguageAction extends NotebookCellAction { + constructor() { + super({ + id: DETECT_CELL_LANGUAGE, + title: localize('detectLanguage', 'Accept Detected Language for Cell'), + f1: true, + precondition: ContextKeyExpr.and(NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE), + keybinding: { primary: KeyCode.KeyD | KeyMod.Alt | KeyMod.Shift, weight: KeybindingWeight.WorkbenchContrib } + }); + } + + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise<void> { + const languageDetectionService = accessor.get(ILanguageDetectionService); + const notificationService = accessor.get(INotificationService); + const kernelService = accessor.get(INotebookKernelService); + const kernel = kernelService.getSelectedOrSuggestedKernel(context.notebookEditor.textModel); + const providerLanguages = [...kernel?.supportedLanguages ?? []]; + providerLanguages.push('markdown'); + const detection = await languageDetectionService.detectLanguage(context.cell.uri, providerLanguages); + if (detection) { + setCellToLanguage(detection, context); + } else { + notificationService.warn(localize('noDetection', "Unable to detect cell language")); + } + } +}); + +async function setCellToLanguage(languageId: string, context: IChangeCellContext) { + if (languageId === 'markdown' && context.cell?.language !== 'markdown') { + const idx = context.notebookEditor.getCellIndex(context.cell); + await changeCellToKind(CellKind.Markup, { cell: context.cell, notebookEditor: context.notebookEditor, ui: true }, 'markdown', Mimes.markdown); + const newCell = context.notebookEditor.cellAt(idx); + + if (newCell) { + context.notebookEditor.focusNotebookCell(newCell, 'editor'); + } + } else if (languageId !== 'markdown' && context.cell?.cellKind === CellKind.Markup) { + await changeCellToKind(CellKind.Code, { cell: context.cell, notebookEditor: context.notebookEditor, ui: true }, languageId); + } else { + const index = context.notebookEditor.textModel.cells.indexOf(context.cell.model); + context.notebookEditor.textModel.applyEdits( + [{ editType: CellEditType.CellLanguage, index, language: languageId }], + true, undefined, () => undefined, undefined, true + ); + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/controller/layoutActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/layoutActions.ts index 27ce6ca5434..4f6a3e489c5 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/layoutActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/layoutActions.ts @@ -173,15 +173,6 @@ registerAction2(class ToggleBreadcrumbFromEditorTitle extends Action2 { } }); -MenuRegistry.appendMenuItem(MenuId.NotebookToolbar, { - command: { - id: 'breadcrumbs.toggle', - title: { value: localize('cmd.toggle', "Toggle Breadcrumbs"), original: 'Toggle Breadcrumbs' }, - }, - group: 'notebookLayout', - order: 2 -}); - registerAction2(class SaveMimeTypeDisplayOrder extends Action2 { constructor() { super({ diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebook.css b/src/vs/workbench/contrib/notebook/browser/media/notebook.css index a660892ff1f..e4220b57b14 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebook.css @@ -429,7 +429,7 @@ outline: none !important; } -.monaco-workbench .notebookOverlay.notebook-editor-editable > .cell-list-container > .monaco-list > .monaco-scrollable-element > .scrollbar.visible { +.monaco-workbench .notebookOverlay.notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .scrollbar.visible { z-index: var(--z-index-notebook-scrollbar); cursor: default; } diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 2b793c40961..466fd1bf565 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -70,7 +70,7 @@ import 'vs/workbench/contrib/notebook/browser/controller/foldingController'; // Editor Contribution import 'vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard'; -import 'vs/workbench/contrib/notebook/browser/contrib/find/findController'; +import 'vs/workbench/contrib/notebook/browser/contrib/find/notebookFind'; import 'vs/workbench/contrib/notebook/browser/contrib/format/formatting'; import 'vs/workbench/contrib/notebook/browser/contrib/gettingStarted/notebookGettingStarted'; import 'vs/workbench/contrib/notebook/browser/contrib/layout/layoutActions'; @@ -892,5 +892,22 @@ configurationRegistry.registerConfiguration({ enum: ['always', 'never', 'fromEditor'], default: 'fromEditor' }, + [NotebookSetting.outputLineHeight]: { + markdownDescription: nls.localize('notebook.outputLineHeight', "Line height of the output text for notebook cells.\n - Values between 0 and 8 will be used as a multiplier with the font size.\n - Values greater than or equal to 8 will be used as effective values."), + type: 'number', + default: 22, + tags: ['notebookLayout'] + }, + [NotebookSetting.outputFontSize]: { + markdownDescription: nls.localize('notebook.outputFontSize', "Font size for the output text for notebook cells. When set to 0 `#editor.fontSize#` is used."), + type: 'number', + default: 0, + tags: ['notebookLayout'] + }, + [NotebookSetting.outputFontFamily]: { + markdownDescription: nls.localize('notebook.outputFontFamily', "The font family for the output text for notebook cells. When set to empty, the `#editor.fontFamily#` is used."), + type: 'string', + tags: ['notebookLayout'] + }, } }); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index 7153bcfe26c..77a50b8c655 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -11,7 +11,7 @@ import { IEditorContributionDescription } from 'vs/editor/browser/editorExtensio import * as editorCommon from 'vs/editor/common/editorCommon'; import { FontInfo } from 'vs/editor/common/config/fontInfo'; import { IPosition } from 'vs/editor/common/core/position'; -import { Range } from 'vs/editor/common/core/range'; +import { IRange, Range } from 'vs/editor/common/core/range'; import { FindMatch, IModelDeltaDecoration, IReadonlyTextBuffer, ITextModel, TrackedRangeStickiness } from 'vs/editor/common/model'; import { MenuId } from 'vs/platform/actions/common/actions'; import { ITextEditorOptions, ITextResourceEditorInput } from 'vs/platform/editor/common/editor'; @@ -31,6 +31,7 @@ import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; //#region Shared commands export const EXPAND_CELL_INPUT_COMMAND_ID = 'notebook.cell.expandCellInput'; export const EXECUTE_CELL_COMMAND_ID = 'notebook.cell.execute'; +export const DETECT_CELL_LANGUAGE = 'notebook.cell.detectLanguage'; export const CHANGE_CELL_LANGUAGE = 'notebook.cell.changeLanguage'; export const QUIT_EDIT_CELL_COMMAND_ID = 'notebook.cell.quitEdit'; export const EXPAND_CELL_OUTPUT_COMMAND_ID = 'notebook.cell.expandCellOutput'; @@ -280,6 +281,7 @@ export interface INotebookEditorOptions extends ITextEditorOptions { readonly cellSelections?: ICellRange[]; readonly isReadOnly?: boolean; readonly viewState?: INotebookEditorViewState; + readonly indexedCellOptions?: { index: number; selection?: IRange }; } export type INotebookEditorContributionCtor = IConstructorSignature<INotebookEditorContribution, [INotebookEditor]>; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index 623686523fa..58edfc13e6b 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -7,7 +7,7 @@ import * as DOM from 'vs/base/browser/dom'; import { IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { IAction, toAction } from 'vs/base/common/actions'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { IErrorWithActions } from 'vs/base/common/errorMessage'; +import { createErrorWithActions } from 'vs/base/common/errorMessage'; import { Emitter, Event } from 'vs/base/common/event'; import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; import { extname, isEqual } from 'vs/base/common/resources'; @@ -289,8 +289,7 @@ export class NotebookEditor extends EditorPane implements IEditorPaneWithSelecti } } } catch (e) { - const error: Error & IErrorWithActions = e instanceof Error ? e : new Error(e.message); - error.actions = [ + const error = createErrorWithActions(e instanceof Error ? e : new Error(e.message), [ toAction({ id: 'workbench.notebook.action.openInTextEditor', label: localize('notebookOpenInTextEditor', "Open in Text Editor"), run: async () => { const activeEditorPane = this._editorService.activeEditorPane; @@ -317,7 +316,7 @@ export class NotebookEditor extends EditorPane implements IEditorPaneWithSelecti return; } }) - ]; + ]); throw error; } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index aa1bcc7853f..20ea78a5058 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -101,7 +101,6 @@ export class BaseCellEditorOptions extends Disposable implements IBaseCellEditor }, renderLineHighlightOnlyWhenFocus: true, overviewRulerLanes: 0, - selectOnLineNumbers: false, lineNumbers: 'off', lineDecorationsWidth: 0, folding: true, @@ -397,7 +396,11 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this._updateForNotebookConfiguration(); } - if (e.compactView || e.focusIndicator || e.insertToolbarPosition || e.cellToolbarLocation || e.dragAndDropEnabled || e.fontSize || e.markupFontSize || e.insertToolbarAlignment) { + if (e.fontFamily) { + this._generateFontInfo(); + } + + if (e.compactView || e.focusIndicator || e.insertToolbarPosition || e.cellToolbarLocation || e.dragAndDropEnabled || e.fontSize || e.outputFontSize || e.markupFontSize || e.fontFamily || e.outputFontFamily || e.insertToolbarAlignment || e.outputLineHeight) { this._styleElement?.remove(); this._createLayoutStyles(); this._webview?.updateOptions({ @@ -1216,8 +1219,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this.viewModel.updateOptions({ isReadOnly: this._readOnly }); // reveal cell if editor options tell to do so - if (options?.cellOptions) { - const cellOptions = options.cellOptions; + const cellOptions = options?.cellOptions ?? this._parseIndexedCellOptions(options); + if (cellOptions) { const cell = this.viewModel.viewCells.find(cell => cell.uri.toString() === cellOptions.resource.toString()); if (cell) { this.focusElement(cell); @@ -1270,6 +1273,24 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this._onDidChangeOptions.fire(); } + private _parseIndexedCellOptions(options: INotebookEditorOptions | undefined) { + if (options?.indexedCellOptions) { + // convert index based selections + const cell = this.cellAt(options.indexedCellOptions.index); + if (cell) { + return { + resource: cell.uri, + options: { + selection: options.indexedCellOptions.selection, + preserveFocus: false + } + }; + } + } + + return undefined; + } + private _detachModel() { this._localStore.clear(); dispose(this._localCellStateListeners); @@ -1802,6 +1823,11 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD const focusRange = this.viewModel.getFocus(); const element = this.viewModel.cellAt(focusRange.start); + // The notebook editor doesn't have focus yet + if (!this.hasEditorFocus()) { + this.focusContainer(); + } + if (element && element.focusMode === CellFocusMode.Editor) { element.updateEditState(CellEditState.Editing, 'editorWidget.focus'); element.focusMode = CellFocusMode.Editor; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookExecutionServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookExecutionServiceImpl.ts index 000c7b3b72c..2d4fe4cebe0 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookExecutionServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookExecutionServiceImpl.ts @@ -4,6 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { IDisposable } from 'vs/base/common/lifecycle'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { ILogService } from 'vs/platform/log/common/log'; import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; @@ -12,10 +14,11 @@ import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/mode 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 } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { INotebookKernelService, NotebookKernelType } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; -export class NotebookExecutionService implements INotebookExecutionService { +export class NotebookExecutionService implements INotebookExecutionService, IDisposable { declare _serviceBrand: undefined; + private _activeProxyKernelExecutionToken: CancellationTokenSource | undefined; constructor( @ICommandService private readonly _commandService: ICommandService, @@ -45,6 +48,29 @@ export class NotebookExecutionService implements INotebookExecutionService { return; } + 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.type === NotebookKernelType.Proxy) { + return; + } + const executeCells: NotebookCellTextModel[] = []; for (const cell of cellsArr) { const cellExe = this._notebookExecutionStateService.getCellExecution(cell.uri); @@ -75,11 +101,20 @@ export class NotebookExecutionService implements INotebookExecutionService { this._logService.debug(`NotebookExecutionService#cancelNotebookCellHandles ${JSON.stringify(cellsArr)}`); const kernel = this._notebookKernelService.getSelectedOrSuggestedKernel(notebook); if (kernel) { - await kernel.cancelNotebookCellExecution(notebook.uri, cellsArr); + if (kernel.type === NotebookKernelType.Proxy) { + this._activeProxyKernelExecutionToken?.dispose(true); + } else { + await kernel.cancelNotebookCellExecution(notebook.uri, cellsArr); + } + } } async cancelNotebookCells(notebook: INotebookTextModel, cells: Iterable<NotebookCellTextModel>): Promise<void> { this.cancelNotebookCellHandles(notebook, Array.from(cells, cell => cell.handle)); } + + dispose() { + this._activeProxyKernelExecutionToken?.dispose(true); + } } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookKernelServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookKernelServiceImpl.ts index 1f2e2e72032..3a14f27d0ff 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookKernelServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookKernelServiceImpl.ts @@ -196,7 +196,16 @@ export class NotebookKernelService extends Disposable implements INotebookKernel getSelectedOrSuggestedKernel(notebook: INotebookTextModel): INotebookKernel | undefined { const info = this.getMatchingKernel(notebook); - return info.selected ?? (info.all.length === 1 ? info.all[0] : undefined); + if (info.selected) { + return info.selected; + } + + const preferred = info.all.filter(kernel => this._kernels.get(kernel.id)?.notebookPriorities.get(notebook.uri) === 2 /* vscode.NotebookControllerPriority.Preferred */); + if (preferred.length === 1) { + return preferred[0]; + } + + return info.all.length === 1 ? info.all[0] : undefined; } // default kernel for notebookType diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellContextKeys.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellContextKeys.ts index e1e2a982be5..364939e3456 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellContextKeys.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellContextKeys.ts @@ -5,15 +5,15 @@ import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { CellEditState, CellFocusMode, ICellViewModel, INotebookEditorDelegate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { NotebookCellExecutionStateContext, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_EDITOR_FOCUSED, NOTEBOOK_CELL_EXECUTING, NOTEBOOK_CELL_EXECUTION_STATE, NOTEBOOK_CELL_FOCUSED, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_LINE_NUMBERS, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_TYPE } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; +import { CellViewModelStateChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; +import { CellPart } from 'vs/workbench/contrib/notebook/browser/view/cellPart'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { MarkupCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel'; import { NotebookCellExecutionState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NotebookCellExecutionStateContext, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_EDITOR_FOCUSED, NOTEBOOK_CELL_EXECUTING, NOTEBOOK_CELL_EXECUTION_STATE, NOTEBOOK_CELL_FOCUSED, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_LINE_NUMBERS, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_RESOURCE, NOTEBOOK_CELL_TYPE } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; -import { CellViewModelStateChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; -import { CellPart } from 'vs/workbench/contrib/notebook/browser/view/cellPart'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; export class CellContextKeyPart extends CellPart { private cellContextKeyManager: CellContextKeyManager; @@ -44,6 +44,7 @@ export class CellContextKeyManager extends Disposable { private cellContentCollapsed!: IContextKey<boolean>; private cellOutputCollapsed!: IContextKey<boolean>; private cellLineNumbers!: IContextKey<'on' | 'off' | 'inherit'>; + private cellResource!: IContextKey<string>; private markdownEditMode!: IContextKey<boolean>; @@ -69,6 +70,7 @@ export class CellContextKeyManager extends Disposable { this.cellContentCollapsed = NOTEBOOK_CELL_INPUT_COLLAPSED.bindTo(this._contextKeyService); this.cellOutputCollapsed = NOTEBOOK_CELL_OUTPUT_COLLAPSED.bindTo(this._contextKeyService); this.cellLineNumbers = NOTEBOOK_CELL_LINE_NUMBERS.bindTo(this._contextKeyService); + this.cellResource = NOTEBOOK_CELL_RESOURCE.bindTo(this._contextKeyService); if (element) { this.updateForElement(element); @@ -112,6 +114,7 @@ export class CellContextKeyManager extends Disposable { this.updateForOutputs(); this.cellLineNumbers.set(this.element!.lineNumbers); + this.cellResource.set(this.element!.uri.toString()); }); } diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDnd.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDnd.ts index 8c0b8a99748..86e0a69074a 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDnd.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDnd.ts @@ -78,7 +78,7 @@ export class CellDragAndDropController extends Disposable { this._register(DOM.addDisposableListener(document.body, DOM.EventType.DRAG_START, this.onGlobalDragStart.bind(this), true)); this._register(DOM.addDisposableListener(document.body, DOM.EventType.DRAG_END, this.onGlobalDragEnd.bind(this), true)); - const addCellDragListener = (eventType: string, handler: (e: CellDragEvent) => void) => { + const addCellDragListener = (eventType: string, handler: (e: CellDragEvent) => void, useCapture = false) => { this._register(DOM.addDisposableListener( notebookEditor.getDomNode(), eventType, @@ -87,14 +87,21 @@ export class CellDragAndDropController extends Disposable { if (cellDragEvent) { handler(cellDragEvent); } - })); + }, useCapture)); }; addCellDragListener(DOM.EventType.DRAG_OVER, event => { + if (!this.currentDraggedCell) { + return; + } event.browserEvent.preventDefault(); + event.browserEvent.stopImmediatePropagation(); this.onCellDragover(event); - }); + }, true); addCellDragListener(DOM.EventType.DROP, event => { + if (!this.currentDraggedCell) { + return; + } event.browserEvent.preventDefault(); this.onCellDrop(event); }); 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 46388b581b4..43c5852753e 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDragRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDragRenderer.ts @@ -65,7 +65,7 @@ class EditorTextRenderer { let result = ''; for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) { - const lineTokens = model.getLineTokens(lineNumber); + const lineTokens = model.tokenization.getLineTokens(lineNumber); const lineContent = lineTokens.getLineContent(); const startOffset = (lineNumber === startLineNumber ? startColumn - 1 : 0); const endOffset = (lineNumber === endLineNumber ? endColumn - 1 : lineContent.length); 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 d4745c02a43..469e7cbef3d 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellExecution.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellExecution.ts @@ -9,6 +9,7 @@ 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()); @@ -41,7 +42,7 @@ export class CellExecutionPart extends CellPart { } private updateExecutionOrder(internalMetadata: NotebookCellInternalMetadata): void { - if (this._notebookEditor.activeKernel?.implementsExecutionOrder) { + if (this._notebookEditor.activeKernel?.type === NotebookKernelType.Resolved && this._notebookEditor.activeKernel?.implementsExecutionOrder) { const executionOrderLabel = typeof internalMetadata.executionOrder === 'number' ? `[${internalMetadata.executionOrder}]` : '[ ]'; @@ -62,7 +63,8 @@ export class CellExecutionPart extends CellPart { DOM.hide(this._executionOrderLabel); } else { DOM.show(this._executionOrderLabel); - this._executionOrderLabel.style.top = `${element.layoutInfo.editorHeight}px`; + const top = element.layoutInfo.editorHeight - 22 + element.layoutInfo.statusBarHeight; + this._executionOrderLabel.style.top = `${top}px`; } } } 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 3f2df96e305..6cd5d2c242d 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts @@ -316,8 +316,17 @@ export class CodeCell extends Disposable { })); } + private shouldUpdateDOMFocus() { + // The DOM focus needs to be adjusted: + // when a cell editor should be focused + // the document active element is inside the notebook editor or the document body (cell editor being disposed previously) + return this.notebookEditor.getActiveCell() === this.viewCell + && this.viewCell.focusMode === CellFocusMode.Editor + && (this.notebookEditor.hasEditorFocus() || document.activeElement === document.body); + } + private updateEditorForFocusModeChange() { - if (this.viewCell.focusMode === CellFocusMode.Editor && this.notebookEditor.getActiveCell() === this.viewCell) { + if (this.shouldUpdateDOMFocus()) { this.templateData.editor?.focus(); } @@ -479,7 +488,7 @@ export class CodeCell extends Disposable { this._isDisposed = true; // move focus back to the cell list otherwise the focus goes to body - if (this.notebookEditor.getActiveCell() === this.viewCell && this.viewCell.focusMode === CellFocusMode.Editor) { + if (this.shouldUpdateDOMFocus()) { this.notebookEditor.focusContainer(); } 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 bf119feb15c..82e710ed494 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/markdownCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/markdownCell.ts @@ -221,7 +221,7 @@ export class StatefulMarkdownCell extends Disposable { override dispose() { // move focus back to the cell list otherwise the focus goes to body - if (this.notebookEditor.getActiveCell() === this.viewCell && this.viewCell.focusMode === CellFocusMode.Editor) { + if (this.notebookEditor.getActiveCell() === this.viewCell && this.viewCell.focusMode === CellFocusMode.Editor && (this.notebookEditor.hasEditorFocus() || document.activeElement === document.body)) { this.notebookEditor.focusContainer(); } @@ -327,6 +327,7 @@ export class StatefulMarkdownCell extends Disposable { width: width, height: editorHeight }, + enableDropIntoEditor: true, // overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode() }, { contributions: this.notebookEditor.creationOptions.cellEditorContributions 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 7bda661ed4e..a7145c9ffc9 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -37,10 +37,11 @@ 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 } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { INotebookKernel, IResolvedNotebookKernel, NotebookKernelType } 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'; +import { WebviewWindowDragMonitor } from 'vs/workbench/contrib/webview/browser/webviewWindowDragMonitor'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { FromWebviewMessage, IAckOutputHeight, IClickedDataUrlMessage, ICodeBlockHighlightRequest, IContentWidgetTopRequest, IControllerPreload, ICreationContent, ICreationRequestMessage, IFindMatch, IMarkupCellInitialization, ToWebviewMessage } from './webviewMessages'; @@ -88,8 +89,11 @@ interface BacklayerWebviewOptions { readonly runGutter: number; readonly dragAndDropEnabled: boolean; readonly fontSize: number; + readonly outputFontSize: number; readonly fontFamily: string; + readonly outputFontFamily: string; readonly markupFontSize: number; + readonly outputLineHeight: number; } export class BackLayerWebView<T extends ICommonCellInfo> extends Disposable { @@ -204,8 +208,9 @@ export class BackLayerWebView<T extends ICommonCellInfo> extends Disposable { 'notebook-output-node-left-padding': `${this.options.outputNodeLeftPadding}px`, 'notebook-markdown-min-height': `${this.options.previewNodePadding * 2}px`, 'notebook-markup-font-size': typeof this.options.markupFontSize === 'number' && this.options.markupFontSize > 0 ? `${this.options.markupFontSize}px` : `calc(${this.options.fontSize}px * 1.2)`, - 'notebook-cell-output-font-size': `${this.options.fontSize}px`, - 'notebook-cell-output-font-family': this.options.fontFamily, + 'notebook-cell-output-font-size': `${this.options.outputFontSize || this.options.fontSize}px`, + 'notebook-cell-output-line-height': `${this.options.outputLineHeight}px`, + 'notebook-cell-output-font-family': this.options.outputFontFamily || this.options.fontFamily, 'notebook-cell-markup-empty-content': nls.localize('notebook.emptyMarkdownPlaceholder', "Empty markdown cell, double click or press enter to edit."), 'notebook-cell-renderer-not-found-error': nls.localize({ key: 'notebook.error.rendererNotFound', @@ -523,6 +528,8 @@ var requirejs = (function() { this.webview.mountTo(this.element); this._register(this.webview); + this._register(new WebviewWindowDragMonitor(() => this.webview)); + this._register(this.webview.onDidClickLink(link => { if (this._disposed) { return; @@ -897,7 +904,7 @@ var requirejs = (function() { } this._preloadsCache.clear(); - if (this._currentKernel) { + if (this._currentKernel?.type === NotebookKernelType.Resolved) { this._updatePreloadsFromKernel(this._currentKernel); } @@ -1394,14 +1401,14 @@ var requirejs = (function() { const previousKernel = this._currentKernel; this._currentKernel = kernel; - if (previousKernel && previousKernel.preloadUris.length > 0) { + if (previousKernel?.type === NotebookKernelType.Resolved && previousKernel.preloadUris.length > 0) { this.webview?.reload(); // preloads will be restored after reload - } else if (kernel) { + } else if (kernel?.type === NotebookKernelType.Resolved) { this._updatePreloadsFromKernel(kernel); } } - private _updatePreloadsFromKernel(kernel: INotebookKernel) { + private _updatePreloadsFromKernel(kernel: IResolvedNotebookKernel) { const resources: IControllerPreload[] = []; for (const preload of kernel.preloadUris) { const uri = this.environmentService.isExtensionDevelopment && (preload.scheme === 'http' || preload.scheme === 'https') @@ -1427,7 +1434,7 @@ var requirejs = (function() { const mixedResourceRoots = [ ...(this.localResourceRootsCache || []), - ...(this._currentKernel ? [this._currentKernel.localResourceRoot] : []), + ...(this._currentKernel?.type === NotebookKernelType.Resolved ? [this._currentKernel.localResourceRoot] : []), ]; this.webview.localResourcesRoot = mixedResourceRoots; diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts index 02beef093cf..2bcf55c8531 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -168,7 +168,7 @@ export class MarkupCellRenderer extends AbstractCellRenderer implements IListRen templateDisposables.add(scopedInstaService.createInstance(BetweenCellToolbar, this.notebookEditor, titleToolbarContainer, bottomCellContainer)), templateDisposables.add(scopedInstaService.createInstance(CellEditorStatusBar, this.notebookEditor, container, editorPart, undefined)), templateDisposables.add(new CellFocusIndicator(this.notebookEditor, titleToolbar, focusIndicatorTop, focusIndicatorLeft, focusIndicatorRight, focusIndicatorBottom)), - templateDisposables.add(scopedInstaService.createInstance(FoldedCellHint, this.notebookEditor, DOM.append(container, $('.notebook-folded-hint')))), + templateDisposables.add(new FoldedCellHint(this.notebookEditor, DOM.append(container, $('.notebook-folded-hint')))), templateDisposables.add(new CellDecorations(rootContainer, decorationContainer)), templateDisposables.add(scopedInstaService.createInstance(CellComments, this.notebookEditor, cellCommentPartContainer)), templateDisposables.add(new CollapsedCellInput(this.notebookEditor, cellInputCollapsedContainer)), @@ -274,6 +274,7 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende width: 0, height: 0 }, + enableDropIntoEditor: true, }, { contributions: this.notebookEditor.creationOptions.cellEditorContributions }); diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts index 8b7c306af28..11b51e17e2f 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts @@ -156,8 +156,6 @@ export abstract class BaseCellViewModel extends Disposable { this._onDidChangeState.fire({ outputCollapsedChanged: true }); } - private _textEditorRestore: any; - constructor( readonly viewType: string, readonly model: NotebookCellTextModel, @@ -236,9 +234,7 @@ export abstract class BaseCellViewModel extends Disposable { this._textEditor = editor; if (this._editorViewStates) { - this._textEditorRestore = setTimeout(() => { - this._restoreViewState(this._editorViewStates); - }); + this._restoreViewState(this._editorViewStates); } if (this._editorTransientState) { @@ -264,7 +260,6 @@ export abstract class BaseCellViewModel extends Disposable { } detachTextEditor() { - clearTimeout(this._textEditorRestore); this.saveViewState(); this.saveTransientState(); // decorations need to be cleared first as editors can be resued. @@ -589,7 +584,6 @@ export abstract class BaseCellViewModel extends Disposable { super.dispose(); dispose(this._editorListeners); - clearTimeout(this._textEditorRestore); // Only remove the undo redo stack if we map this cell uri to itself // If we are not in perCell mode, it will map to the full NotebookDocument and diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorWidgetContextKeys.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorWidgetContextKeys.ts index a90477db542..b1cf70a4946 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorWidgetContextKeys.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorWidgetContextKeys.ts @@ -8,7 +8,7 @@ import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/c 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 { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; -import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { INotebookKernelService, NotebookKernelType } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; export class NotebookEditorContextKeys { @@ -148,7 +148,7 @@ export class NotebookEditorContextKeys { const { selected, all } = this._notebookKernelService.getMatchingKernel(this._editor.textModel); this._notebookKernelCount.set(all.length); - this._interruptibleKernel.set(selected?.implementsInterrupt ?? false); + this._interruptibleKernel.set((selected?.type === NotebookKernelType.Resolved && 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 dc9a494193e..e5814ce98ec 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelActionViewItem.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelActionViewItem.ts @@ -6,10 +6,11 @@ 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 } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { INotebookKernelMatchResult, INotebookKernelService, NotebookKernelType, ProxyKernelState } 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'; @@ -17,6 +18,7 @@ import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookB export class NotebooKernelActionViewItem extends ActionViewItem { private _kernelLabel?: HTMLAnchorElement; + private _kernelDisposable: DisposableStore; constructor( actualAction: IAction, @@ -31,6 +33,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()); } override render(container: HTMLElement): void { @@ -63,9 +66,9 @@ export class NotebooKernelActionViewItem extends ActionViewItem { } private _updateActionFromKernelInfo(info: INotebookKernelMatchResult): void { - + this._kernelDisposable.clear(); this._action.enabled = true; - const selectedOrSuggested = info.selected ?? (info.all.length === 1 && info.suggestions.length === 1 ? info.suggestions[0] : undefined); + const selectedOrSuggested = info.selected ?? ((info.suggestions.length === 1 && info.suggestions[0].type === NotebookKernelType.Resolved) ? info.suggestions[0] : undefined); if (selectedOrSuggested) { // selected or suggested kernel this._action.label = selectedOrSuggested.label; @@ -74,6 +77,23 @@ export class NotebooKernelActionViewItem extends ActionViewItem { // 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"); diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index bc1f504dc36..28ff3f4f885 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -25,8 +25,7 @@ import { IEditorModel } from 'vs/platform/editor/common/editor'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { ThemeColor } from 'vs/platform/theme/common/themeService'; import { UndoRedoGroup } from 'vs/platform/undoRedo/common/undoRedo'; -import { IRevertOptions, ISaveOptions } from 'vs/workbench/common/editor'; -import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { IRevertOptions, ISaveOptions, IUntypedEditorInput } from 'vs/workbench/common/editor'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { IWorkingCopyBackupMeta, IWorkingCopySaveEvent } from 'vs/workbench/services/workingCopy/common/workingCopy'; @@ -792,7 +791,7 @@ export interface INotebookEditorModel extends IEditorModel { hasAssociatedFilePath(): boolean; load(options?: INotebookLoadOptions): Promise<IResolvedNotebookEditorModel>; save(options?: ISaveOptions): Promise<boolean>; - saveAs(target: URI): Promise<EditorInput | undefined>; + saveAs(target: URI): Promise<IUntypedEditorInput | undefined>; revert(options?: IRevertOptions): Promise<void>; } @@ -925,7 +924,10 @@ export const NotebookSetting = { textOutputLineLimit: 'notebook.output.textLineLimit', globalToolbarShowLabel: 'notebook.globalToolbarShowLabel', markupFontSize: 'notebook.markup.fontSize', - interactiveWindowCollapseCodeCells: 'interactiveWindow.collapseCellInputCode' + interactiveWindowCollapseCodeCells: 'interactiveWindow.collapseCellInputCode', + outputLineHeight: 'notebook.outputLineHeight', + outputFontSize: 'notebook.outputFontSize', + outputFontFamily: 'notebook.outputFontFamily' } as const; export const enum CellStatusbarAlignment { diff --git a/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts b/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts index fd2279cba63..689f9ca995e 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts @@ -38,6 +38,8 @@ export const NOTEBOOK_CELL_EXECUTING = new RawContextKey<boolean>('notebookCellE export const NOTEBOOK_CELL_HAS_OUTPUTS = new RawContextKey<boolean>('notebookCellHasOutputs', false); export const NOTEBOOK_CELL_INPUT_COLLAPSED = new RawContextKey<boolean>('notebookCellInputIsCollapsed', false); export const NOTEBOOK_CELL_OUTPUT_COLLAPSED = new RawContextKey<boolean>('notebookCellOutputIsCollapsed', false); +export const NOTEBOOK_CELL_RESOURCE = new RawContextKey<string>('notebookCellResource', ''); + // Kernels export const NOTEBOOK_KERNEL = new RawContextKey<string>('notebookKernel', undefined); export const NOTEBOOK_KERNEL_COUNT = new RawContextKey<number>('notebookKernelCount', 0); diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts index cffd2b0362d..b8cbd4985d0 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts @@ -102,6 +102,10 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { } } + if (!(capabilities & EditorInputCapabilities.Readonly)) { + capabilities |= EditorInputCapabilities.CanDropIntoEditor; + } + return capabilities; } @@ -120,7 +124,7 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { return this._editorModelReference.object.isDirty(); } - override async save(group: GroupIdentifier, options?: ISaveOptions): Promise<EditorInput | undefined> { + override async save(group: GroupIdentifier, options?: ISaveOptions): Promise<EditorInput | IUntypedEditorInput | undefined> { if (this._editorModelReference) { if (this.hasCapability(EditorInputCapabilities.Untitled)) { @@ -135,7 +139,7 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { return undefined; } - override async saveAs(group: GroupIdentifier, options?: ISaveOptions): Promise<EditorInput | undefined> { + override async saveAs(group: GroupIdentifier, options?: ISaveOptions): Promise<IUntypedEditorInput | undefined> { if (!this._editorModelReference) { return undefined; } diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts index 8938cff2e80..7cd345036a7 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts @@ -4,8 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import { IRevertOptions, ISaveOptions } from 'vs/workbench/common/editor'; -import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { IRevertOptions, ISaveOptions, IUntypedEditorInput } from 'vs/workbench/common/editor'; import { EditorModel } from 'vs/workbench/common/editor/editorModel'; import { Emitter, Event } from 'vs/base/common/event'; import { ICellDto2, INotebookEditorModel, INotebookLoadOptions, IResolvedNotebookEditorModel, NotebookCellsChangeType, NotebookData, NotebookDocumentBackupData } from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -28,8 +27,6 @@ import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/commo import { StoredFileWorkingCopyState, IStoredFileWorkingCopy, IStoredFileWorkingCopyModel, IStoredFileWorkingCopyModelContentChangedEvent, IStoredFileWorkingCopyModelFactory, IStoredFileWorkingCopySaveEvent } from 'vs/workbench/services/workingCopy/common/storedFileWorkingCopy'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { CancellationError } from 'vs/base/common/errors'; -import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { filter } from 'vs/base/common/objects'; import { IFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager'; import { IUntitledFileWorkingCopy, IUntitledFileWorkingCopyModel, IUntitledFileWorkingCopyModelContentChangedEvent, IUntitledFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopy'; @@ -59,7 +56,6 @@ export class ComplexNotebookEditorModel extends EditorModel implements INotebook readonly resource: URI, readonly viewType: string, private readonly _contentProvider: INotebookContentProvider, - @IInstantiationService private readonly _instantiationService: IInstantiationService, @INotebookService private readonly _notebookService: INotebookService, @IWorkingCopyService private readonly _workingCopyService: IWorkingCopyService, @IWorkingCopyBackupService private readonly _workingCopyBackupService: IWorkingCopyBackupService, @@ -393,7 +389,7 @@ export class ComplexNotebookEditorModel extends EditorModel implements INotebook }); } - async saveAs(targetResource: URI): Promise<EditorInput | undefined> { + async saveAs(targetResource: URI): Promise<IUntypedEditorInput | undefined> { if (!this.isResolved()) { return undefined; @@ -419,7 +415,7 @@ export class ComplexNotebookEditorModel extends EditorModel implements INotebook } this.setDirty(false); this._onDidSave.fire({}); - return this._instantiationService.createInstance(NotebookEditorInput, targetResource, this.viewType, {}); + return { resource: targetResource }; } private async _resolveStats(resource: URI) { @@ -462,7 +458,6 @@ export class SimpleNotebookEditorModel extends EditorModel implements INotebookE private readonly _hasAssociatedFilePath: boolean, readonly viewType: string, private readonly _workingCopyManager: IFileWorkingCopyManager<NotebookFileWorkingCopyModel, NotebookFileWorkingCopyModel>, - @IInstantiationService private readonly _instantiationService: IInstantiationService, @IFileService private readonly _fileService: IFileService ) { super(); @@ -547,14 +542,14 @@ export class SimpleNotebookEditorModel extends EditorModel implements INotebookE return this; } - async saveAs(target: URI): Promise<EditorInput | undefined> { + async saveAs(target: URI): Promise<IUntypedEditorInput | undefined> { const newWorkingCopy = await this._workingCopyManager.saveAs(this.resource, target); if (!newWorkingCopy) { return undefined; } // this is a little hacky because we leave the new working copy alone. BUT // the newly created editor input will pick it up and claim ownership of it. - return this._instantiationService.createInstance(NotebookEditorInput, newWorkingCopy.resource, this.viewType, {}); + return { resource: newWorkingCopy.resource }; } private static _isStoredFileWorkingCopy(candidate?: IStoredFileWorkingCopy<NotebookFileWorkingCopyModel> | IUntitledFileWorkingCopy<NotebookFileWorkingCopyModel>): candidate is IStoredFileWorkingCopy<NotebookFileWorkingCopyModel> { diff --git a/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts b/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts index 6610fe7177d..4f5304600b0 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts @@ -31,8 +31,13 @@ export interface INotebookKernelChangeEvent { hasExecutionOrder?: true; } -export interface INotebookKernel { +export const enum NotebookKernelType { + Resolved, + Proxy = 1 +} +export interface IResolvedNotebookKernel { + readonly type: NotebookKernelType.Resolved; readonly id: string; readonly viewType: string; readonly onDidChange: Event<Readonly<INotebookKernelChangeEvent>>; @@ -54,6 +59,34 @@ export interface INotebookKernel { cancelNotebookCellExecution(uri: URI, cellHandles: number[]): Promise<void>; } +export const enum ProxyKernelState { + Disconnected = 1, + Connected = 2, + Initializing = 3 +} + +export interface INotebookProxyKernelChangeEvent extends INotebookKernelChangeEvent { + 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 type INotebookKernel = IResolvedNotebookKernel | INotebookProxyKernel; + export interface INotebookTextModelLike { uri: URI; viewType: string } export const INotebookKernelService = createDecorator<INotebookKernelService>('INotebookKernelService'); diff --git a/src/vs/workbench/contrib/notebook/common/notebookOptions.ts b/src/vs/workbench/contrib/notebook/common/notebookOptions.ts index 9564a563def..426871f84fa 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookOptions.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookOptions.ts @@ -62,6 +62,9 @@ export interface NotebookLayoutConfiguration { showFoldingControls: 'always' | 'mouseover'; dragAndDropEnabled: boolean; fontSize: number; + outputFontSize: number; + outputFontFamily: string; + outputLineHeight: number; markupFontSize: number; focusIndicatorLeftMargin: number; editorOptionsCustomizations: any | undefined; @@ -84,9 +87,13 @@ export interface NotebookOptionsChangeEvent { readonly consolidatedRunButton?: boolean; readonly dragAndDropEnabled?: boolean; readonly fontSize?: boolean; + readonly outputFontSize?: boolean; readonly markupFontSize?: boolean; + readonly fontFamily?: boolean; + readonly outputFontFamily?: boolean; readonly editorOptionsCustomizations?: boolean; readonly interactiveWindowCollapseCodeCells?: boolean; + readonly outputLineHeight?: boolean; } const defaultConfigConstants = Object.freeze({ @@ -134,9 +141,12 @@ export class NotebookOptions extends Disposable { const showFoldingControls = this._computeShowFoldingControlsOption(); // const { bottomToolbarGap, bottomToolbarHeight } = this._computeBottomToolbarDimensions(compactView, insertToolbarPosition, insertToolbarAlignment); const fontSize = this.configurationService.getValue<number>('editor.fontSize'); + const outputFontSize = this.configurationService.getValue<number>(NotebookSetting.outputFontSize); + const outputFontFamily = this.configurationService.getValue<string>(NotebookSetting.outputFontFamily); const markupFontSize = this.configurationService.getValue<number>(NotebookSetting.markupFontSize); const editorOptionsCustomizations = this.configurationService.getValue(NotebookSetting.cellEditorOptionsCustomizations); const interactiveWindowCollapseCodeCells: InteractiveWindowCollapseCodeCells = this.configurationService.getValue(NotebookSetting.interactiveWindowCollapseCodeCells); + const outputLineHeight = this._computeOutputLineHeight(); this._layoutConfiguration = { ...(compactView ? compactConfigConstants : defaultConfigConstants), @@ -166,6 +176,9 @@ export class NotebookOptions extends Disposable { insertToolbarAlignment, showFoldingControls, fontSize, + outputFontSize, + outputFontFamily, + outputLineHeight, markupFontSize, editorOptionsCustomizations, focusIndicatorGap: 3, @@ -185,6 +198,29 @@ export class NotebookOptions extends Disposable { })); } + private _computeOutputLineHeight(): number { + const minimumLineHeight = 8; + let lineHeight = this.configurationService.getValue<number>(NotebookSetting.outputLineHeight); + + if (lineHeight < minimumLineHeight) { + // Values too small to be line heights in pixels are in ems. + let fontSize = this.configurationService.getValue<number>(NotebookSetting.outputFontSize); + if (fontSize === 0) { + fontSize = this.configurationService.getValue<number>('editor.fontSize'); + } + + lineHeight = lineHeight * fontSize; + } + + // Enforce integer, minimum constraints + lineHeight = Math.round(lineHeight); + if (lineHeight < minimumLineHeight) { + lineHeight = minimumLineHeight; + } + + return lineHeight; + } + private _updateConfiguration(e: IConfigurationChangeEvent) { const cellStatusBarVisibility = e.affectsConfiguration(NotebookSetting.showCellStatusBar); const cellToolbarLocation = e.affectsConfiguration(NotebookSetting.cellToolbarLocation); @@ -199,9 +235,13 @@ export class NotebookOptions extends Disposable { const showFoldingControls = e.affectsConfiguration(NotebookSetting.showFoldingControls); const dragAndDropEnabled = e.affectsConfiguration(NotebookSetting.dragAndDropEnabled); const fontSize = e.affectsConfiguration('editor.fontSize'); + const outputFontSize = e.affectsConfiguration(NotebookSetting.outputFontSize); const markupFontSize = e.affectsConfiguration(NotebookSetting.markupFontSize); + const fontFamily = e.affectsConfiguration('editor.fontFamily'); + const outputFontFamily = e.affectsConfiguration(NotebookSetting.outputFontFamily); const editorOptionsCustomizations = e.affectsConfiguration(NotebookSetting.cellEditorOptionsCustomizations); const interactiveWindowCollapseCodeCells = e.affectsConfiguration(NotebookSetting.interactiveWindowCollapseCodeCells); + const outputLineHeight = e.affectsConfiguration(NotebookSetting.outputLineHeight); if ( !cellStatusBarVisibility @@ -217,9 +257,13 @@ export class NotebookOptions extends Disposable { && !showFoldingControls && !dragAndDropEnabled && !fontSize + && !outputFontSize && !markupFontSize + && !fontFamily + && !outputFontFamily && !editorOptionsCustomizations - && !interactiveWindowCollapseCodeCells) { + && !interactiveWindowCollapseCodeCells + && !outputLineHeight) { return; } @@ -281,10 +325,18 @@ export class NotebookOptions extends Disposable { configuration.fontSize = this.configurationService.getValue<number>('editor.fontSize'); } + if (outputFontSize) { + configuration.outputFontSize = this.configurationService.getValue<number>(NotebookSetting.outputFontSize) ?? configuration.fontSize; + } + if (markupFontSize) { configuration.markupFontSize = this.configurationService.getValue<number>(NotebookSetting.markupFontSize); } + if (outputFontFamily) { + configuration.outputFontFamily = this.configurationService.getValue<string>(NotebookSetting.outputFontFamily); + } + if (editorOptionsCustomizations) { configuration.editorOptionsCustomizations = this.configurationService.getValue(NotebookSetting.cellEditorOptionsCustomizations); } @@ -293,6 +345,10 @@ export class NotebookOptions extends Disposable { configuration.interactiveWindowCollapseCodeCells = this.configurationService.getValue(NotebookSetting.interactiveWindowCollapseCodeCells); } + if (outputLineHeight || fontSize || outputFontSize) { + configuration.outputLineHeight = this._computeOutputLineHeight(); + } + this._layoutConfiguration = Object.freeze(configuration); // trigger event @@ -310,9 +366,13 @@ export class NotebookOptions extends Disposable { consolidatedRunButton, dragAndDropEnabled, fontSize, + outputFontSize, markupFontSize, + fontFamily, + outputFontFamily, editorOptionsCustomizations, - interactiveWindowCollapseCodeCells + interactiveWindowCollapseCodeCells, + outputLineHeight }); } @@ -502,7 +562,10 @@ export class NotebookOptions extends Disposable { runGutter: this._layoutConfiguration.cellRunGutter, dragAndDropEnabled: this._layoutConfiguration.dragAndDropEnabled, fontSize: this._layoutConfiguration.fontSize, + outputFontSize: this._layoutConfiguration.outputFontSize, + outputFontFamily: this._layoutConfiguration.outputFontFamily, markupFontSize: this._layoutConfiguration.markupFontSize, + outputLineHeight: this._layoutConfiguration.outputLineHeight, }; } @@ -517,7 +580,10 @@ export class NotebookOptions extends Disposable { runGutter: 0, dragAndDropEnabled: false, fontSize: this._layoutConfiguration.fontSize, + outputFontSize: this._layoutConfiguration.outputFontSize, + outputFontFamily: this._layoutConfiguration.outputFontFamily, markupFontSize: this._layoutConfiguration.markupFontSize, + outputLineHeight: this._layoutConfiguration.outputLineHeight, }; } diff --git a/src/vs/workbench/contrib/notebook/test/browser/contrib/find.test.ts b/src/vs/workbench/contrib/notebook/test/browser/contrib/find.test.ts index b9822f87393..6b1282d9afb 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/contrib/find.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/contrib/find.test.ts @@ -68,11 +68,11 @@ suite('Notebook Find', () => { await found; assert.strictEqual(model.findMatches.length, 2); assert.strictEqual(model.currentMatch, -1); - model.find(false); + model.find({ previous: false }); assert.strictEqual(model.currentMatch, 0); - model.find(false); + model.find({ previous: false }); assert.strictEqual(model.currentMatch, 1); - model.find(false); + model.find({ previous: false }); assert.strictEqual(model.currentMatch, 0); assert.strictEqual(editor.textModel.length, 3); @@ -115,11 +115,11 @@ suite('Notebook Find', () => { // find matches is not necessarily find results assert.strictEqual(model.findMatches.length, 4); assert.strictEqual(model.currentMatch, -1); - model.find(false); + model.find({ previous: false }); assert.strictEqual(model.currentMatch, 0); - model.find(false); + model.find({ previous: false }); assert.strictEqual(model.currentMatch, 1); - model.find(false); + model.find({ previous: false }); assert.strictEqual(model.currentMatch, 2); const found2 = new Promise<boolean>(resolve => state.onFindReplaceStateChange(e => { @@ -132,13 +132,13 @@ suite('Notebook Find', () => { assert.strictEqual(model.findMatches.length, 3); assert.strictEqual(model.currentMatch, 2); - model.find(true); + model.find({ previous: true }); assert.strictEqual(model.currentMatch, 1); - model.find(false); + model.find({ previous: false }); assert.strictEqual(model.currentMatch, 2); - model.find(false); + model.find({ previous: false }); assert.strictEqual(model.currentMatch, 3); - model.find(false); + model.find({ previous: false }); assert.strictEqual(model.currentMatch, 0); }); }); @@ -166,7 +166,7 @@ suite('Notebook Find', () => { // find matches is not necessarily find results assert.strictEqual(model.findMatches.length, 4); assert.strictEqual(model.currentMatch, -1); - model.find(true); + model.find({ previous: true }); assert.strictEqual(model.currentMatch, 4); const found2 = new Promise<boolean>(resolve => state.onFindReplaceStateChange(e => { @@ -178,9 +178,9 @@ suite('Notebook Find', () => { await found2; assert.strictEqual(model.findMatches.length, 3); assert.strictEqual(model.currentMatch, 0); - model.find(true); + model.find({ previous: true }); assert.strictEqual(model.currentMatch, 3); - model.find(true); + model.find({ previous: true }); assert.strictEqual(model.currentMatch, 2); }); }); @@ -208,9 +208,9 @@ suite('Notebook Find', () => { // find matches is not necessarily find results assert.strictEqual(model.findMatches.length, 4); assert.strictEqual(model.currentMatch, -1); - model.find(false); - model.find(false); - model.find(false); + model.find({ previous: false }); + model.find({ previous: false }); + model.find({ previous: false }); assert.strictEqual(model.currentMatch, 2); const found2 = new Promise<boolean>(resolve => state.onFindReplaceStateChange(e => { if (e.matchesCount) { resolve(true); } @@ -244,11 +244,11 @@ suite('Notebook Find', () => { await found; assert.strictEqual(model.findMatches.length, 2); assert.strictEqual(model.currentMatch, -1); - model.find(false); + model.find({ previous: false }); assert.strictEqual(model.currentMatch, 0); - model.find(false); + model.find({ previous: false }); assert.strictEqual(model.currentMatch, 1); - model.find(false); + model.find({ previous: false }); assert.strictEqual(model.currentMatch, 0); assert.strictEqual(editor.textModel.length, 3); diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookEditorModel.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookEditorModel.test.ts index d71a25076b3..124729d29b4 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookEditorModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookEditorModel.test.ts @@ -10,7 +10,6 @@ import { isEqual } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { mock } from 'vs/base/test/common/mock'; import { IFileService } from 'vs/platform/files/common/files'; -import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; import { ILabelService } from 'vs/platform/label/common/label'; import { NullLogService } from 'vs/platform/log/common/log'; import { INotificationService } from 'vs/platform/notification/common/notification'; @@ -187,7 +186,6 @@ suite('NotebookFileWorkingCopyModel', function () { suite('ComplexNotebookEditorModel', function () { - const instaService = new InstantiationService(); const notebokService = new class extends mock<INotebookService>() { }; const backupService = new class extends mock<IWorkingCopyBackupService>() { }; const notificationService = new class extends mock<INotificationService>() { }; @@ -214,8 +212,8 @@ suite('ComplexNotebookEditorModel', function () { } }; - new ComplexNotebookEditorModel(r1, 'fff', notebookDataProvider, instaService, notebokService, workingCopyService, backupService, fileService, notificationService, new NullLogService(), untitledTextEditorService, labelService); - new ComplexNotebookEditorModel(r2, 'fff', notebookDataProvider, instaService, notebokService, workingCopyService, backupService, fileService, notificationService, new NullLogService(), untitledTextEditorService, labelService); + new ComplexNotebookEditorModel(r1, 'fff', notebookDataProvider, notebokService, workingCopyService, backupService, fileService, notificationService, new NullLogService(), untitledTextEditorService, labelService); + new ComplexNotebookEditorModel(r2, 'fff', notebookDataProvider, notebokService, workingCopyService, backupService, fileService, notificationService, new NullLogService(), untitledTextEditorService, labelService); assert.strictEqual(copies.length, 2); assert.strictEqual(!isEqual(copies[0].resource, copies[1].resource), true); 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 6f65268830f..8d3bff7a83a 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionService.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionService.test.ts @@ -20,7 +20,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 { INotebookKernel, INotebookKernelService, ISelectedNotebooksChangeEvent } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { INotebookKernelService, IResolvedNotebookKernel, ISelectedNotebooksChangeEvent, NotebookKernelType } 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'; @@ -166,7 +166,8 @@ suite('NotebookExecutionService', () => { }); }); -class TestNotebookKernel implements INotebookKernel { +class TestNotebookKernel implements IResolvedNotebookKernel { + type: NotebookKernelType.Resolved = NotebookKernelType.Resolved; id: string = 'test'; label: string = ''; viewType = '*'; 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 887a4a05443..e5b7f84b8bf 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionStateService.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionStateService.test.ts @@ -21,7 +21,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 { INotebookKernel, INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { INotebookKernelService, IResolvedNotebookKernel, NotebookKernelType } 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'; @@ -170,7 +170,8 @@ suite('NotebookExecutionStateService', () => { }); }); -class TestNotebookKernel implements INotebookKernel { +class TestNotebookKernel implements IResolvedNotebookKernel { + type: NotebookKernelType.Resolved = NotebookKernelType.Resolved; 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 b1ebabc1db2..62d6afecb8b 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 { INotebookKernel, INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { INotebookKernelService, IResolvedNotebookKernel, NotebookKernelType } 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'; @@ -159,7 +159,8 @@ suite('NotebookKernelService', () => { }); }); -class TestNotebookKernel implements INotebookKernel { +class TestNotebookKernel implements IResolvedNotebookKernel { + type: NotebookKernelType.Resolved = NotebookKernelType.Resolved; id: string = Math.random() + 'kernel'; label: string = 'test-label'; viewType = '*'; diff --git a/src/vs/workbench/contrib/output/browser/logViewer.ts b/src/vs/workbench/contrib/output/browser/logViewer.ts index a542e7dbe91..3c78aa114d1 100644 --- a/src/vs/workbench/contrib/output/browser/logViewer.ts +++ b/src/vs/workbench/contrib/output/browser/logViewer.ts @@ -15,8 +15,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { TextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput'; import { URI } from 'vs/base/common/uri'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; -import { LOG_SCHEME } from 'vs/workbench/contrib/output/common/output'; -import { IFileOutputChannelDescriptor } from 'vs/workbench/services/output/common/output'; +import { LOG_SCHEME, IFileOutputChannelDescriptor } from 'vs/workbench/services/output/common/output'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; diff --git a/src/vs/workbench/contrib/output/browser/output.contribution.ts b/src/vs/workbench/contrib/output/browser/output.contribution.ts index b6dbbb8caf7..f9e4814794e 100644 --- a/src/vs/workbench/contrib/output/browser/output.contribution.ts +++ b/src/vs/workbench/contrib/output/browser/output.contribution.ts @@ -12,7 +12,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { MenuId, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { OutputService, LogContentProvider } from 'vs/workbench/contrib/output/browser/outputServices'; -import { OUTPUT_MODE_ID, OUTPUT_MIME, OUTPUT_VIEW_ID, IOutputService, CONTEXT_IN_OUTPUT, LOG_SCHEME, LOG_MODE_ID, LOG_MIME, CONTEXT_ACTIVE_LOG_OUTPUT, CONTEXT_OUTPUT_SCROLL_LOCK } from 'vs/workbench/contrib/output/common/output'; +import { OUTPUT_MODE_ID, OUTPUT_MIME, OUTPUT_VIEW_ID, IOutputService, CONTEXT_IN_OUTPUT, LOG_SCHEME, LOG_MODE_ID, LOG_MIME, CONTEXT_ACTIVE_LOG_OUTPUT, CONTEXT_OUTPUT_SCROLL_LOCK, IOutputChannelDescriptor, IFileOutputChannelDescriptor } from 'vs/workbench/services/output/common/output'; import { OutputViewPane } from 'vs/workbench/contrib/output/browser/outputView'; import { IEditorPaneRegistry, EditorPaneDescriptor } from 'vs/workbench/browser/editor'; import { LogViewer, LogViewerInput } from 'vs/workbench/contrib/output/browser/logViewer'; @@ -25,7 +25,6 @@ import { ViewContainer, IViewContainersRegistry, ViewContainerLocation, Extensio import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { IQuickPickItem, IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; -import { IOutputChannelDescriptor, IFileOutputChannelDescriptor } from 'vs/workbench/services/output/common/output'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { assertIsDefined } from 'vs/base/common/types'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; diff --git a/src/vs/workbench/contrib/output/browser/outputLinkProvider.ts b/src/vs/workbench/contrib/output/browser/outputLinkProvider.ts index eb346086655..ca06b37e21f 100644 --- a/src/vs/workbench/contrib/output/browser/outputLinkProvider.ts +++ b/src/vs/workbench/contrib/output/browser/outputLinkProvider.ts @@ -8,7 +8,7 @@ import { RunOnceScheduler } from 'vs/base/common/async'; import { IModelService } from 'vs/editor/common/services/model'; import { ILink } from 'vs/editor/common/languages'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { OUTPUT_MODE_ID, LOG_MODE_ID } from 'vs/workbench/contrib/output/common/output'; +import { OUTPUT_MODE_ID, LOG_MODE_ID } from 'vs/workbench/services/output/common/output'; import { MonacoWebWorker, createWebWorker } from 'vs/editor/browser/services/webWorker'; import { ICreateData, OutputLinkComputer } from 'vs/workbench/contrib/output/common/outputLinkComputer'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; diff --git a/src/vs/workbench/contrib/output/browser/outputServices.ts b/src/vs/workbench/contrib/output/browser/outputServices.ts index 4901c9f23ce..dffb7046911 100644 --- a/src/vs/workbench/contrib/output/browser/outputServices.ts +++ b/src/vs/workbench/contrib/output/browser/outputServices.ts @@ -9,8 +9,7 @@ import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { Registry } from 'vs/platform/registry/common/platform'; -import { IOutputChannel, IOutputService, OUTPUT_VIEW_ID, OUTPUT_SCHEME, LOG_SCHEME, LOG_MIME, OUTPUT_MIME, OutputChannelUpdateMode } from 'vs/workbench/contrib/output/common/output'; -import { IOutputChannelDescriptor, Extensions, IOutputChannelRegistry } from 'vs/workbench/services/output/common/output'; +import { IOutputChannel, IOutputService, OUTPUT_VIEW_ID, OUTPUT_SCHEME, LOG_SCHEME, LOG_MIME, OUTPUT_MIME, OutputChannelUpdateMode, IOutputChannelDescriptor, Extensions, IOutputChannelRegistry } from 'vs/workbench/services/output/common/output'; import { OutputLinkProvider } from 'vs/workbench/contrib/output/browser/outputLinkProvider'; import { ITextModelService, ITextModelContentProvider } from 'vs/editor/common/services/resolverService'; import { ITextModel } from 'vs/editor/common/model'; diff --git a/src/vs/workbench/contrib/output/browser/outputView.ts b/src/vs/workbench/contrib/output/browser/outputView.ts index ee9c7eadf5d..545d41fdcee 100644 --- a/src/vs/workbench/contrib/output/browser/outputView.ts +++ b/src/vs/workbench/contrib/output/browser/outputView.ts @@ -14,7 +14,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IEditorOpenContext } from 'vs/workbench/common/editor'; import { AbstractTextResourceEditor } from 'vs/workbench/browser/parts/editor/textResourceEditor'; -import { OUTPUT_VIEW_ID, IOutputService, CONTEXT_IN_OUTPUT, IOutputChannel, CONTEXT_ACTIVE_LOG_OUTPUT, CONTEXT_OUTPUT_SCROLL_LOCK } from 'vs/workbench/contrib/output/common/output'; +import { OUTPUT_VIEW_ID, IOutputService, CONTEXT_IN_OUTPUT, IOutputChannel, CONTEXT_ACTIVE_LOG_OUTPUT, CONTEXT_OUTPUT_SCROLL_LOCK, IOutputChannelDescriptor, IOutputChannelRegistry, Extensions } from 'vs/workbench/services/output/common/output'; import { IThemeService, registerThemingParticipant, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; @@ -27,7 +27,6 @@ import { IContextMenuService, IContextViewService } from 'vs/platform/contextvie import { IViewDescriptorService } from 'vs/workbench/common/views'; import { TextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { IOutputChannelDescriptor, IOutputChannelRegistry, Extensions } from 'vs/workbench/services/output/common/output'; import { Registry } from 'vs/platform/registry/common/platform'; import { attachSelectBoxStyler, attachStylerCallback } from 'vs/platform/theme/common/styler'; import { ISelectOptionItem } from 'vs/base/browser/ui/selectBox/selectBox'; diff --git a/src/vs/workbench/contrib/output/common/outputChannelModel.ts b/src/vs/workbench/contrib/output/common/outputChannelModel.ts index cb24f441e21..4c01e67797c 100644 --- a/src/vs/workbench/contrib/output/common/outputChannelModel.ts +++ b/src/vs/workbench/contrib/output/common/outputChannelModel.ts @@ -21,7 +21,7 @@ import { Range } from 'vs/editor/common/core/range'; import { VSBuffer } from 'vs/base/common/buffer'; import { ILogger, ILoggerService, ILogService } from 'vs/platform/log/common/log'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; -import { OutputChannelUpdateMode } from 'vs/workbench/contrib/output/common/output'; +import { OutputChannelUpdateMode } from 'vs/workbench/services/output/common/output'; export interface IOutputChannelModel extends IDisposable { readonly onDispose: Event<void>; diff --git a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css index 7145fe94aa9..a09f097b305 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css +++ b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css @@ -39,7 +39,7 @@ .settings-editor > .settings-header > .search-container > .settings-count-widget { position: absolute; - right: 35px; + right: 46px; top: 0px; margin: 4px 0px; } @@ -55,7 +55,7 @@ top: 0; right: 0; height: 100%; - width: 30px; + width: 43px; } .settings-editor > .settings-header > .search-container > .settings-clear-widget .action-label { diff --git a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts index 42365a36d4f..b5230a5af22 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts @@ -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, MODIFIED_SETTING_TAG, 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_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'; @@ -54,7 +54,6 @@ const SETTINGS_EDITOR_COMMAND_FOCUS_CONTROL = 'settings.action.focusSettingContr const SETTINGS_EDITOR_COMMAND_FOCUS_UP = 'settings.action.focusLevelUp'; const SETTINGS_EDITOR_COMMAND_SWITCH_TO_JSON = 'settings.switchToJSON'; -const SETTINGS_EDITOR_COMMAND_FILTER_MODIFIED = 'settings.filterByModified'; const SETTINGS_EDITOR_COMMAND_FILTER_ONLINE = 'settings.filterByOnline'; const SETTINGS_EDITOR_COMMAND_FILTER_TELEMETRY = 'settings.filterByTelemetry'; const SETTINGS_EDITOR_COMMAND_FILTER_UNTRUSTED = 'settings.filterUntrusted'; @@ -396,33 +395,12 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon registerAction2(class extends Action2 { constructor() { super({ - id: SETTINGS_EDITOR_COMMAND_FILTER_MODIFIED, - title: { value: nls.localize('filterModifiedLabel', "Show modified settings"), original: 'Show modified settings' }, - menu: { - id: MenuId.EditorTitle, - group: '1_filter', - order: 1, - when: ContextKeyExpr.and(CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_JSON_EDITOR.toNegated()) - } - }); - } - run(accessor: ServicesAccessor, resource: URI) { - const editorPane = accessor.get(IEditorService).activeEditorPane; - if (editorPane instanceof SettingsEditor2) { - editorPane.focusSearch(`@${MODIFIED_SETTING_TAG}`); - } - } - }); - registerAction2(class extends Action2 { - constructor() { - super({ id: SETTINGS_EDITOR_COMMAND_FILTER_ONLINE, - title: { value: nls.localize('filterOnlineServicesLabel', "Show settings for online services"), original: 'Show settings for online services' }, + title: nls.localize({ key: 'miOpenOnlineSettings', comment: ['&& denotes a mnemonic'] }, "&&Online Services Settings"), menu: { - id: MenuId.EditorTitle, - group: '1_filter', + id: MenuId.MenubarPreferencesMenu, + group: '1_settings', order: 2, - when: ContextKeyExpr.and(CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_JSON_EDITOR.toNegated()) } }); } @@ -435,22 +413,6 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon } } }); - MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { - group: '1_settings', - command: { - id: SETTINGS_EDITOR_COMMAND_FILTER_ONLINE, - title: nls.localize({ key: 'miOpenOnlineSettings', comment: ['&& denotes a mnemonic'] }, "&&Online Services Settings") - }, - order: 2 - }); - MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { - group: '2_configuration', - command: { - id: SETTINGS_EDITOR_COMMAND_FILTER_ONLINE, - title: nls.localize('onlineServices', "Online Services Settings") - }, - order: 2 - }); registerAction2(class extends Action2 { constructor() { diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesIcons.ts b/src/vs/workbench/contrib/preferences/browser/preferencesIcons.ts index 6692bf47bd4..0c31b455aaf 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesIcons.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesIcons.ts @@ -25,4 +25,5 @@ export const settingsRemoveIcon = registerIcon('settings-remove', Codicon.close, export const settingsDiscardIcon = registerIcon('settings-discard', Codicon.discard, localize('preferencesDiscardIcon', 'Icon for the discard action in the Settings UI.')); export const preferencesClearInputIcon = registerIcon('preferences-clear-input', Codicon.clearAll, localize('preferencesClearInput', 'Icon for clear input in the Settings and keybinding UI.')); +export const preferencesFilterIcon = registerIcon('preferences-filter', Codicon.filter, localize('settingsFilter', 'Icon for the button that suggests filters for the Settings UI.')); export const preferencesOpenSettingsIcon = registerIcon('preferences-open-settings', Codicon.goToFile, localize('preferencesOpenSettings', 'Icon for open settings commands.')); diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts b/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts index fbddbef9008..8f833f178f6 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts @@ -19,7 +19,7 @@ import { IExtensionManagementService, ILocalExtension } from 'vs/platform/extens import { IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { ILogService } from 'vs/platform/log/common/log'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { canceled } from 'vs/base/common/errors'; +import { CancellationError } from 'vs/base/common/errors'; import { ExtensionType } from 'vs/platform/extensions/common/extensions'; import { nullRange } from 'vs/workbench/services/preferences/common/preferencesModels'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -191,7 +191,7 @@ class RemoteSearchProvider implements ISearchProvider { } if (token && token.isCancellationRequested) { - throw canceled(); + throw new CancellationError(); } const resultKeys = Object.keys(remoteResult.scoredResults); @@ -379,7 +379,7 @@ class RemoteSearchProvider implements ISearchProvider { const hasMoreFilters = filters.length > (filterPage + 1) * RemoteSearchProvider.MAX_REQUEST_FILTERS; const body = JSON.stringify({ - query: encodedQuery, + search: encodedQuery, filters: encodeURIComponent(filterStr), rawQuery: encodeURIComponent(verbatimQuery) }); diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts b/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts index 9ec6a0b1083..4c942330f52 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts @@ -36,6 +36,7 @@ import { PANEL_ACTIVE_TITLE_BORDER, PANEL_ACTIVE_TITLE_FOREGROUND, PANEL_INACTIV import { settingsEditIcon, settingsScopeDropDownIcon } from 'vs/workbench/contrib/preferences/browser/preferencesIcons'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; +import { ILanguageService } from 'vs/editor/common/languages/language'; export class FolderSettingsActionViewItem extends BaseActionViewItem { @@ -216,6 +217,7 @@ export class SettingsTargetsWidget extends Widget { private userLocalSettings!: Action; private userRemoteSettings!: Action; private workspaceSettings!: Action; + private folderSettingsAction!: Action; private folderSettings!: FolderSettingsActionViewItem; private options: ISettingsTargetsWidgetOptions; @@ -232,6 +234,7 @@ export class SettingsTargetsWidget extends Widget { @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @ILabelService private readonly labelService: ILabelService, @IPreferencesService private readonly preferencesService: IPreferencesService, + @ILanguageService private readonly languageService: ILanguageService, ) { super(); this.options = options || {}; @@ -240,6 +243,15 @@ export class SettingsTargetsWidget extends Widget { this._register(this.contextService.onDidChangeWorkspaceFolders(() => this.update())); } + private resetLabels() { + const remoteAuthority = this.environmentService.remoteAuthority; + const hostLabel = remoteAuthority && this.labelService.getHostLabel(Schemas.vscodeRemote, remoteAuthority); + this.userLocalSettings.label = localize('userSettings', "User"); + this.userRemoteSettings.label = localize('userSettingsRemote', "Remote") + (hostLabel ? ` [${hostLabel}]` : ''); + this.workspaceSettings.label = localize('workspaceSettings', "Workspace"); + this.folderSettingsAction.label = localize('folderSettings', "Folder"); + } + private create(parent: HTMLElement): void { const settingsTabsWidget = DOM.append(parent, DOM.$('.settings-tabs-widget')); this.settingsSwitcherBar = this._register(new ActionBar(settingsTabsWidget, { @@ -250,31 +262,28 @@ export class SettingsTargetsWidget extends Widget { actionViewItemProvider: (action: IAction) => action.id === 'folderSettings' ? this.folderSettings : undefined })); - this.userLocalSettings = new Action('userSettings', localize('userSettings', "User"), '.settings-tab', true, () => this.updateTarget(ConfigurationTarget.USER_LOCAL)); + this.userLocalSettings = new Action('userSettings', '', '.settings-tab', true, () => this.updateTarget(ConfigurationTarget.USER_LOCAL)); this.preferencesService.getEditableSettingsURI(ConfigurationTarget.USER_LOCAL).then(uri => { // Don't wait to create UI on resolving remote this.userLocalSettings.tooltip = uri?.fsPath || ''; }); - const remoteAuthority = this.environmentService.remoteAuthority; - const hostLabel = remoteAuthority && this.labelService.getHostLabel(Schemas.vscodeRemote, remoteAuthority); - const remoteSettingsLabel = localize('userSettingsRemote', "Remote") + - (hostLabel ? ` [${hostLabel}]` : ''); - this.userRemoteSettings = new Action('userSettingsRemote', remoteSettingsLabel, '.settings-tab', true, () => this.updateTarget(ConfigurationTarget.USER_REMOTE)); + this.userRemoteSettings = new Action('userSettingsRemote', '', '.settings-tab', true, () => this.updateTarget(ConfigurationTarget.USER_REMOTE)); this.preferencesService.getEditableSettingsURI(ConfigurationTarget.USER_REMOTE).then(uri => { this.userRemoteSettings.tooltip = uri?.fsPath || ''; }); - this.workspaceSettings = new Action('workspaceSettings', localize('workspaceSettings', "Workspace"), '.settings-tab', false, () => this.updateTarget(ConfigurationTarget.WORKSPACE)); + this.workspaceSettings = new Action('workspaceSettings', '', '.settings-tab', false, () => this.updateTarget(ConfigurationTarget.WORKSPACE)); - const folderSettingsAction = new Action('folderSettings', localize('folderSettings', "Folder"), '.settings-tab', false, async folder => { + this.folderSettingsAction = new Action('folderSettings', '', '.settings-tab', false, async folder => { this.updateTarget(isWorkspaceFolder(folder) ? folder.uri : ConfigurationTarget.USER_LOCAL); }); - this.folderSettings = this.instantiationService.createInstance(FolderSettingsActionViewItem, folderSettingsAction); + this.folderSettings = this.instantiationService.createInstance(FolderSettingsActionViewItem, this.folderSettingsAction); + this.resetLabels(); this.update(); - this.settingsSwitcherBar.push([this.userLocalSettings, this.userRemoteSettings, this.workspaceSettings, folderSettingsAction]); + this.settingsSwitcherBar.push([this.userLocalSettings, this.userRemoteSettings, this.workspaceSettings, this.folderSettingsAction]); } get settingsTarget(): SettingsTarget | null { @@ -314,6 +323,19 @@ export class SettingsTargetsWidget extends Widget { } } + updateLanguageFilterIndicators(filter: string | undefined) { + this.resetLabels(); + if (filter) { + const languageToUse = this.languageService.getLanguageName(filter); + if (languageToUse) { + this.userLocalSettings.label += ` [${languageToUse}]`; + this.userRemoteSettings.label += ` [${languageToUse}]`; + this.workspaceSettings.label += ` [${languageToUse}]`; + this.folderSettingsAction.label += ` [${languageToUse}]`; + } + } + } + private onWorkbenchStateChanged(): void { this.folderSettings.folder = null; this.update(); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index 77942bbbf0a..abdaa391f5a 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -43,14 +43,14 @@ import { commonlyUsedData, tocData } from 'vs/workbench/contrib/preferences/brow import { AbstractSettingRenderer, HeightChangeParams, ISettingLinkClickEvent, ISettingOverrideClickEvent, 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, 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, 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'; import { SettingsEditor2Input } from 'vs/workbench/services/preferences/common/preferencesEditorInput'; import { Settings2EditorModel } from 'vs/workbench/services/preferences/common/preferencesModels'; import { IUserDataSyncWorkbenchService } from 'vs/workbench/services/userDataSync/common/userDataSync'; -import { preferencesClearInputIcon } from 'vs/workbench/contrib/preferences/browser/preferencesIcons'; +import { preferencesClearInputIcon, preferencesFilterIcon } from 'vs/workbench/contrib/preferences/browser/preferencesIcons'; import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; import { IWorkbenchConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; @@ -58,6 +58,8 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { Orientation, Sizing, SplitView } from 'vs/base/browser/ui/splitview/splitview'; 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'; export const enum SettingsFocusContext { Search, @@ -201,6 +203,8 @@ export class SettingsEditor2 extends EditorPane { private settingsTreeScrollTop = 0; private dimension!: DOM.Dimension; + private installedExtensionIds: string[] = []; + constructor( @ITelemetryService telemetryService: ITelemetryService, @IWorkbenchConfigurationService private readonly configurationService: IWorkbenchConfigurationService, @@ -217,7 +221,8 @@ export class SettingsEditor2 extends EditorPane { @IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService, @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, @IExtensionService private readonly extensionService: IExtensionService, - @ILanguageService private readonly languageService: ILanguageService + @ILanguageService private readonly languageService: ILanguageService, + @IExtensionManagementService extensionManagementService: IExtensionManagementService ) { super(SettingsEditor2.ID, telemetryService, themeService, storageService); this.delayedFilterLogging = new Delayer<void>(1000); @@ -268,6 +273,12 @@ export class SettingsEditor2 extends EditorPane { if (ENABLE_LANGUAGE_FILTER && !SettingsEditor2.SUGGESTIONS.includes(`@${LANGUAGE_SETTING_TAG}`)) { SettingsEditor2.SUGGESTIONS.push(`@${LANGUAGE_SETTING_TAG}`); } + + extensionManagementService.getInstalled().then(extensions => { + this.installedExtensionIds = extensions + .filter(ext => ext.manifest && ext.manifest.contributes && ext.manifest.contributes.configuration) + .map(ext => ext.identifier.id); + }); } override get minimumWidth(): number { return SettingsEditor2.EDITOR_MIN_WIDTH; } @@ -499,11 +510,11 @@ export class SettingsEditor2 extends EditorPane { clearSearchFilters(): void { let query = this.searchWidget.getValue(); - SettingsEditor2.SUGGESTIONS.forEach(suggestion => { - query = query.replace(suggestion, ''); + const splitQuery = query.split(' ').filter(word => { + return word.length && !SettingsEditor2.SUGGESTIONS.some(suggestion => word.startsWith(suggestion)); }); - this.searchWidget.setValue(query.trim()); + this.searchWidget.setValue(splitQuery.join(' ')); } private updateInputAriaLabel() { @@ -525,7 +536,7 @@ export class SettingsEditor2 extends EditorPane { const searchContainer = DOM.append(this.headerContainer, $('.search-container')); const clearInputAction = new Action(SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, localize('clearInput', "Clear Settings Search Input"), ThemeIcon.asClassName(preferencesClearInputIcon), false, async () => this.clearSearchResults()); - + const filterAction = new Action(SETTINGS_EDITOR_COMMAND_SUGGEST_FILTERS, localize('filterInput', "Filter Settings"), ThemeIcon.asClassName(preferencesFilterIcon)); this.searchWidget = this._register(this.instantiationService.createInstance(SuggestEnabledInput, `${SettingsEditor2.ID}.searchbox`, searchContainer, { triggerCharacters: ['@', ':'], provideResults: (query: string) => { @@ -533,9 +544,15 @@ export class SettingsEditor2 extends EditorPane { // for the ':' trigger, only return suggestions if there was a '@' before it in the same word. const queryParts = query.split(/\s/g); if (queryParts[queryParts.length - 1].startsWith(`@${LANGUAGE_SETTING_TAG}`)) { - return this.languageService.getRegisteredLanguageIds().map(languageId => { + const sortedLanguages = this.languageService.getRegisteredLanguageIds().map(languageId => { return `@${LANGUAGE_SETTING_TAG}${languageId} `; }).sort(); + return sortedLanguages.filter(langFilter => !query.includes(langFilter)); + } else if (queryParts[queryParts.length - 1].startsWith(`@${EXTENSION_SETTING_TAG}`)) { + const installedExtensionsTags = this.installedExtensionIds.map(extensionId => { + return `@${EXTENSION_SETTING_TAG}${extensionId} `; + }).sort(); + return installedExtensionsTags.filter(extFilter => !query.includes(extFilter)); } else if (queryParts[queryParts.length - 1].startsWith('@')) { return SettingsEditor2.SUGGESTIONS.filter(tag => !query.includes(tag)).map(tag => tag.endsWith(':') ? tag : tag + ' '); } @@ -603,10 +620,15 @@ export class SettingsEditor2 extends EditorPane { const actionBar = this._register(new ActionBar(this.controlsElement, { animated: false, - actionViewItemProvider: (_action) => { return undefined; } + actionViewItemProvider: (action) => { + if (action.id === filterAction.id) { + return this.instantiationService.createInstance(SettingsSearchFilterDropdownMenuActionViewItem, action, this.actionRunner, this.searchWidget); + } + return undefined; + } })); - actionBar.push([clearInputAction], { label: false, icon: true }); + actionBar.push([clearInputAction, filterAction], { label: false, icon: true }); } private onDidSettingsTargetChange(target: SettingsTarget): void { @@ -831,7 +853,11 @@ export class SettingsEditor2 extends EditorPane { } })); this._register(this.settingRenderers.onApplyLanguageFilter((lang: string) => { - this.focusSearch(`@${LANGUAGE_SETTING_TAG}${lang}`); + 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.settingsTree = this._register(this.instantiationService.createInstance(SettingsTree, @@ -1018,13 +1044,15 @@ export class SettingsEditor2 extends EditorPane { target: string; }; type SettingsEditorModifiedSettingClassification = { - key: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'rzhao271'; comment: 'The setting that is being modified.' }; - groupId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'rzhao271'; comment: 'Whether the setting is from the local search or remote search provider, if applicable.' }; - nlpIndex: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; owner: 'rzhao271'; comment: 'The index of the setting in the remote search provider results, if applicable.' }; - displayIndex: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; owner: 'rzhao271'; comment: 'The index of the setting in the combined search results, if applicable.' }; - showConfiguredOnly: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; 'owner': 'rzhao271'; comment: 'Whether the user is in the modified view, which shows configured settings only.' }; - isReset: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'rzhao271'; comment: 'Identifies whether a setting was reset to its default value.' }; - target: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'rzhao271'; comment: 'The scope of the setting, such as user or workspace.' }; + key: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The setting that is being modified.' }; + groupId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the setting is from the local search or remote search provider, if applicable.' }; + nlpIndex: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The index of the setting in the remote search provider results, if applicable.' }; + displayIndex: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The index of the setting in the combined search results, if applicable.' }; + showConfiguredOnly: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the user is in the modified view, which shows configured settings only.' }; + isReset: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Identifies whether a setting was reset to its default value.' }; + target: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The scope of the setting, such as user or workspace.' }; + owner: 'rzhao271'; + comment: 'Event which fires when the user modifies a setting in the settings editor'; }; this.pendingSettingUpdate = null; @@ -1304,6 +1332,8 @@ export class SettingsEditor2 extends EditorPane { this.viewState.languageFilter = parsedQuery.languageFilter; } + this.settingsTargetsWidget.updateLanguageFilterIndicators(this.viewState.languageFilter); + if (query && query !== '@') { query = this.parseSettingFromJSON(query) || query; return this.triggerFilterPreferences(query); @@ -1376,10 +1406,12 @@ export class SettingsEditor2 extends EditorPane { 'requestCount': number | undefined; }; type SettingsEditorFilterClassification = { - 'durations.nlpResult': { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; owner: 'rzhao271'; 'comment': 'How long the remote search provider took, if applicable.' }; - 'counts.nlpResult': { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; owner: 'rzhao271'; 'comment': 'The number of matches found by the remote search provider, if applicable.' }; - 'counts.filterResult': { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; owner: 'rzhao271'; 'comment': 'The number of matches found by the local search provider, if applicable.' }; - 'requestCount': { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; owner: 'rzhao271'; 'comment': 'The number of requests sent to Bing, if applicable.' }; + 'durations.nlpResult': { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; 'comment': 'How long the remote search provider took, if applicable.' }; + 'counts.nlpResult': { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; 'comment': 'The number of matches found by the remote search provider, if applicable.' }; + 'counts.filterResult': { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; 'comment': 'The number of matches found by the local search provider, if applicable.' }; + 'requestCount': { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; 'comment': 'The number of requests sent to Bing, if applicable.' }; + owner: 'rzhao271'; + comment: 'Tracks the number of requests and performance of the built-in search providers'; }; const nlpResult = results[SearchResultIdx.Remote]; diff --git a/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts b/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts index d631ff74b4a..9d2c8958ba0 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts @@ -145,7 +145,7 @@ export const tocData: ITOCEntry<string> = { }, { id: 'features/scm', - label: localize('scm', "SCM"), + label: localize('scm', "Source Control"), settings: ['scm.*'] }, { diff --git a/src/vs/workbench/contrib/preferences/browser/settingsSearchMenu.ts b/src/vs/workbench/contrib/preferences/browser/settingsSearchMenu.ts new file mode 100644 index 00000000000..e39d23eb58e --- /dev/null +++ b/src/vs/workbench/contrib/preferences/browser/settingsSearchMenu.ts @@ -0,0 +1,141 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview'; +import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem'; +import { IAction, IActionRunner } from 'vs/base/common/actions'; +import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestController'; +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'; + +export class SettingsSearchFilterDropdownMenuActionViewItem extends DropdownMenuActionViewItem { + private readonly suggestController: SuggestController | null; + + constructor( + action: IAction, + actionRunner: IActionRunner | undefined, + private readonly searchWidget: SuggestEnabledInput, + @IContextMenuService contextMenuService: IContextMenuService + ) { + super(action, + { getActions: () => this.getActions() }, + contextMenuService, + { + actionRunner, + classNames: action.class, + anchorAlignmentProvider: () => AnchorAlignment.RIGHT, + menuAsChild: true + } + ); + + this.suggestController = SuggestController.get(this.searchWidget.inputWidget); + } + + override render(container: HTMLElement): void { + super.render(container); + } + + private doSearchWidgetAction(queryToAppend: string, triggerSuggest: boolean) { + this.searchWidget.setValue(this.searchWidget.getValue().trimEnd() + ' ' + queryToAppend); + this.searchWidget.focus(); + if (triggerSuggest && this.suggestController) { + this.suggestController.triggerSuggest(); + } + } + + /** + * The created action appends a query to the search widget search string. It optionally triggers suggestions. + */ + private createAction(id: string, label: string, tooltip: string, queryToAppend: string, triggerSuggest: boolean): IAction { + return { + id, + label, + tooltip, + class: undefined, + enabled: true, + checked: false, + run: () => { this.doSearchWidgetAction(queryToAppend, triggerSuggest); }, + dispose: () => { } + }; + } + + /** + * The created action appends a query to the search widget search string, if the query does not exist. + * Otherwise, it removes the query from the search widget search string. + * The action does not trigger suggestions after adding or removing the query. + */ + private createToggleAction(id: string, label: string, tooltip: string, queryToAppend: string): IAction { + const splitCurrentQuery = this.searchWidget.getValue().split(' '); + const queryContainsQueryToAppend = splitCurrentQuery.includes(queryToAppend); + return { + id, + label, + tooltip, + class: undefined, + enabled: true, + checked: queryContainsQueryToAppend, + run: () => { + if (!queryContainsQueryToAppend) { + const trimmedCurrentQuery = this.searchWidget.getValue().trimEnd(); + const newQuery = trimmedCurrentQuery ? trimmedCurrentQuery + ' ' + queryToAppend : queryToAppend; + this.searchWidget.setValue(newQuery); + } else { + const queryWithRemovedTags = this.searchWidget.getValue().split(' ') + .filter(word => word !== queryToAppend).join(' '); + this.searchWidget.setValue(queryWithRemovedTags); + } + this.searchWidget.focus(); + }, + dispose: () => { } + }; + } + + getActions(): IAction[] { + return [ + this.createToggleAction( + 'modifiedSettingsSearch', + localize('modifiedSettingsSearch', "Modified"), + localize('modifiedSettingsSearchTooltip', "Add or remove modified settings filter"), + `@${MODIFIED_SETTING_TAG}` + ), + this.createAction( + 'extSettingsSearch', + localize('extSettingsSearch', "Extension ID..."), + localize('extSettingsSearchTooltip', "Add extension ID filter"), + `@${EXTENSION_SETTING_TAG}`, + true + ), + this.createAction( + 'featuresSettingsSearch', + localize('featureSettingsSearch', "Feature..."), + localize('featureSettingsSearchTooltip', "Add feature filter"), + `@${FEATURE_SETTING_TAG}`, + true + ), + this.createAction( + 'tagSettingsSearch', + localize('tagSettingsSearch', "Tag..."), + localize('tagSettingsSearchTooltip', "Add tag filter"), + `@${GENERAL_TAG_SETTING_TAG}`, + true + ), + this.createAction( + 'langSettingsSearch', + localize('langSettingsSearch', "Language..."), + localize('langSettingsSearchTooltip', "Add language ID filter"), + `@${LANGUAGE_SETTING_TAG}`, + true + ), + this.createToggleAction( + 'onlineSettingsSearch', + localize('onlineSettingsSearch', "Online services"), + localize('onlineSettingsSearchTooltip', "Show settings for online services"), + '@tag:usesOnlineServices' + ) + ]; + } +} diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index f4b4b41ad94..7b551ab1466 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -2234,13 +2234,6 @@ export class SettingsTreeFilter implements ITreeFilter<SettingsTreeElement> { } } - // @modified or tag - if (element instanceof SettingsTreeSettingElement && this.viewState.tagFilters) { - if (!element.matchesAllTags(this.viewState.tagFilters)) { - return false; - } - } - // Group with no visible children if (element instanceof SettingsTreeGroupElement) { if (typeof element.count === 'number') { diff --git a/src/vs/workbench/contrib/preferences/common/preferences.ts b/src/vs/workbench/contrib/preferences/common/preferences.ts index ec3a14f1bf4..6138098d36f 100644 --- a/src/vs/workbench/contrib/preferences/common/preferences.ts +++ b/src/vs/workbench/contrib/preferences/common/preferences.ts @@ -42,6 +42,7 @@ export interface ISearchProvider { export const SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS = 'settings.action.clearSearchResults'; export const SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU = 'settings.action.showContextMenu'; +export const SETTINGS_EDITOR_COMMAND_SUGGEST_FILTERS = 'settings.action.suggestFilters'; export const CONTEXT_SETTINGS_EDITOR = new RawContextKey<boolean>('inSettingsEditor', false); export const CONTEXT_SETTINGS_JSON_EDITOR = new RawContextKey<boolean>('inSettingsJSONEditor', false); @@ -76,6 +77,7 @@ export const EXTENSION_SETTING_TAG = 'ext:'; 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 WORKSPACE_TRUST_SETTING_TAG = 'workspaceTrust'; export const REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG = 'requireTrustedWorkspace'; export const KEYBOARD_LAYOUT_OPEN_PICKER = 'workbench.action.openKeyboardLayoutPicker'; diff --git a/src/vs/workbench/browser/parts/editor/media/binaryeditor.css b/src/vs/workbench/contrib/profiles/common/profiles.contribution.ts index d68e045cc4e..facfa51c4a3 100644 --- a/src/vs/workbench/browser/parts/editor/media/binaryeditor.css +++ b/src/vs/workbench/contrib/profiles/common/profiles.contribution.ts @@ -3,18 +3,4 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-binary-resource-editor:focus { - outline: none !important; -} - -.monaco-binary-resource-editor { - padding: 0 0 0 16px; - box-sizing: border-box; -} - -.monaco-binary-resource-editor .monaco-link, -.monaco-binary-resource-editor .monaco-link:hover { - cursor: pointer; - text-decoration: underline; - margin-left: 5px; -} +import './profilesActions'; diff --git a/src/vs/workbench/contrib/profiles/common/profilesActions.ts b/src/vs/workbench/contrib/profiles/common/profilesActions.ts new file mode 100644 index 00000000000..bae901d966d --- /dev/null +++ b/src/vs/workbench/contrib/profiles/common/profilesActions.ts @@ -0,0 +1,136 @@ +/*--------------------------------------------------------------------------------------------- + * 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/commandsQuickAccess.ts b/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts index 6d2258b96f1..0f8b4edc30d 100644 --- a/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts +++ b/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts @@ -160,12 +160,17 @@ export class ShowAllCommandsAction extends Action2 { super({ id: ShowAllCommandsAction.ID, title: { value: localize('showTriggerActions', "Show All Commands"), original: 'Show All Commands' }, - f1: true, keybinding: { weight: KeybindingWeight.WorkbenchContrib, when: undefined, primary: !isFirefox ? (KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyP) : undefined, secondary: [KeyCode.F1] + }, + f1: true, + menu: { + id: MenuId.TitleMenuQuickPick, + group: '1/workspaceNav', + order: 3 } }); } @@ -188,9 +193,23 @@ export class ClearCommandHistoryAction extends Action2 { async run(accessor: ServicesAccessor): Promise<void> { const configurationService = accessor.get(IConfigurationService); const storageService = accessor.get(IStorageService); + const dialogService = accessor.get(IDialogService); const commandHistoryLength = CommandsHistory.getConfiguredCommandHistoryLength(configurationService); if (commandHistoryLength > 0) { + + // Ask for confirmation + const { confirmed } = await dialogService.confirm({ + message: localize('confirmClearMessage', "Do you want to clear the history of recently used commands?"), + detail: localize('confirmClearDetail', "This action is irreversible!"), + primaryButton: localize({ key: 'clearButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Clear"), + type: 'warning' + }); + + if (!confirmed) { + return; + } + CommandsHistory.clearHistory(configurationService, storageService); } } diff --git a/src/vs/workbench/contrib/quickaccess/browser/viewQuickAccess.ts b/src/vs/workbench/contrib/quickaccess/browser/viewQuickAccess.ts index d1a3d7680d5..4a278af2146 100644 --- a/src/vs/workbench/contrib/quickaccess/browser/viewQuickAccess.ts +++ b/src/vs/workbench/contrib/quickaccess/browser/viewQuickAccess.ts @@ -7,7 +7,7 @@ import { localize } from 'vs/nls'; import { IQuickPickSeparator, IQuickInputService, ItemActivation } from 'vs/platform/quickinput/common/quickInput'; import { IPickerQuickAccessItem, PickerQuickAccessProvider } from 'vs/platform/quickinput/browser/pickerQuickAccess'; import { IViewDescriptorService, IViewsService, ViewContainer, ViewContainerLocation } from 'vs/workbench/common/views'; -import { IOutputService } from 'vs/workbench/contrib/output/common/output'; +import { IOutputService } from 'vs/workbench/services/output/common/output'; import { ITerminalGroupService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { PaneCompositeDescriptor } from 'vs/workbench/browser/panecomposite'; diff --git a/src/vs/workbench/contrib/scm/browser/activity.ts b/src/vs/workbench/contrib/scm/browser/activity.ts index 399e48dcce8..38a53b72c60 100644 --- a/src/vs/workbench/contrib/scm/browser/activity.ts +++ b/src/vs/workbench/contrib/scm/browser/activity.ts @@ -18,6 +18,7 @@ import { EditorResourceAccessor } from 'vs/workbench/common/editor'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { stripIcons } from 'vs/base/common/iconLabels'; import { Schemas } from 'vs/base/common/network'; +import { Iterable } from 'vs/base/common/iterator'; function getCount(repository: ISCMRepository): number { if (typeof repository.provider.count === 'number') { @@ -116,7 +117,7 @@ export class SCMStatusController implements IWorkbenchContribution { return; } - this.focusRepository(this.scmService.repositories[0]); + this.focusRepository(Iterable.first(this.scmService.repositories)); } private focusRepository(repository: ISCMRepository | undefined): void { @@ -148,7 +149,8 @@ export class SCMStatusController implements IWorkbenchContribution { : repository.provider.label; const disposables = new DisposableStore(); - for (const command of commands) { + for (let index = 0; index < commands.length; index++) { + const command = commands[index]; const tooltip = `${label}${command.tooltip ? ` - ${command.tooltip}` : ''}`; let ariaLabel = stripIcons(command.title).trim(); @@ -160,7 +162,7 @@ export class SCMStatusController implements IWorkbenchContribution { ariaLabel: `${ariaLabel}${command.tooltip ? ` - ${command.tooltip}` : ''}`, tooltip, command: command.id ? command : undefined - }, 'status.scm', MainThreadStatusBarAlignment.LEFT, 10000)); + }, `status.scm.${index}`, MainThreadStatusBarAlignment.LEFT, 10000)); } this.statusBarDisposable = disposables; @@ -172,7 +174,7 @@ export class SCMStatusController implements IWorkbenchContribution { let count = 0; if (countBadgeType === 'all') { - count = this.scmService.repositories.reduce((r, repository) => r + getCount(repository), 0); + count = Iterable.reduce(this.scmService.repositories, (r, repository) => r + getCount(repository), 0); } else if (countBadgeType === 'focused' && this.focusedRepository) { count = getCount(this.focusedRepository); } @@ -198,7 +200,7 @@ export class SCMStatusController implements IWorkbenchContribution { export class SCMActiveResourceContextKeyController implements IWorkbenchContribution { private activeResourceHasChangesContextKey: IContextKey<boolean>; - private activeResourceRepositoryContextKey: IContextKey<ISCMRepository | undefined>; + private activeResourceRepositoryContextKey: IContextKey<string | undefined>; private disposables = new DisposableStore(); private repositoryDisposables = new Set<IDisposable>(); @@ -239,11 +241,12 @@ export class SCMActiveResourceContextKeyController implements IWorkbenchContribu const activeResource = EditorResourceAccessor.getOriginalUri(this.editorService.activeEditor); if (activeResource?.scheme === Schemas.file || activeResource?.scheme === Schemas.vscodeRemote) { - const activeResourceRepository = this.scmService.repositories - .find(r => r.provider.rootUri && - this.uriIdentityService.extUri.isEqualOrParent(activeResource, r.provider.rootUri)); + const activeResourceRepository = Iterable.find( + this.scmService.repositories, + r => Boolean(r.provider.rootUri && this.uriIdentityService.extUri.isEqualOrParent(activeResource, r.provider.rootUri)) + ); - this.activeResourceRepositoryContextKey.set(activeResourceRepository); + this.activeResourceRepositoryContextKey.set(activeResourceRepository?.id); for (const resourceGroup of activeResourceRepository?.provider.groups.elements ?? []) { if (resourceGroup.elements diff --git a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts index 600b2af4b21..c05f0b2f4be 100644 --- a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts +++ b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts @@ -20,7 +20,7 @@ import { URI } from 'vs/base/common/uri'; import { ISCMService, ISCMRepository, ISCMProvider } from 'vs/workbench/contrib/scm/common/scm'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { registerThemingParticipant, IColorTheme, ICssStyleCollector, themeColorFromId, IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { editorBackground, editorErrorForeground, registerColor, transparent } from 'vs/platform/theme/common/colorRegistry'; +import { editorErrorForeground, registerColor, transparent } from 'vs/platform/theme/common/colorRegistry'; import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import { registerEditorAction, registerEditorContribution, ServicesAccessor, EditorAction } from 'vs/editor/browser/editorExtensions'; import { PeekViewWidget, getOuterEditor, peekViewBorder, peekViewTitleBackground, peekViewTitleForeground, peekViewTitleInfoForeground } from 'vs/editor/contrib/peekView/browser/peekView'; @@ -52,6 +52,8 @@ import { TextCompareEditorActiveContext } from 'vs/workbench/common/contextkeys' import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; import { IChange } from 'vs/editor/common/diff/diffComputer'; import { Color } from 'vs/base/common/color'; +import { editorGutter } from 'vs/editor/common/core/editorColorRegistry'; +import { Iterable } from 'vs/base/common/iterator'; class DiffActionRunner extends ActionRunner { @@ -608,7 +610,8 @@ export class DirtyDiffController extends Disposable implements IEditorContributi } .monaco-editor .margin-view-overlays .dirty-diff-glyph:hover::before { - width: 9px; + height: 100%; + width: 6px; left: -6px; } @@ -923,8 +926,10 @@ class DirtyDiffDecorator extends Disposable { return ModelDecorationOptions.createDynamic(decorationOptions); } - private modifiedOptions: ModelDecorationOptions; private addedOptions: ModelDecorationOptions; + private addedPatternOptions: ModelDecorationOptions; + private modifiedOptions: ModelDecorationOptions; + private modifiedPatternOptions: ModelDecorationOptions; private deletedOptions: ModelDecorationOptions; private decorations: string[] = []; private editorModel: ITextModel | null; @@ -932,25 +937,38 @@ class DirtyDiffDecorator extends Disposable { constructor( editorModel: ITextModel, private model: DirtyDiffModel, - @IConfigurationService configurationService: IConfigurationService + @IConfigurationService private readonly configurationService: IConfigurationService ) { super(); this.editorModel = editorModel; + const decorations = configurationService.getValue<string>('scm.diffDecorations'); const gutter = decorations === 'all' || decorations === 'gutter'; const overview = decorations === 'all' || decorations === 'overview'; const minimap = decorations === 'all' || decorations === 'minimap'; + this.addedOptions = DirtyDiffDecorator.createDecoration('dirty-diff-added', { + gutter, + overview: { active: overview, color: overviewRulerAddedForeground }, + minimap: { active: minimap, color: minimapGutterAddedBackground }, + isWholeLine: true + }); + this.addedPatternOptions = DirtyDiffDecorator.createDecoration('dirty-diff-added-pattern', { + gutter, + overview: { active: overview, color: overviewRulerAddedForeground }, + minimap: { active: minimap, color: minimapGutterAddedBackground }, + isWholeLine: true + }); this.modifiedOptions = DirtyDiffDecorator.createDecoration('dirty-diff-modified', { gutter, overview: { active: overview, color: overviewRulerModifiedForeground }, minimap: { active: minimap, color: minimapGutterModifiedBackground }, isWholeLine: true }); - this.addedOptions = DirtyDiffDecorator.createDecoration('dirty-diff-added', { + this.modifiedPatternOptions = DirtyDiffDecorator.createDecoration('dirty-diff-modified-pattern', { gutter, - overview: { active: overview, color: overviewRulerAddedForeground }, - minimap: { active: minimap, color: minimapGutterAddedBackground }, + overview: { active: overview, color: overviewRulerModifiedForeground }, + minimap: { active: minimap, color: minimapGutterModifiedBackground }, isWholeLine: true }); this.deletedOptions = DirtyDiffDecorator.createDecoration('dirty-diff-deleted', { @@ -960,6 +978,12 @@ class DirtyDiffDecorator extends Disposable { isWholeLine: false }); + this._register(configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('scm.diffDecorationsGutterPattern')) { + this.onDidChange(); + } + })); + this._register(model.onDidChange(this.onDidChange, this)); } @@ -967,6 +991,8 @@ class DirtyDiffDecorator extends Disposable { if (!this.editorModel) { return; } + + const pattern = this.configurationService.getValue<{ added: boolean; modified: boolean }>('scm.diffDecorationsGutterPattern'); const decorations = this.model.changes.map((change) => { const changeType = getChangeType(change); const startLineNumber = change.modifiedStartLineNumber; @@ -979,7 +1005,7 @@ class DirtyDiffDecorator extends Disposable { startLineNumber: startLineNumber, startColumn: 1, endLineNumber: endLineNumber, endColumn: 1 }, - options: this.addedOptions + options: pattern.added ? this.addedPatternOptions : this.addedOptions }; case ChangeType.Delete: return { @@ -995,7 +1021,7 @@ class DirtyDiffDecorator extends Disposable { startLineNumber: startLineNumber, startColumn: 1, endLineNumber: endLineNumber, endColumn: 1 }, - options: this.modifiedOptions + options: pattern.modified ? this.modifiedPatternOptions : this.modifiedOptions }; } }); @@ -1055,8 +1081,8 @@ export function createProviderComparer(uri: URI): (a: ISCMProvider, b: ISCMProvi } export async function getOriginalResource(scmService: ISCMService, uri: URI): Promise<URI | null> { - const providers = scmService.repositories.map(r => r.provider); - const rootedProviders = providers.filter(p => !!p.rootUri); + const providers = Iterable.map(scmService.repositories, r => r.provider); + const rootedProviders = Iterable.collect(Iterable.filter(providers, p => !!p.rootUri)); rootedProviders.sort(createProviderComparer(uri)); @@ -1066,8 +1092,8 @@ export async function getOriginalResource(scmService: ISCMService, uri: URI): Pr return result; } - const nonRootedProviders = providers.filter(p => !p.rootUri); - return first(nonRootedProviders.map(p => () => p.getOriginalResource(uri))); + const nonRootedProviders = Iterable.filter(providers, p => !p.rootUri); + return first(Iterable.collect(Iterable.map(nonRootedProviders, p => () => p.getOriginalResource(uri)))); } export class DirtyDiffModel extends Disposable { @@ -1108,7 +1134,7 @@ export class DirtyDiffModel extends Disposable { )(this.triggerDiff, this) ); this._register(scmService.onDidAddRepository(this.onDidAddRepository, this)); - scmService.repositories.forEach(r => this.onDidAddRepository(r)); + Iterable.forEach(scmService.repositories, r => this.onDidAddRepository(r)); this._register(this._model.onDidChangeEncoding(() => { this.diffDelayer.cancel(); @@ -1371,9 +1397,21 @@ export class DirtyDiffWorkbenchController extends Disposable implements ext.IWor private setViewState(state: IViewState): void { this.viewState = state; this.stylesheet.textContent = ` - .monaco-editor .dirty-diff-modified { background-size: ${state.width}px 4.5px; } - .monaco-editor .dirty-diff-modified, .monaco-editor .dirty-diff-added{border-left-width:${state.width}px;} - .monaco-editor .dirty-diff-modified, .monaco-editor .dirty-diff-added, .monaco-editor .dirty-diff-deleted { + .monaco-editor .dirty-diff-added, + .monaco-editor .dirty-diff-modified { + border-left-width:${state.width}px; + } + .monaco-editor .dirty-diff-added-pattern, + .monaco-editor .dirty-diff-added-pattern:before, + .monaco-editor .dirty-diff-modified-pattern, + .monaco-editor .dirty-diff-modified-pattern:before { + background-size: ${state.width}px ${state.width}px; + } + .monaco-editor .dirty-diff-added, + .monaco-editor .dirty-diff-added-pattern, + .monaco-editor .dirty-diff-modified, + .monaco-editor .dirty-diff-modified-pattern, + .monaco-editor .dirty-diff-deleted { opacity: ${state.visibility === 'always' ? 1 : 0}; } `; @@ -1468,39 +1506,61 @@ export class DirtyDiffWorkbenchController extends Disposable implements ext.IWor registerEditorContribution(DirtyDiffController.ID, DirtyDiffController); registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { - const editorBackgroundColor = theme.getColor(editorBackground); + const editorGutterBackgroundColor = theme.getColor(editorGutter); const editorGutterModifiedBackgroundColor = theme.getColor(editorGutterModifiedBackground); - const linearGradient = `-45deg, ${editorGutterModifiedBackgroundColor} 25%, ${editorBackgroundColor} 25%, ${editorBackgroundColor} 50%, ${editorGutterModifiedBackgroundColor} 50%, ${editorGutterModifiedBackgroundColor} 75%, ${editorBackgroundColor} 75%, ${editorBackgroundColor}`; - if (editorGutterModifiedBackgroundColor) { + const getLinearGradient = (color: Color): string => { + return `-45deg, ${color} 25%, ${editorGutterBackgroundColor} 25%, ${editorGutterBackgroundColor} 50%, ${color} 50%, ${color} 75%, ${editorGutterBackgroundColor} 75%, ${editorGutterBackgroundColor}`; + }; + + if (editorGutterBackgroundColor && editorGutterModifiedBackgroundColor) { collector.addRule(` .monaco-editor .dirty-diff-modified { - background-repeat: repeat-y; - background-image: linear-gradient(${linearGradient}); + border-left-color: ${editorGutterModifiedBackgroundColor}; + border-left-style: solid; transition: opacity 0.5s; } .monaco-editor .dirty-diff-modified:before { + background: ${editorGutterModifiedBackgroundColor}; + } + .monaco-editor .dirty-diff-modified-pattern { + background-image: linear-gradient(${getLinearGradient(editorGutterModifiedBackgroundColor)}); + background-repeat: repeat-y; + transition: opacity 0.5s; + } + .monaco-editor .dirty-diff-modified-pattern:before { + background-image: linear-gradient(${getLinearGradient(editorGutterModifiedBackgroundColor)}); transform: translateX(3px); - background-size: 3px 3px; - background-image: linear-gradient(${linearGradient}); } - .monaco-editor .margin:hover .dirty-diff-modified { + .monaco-editor .margin:hover .dirty-diff-modified, + .monaco-editor .margin:hover .dirty-diff-modified-pattern { opacity: 1; } `); } const editorGutterAddedBackgroundColor = theme.getColor(editorGutterAddedBackground); - if (editorGutterAddedBackgroundColor) { + if (editorGutterBackgroundColor && editorGutterAddedBackgroundColor) { collector.addRule(` .monaco-editor .dirty-diff-added { - border-left: 3px solid ${editorGutterAddedBackgroundColor}; + border-left-color: ${editorGutterAddedBackgroundColor}; + border-left-style: solid; transition: opacity 0.5s; } .monaco-editor .dirty-diff-added:before { background: ${editorGutterAddedBackgroundColor}; } - .monaco-editor .margin:hover .dirty-diff-added { + .monaco-editor .dirty-diff-added-pattern { + background-image: linear-gradient(${getLinearGradient(editorGutterAddedBackgroundColor)}); + background-repeat: repeat-y; + transition: opacity 0.5s; + } + .monaco-editor .dirty-diff-added-pattern:before { + background-image: linear-gradient(${getLinearGradient(editorGutterAddedBackgroundColor)}); + transform: translateX(3px); + } + .monaco-editor .margin:hover .dirty-diff-added, + .monaco-editor .margin:hover .dirty-diff-added-pattern { opacity: 1; } `); diff --git a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts index 27fa26a947d..956e867a21f 100644 --- a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts +++ b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts @@ -7,7 +7,7 @@ import { localize } from 'vs/nls'; import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { DirtyDiffWorkbenchController } from './dirtydiffDecorator'; -import { VIEWLET_ID, ISCMRepository, ISCMService, VIEW_PANE_ID, ISCMProvider, ISCMViewService, REPOSITORIES_VIEW_PANE_ID } from 'vs/workbench/contrib/scm/common/scm'; +import { VIEWLET_ID, ISCMService, VIEW_PANE_ID, ISCMProvider, ISCMViewService, REPOSITORIES_VIEW_PANE_ID } from 'vs/workbench/contrib/scm/common/scm'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { SCMActiveResourceContextKeyController, SCMStatusController } from './activity'; @@ -83,7 +83,7 @@ viewsRegistry.registerViews([{ containerIcon: sourceControlViewIcon, openCommandActionDescriptor: { id: viewContainer.id, - mnemonicTitle: localize({ key: 'miViewSCM', comment: ['&& denotes a mnemonic'] }, "S&&CM"), + mnemonicTitle: localize({ key: 'miViewSCM', comment: ['&& denotes a mnemonic'] }, "Source &&Control"), keybindings: { primary: 0, win: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyG }, @@ -118,7 +118,7 @@ Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench) Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).registerConfiguration({ id: 'scm', order: 5, - title: localize('scmConfigurationTitle', "SCM"), + title: localize('scmConfigurationTitle', "Source Control"), type: 'object', scope: ConfigurationScope.RESOURCE, properties: { @@ -161,6 +161,25 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).regis description: localize('scm.diffDecorationsGutterAction', "Controls the behavior of Source Control diff gutter decorations."), default: 'diff' }, + 'scm.diffDecorationsGutterPattern': { + type: 'object', + description: localize('diffGutterPattern', "Controls whether a pattern is used for the diff decorations in gutter."), + additionalProperties: false, + properties: { + 'added': { + type: 'boolean', + description: localize('diffGutterPatternAdded', "Use pattern for the diff decorations in gutter for added lines."), + }, + 'modified': { + type: 'boolean', + description: localize('diffGutterPatternModifed', "Use pattern for the diff decorations in gutter for modified lines."), + }, + }, + default: { + 'added': false, + 'modified': true + } + }, 'scm.diffDecorationsIgnoreTrimWhitespace': { type: 'string', enum: ['true', 'false', 'inherit'], @@ -215,14 +234,14 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).regis enumDescriptions: [ localize('scm.defaultViewSortKey.name', "Sort the repository changes by file name."), localize('scm.defaultViewSortKey.path', "Sort the repository changes by path."), - localize('scm.defaultViewSortKey.status', "Sort the repository changes by SCM status.") + localize('scm.defaultViewSortKey.status', "Sort the repository changes by Source Control status.") ], - description: localize('scm.defaultViewSortKey', "Controls the default Source Control repository sort mode."), + description: localize('scm.defaultViewSortKey', "Controls the default Source Control repository changes sort order when viewed as a list."), default: 'path' }, 'scm.autoReveal': { type: 'boolean', - description: localize('autoReveal', "Controls whether the SCM view should automatically reveal and select files when opening them."), + description: localize('autoReveal', "Controls whether the Source Control view should automatically reveal and select files when opening them."), default: true }, 'scm.inputFontFamily': { @@ -237,9 +256,20 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).regis }, 'scm.alwaysShowRepositories': { type: 'boolean', - markdownDescription: localize('alwaysShowRepository', "Controls whether repositories should always be visible in the SCM view."), + markdownDescription: localize('alwaysShowRepository', "Controls whether repositories should always be visible in the Source Control view."), default: false }, + 'scm.repositories.sortOrder': { + type: 'string', + enum: ['discovery time', 'name', 'path'], + enumDescriptions: [ + localize('scm.repositoriesSortOrder.discoveryTime', "Repositories in the Source Control Repositories view are sorted by discovery time. Repositories in the Source Control view are sorted in the order that they were selected."), + localize('scm.repositoriesSortOrder.name', "Repositories in the Source Control Repositories and Source Control views are sorted by repository name."), + localize('scm.repositoriesSortOrder.path', "Repositories in the Source Control Repositories and Source Control views are sorted by repository path.") + ], + description: localize('repositoriesSortOrder', "Controls the sort order of the repositories in the source control repositories view."), + default: 'discovery time' + }, 'scm.repositories.visible': { type: 'number', description: localize('providersVisible', "Controls how many repositories are visible in the Source Control Repositories section. Set to `0` to be able to manually resize the view."), @@ -247,7 +277,7 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).regis }, 'scm.showActionButton': { type: 'boolean', - markdownDescription: localize('showActionButton', "Controls whether an action button can be shown in the SCM view."), + markdownDescription: localize('showActionButton', "Controls whether an action button can be shown in the Source Control view."), default: true } } @@ -255,44 +285,56 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).regis KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'scm.acceptInput', - description: { description: localize('scm accept', "SCM: Accept Input"), args: [] }, + description: { description: localize('scm accept', "Source Control: Accept Input"), args: [] }, weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.has('scmRepository'), primary: KeyMod.CtrlCmd | KeyCode.Enter, handler: accessor => { const contextKeyService = accessor.get(IContextKeyService); const context = contextKeyService.getContext(document.activeElement); - const repository = context.getValue<ISCMRepository>('scmRepository'); + const repositoryId = context.getValue<string | undefined>('scmRepository'); + + if (!repositoryId) { + return Promise.resolve(null); + } + + const scmService = accessor.get(ISCMService); + const repository = scmService.getRepository(repositoryId); - if (!repository || !repository.provider.acceptInputCommand) { + if (!repository?.provider.acceptInputCommand) { return Promise.resolve(null); } + const id = repository.provider.acceptInputCommand.id; const args = repository.provider.acceptInputCommand.arguments; - const commandService = accessor.get(ICommandService); + return commandService.executeCommand(id, ...(args || [])); } }); const viewNextCommitCommand = { - description: { description: localize('scm view next commit', "SCM: View Next Commit"), args: [] }, + description: { description: localize('scm view next commit', "Source Control: View Next Commit"), args: [] }, weight: KeybindingWeight.WorkbenchContrib, handler: (accessor: ServicesAccessor) => { const contextKeyService = accessor.get(IContextKeyService); + const scmService = accessor.get(ISCMService); const context = contextKeyService.getContext(document.activeElement); - const repository = context.getValue<ISCMRepository>('scmRepository'); + const repositoryId = context.getValue<string | undefined>('scmRepository'); + const repository = repositoryId ? scmService.getRepository(repositoryId) : undefined; repository?.input.showNextHistoryValue(); } }; const viewPreviousCommitCommand = { - description: { description: localize('scm view previous commit', "SCM: View Previous Commit"), args: [] }, + description: { description: localize('scm view previous commit', "Source Control: View Previous Commit"), args: [] }, weight: KeybindingWeight.WorkbenchContrib, handler: (accessor: ServicesAccessor) => { const contextKeyService = accessor.get(IContextKeyService); + const scmService = accessor.get(ISCMService); const context = contextKeyService.getContext(document.activeElement); - const repository = context.getValue<ISCMRepository>('scmRepository'); + const repositoryId = context.getValue<string | undefined>('scmRepository'); + const repository = repositoryId ? scmService.getRepository(repositoryId) : undefined; repository?.input.showPreviousHistoryValue(); } }; diff --git a/src/vs/workbench/contrib/scm/browser/scmRepositoriesViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmRepositoriesViewPane.ts index e66a0f24fd5..3772fac9387 100644 --- a/src/vs/workbench/contrib/scm/browser/scmRepositoriesViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmRepositoriesViewPane.ts @@ -23,6 +23,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { RepositoryRenderer } from 'vs/workbench/contrib/scm/browser/scmRepositoryRenderer'; import { collectContextMenuActions, getActionViewItemProvider } from 'vs/workbench/contrib/scm/browser/util'; import { Orientation } from 'vs/base/browser/ui/sash/sash'; +import { Iterable } from 'vs/base/common/iterator'; class ListDelegate implements IListVirtualDelegate<ISCMRepository> { @@ -155,23 +156,28 @@ export class SCMRepositoriesViewPane extends ViewPane { } private updateListSelection(): void { - const set = new Set(); + const oldSelection = this.list.getSelection(); + const oldSet = new Set(Iterable.map(oldSelection, i => this.list.element(i))); + const set = new Set(this.scmViewService.visibleRepositories); + const added = new Set(Iterable.filter(set, r => !oldSet.has(r))); + const removed = new Set(Iterable.filter(oldSet, r => !set.has(r))); - for (const repository of this.scmViewService.visibleRepositories) { - set.add(repository); + if (added.size === 0 && removed.size === 0) { + return; } - const selection: number[] = []; + const selection = oldSelection + .filter(i => !removed.has(this.list.element(i))); for (let i = 0; i < this.list.length; i++) { - if (set.has(this.list.element(i))) { + if (added.has(this.list.element(i))) { selection.push(i); } } this.list.setSelection(selection); - if (selection.length > 0) { + if (selection.length > 0 && selection.indexOf(this.list.getFocus()[0]) === -1) { this.list.setAnchor(selection[0]); this.list.setFocus([selection[0]]); } diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 79cce93a380..7e87c10e0fc 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -10,7 +10,7 @@ import { IDisposable, Disposable, DisposableStore, combinedDisposable, dispose, import { ViewPane, IViewPaneOptions, ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { append, $, Dimension, asCSSUrl, trackFocus, clearNode } from 'vs/base/browser/dom'; import { IListVirtualDelegate, IIdentityProvider } from 'vs/base/browser/ui/list/list'; -import { ISCMResourceGroup, ISCMResource, InputValidationType, ISCMRepository, ISCMInput, IInputValidation, ISCMViewService, ISCMViewVisibleRepositoryChangeEvent, ISCMService, SCMInputChangeReason, VIEW_PANE_ID, ISCMActionButton, ISCMActionButtonDescriptor } from 'vs/workbench/contrib/scm/common/scm'; +import { ISCMResourceGroup, ISCMResource, InputValidationType, ISCMRepository, ISCMInput, IInputValidation, ISCMViewService, ISCMViewVisibleRepositoryChangeEvent, ISCMService, SCMInputChangeReason, VIEW_PANE_ID, ISCMActionButton, ISCMActionButtonDescriptor, ISCMRepositorySortKey, REPOSITORIES_VIEW_PANE_ID } from 'vs/workbench/contrib/scm/common/scm'; import { ResourceLabels, IResourceLabel, IFileLabelOptions } from 'vs/workbench/browser/labels'; import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -84,6 +84,7 @@ 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 { INotificationService } from 'vs/platform/notification/common/notification'; +import { RepositoryContextKeys } from 'vs/workbench/contrib/scm/browser/scmViewService'; type TreeElement = ISCMRepository | ISCMInput | ISCMActionButton | ISCMResourceGroup | IResourceNode<ISCMResource, ISCMResourceGroup> | ISCMResource; @@ -944,7 +945,7 @@ class RepositoryVisibilityAction extends Action2 { f1: false, precondition: ContextKeyExpr.or(ContextKeys.RepositoryVisibilityCount.notEqualsTo(1), ContextKeys.RepositoryVisibility(repository).isEqualTo(false)), toggled: ContextKeys.RepositoryVisibility(repository).isEqualTo(true), - menu: { id: Menus.Repositories } + menu: { id: Menus.Repositories, group: '0_repositories' } }); this.repository = repository; } @@ -1069,9 +1070,14 @@ class ViewModel { } } + // Update sort key based on view mode + this.sortKey = this.getViewModelSortKey(); + this.refresh(); this._onDidChangeMode.fire(mode); this.modeContextKey.set(mode); + + this.storageService.store(`scm.viewMode`, mode, StorageScope.WORKSPACE, StorageTarget.USER); } get sortKey(): ViewModelSortKey { return this._sortKey; } @@ -1085,6 +1091,10 @@ class ViewModel { this.refresh(); this._onDidChangeSortKey.fire(sortKey); this.sortKeyContextKey.set(sortKey); + + if (this._mode === ViewModelMode.List) { + this.storageService.store(`scm.viewSortKey`, sortKey, StorageScope.WORKSPACE, StorageTarget.USER); + } } private _treeViewStateIsStale = false; @@ -1113,23 +1123,37 @@ class ViewModel { private scmProviderRootUriContextKey: IContextKey<string | undefined>; private scmProviderHasRootUriContextKey: IContextKey<boolean>; + private _mode: ViewModelMode; + private _sortKey: ViewModelSortKey; + private _treeViewState: ITreeViewState | undefined; + constructor( private tree: WorkbenchCompressibleObjectTree<TreeElement, FuzzyScore>, private inputRenderer: InputRenderer, - private _mode: ViewModelMode, - private _sortKey: ViewModelSortKey, - private _treeViewState: ITreeViewState | undefined, @IInstantiationService protected instantiationService: IInstantiationService, @IEditorService protected editorService: IEditorService, @IConfigurationService protected configurationService: IConfigurationService, @ISCMViewService private scmViewService: ISCMViewService, + @IStorageService private storageService: IStorageService, @IUriIdentityService private uriIdentityService: IUriIdentityService, @IContextKeyService contextKeyService: IContextKeyService ) { + // View mode and sort key + this._mode = this.getViewModelMode(); + this._sortKey = this.getViewModelSortKey(); + + // TreeView state + const storageViewState = this.storageService.get(`scm.viewState`, StorageScope.WORKSPACE); + if (storageViewState) { + try { + this._treeViewState = JSON.parse(storageViewState); + } catch {/* noop */ } + } + this.modeContextKey = ContextKeys.ViewModelMode.bindTo(contextKeyService); - this.modeContextKey.set(_mode); + this.modeContextKey.set(this._mode); this.sortKeyContextKey = ContextKeys.ViewModelSortKey.bindTo(contextKeyService); - this.sortKeyContextKey.set(_sortKey); + this.sortKeyContextKey.set(this._sortKey); this.areAllRepositoriesCollapsedContextKey = ContextKeys.ViewModelAreAllRepositoriesCollapsed.bindTo(contextKeyService); this.isAnyRepositoryCollapsibleContextKey = ContextKeys.ViewModelIsAnyRepositoryCollapsible.bindTo(contextKeyService); this.scmProviderContextKey = ContextKeys.SCMProvider.bindTo(contextKeyService); @@ -1143,6 +1167,12 @@ class ViewModel { (this.updateRepositoryCollapseAllContextKeys, this, this.disposables); this.disposables.add(this.tree.onDidChangeCollapseState(() => this._treeViewStateIsStale = true)); + + this.storageService.onWillSaveState(e => { + if (e.reason === WillSaveStateReason.SHUTDOWN) { + this.storageService.store(`scm.viewState`, JSON.stringify(this.treeViewState), StorageScope.WORKSPACE, StorageTarget.MACHINE); + } + }); } private onDidChangeConfiguration(e?: IConfigurationChangeEvent): void { @@ -1432,6 +1462,45 @@ class ViewModel { } } + private getViewModelMode(): ViewModelMode { + let mode = this.configurationService.getValue<'tree' | 'list'>('scm.defaultViewMode') === 'list' ? ViewModelMode.List : ViewModelMode.Tree; + const storageMode = this.storageService.get(`scm.viewMode`, StorageScope.WORKSPACE) as ViewModelMode; + if (typeof storageMode === 'string') { + mode = storageMode; + } + + return mode; + } + + private getViewModelSortKey(): ViewModelSortKey { + // Tree + if (this._mode === ViewModelMode.Tree) { + return ViewModelSortKey.Path; + } + + // List + let viewSortKey: ViewModelSortKey; + const viewSortKeyString = this.configurationService.getValue<'path' | 'name' | 'status'>('scm.defaultViewSortKey'); + switch (viewSortKeyString) { + case 'name': + viewSortKey = ViewModelSortKey.Name; + break; + case 'status': + viewSortKey = ViewModelSortKey.Status; + break; + default: + viewSortKey = ViewModelSortKey.Path; + break; + } + + const storageSortKey = this.storageService.get(`scm.viewSortKey`, StorageScope.WORKSPACE) as ViewModelSortKey; + if (typeof storageSortKey === 'string') { + viewSortKey = storageSortKey; + } + + return viewSortKey; + } + dispose(): void { this.visibilityDisposables.dispose(); this.disposables.dispose(); @@ -1503,6 +1572,56 @@ registerAction2(SetTreeViewModeAction); registerAction2(SetListViewModeNavigationAction); registerAction2(SetTreeViewModeNavigationAction); +abstract class RepositorySortAction extends ViewAction<SCMViewPane> { + constructor(private sortKey: ISCMRepositorySortKey, title: string) { + super({ + id: `workbench.scm.action.repositories.setSortKey.${sortKey}`, + title, + viewId: VIEW_PANE_ID, + f1: false, + toggled: RepositoryContextKeys.RepositorySortKey.isEqualTo(sortKey), + menu: [ + { + id: Menus.Repositories, + group: '1_sort' + }, + { + id: MenuId.ViewTitle, + when: ContextKeyExpr.equals('view', REPOSITORIES_VIEW_PANE_ID), + group: '1_sort', + }, + ] + }); + } + + runInView(accessor: ServicesAccessor) { + accessor.get(ISCMViewService).toggleSortKey(this.sortKey); + } +} + + +class RepositorySortByDiscoveryTimeAction extends RepositorySortAction { + constructor() { + super(ISCMRepositorySortKey.DiscoveryTime, localize('repositorySortByDiscoveryTime', "Sort by Discovery Time")); + } +} + +class RepositorySortByNameAction extends RepositorySortAction { + constructor() { + super(ISCMRepositorySortKey.Name, localize('repositorySortByName', "Sort by Name")); + } +} + +class RepositorySortByPathAction extends RepositorySortAction { + constructor() { + super(ISCMRepositorySortKey.Path, localize('repositorySortByPath', "Sort by Path")); + } +} + +registerAction2(RepositorySortByDiscoveryTimeAction); +registerAction2(RepositorySortByNameAction); +registerAction2(RepositorySortByPathAction); + abstract class SetSortKeyAction extends ViewAction<SCMViewPane> { constructor(private sortKey: ViewModelSortKey, title: string) { super({ @@ -1511,6 +1630,7 @@ abstract class SetSortKeyAction extends ViewAction<SCMViewPane> { viewId: VIEW_PANE_ID, f1: false, toggled: ContextKeys.ViewModelSortKey.isEqualTo(sortKey), + precondition: ContextKeys.ViewModelMode.isEqualTo(ViewModelMode.List), menu: { id: Menus.ViewSort, group: '2_sort' } }); } @@ -1522,19 +1642,19 @@ abstract class SetSortKeyAction extends ViewAction<SCMViewPane> { class SetSortByNameAction extends SetSortKeyAction { constructor() { - super(ViewModelSortKey.Name, localize('sortByName', "Sort by Name")); + super(ViewModelSortKey.Name, localize('sortChangesByName', "Sort Changes by Name")); } } class SetSortByPathAction extends SetSortKeyAction { constructor() { - super(ViewModelSortKey.Path, localize('sortByPath', "Sort by Path")); + super(ViewModelSortKey.Path, localize('sortChangesByPath', "Sort Changes by Path")); } } class SetSortByStatusAction extends SetSortKeyAction { constructor() { - super(ViewModelSortKey.Status, localize('sortByStatus', "Sort by Status")); + super(ViewModelSortKey.Status, localize('sortChangesByStatus', "Sort Changes by Status")); } } @@ -1604,7 +1724,7 @@ class SCMInputWidget extends Disposable { private inputEditor: CodeEditorWidget; private model: { readonly input: ISCMInput; readonly textModel: ITextModel } | undefined; - private repositoryContextKey: IContextKey<ISCMRepository | undefined>; + private repositoryIdContextKey: IContextKey<string | undefined>; private repositoryDisposables = new DisposableStore(); private validation: IInputValidation | undefined; @@ -1633,7 +1753,7 @@ class SCMInputWidget extends Disposable { this.repositoryDisposables.dispose(); this.repositoryDisposables = new DisposableStore(); - this.repositoryContextKey.set(input?.repository); + this.repositoryIdContextKey.set(input?.repository.id); if (!input) { this.model?.textModel.dispose(); @@ -1799,7 +1919,7 @@ class SCMInputWidget extends Disposable { this.setPlaceholderFontStyles(fontFamily, fontSize, lineHeight); const contextKeyService2 = contextKeyService.createScoped(this.element); - this.repositoryContextKey = contextKeyService2.createKey('scmRepository', undefined); + this.repositoryIdContextKey = contextKeyService2.createKey('scmRepository', undefined); const editorOptions: IEditorConstructionOptions = { ...getSimpleEditorOptions(), @@ -2058,7 +2178,6 @@ export class SCMViewPane extends ViewPane { @IConfigurationService configurationService: IConfigurationService, @IContextKeyService contextKeyService: IContextKeyService, @IMenuService private menuService: IMenuService, - @IStorageService private storageService: IStorageService, @IOpenerService openerService: IOpenerService, @ITelemetryService telemetryService: ITelemetryService, ) { @@ -2145,44 +2264,9 @@ export class SCMViewPane extends ViewPane { append(this.listContainer, overflowWidgetsDomNode); - let viewMode = this.configurationService.getValue<'tree' | 'list'>('scm.defaultViewMode') === 'list' ? ViewModelMode.List : ViewModelMode.Tree; - - const storageMode = this.storageService.get(`scm.viewMode`, StorageScope.WORKSPACE) as ViewModelMode; - if (typeof storageMode === 'string') { - viewMode = storageMode; - } - - let viewSortKey: ViewModelSortKey; - const viewSortKeyString = this.configurationService.getValue<'path' | 'name' | 'status'>('scm.defaultViewSortKey'); - switch (viewSortKeyString) { - case 'name': - viewSortKey = ViewModelSortKey.Name; - break; - case 'status': - viewSortKey = ViewModelSortKey.Status; - break; - default: - viewSortKey = ViewModelSortKey.Path; - break; - } - - const storageSortKey = this.storageService.get(`scm.viewSortKey`, StorageScope.WORKSPACE) as ViewModelSortKey; - if (typeof storageSortKey === 'string') { - viewSortKey = storageSortKey; - } - - let viewState: ITreeViewState | undefined; - - const storageViewState = this.storageService.get(`scm.viewState`, StorageScope.WORKSPACE); - if (storageViewState) { - try { - viewState = JSON.parse(storageViewState); - } catch {/* noop */ } - } - this._register(this.instantiationService.createInstance(RepositoryVisibilityActionController)); - this._viewModel = this.instantiationService.createInstance(ViewModel, this.tree, this.inputRenderer, viewMode, viewSortKey, viewState); + this._viewModel = this.instantiationService.createInstance(ViewModel, this.tree, this.inputRenderer); this._register(this._viewModel); this.listContainer.classList.add('file-icon-themable-tree'); @@ -2191,18 +2275,11 @@ export class SCMViewPane extends ViewPane { this.updateIndentStyles(this.themeService.getFileIconTheme()); this._register(this.themeService.onDidFileIconThemeChange(this.updateIndentStyles, this)); this._register(this._viewModel.onDidChangeMode(this.onDidChangeMode, this)); - this._register(this._viewModel.onDidChangeSortKey(this.onDidChangeSortKey, this)); 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.updateActions(); - - this._register(this.storageService.onWillSaveState(e => { - if (e.reason === WillSaveStateReason.SHUTDOWN) { - this.storageService.store(`scm.viewState`, JSON.stringify(this._viewModel.treeViewState), StorageScope.WORKSPACE, StorageTarget.MACHINE); - } - })); } private updateIndentStyles(theme: IFileIconTheme): void { @@ -2214,11 +2291,6 @@ export class SCMViewPane extends ViewPane { private onDidChangeMode(): void { this.updateIndentStyles(this.themeService.getFileIconTheme()); - this.storageService.store(`scm.viewMode`, this._viewModel.mode, StorageScope.WORKSPACE, StorageTarget.USER); - } - - private onDidChangeSortKey(): void { - this.storageService.store(`scm.viewSortKey`, this._viewModel.sortKey, StorageScope.WORKSPACE, StorageTarget.USER); } override layoutBody(height: number | undefined = this.layoutCache.height, width: number | undefined = this.layoutCache.width): void { @@ -2254,14 +2326,14 @@ export class SCMViewPane extends ViewPane { return; } else if (isSCMResourceGroup(e.element)) { const provider = e.element.provider; - const repository = this.scmService.repositories.find(r => r.provider === provider); + const repository = Iterable.find(this.scmService.repositories, r => r.provider === provider); if (repository) { this.scmViewService.focus(repository); } return; } else if (ResourceTree.isResourceNode(e.element)) { const provider = e.element.context.provider; - const repository = this.scmService.repositories.find(r => r.provider === provider); + const repository = Iterable.find(this.scmService.repositories, r => r.provider === provider); if (repository) { this.scmViewService.focus(repository); } @@ -2304,7 +2376,7 @@ export class SCMViewPane extends ViewPane { } const provider = e.element.resourceGroup.provider; - const repository = this.scmService.repositories.find(r => r.provider === provider); + const repository = Iterable.find(this.scmService.repositories, r => r.provider === provider); if (repository) { this.scmViewService.focus(repository); @@ -2379,7 +2451,11 @@ export class SCMViewPane extends ViewPane { } override shouldShowWelcome(): boolean { - return this.scmService.repositories.length === 0; + return this.scmService.repositoryCount === 0; + } + + override getActionsContext(): unknown { + return this.scmViewService.visibleRepositories.length === 1 ? this.scmViewService.visibleRepositories[0].provider : undefined; } } diff --git a/src/vs/workbench/contrib/scm/browser/scmViewService.ts b/src/vs/workbench/contrib/scm/browser/scmViewService.ts index 757c5526c7a..be0176c4919 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewService.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewService.ts @@ -5,7 +5,7 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { Emitter, Event } from 'vs/base/common/event'; -import { ISCMViewService, ISCMRepository, ISCMService, ISCMViewVisibleRepositoryChangeEvent, ISCMMenus, ISCMProvider } from 'vs/workbench/contrib/scm/common/scm'; +import { ISCMViewService, ISCMRepository, ISCMService, ISCMViewVisibleRepositoryChangeEvent, ISCMMenus, ISCMProvider, ISCMRepositorySortKey } from 'vs/workbench/contrib/scm/common/scm'; import { Iterable } from 'vs/base/common/iterator'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { SCMMenus } from 'vs/workbench/contrib/scm/browser/menus'; @@ -15,6 +15,8 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { compareFileNames, comparePaths } from 'vs/base/common/comparers'; import { basename } from 'vs/base/common/resources'; import { binarySearch } from 'vs/base/common/arrays'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; function getProviderStorageKey(provider: ISCMProvider): string { return `${provider.contextValue}:${provider.label}${provider.rootUri ? `:${provider.rootUri.toString()}` : ''}`; @@ -29,8 +31,20 @@ function getRepositoryName(workspaceContextService: IWorkspaceContextService, re return folder?.uri.toString() === repository.provider.rootUri.toString() ? folder.name : basename(repository.provider.rootUri); } +export const RepositoryContextKeys = { + RepositorySortKey: new RawContextKey<ISCMRepositorySortKey>('scmRepositorySortKey', ISCMRepositorySortKey.DiscoveryTime), +}; + +interface ISCMRepositoryView { + readonly repository: ISCMRepository; + readonly discoveryTime: number; + focused: boolean; + selectionIndex: number; +} + export interface ISCMViewServiceState { readonly all: string[]; + readonly sortKey: ISCMRepositorySortKey; readonly visible: number[]; } @@ -45,20 +59,24 @@ export class SCMViewService implements ISCMViewService { private previousState: ISCMViewServiceState | undefined; private disposables = new DisposableStore(); - private _repositories: ISCMRepository[] = []; + private _repositories: ISCMRepositoryView[] = []; get repositories(): ISCMRepository[] { - return this._repositories; + return this._repositories.map(r => r.repository); } - private _onDidChangeRepositories = new Emitter<ISCMViewVisibleRepositoryChangeEvent>(); - readonly onDidChangeRepositories = this._onDidChangeRepositories.event; - - private _visibleRepositoriesSet = new Set<ISCMRepository>(); - private _visibleRepositories: ISCMRepository[] = []; - get visibleRepositories(): ISCMRepository[] { - return this._visibleRepositories; + // In order to match the legacy behaviour, when the repositories are sorted by discovery time, + // the visible repositories are sorted by the selection index instead of the discovery time. + if (this._repositoriesSortKey === ISCMRepositorySortKey.DiscoveryTime) { + return this._repositories.filter(r => r.selectionIndex !== -1) + .sort((r1, r2) => r1.selectionIndex - r2.selectionIndex) + .map(r => r.repository); + } + + return this._repositories + .filter(r => r.selectionIndex !== -1) + .map(r => r.repository); } set visibleRepositories(visibleRepositories: ISCMRepository[]) { @@ -66,15 +84,18 @@ export class SCMViewService implements ISCMViewService { const added = new Set<ISCMRepository>(); const removed = new Set<ISCMRepository>(); - for (const repository of visibleRepositories) { - if (!this._visibleRepositoriesSet.has(repository)) { - added.add(repository); + for (const repositoryView of this._repositories) { + // Selected -> !Selected + if (!set.has(repositoryView.repository) && repositoryView.selectionIndex !== -1) { + repositoryView.selectionIndex = -1; + removed.add(repositoryView.repository); } - } - - for (const repository of this._visibleRepositories) { - if (!set.has(repository)) { - removed.add(repository); + // Selected | !Selected -> Selected + if (set.has(repositoryView.repository)) { + if (repositoryView.selectionIndex === -1) { + added.add(repositoryView.repository); + } + repositoryView.selectionIndex = visibleRepositories.indexOf(repositoryView.repository); } } @@ -82,15 +103,17 @@ export class SCMViewService implements ISCMViewService { return; } - this._visibleRepositories = visibleRepositories.sort(this._compareRepositories); - this._visibleRepositoriesSet = set; this._onDidSetVisibleRepositories.fire({ added, removed }); - if (this._focusedRepository && removed.has(this._focusedRepository)) { - this.focus(this._visibleRepositories[0]); + // Update focus if the focused repository is not visible anymore + if (this._repositories.find(r => r.focused && r.selectionIndex === -1)) { + this.focus(this._repositories.find(r => r.selectionIndex !== -1)?.repository); } } + private _onDidChangeRepositories = new Emitter<ISCMViewVisibleRepositoryChangeEvent>(); + readonly onDidChangeRepositories = this._onDidChangeRepositories.event; + private _onDidSetVisibleRepositories = new Emitter<ISCMViewVisibleRepositoryChangeEvent>(); readonly onDidChangeVisibleRepositories = Event.any( this._onDidSetVisibleRepositories.event, @@ -101,53 +124,61 @@ export class SCMViewService implements ISCMViewService { return e; } - return { - added: Iterable.concat(last.added, e.added), - removed: Iterable.concat(last.removed, e.removed), - }; + const added = new Set(last.added); + const removed = new Set(last.removed); + + for (const repository of e.added) { + if (removed.has(repository)) { + removed.delete(repository); + } else { + added.add(repository); + } + } + for (const repository of e.removed) { + if (added.has(repository)) { + added.delete(repository); + } else { + removed.add(repository); + } + } + + return { added, removed }; }, 0) ); - private _focusedRepository: ISCMRepository | undefined; - get focusedRepository(): ISCMRepository | undefined { - return this._focusedRepository; + return this._repositories.find(r => r.focused)?.repository; } private _onDidFocusRepository = new Emitter<ISCMRepository | undefined>(); readonly onDidFocusRepository = this._onDidFocusRepository.event; - private _compareRepositories: (op1: ISCMRepository, op2: ISCMRepository) => number; + private _repositoriesSortKey: ISCMRepositorySortKey; + private _sortKeyContextKey: IContextKey<ISCMRepositorySortKey>; constructor( - @ISCMService private readonly scmService: ISCMService, + @ISCMService scmService: ISCMService, + @IContextKeyService contextKeyService: IContextKeyService, @IInstantiationService instantiationService: IInstantiationService, + @IConfigurationService private readonly configurationService: IConfigurationService, @IStorageService private readonly storageService: IStorageService, - @IWorkspaceContextService workspaceContextService: IWorkspaceContextService + @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService ) { this.menus = instantiationService.createInstance(SCMMenus); - this._compareRepositories = (op1: ISCMRepository, op2: ISCMRepository): number => { - const name1 = getRepositoryName(workspaceContextService, op1); - const name2 = getRepositoryName(workspaceContextService, op2); - - const nameComparison = compareFileNames(name1, name2); - if (nameComparison === 0 && op1.provider.rootUri && op2.provider.rootUri) { - return comparePaths(op1.provider.rootUri.fsPath, op2.provider.rootUri.fsPath); - } - - return nameComparison; - }; - - scmService.onDidAddRepository(this.onDidAddRepository, this, this.disposables); - scmService.onDidRemoveRepository(this.onDidRemoveRepository, this, this.disposables); - try { this.previousState = JSON.parse(storageService.get('scm:view:visibleRepositories', StorageScope.WORKSPACE, '')); } catch { // noop } + this._repositoriesSortKey = this.previousState?.sortKey ?? this.getViewSortOrder(); + this._sortKeyContextKey = RepositoryContextKeys.RepositorySortKey.bindTo(contextKeyService); + this._sortKeyContextKey.set(this._repositoriesSortKey); + + scmService.onDidAddRepository(this.onDidAddRepository, this, this.disposables); + scmService.onDidRemoveRepository(this.onDidRemoveRepository, this, this.disposables); + for (const repository of scmService.repositories) { this.onDidAddRepository(repository); } @@ -160,50 +191,61 @@ export class SCMViewService implements ISCMViewService { this.eventuallyFinishLoading(); } - this.insertRepository(this._repositories, repository); + const repositoryView: ISCMRepositoryView = { + repository, discoveryTime: Date.now(), focused: false, selectionIndex: -1 + }; + let removed: Iterable<ISCMRepository> = Iterable.empty(); if (this.previousState) { const index = this.previousState.all.indexOf(getProviderStorageKey(repository.provider)); - if (index === -1) { // saw a repo we did not expect + if (index === -1) { + // This repository is not part of the previous state which means that it + // was either manually closed in the previous session, or the repository + // was added after the previous session.In this case, we should select all + // of the repositories. const added: ISCMRepository[] = []; - for (const repo of this.scmService.repositories) { // all should be visible - if (!this._visibleRepositoriesSet.has(repo)) { - added.push(repository); + + this.insertRepositoryView(this._repositories, repositoryView); + this._repositories.forEach((repositoryView, index) => { + if (repositoryView.selectionIndex === -1) { + added.push(repositoryView.repository); } - } + repositoryView.selectionIndex = index; + }); - this._visibleRepositoriesSet = new Set(this.scmService.repositories); - this._visibleRepositories = [...this.scmService.repositories.sort(this._compareRepositories)]; this._onDidChangeRepositories.fire({ added, removed: Iterable.empty() }); - this.finishLoading(); + this.didSelectRepository = false; return; } - if (this.previousState.visible.indexOf(index) > -1) { - // First visible repository - if (!this.didSelectRepository) { - removed = this._visibleRepositories; - - this._visibleRepositories = []; - this._visibleRepositoriesSet = new Set(); - this.didSelectRepository = true; - } - } else { + if (this.previousState.visible.indexOf(index) === -1) { // Explicit selection started if (this.didSelectRepository) { + this.insertRepositoryView(this._repositories, repositoryView); this._onDidChangeRepositories.fire({ added: Iterable.empty(), removed: Iterable.empty() }); return; } + } else { + // First visible repository + if (!this.didSelectRepository) { + removed = [...this.visibleRepositories]; + this._repositories.forEach(r => { + r.focused = false; + r.selectionIndex = -1; + }); + + this.didSelectRepository = true; + } } } - this._visibleRepositoriesSet.add(repository); - this.insertRepository(this._visibleRepositories, repository); - this._onDidChangeRepositories.fire({ added: [repository], removed }); + const maxSelectionIndex = this.getMaxSelectionIndex(); + this.insertRepositoryView(this._repositories, { ...repositoryView, selectionIndex: maxSelectionIndex + 1 }); + this._onDidChangeRepositories.fire({ added: [repositoryView.repository], removed }); - if (!this._focusedRepository) { + if (!this._repositories.find(r => r.focused)) { this.focus(repository); } } @@ -213,39 +255,29 @@ export class SCMViewService implements ISCMViewService { this.eventuallyFinishLoading(); } - let added: Iterable<ISCMRepository> = Iterable.empty(); + const repositoriesIndex = this._repositories.findIndex(r => r.repository === repository); - const repositoriesIndex = this._repositories.indexOf(repository); - const visibleRepositoriesIndex = this._visibleRepositories.indexOf(repository); - - if (repositoriesIndex > -1) { - this._repositories.splice(repositoriesIndex, 1); + if (repositoriesIndex === -1) { + return; } - if (visibleRepositoriesIndex > -1) { - this._visibleRepositories.splice(visibleRepositoriesIndex, 1); - this._visibleRepositoriesSet.delete(repository); - - if (this._repositories.length > 0 && this._visibleRepositories.length === 0) { - const first = this._repositories[0]; + let added: Iterable<ISCMRepository> = Iterable.empty(); + const repositoryView = this._repositories.splice(repositoriesIndex, 1); - this._visibleRepositories.push(first); - this._visibleRepositoriesSet.add(first); - added = [first]; - } + if (this._repositories.length > 0 && this.visibleRepositories.length === 0) { + this._repositories[0].selectionIndex = 0; + added = [this._repositories[0].repository]; } - if (repositoriesIndex > -1 || visibleRepositoriesIndex > -1) { - this._onDidChangeRepositories.fire({ added, removed: [repository] }); - } + this._onDidChangeRepositories.fire({ added, removed: repositoryView.map(r => r.repository) }); - if (this._focusedRepository === repository) { - this.focus(this._visibleRepositories[0]); + if (repositoryView.length === 1 && repositoryView[0].focused && this.visibleRepositories.length > 0) { + this.focus(this.visibleRepositories[0]); } } isVisible(repository: ISCMRepository): boolean { - return this._visibleRepositoriesSet.has(repository); + return this._repositories.find(r => r.repository === repository)?.selectionIndex !== -1; } toggleVisibility(repository: ISCMRepository, visible?: boolean): void { @@ -269,18 +301,71 @@ export class SCMViewService implements ISCMViewService { } } + toggleSortKey(sortKey: ISCMRepositorySortKey): void { + this._repositoriesSortKey = sortKey; + this._sortKeyContextKey.set(this._repositoriesSortKey); + this._repositories.sort(this.compareRepositories.bind(this)); + + this._onDidChangeRepositories.fire({ added: Iterable.empty(), removed: Iterable.empty() }); + } + focus(repository: ISCMRepository | undefined): void { - if (repository && !this.visibleRepositories.includes(repository)) { + if (repository && !this.isVisible(repository)) { return; } - this._focusedRepository = repository; - this._onDidFocusRepository.fire(repository); + this._repositories.forEach(r => r.focused = r.repository === repository); + + if (this._repositories.find(r => r.focused)) { + this._onDidFocusRepository.fire(repository); + } + } + + private compareRepositories(op1: ISCMRepositoryView, op2: ISCMRepositoryView): number { + // Sort by discovery time + if (this._repositoriesSortKey === ISCMRepositorySortKey.DiscoveryTime) { + return op1.discoveryTime - op2.discoveryTime; + } + + // Sort by path + if (this._repositoriesSortKey === 'path' && op1.repository.provider.rootUri && op2.repository.provider.rootUri) { + return comparePaths(op1.repository.provider.rootUri.fsPath, op2.repository.provider.rootUri.fsPath); + } + + // Sort by name, path + const name1 = getRepositoryName(this.workspaceContextService, op1.repository); + const name2 = getRepositoryName(this.workspaceContextService, op2.repository); + + const nameComparison = compareFileNames(name1, name2); + if (nameComparison === 0 && op1.repository.provider.rootUri && op2.repository.provider.rootUri) { + return comparePaths(op1.repository.provider.rootUri.fsPath, op2.repository.provider.rootUri.fsPath); + } + + return nameComparison; + } + + private getMaxSelectionIndex(): number { + return this._repositories.length === 0 ? -1 : + Math.max(...this._repositories.map(r => r.selectionIndex)); + } + + private getViewSortOrder(): ISCMRepositorySortKey { + const sortOder = this.configurationService.getValue<'discovery time' | 'name' | 'path'>('scm.repositories.sortOrder'); + switch (sortOder) { + case 'discovery time': + return ISCMRepositorySortKey.DiscoveryTime; + case 'name': + return ISCMRepositorySortKey.Name; + case 'path': + return ISCMRepositorySortKey.Path; + default: + return ISCMRepositorySortKey.DiscoveryTime; + } } - private insertRepository(repositories: ISCMRepository[], repository: ISCMRepository): void { - const index = binarySearch(repositories, repository, this._compareRepositories); - repositories.splice(index < 0 ? ~index : index, 0, repository); + private insertRepositoryView(repositories: ISCMRepositoryView[], repositoryView: ISCMRepositoryView): void { + const index = binarySearch(repositories, repositoryView, this.compareRepositories.bind(this)); + repositories.splice(index < 0 ? ~index : index, 0, repositoryView); } private onWillSaveState(): void { @@ -290,7 +375,7 @@ export class SCMViewService implements ISCMViewService { const all = this.repositories.map(r => getProviderStorageKey(r.provider)); const visible = this.visibleRepositories.map(r => all.indexOf(getProviderStorageKey(r.provider))); - const raw = JSON.stringify({ all, visible }); + const raw = JSON.stringify({ all, sortKey: this._repositoriesSortKey, visible }); this.storageService.store('scm:view:visibleRepositories', raw, StorageScope.WORKSPACE, StorageTarget.MACHINE); } diff --git a/src/vs/workbench/contrib/scm/common/scm.ts b/src/vs/workbench/contrib/scm/common/scm.ts index c46af5c54b5..e180e8e9d3c 100644 --- a/src/vs/workbench/contrib/scm/common/scm.ts +++ b/src/vs/workbench/contrib/scm/common/scm.ts @@ -135,6 +135,7 @@ export interface ISCMInput { } export interface ISCMRepository extends IDisposable { + readonly id: string; readonly provider: ISCMProvider; readonly input: ISCMInput; } @@ -144,9 +145,11 @@ export interface ISCMService { readonly _serviceBrand: undefined; readonly onDidAddRepository: Event<ISCMRepository>; readonly onDidRemoveRepository: Event<ISCMRepository>; - readonly repositories: ISCMRepository[]; + readonly repositories: Iterable<ISCMRepository>; + readonly repositoryCount: number; registerSCMProvider(provider: ISCMProvider): ISCMRepository; + getRepository(id: string): ISCMRepository | undefined; } export interface ISCMTitleMenu { @@ -168,6 +171,12 @@ export interface ISCMMenus { getRepositoryMenus(provider: ISCMProvider): ISCMRepositoryMenus; } +export const enum ISCMRepositorySortKey { + DiscoveryTime = 'discoveryTime', + Name = 'name', + Path = 'path' +} + export const ISCMViewService = createDecorator<ISCMViewService>('scmView'); export interface ISCMViewVisibleRepositoryChangeEvent { @@ -189,6 +198,8 @@ export interface ISCMViewService { isVisible(repository: ISCMRepository): boolean; toggleVisibility(repository: ISCMRepository, visible?: boolean): void; + toggleSortKey(sortKey: ISCMRepositorySortKey): void; + readonly focusedRepository: ISCMRepository | undefined; readonly onDidFocusRepository: Event<ISCMRepository | undefined>; focus(repository: ISCMRepository): void; diff --git a/src/vs/workbench/contrib/scm/common/scmService.ts b/src/vs/workbench/contrib/scm/common/scmService.ts index 8fbe71b55e8..7877ba1d34e 100644 --- a/src/vs/workbench/contrib/scm/common/scmService.ts +++ b/src/vs/workbench/contrib/scm/common/scmService.ts @@ -175,10 +175,10 @@ class SCMInput implements ISCMInput { const history = [...this.historyNavigator].map(s => s ?? ''); if (history.length === 0 || (history.length === 1 && history[0] === '')) { - return; + storageService.remove(key, StorageScope.GLOBAL); + } 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.GLOBAL, StorageTarget.MACHINE); this.didChangeHistory = false; }); } @@ -242,6 +242,7 @@ class SCMRepository implements ISCMRepository { readonly input: ISCMInput = new SCMInput(this, this.storageService); constructor( + public readonly id: string, public readonly provider: ISCMProvider, private disposable: IDisposable, @IStorageService private storageService: IStorageService @@ -266,9 +267,9 @@ export class SCMService implements ISCMService { declare readonly _serviceBrand: undefined; - private _providerIds = new Set<string>(); - private _repositories: ISCMRepository[] = []; - get repositories(): ISCMRepository[] { return [...this._repositories]; } + private _repositories = new Map<string, ISCMRepository>(); + get repositories(): Iterable<ISCMRepository> { return this._repositories.values(); } + get repositoryCount(): number { return this._repositories.size; } private providerCount: IContextKey<number>; @@ -289,31 +290,25 @@ export class SCMService implements ISCMService { registerSCMProvider(provider: ISCMProvider): ISCMRepository { this.logService.trace('SCMService#registerSCMProvider'); - if (this._providerIds.has(provider.id)) { + if (this._repositories.has(provider.id)) { throw new Error(`SCM Provider ${provider.id} already exists.`); } - this._providerIds.add(provider.id); - const disposable = toDisposable(() => { - const index = this._repositories.indexOf(repository); - - if (index < 0) { - return; - } - - this._providerIds.delete(provider.id); - this._repositories.splice(index, 1); + this._repositories.delete(provider.id); this._onDidRemoveProvider.fire(repository); - - this.providerCount.set(this._repositories.length); + this.providerCount.set(this._repositories.size); }); - const repository = new SCMRepository(provider, disposable, this.storageService); - this._repositories.push(repository); + const repository = new SCMRepository(provider.id, provider, disposable, this.storageService); + this._repositories.set(provider.id, repository); this._onDidAddProvider.fire(repository); - this.providerCount.set(this._repositories.length); + this.providerCount.set(this._repositories.size); return repository; } + + getRepository(id: string): ISCMRepository | undefined { + return this._repositories.get(id); + } } diff --git a/src/vs/workbench/contrib/search/browser/search.contribution.ts b/src/vs/workbench/contrib/search/browser/search.contribution.ts index 4ed1c60a5ca..fb3d698f583 100644 --- a/src/vs/workbench/contrib/search/browser/search.contribution.ts +++ b/src/vs/workbench/contrib/search/browser/search.contribution.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Action } from 'vs/base/common/actions'; import { onUnexpectedError } from 'vs/base/common/errors'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import * as platform from 'vs/base/common/platform'; @@ -594,25 +593,37 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { when: ContextKeyExpr.and(ExplorerRootContext, ExplorerFolderContext.toNegated()) }); - -class ShowAllSymbolsAction extends Action { +registerAction2(class ShowAllSymbolsAction extends Action2 { static readonly ID = 'workbench.action.showAllSymbols'; static readonly LABEL = nls.localize('showTriggerActions', "Go to Symbol in Workspace..."); static readonly ALL_SYMBOLS_PREFIX = '#'; constructor( - actionId: string, - actionLabel: string, - @IQuickInputService private readonly quickInputService: IQuickInputService ) { - super(actionId, actionLabel); + super({ + id: 'workbench.action.showAllSymbols', + title: { + value: nls.localize('showTriggerActions', "Go to Symbol in Workspace..."), + original: 'Go to Symbol in Workspace...' + }, + f1: true, + menu: { + id: MenuId.TitleMenuQuickPick, + group: '1/workspaceNav', + order: 2 + }, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyCode.KeyT + } + }); } - override async run(): Promise<void> { - this.quickInputService.quickAccess.show(ShowAllSymbolsAction.ALL_SYMBOLS_PREFIX); + override async run(accessor: ServicesAccessor): Promise<void> { + accessor.get(IQuickInputService).quickAccess.show(ShowAllSymbolsAction.ALL_SYMBOLS_PREFIX); } -} +}); const SEARCH_MODE_CONFIG = 'search.mode'; @@ -793,7 +804,6 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } }); -registry.registerWorkbenchAction(SyncActionDescriptor.from(ShowAllSymbolsAction, { primary: KeyMod.CtrlCmd | KeyCode.KeyT }), 'Go to Symbol in Workspace...'); registry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleSearchOnTypeAction), 'Search: Toggle Search on Type', category.value); // Register Quick Access Handler diff --git a/src/vs/workbench/contrib/search/browser/searchResultsView.ts b/src/vs/workbench/contrib/search/browser/searchResultsView.ts index eebc83d22a0..976f351c60e 100644 --- a/src/vs/workbench/contrib/search/browser/searchResultsView.ts +++ b/src/vs/workbench/contrib/search/browser/searchResultsView.ts @@ -54,8 +54,10 @@ interface IMatchTemplate { export class SearchDelegate implements IListVirtualDelegate<RenderableMatch> { + public static ITEM_HEIGHT = 22; + getHeight(element: RenderableMatch): number { - return 22; + return SearchDelegate.ITEM_HEIGHT; } getTemplateId(element: RenderableMatch): string { diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index 90411d0caec..17baf536043 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -8,7 +8,6 @@ import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import * as aria from 'vs/base/browser/ui/aria/aria'; import { MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; import { IIdentityProvider } from 'vs/base/browser/ui/list/list'; -import { Orientation } from 'vs/base/browser/ui/sash/sash'; import { ITreeContextMenuEvent, ITreeElement } from 'vs/base/browser/ui/tree/tree'; import { IAction } from 'vs/base/common/actions'; import { Delayer } from 'vs/base/common/async'; @@ -45,20 +44,24 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { getSelectionKeyboardEvent, WorkbenchObjectTree } from 'vs/platform/list/browser/listService'; -import { INotificationService, } from 'vs/platform/notification/common/notification'; +import { INotificationService } from 'vs/platform/notification/common/notification'; import { IOpenerService, withSelection } from 'vs/platform/opener/common/opener'; import { IProgress, IProgressService, IProgressStep } from 'vs/platform/progress/common/progress'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { diffInserted, diffInsertedOutline, diffRemoved, diffRemovedOutline, editorFindMatchHighlight, editorFindMatchHighlightBorder, foreground, listActiveSelectionForeground, textLinkActiveForeground, textLinkForeground, toolbarActiveBackground, toolbarHoverBackground } from 'vs/platform/theme/common/colorRegistry'; +import { isHighContrast } from 'vs/platform/theme/common/theme'; import { IColorTheme, ICssStyleCollector, IThemeService, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { OpenFileFolderAction, OpenFolderAction } from 'vs/workbench/browser/actions/workspaceActions'; +import { ResourceListDnDHandler } from 'vs/workbench/browser/dnd'; import { ResourceLabels } from 'vs/workbench/browser/labels'; import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; import { IEditorPane } from 'vs/workbench/common/editor'; import { Memento, MementoObject } from 'vs/workbench/common/memento'; import { IViewDescriptorService } from 'vs/workbench/common/views'; +import { NotebookFindWidget } from 'vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget'; +import { NotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookEditor'; import { ExcludePatternInputWidget, IncludePatternInputWidget } from 'vs/workbench/contrib/search/browser/patternInputWidget'; import { appendKeyBindingLabel, IFindInFilesArgs } from 'vs/workbench/contrib/search/browser/searchActions'; import { searchDetailsIcon } from 'vs/workbench/contrib/search/browser/searchIcons'; @@ -66,7 +69,6 @@ import { renderSearchMessage } from 'vs/workbench/contrib/search/browser/searchM import { FileMatchRenderer, FolderMatchRenderer, MatchRenderer, SearchAccessibilityProvider, SearchDelegate } from 'vs/workbench/contrib/search/browser/searchResultsView'; import { ISearchWidgetOptions, SearchWidget } from 'vs/workbench/contrib/search/browser/searchWidget'; import * as Constants from 'vs/workbench/contrib/search/common/constants'; -import { ITextQueryBuilderOptions, QueryBuilder } from 'vs/workbench/services/search/common/queryBuilder'; import { IReplaceService } from 'vs/workbench/contrib/search/common/replace'; import { getOutOfWorkspaceEditorResources, SearchStateKey, SearchUIState } from 'vs/workbench/contrib/search/common/search'; import { ISearchHistoryService, ISearchHistoryValues } from 'vs/workbench/contrib/search/common/searchHistoryService'; @@ -74,11 +76,10 @@ import { FileMatch, FileMatchOrMatch, FolderMatch, FolderMatchWithResource, ICha import { createEditorFromSearchResult } from 'vs/workbench/contrib/searchEditor/browser/searchEditorActions'; import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IPreferencesService, ISettingsEditorOptions } from 'vs/workbench/services/preferences/common/preferences'; +import { ITextQueryBuilderOptions, QueryBuilder } from 'vs/workbench/services/search/common/queryBuilder'; import { IPatternInfo, ISearchComplete, ISearchConfiguration, ISearchConfigurationProperties, ITextQuery, SearchCompletionExitCode, SearchSortOrder, TextSearchCompleteMessageType } from 'vs/workbench/services/search/common/search'; import { TextSearchCompleteMessage } from 'vs/workbench/services/search/common/searchExtTypes'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { ResourceListDnDHandler } from 'vs/workbench/browser/dnd'; -import { isHighContrast } from 'vs/platform/theme/common/theme'; const $ = dom.$; @@ -739,7 +740,8 @@ export class SearchView extends ViewPane { selectionNavigation: true, overrideStyles: { listBackground: this.getBackgroundColor() - } + }, + additionalScrollHeight: SearchDelegate.ITEM_HEIGHT })); this._register(this.tree.onContextMenu(e => this.onContextMenu(e))); const updateHasSomeCollapsible = () => this.toggleCollapseStateDelayer.trigger(() => this.hasSomeCollapsibleResultKey.set(this.hasSomeCollapsible())); @@ -1065,7 +1067,9 @@ export class SearchView extends ViewPane { this.inputPatternExcludes.setWidth(this.size.width - 28 /* container margin */); this.inputPatternIncludes.setWidth(this.size.width - 28 /* container margin */); - this.tree.layout(); // The tree will measure its container + const widgetHeight = dom.getTotalHeight(this.searchWidgetsContainerElement); + const messagesHeight = dom.getTotalHeight(this.messagesElement); + this.tree.layout(this.size.height - widgetHeight - messagesHeight, this.size.width - 28); } protected override layoutBody(height: number, width: number): void { @@ -1282,7 +1286,7 @@ export class SearchView extends ViewPane { } if (!skipLayout && this.size) { - this.layout(this._orientation === Orientation.VERTICAL ? this.size.height : this.size.width); + this.reLayout(); } } @@ -1710,18 +1714,22 @@ export class SearchView extends ViewPane { this.open(lineMatch, preserveFocus, sideBySide, pinned); } - open(element: FileMatchOrMatch, preserveFocus?: boolean, sideBySide?: boolean, pinned?: boolean): Promise<void> { + async open(element: FileMatchOrMatch, preserveFocus?: boolean, sideBySide?: boolean, pinned?: boolean): Promise<void> { const selection = this.getSelectionFrom(element); const resource = element instanceof Match ? element.parent().resource : (<FileMatch>element).resource; - return this.editorService.openEditor({ - resource: resource, - options: { - preserveFocus, - pinned, - selection, - revealIfVisible: true - } - }, sideBySide ? SIDE_GROUP : ACTIVE_GROUP).then(editor => { + + let editor: IEditorPane | undefined; + try { + editor = await this.editorService.openEditor({ + resource: resource, + options: { + preserveFocus, + pinned, + selection, + revealIfVisible: true + } + }, sideBySide ? SIDE_GROUP : ACTIVE_GROUP); + const editorControl = editor?.getControl(); if (element instanceof Match && preserveFocus && isCodeEditor(editorControl)) { this.viewModel.searchResult.rangeHighlightDecorations.highlightRange( @@ -1731,7 +1739,16 @@ export class SearchView extends ViewPane { } else { this.viewModel.searchResult.rangeHighlightDecorations.removeHighlightRange(); } - }, errors.onUnexpectedError); + } catch (err) { + errors.onUnexpectedError(err); + return; + } + + if (editor instanceof NotebookEditor) { + const controller = editor.getControl()?.getContribution<NotebookFindWidget>(NotebookFindWidget.id); + const matchIndex = element instanceof Match ? element.parent().matches().findIndex(e => e.id() === element.id()) : undefined; + controller?.show(this.searchWidget.searchInput.getValue(), { matchIndex, focus: false }); + } } openEditorWithMultiCursor(element: FileMatchOrMatch): Promise<void> { diff --git a/src/vs/workbench/contrib/search/browser/searchWidget.ts b/src/vs/workbench/contrib/search/browser/searchWidget.ts index d9d0f908be9..aed2458e00b 100644 --- a/src/vs/workbench/contrib/search/browser/searchWidget.ts +++ b/src/vs/workbench/contrib/search/browser/searchWidget.ts @@ -366,7 +366,12 @@ export class SearchWidget extends Widget { this.contextLinesInput = new InputBox(searchInputContainer, this.contextViewService, { type: 'number' }); this.contextLinesInput.element.classList.add('context-lines-input'); this.contextLinesInput.value = '' + (this.configurationService.getValue<ISearchConfigurationProperties>('search').searchEditor.defaultNumberOfContextLines ?? 1); - this._register(this.contextLinesInput.onDidChange(() => this.onContextLinesChanged())); + this._register(this.contextLinesInput.onDidChange((value: string) => { + if (value !== '0') { + this.showContextToggle.checked = true; + } + this.onContextLinesChanged(); + })); this._register(attachInputBoxStyler(this.contextLinesInput, this.themeService)); dom.append(searchInputContainer, this.showContextToggle.domNode); } diff --git a/src/vs/workbench/contrib/search/test/electron-browser/textsearch.perf.integrationTest.ts b/src/vs/workbench/contrib/search/test/electron-browser/textsearch.perf.integrationTest.ts index d1a74339e79..cfcbec62d50 100644 --- a/src/vs/workbench/contrib/search/test/electron-browser/textsearch.perf.integrationTest.ts +++ b/src/vs/workbench/contrib/search/test/electron-browser/textsearch.perf.integrationTest.ts @@ -43,6 +43,7 @@ import { TestContextService, TestTextResourcePropertiesService } from 'vs/workbe import { TestEnvironmentService } from 'vs/workbench/test/electron-browser/workbenchTestServices'; import { LanguageFeatureDebounceService } from 'vs/editor/common/services/languageFeatureDebounce'; import { LanguageFeaturesService } from 'vs/editor/common/services/languageFeaturesService'; +import { staticObservableValue } from 'vs/base/common/observableValue'; // declare var __dirname: string; @@ -182,7 +183,7 @@ suite.skip('TextSearch performance (integration)', () => { class TestTelemetryService implements ITelemetryService { public _serviceBrand: undefined; - public telemetryLevel = TelemetryLevel.USAGE; + public telemetryLevel = staticObservableValue(TelemetryLevel.USAGE); public sendErrorTelemetry = true; public events: any[] = []; diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts index 4d0742dc00a..ed23a5f2632 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts @@ -121,7 +121,7 @@ class SearchEditorInputSerializer implements IEditorSerializer { const config = input.tryReadConfigSync(); const dirty = input.isDirty(); - const matchRanges = input.getMatchRanges(); + const matchRanges = dirty ? input.getMatchRanges() : []; const backingUri = input.backingUri; return JSON.stringify({ modelUri, dirty, config, name: input.getName(), matchRanges, backingUri: backingUri?.toString() } as SerializedSearchEditor); diff --git a/src/vs/workbench/contrib/snippets/browser/configureSnippets.ts b/src/vs/workbench/contrib/snippets/browser/configureSnippets.ts index bbee0e94ef5..9b5871c2e89 100644 --- a/src/vs/workbench/contrib/snippets/browser/configureSnippets.ts +++ b/src/vs/workbench/contrib/snippets/browser/configureSnippets.ts @@ -4,11 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { extname } from 'vs/base/common/path'; -import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; +import { MenuId, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { URI } from 'vs/base/common/uri'; import { ISnippetsService } from 'vs/workbench/contrib/snippets/browser/snippets.contribution'; @@ -19,8 +18,7 @@ import { IFileService } from 'vs/platform/files/common/files'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { isValidBasename } from 'vs/base/common/extpath'; import { joinPath, basename } from 'vs/base/common/resources'; - -const id = 'workbench.action.openSnippets'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; namespace ISnippetPick { export function is(thing: object | undefined): thing is ISnippetPick { @@ -201,82 +199,80 @@ async function createLanguageSnippetFile(pick: ISnippetPick, fileService: IFileS await textFileService.write(pick.filepath, contents); } -CommandsRegistry.registerCommand(id, async (accessor): Promise<any> => { - - const snippetService = accessor.get(ISnippetsService); - const quickInputService = accessor.get(IQuickInputService); - const opener = accessor.get(IOpenerService); - const languageService = accessor.get(ILanguageService); - const envService = accessor.get(IEnvironmentService); - const workspaceService = accessor.get(IWorkspaceContextService); - const fileService = accessor.get(IFileService); - const textFileService = accessor.get(ITextFileService); - - const picks = await computePicks(snippetService, envService, 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 - }]; - - const workspaceSnippetPicks: SnippetPick[] = []; - for (const folder of workspaceService.getWorkspace().folders) { - workspaceSnippetPicks.push({ - scope: nls.localize('new.workspace_scope', "{0} workspace", folder.name), - label: nls.localize('new.folder', "New Snippets file for '{0}'...", folder.name), - uri: folder.toResource('.vscode') +registerAction2(class ConfigureSnippets extends Action2 { + + constructor() { + super({ + id: 'workbench.action.openSnippets', + title: { + value: nls.localize('openSnippet.label', "Configure User Snippets"), + original: 'Configure User Snippets' + }, + shortTitle: { + value: nls.localize('userSnippets', "User Snippets"), + mnemonicTitle: nls.localize({ key: 'miOpenSnippets', comment: ['&& denotes a mnemonic'] }, "User &&Snippets"), + original: 'User Snippets' + }, + menu: [ + { id: MenuId.CommandPalette }, + { id: MenuId.MenubarPreferencesMenu, group: '3_snippets', order: 1 }, + { id: MenuId.GlobalActivity, group: '3_snippets', order: 1 }, + ] }); } - if (existing.length > 0) { - existing.unshift({ type: 'separator', label: nls.localize('group.global', "Existing Snippets") }); - existing.push({ type: 'separator', label: nls.localize('new.global.sep', "New Snippets") }); - } else { - existing.push({ type: 'separator', label: nls.localize('new.global.sep', "New Snippets") }); - } - - const pick = await quickInputService.pick(([] as QuickPickInput[]).concat(existing, globalSnippetPicks, workspaceSnippetPicks, picks.future), { - placeHolder: nls.localize('openSnippet.pickLanguage', "Select Snippets File or Create Snippets"), - matchOnDescription: true - }); + async run(accessor: ServicesAccessor, ...args: any[]): Promise<any> { + + const snippetService = accessor.get(ISnippetsService); + const quickInputService = accessor.get(IQuickInputService); + const opener = accessor.get(IOpenerService); + const languageService = accessor.get(ILanguageService); + const envService = accessor.get(IEnvironmentService); + const workspaceService = accessor.get(IWorkspaceContextService); + const fileService = accessor.get(IFileService); + const textFileService = accessor.get(ITextFileService); + + const picks = await computePicks(snippetService, envService, 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 + }]; + + const workspaceSnippetPicks: SnippetPick[] = []; + for (const folder of workspaceService.getWorkspace().folders) { + workspaceSnippetPicks.push({ + scope: nls.localize('new.workspace_scope', "{0} workspace", folder.name), + label: nls.localize('new.folder', "New Snippets file for '{0}'...", folder.name), + uri: folder.toResource('.vscode') + }); + } - if (globalSnippetPicks.indexOf(pick as SnippetPick) >= 0) { - return createSnippetFile((pick as SnippetPick).scope, (pick as SnippetPick).uri, quickInputService, fileService, textFileService, opener); - } else if (workspaceSnippetPicks.indexOf(pick as SnippetPick) >= 0) { - return createSnippetFile((pick as SnippetPick).scope, (pick as SnippetPick).uri, quickInputService, fileService, textFileService, opener); - } else if (ISnippetPick.is(pick)) { - if (pick.hint) { - await createLanguageSnippetFile(pick, fileService, textFileService); + if (existing.length > 0) { + existing.unshift({ type: 'separator', label: nls.localize('group.global', "Existing Snippets") }); + existing.push({ type: 'separator', label: nls.localize('new.global.sep', "New Snippets") }); + } else { + existing.push({ type: 'separator', label: nls.localize('new.global.sep', "New Snippets") }); } - return opener.open(pick.filepath); - } -}); -MenuRegistry.appendMenuItem(MenuId.CommandPalette, { - command: { - id, - title: { value: nls.localize('openSnippet.label', "Configure User Snippets"), original: 'Configure User Snippets' }, - category: { value: nls.localize('preferences', "Preferences"), original: 'Preferences' } - } -}); + const pick = await quickInputService.pick(([] as QuickPickInput[]).concat(existing, globalSnippetPicks, workspaceSnippetPicks, picks.future), { + placeHolder: nls.localize('openSnippet.pickLanguage', "Select Snippets File or Create Snippets"), + matchOnDescription: true + }); -MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { - group: '3_snippets', - command: { - id, - title: nls.localize({ key: 'miOpenSnippets', comment: ['&& denotes a mnemonic'] }, "User &&Snippets") - }, - order: 1 -}); + if (globalSnippetPicks.indexOf(pick as SnippetPick) >= 0) { + return createSnippetFile((pick as SnippetPick).scope, (pick as SnippetPick).uri, quickInputService, fileService, textFileService, opener); + } else if (workspaceSnippetPicks.indexOf(pick as SnippetPick) >= 0) { + return createSnippetFile((pick as SnippetPick).scope, (pick as SnippetPick).uri, quickInputService, fileService, textFileService, opener); + } else if (ISnippetPick.is(pick)) { + if (pick.hint) { + await createLanguageSnippetFile(pick, fileService, textFileService); + } + return opener.open(pick.filepath); + } -MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { - group: '3_snippets', - command: { - id, - title: nls.localize('userSnippets', "User Snippets") - }, - order: 1 + } }); diff --git a/src/vs/workbench/contrib/snippets/browser/insertSnippet.ts b/src/vs/workbench/contrib/snippets/browser/insertSnippet.ts index 782cb1493fd..3a0088a1f48 100644 --- a/src/vs/workbench/contrib/snippets/browser/insertSnippet.ts +++ b/src/vs/workbench/contrib/snippets/browser/insertSnippet.ts @@ -112,7 +112,7 @@ class InsertSnippetAction extends EditorAction { } languageId = langId; } else { - editor.getModel().tokenizeIfCheap(lineNumber); + editor.getModel().tokenization.tokenizeIfCheap(lineNumber); languageId = editor.getModel().getLanguageIdAtPosition(lineNumber, column); // validate the `languageId` to ensure this is a user diff --git a/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts b/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts index aa6e96d9afe..2b7c3c8ed98 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts @@ -18,6 +18,7 @@ import { isPatternInWord } from 'vs/base/common/filters'; import { StopWatch } from 'vs/base/common/stopwatch'; import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { getWordAtText } from 'vs/editor/common/core/wordHelper'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; export class SnippetCompletion implements CompletionItem { @@ -29,6 +30,7 @@ export class SnippetCompletion implements CompletionItem { sortText: string; kind: CompletionItemKind; insertTextRules: CompletionItemInsertTextRule; + extensionId?: ExtensionIdentifier; constructor( readonly snippet: Snippet, @@ -37,6 +39,7 @@ export class SnippetCompletion implements CompletionItem { this.label = { label: snippet.prefix, description: snippet.name }; this.detail = localize('detail.snippet', "{0} ({1})", snippet.description || snippet.name, snippet.source); this.insertText = snippet.codeSnippet; + this.extensionId = snippet.extensionId; this.range = range; this.sortText = `${snippet.snippetSource === SnippetSource.Extension ? 'z' : 'a'}-${snippet.prefix}`; this.kind = CompletionItemKind.Snippet; @@ -182,7 +185,7 @@ export class SnippetCompletionProvider implements CompletionItemProvider { // validate the `languageId` to ensure this is a user // facing language with a name and the chance to have // snippets, else fall back to the outer language - model.tokenizeIfCheap(position.lineNumber); + model.tokenization.tokenizeIfCheap(position.lineNumber); let languageId = model.getLanguageIdAtPosition(position.lineNumber, position.column); if (!this._languageService.getLanguageName(languageId)) { languageId = model.getLanguageId(); diff --git a/src/vs/workbench/contrib/snippets/browser/snippetsFile.ts b/src/vs/workbench/contrib/snippets/browser/snippetsFile.ts index 25fafbfeda6..84f407b5f3e 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippetsFile.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippetsFile.ts @@ -12,7 +12,7 @@ import { KnownSnippetVariableNames } from 'vs/editor/contrib/snippet/browser/sni import { isFalsyOrWhitespace } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; import { IFileService } from 'vs/platform/files/common/files'; -import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { IdleValue } from 'vs/base/common/async'; import { IExtensionResourceLoaderService } from 'vs/workbench/services/extensionResourceLoader/common/extensionResourceLoader'; import { relativePath } from 'vs/base/common/resources'; @@ -114,7 +114,8 @@ export class Snippet { readonly body: string, readonly source: string, readonly snippetSource: SnippetSource, - readonly snippetIdentifier?: string + readonly snippetIdentifier?: string, + readonly extensionId?: ExtensionIdentifier, ) { this.prefixLow = prefix.toLowerCase(); this._bodyInsights = new IdleValue(() => new SnippetBodyInsights(this.body)); @@ -332,7 +333,8 @@ export class SnippetFile { body, source, this.source, - this._extension && `${relativePath(this._extension.extensionLocation, this.location)}/${name}` + this._extension && `${relativePath(this._extension.extensionLocation, this.location)}/${name}`, + this._extension?.identifier, )); } } diff --git a/src/vs/workbench/contrib/snippets/browser/surroundWithSnippet.ts b/src/vs/workbench/contrib/snippets/browser/surroundWithSnippet.ts index 96c1ecfc5a7..80d25b05968 100644 --- a/src/vs/workbench/contrib/snippets/browser/surroundWithSnippet.ts +++ b/src/vs/workbench/contrib/snippets/browser/surroundWithSnippet.ts @@ -38,7 +38,7 @@ registerAction2(class SurroundWithAction extends EditorAction2 { } const { lineNumber, column } = editor.getPosition(); - editor.getModel().tokenizeIfCheap(lineNumber); + editor.getModel().tokenization.tokenizeIfCheap(lineNumber); const languageId = editor.getModel().getLanguageIdAtPosition(lineNumber, column); const allSnippets = await snippetService.getSnippets(languageId, { includeNoPrefixSnippets: true, includeDisabledSnippets: true }); diff --git a/src/vs/workbench/contrib/snippets/browser/tabCompletion.ts b/src/vs/workbench/contrib/snippets/browser/tabCompletion.ts index 504da91fa71..1f4afaa0a94 100644 --- a/src/vs/workbench/contrib/snippets/browser/tabCompletion.ts +++ b/src/vs/workbench/contrib/snippets/browser/tabCompletion.ts @@ -91,7 +91,7 @@ export class TabCompletionController implements IEditorContribution { // lots of dance for getting the const selection = this._editor.getSelection(); const model = this._editor.getModel(); - model.tokenizeIfCheap(selection.positionLineNumber); + model.tokenization.tokenizeIfCheap(selection.positionLineNumber); const id = model.getLanguageIdAtPosition(selection.positionLineNumber, selection.positionColumn); const snippets = this._snippetService.getSnippetsSync(id); @@ -147,7 +147,7 @@ export class TabCompletionController implements IEditorContribution { } }; const registration = this._languageFeaturesService.completionProvider.register( - { language: model.getLanguageId(), pattern: model.uri.path, scheme: model.uri.scheme }, + { language: model.getLanguageId(), pattern: model.uri.fsPath, scheme: model.uri.scheme }, this._completionProvider ); } 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 540e3fce155..9a602558814 100644 --- a/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts +++ b/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts @@ -6,15 +6,15 @@ import * as assert from 'assert'; import { SnippetCompletion, SnippetCompletionProvider } from 'vs/workbench/contrib/snippets/browser/snippetCompletionProvider'; import { Position } from 'vs/editor/common/core/position'; -import { ModesRegistry } from 'vs/editor/common/languages/modesRegistry'; -import { LanguageService } from 'vs/editor/common/services/languageService'; -import { createTextModel } from 'vs/editor/test/common/testTextModel'; +import { createModelServices, instantiateTextModel } from 'vs/editor/test/common/testTextModel'; import { ISnippetsService } from 'vs/workbench/contrib/snippets/browser/snippets.contribution'; import { Snippet, SnippetSource } from 'vs/workbench/contrib/snippets/browser/snippetsFile'; import { CompletionContext, CompletionItemLabel, CompletionItemRanges, CompletionTriggerKind } from 'vs/editor/common/languages'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; import { EditOperation } from 'vs/editor/common/core/editOperation'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { ILanguageService } from 'vs/editor/common/languages/language'; class SimpleSnippetService implements ISnippetsService { declare readonly _serviceBrand: undefined; @@ -37,27 +37,21 @@ class SimpleSnippetService implements ISnippetsService { } suite('SnippetsService', function () { - const disposableStore: DisposableStore = new DisposableStore(); const context: CompletionContext = { triggerKind: CompletionTriggerKind.Invoke }; - suiteSetup(function () { - disposableStore.add(ModesRegistry.registerLanguage({ - id: 'fooLang', - extensions: ['.fooLang',] - })); - }); - - suiteTeardown(function () { - disposableStore.dispose(); - }); - let disposables: DisposableStore; - let languageService: LanguageService; + let instantiationService: TestInstantiationService; + let languageService: ILanguageService; let snippetService: ISnippetsService; setup(function () { disposables = new DisposableStore(); - languageService = disposables.add(new LanguageService()); + instantiationService = createModelServices(disposables); + languageService = instantiationService.get(ILanguageService); + disposables.add(languageService.registerLanguage({ + id: 'fooLang', + extensions: ['.fooLang',] + })); snippetService = new SimpleSnippetService([new Snippet( ['fooLang'], 'barTest', @@ -84,7 +78,7 @@ suite('SnippetsService', function () { test('snippet completions - simple', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - const model = disposables.add(createTextModel('', 'fooLang')); + const model = disposables.add(instantiateTextModel(instantiationService, '', 'fooLang')); return provider.provideCompletionItems(model, new Position(1, 1), context)!.then(result => { assert.strictEqual(result.incomplete, undefined); @@ -95,7 +89,7 @@ suite('SnippetsService', function () { test('snippet completions - simple 2', async function () { const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - const model = disposables.add(createTextModel('hello ', 'fooLang')); + const model = disposables.add(instantiateTextModel(instantiationService, 'hello ', 'fooLang')); await provider.provideCompletionItems(model, new Position(1, 6) /* hello| */, context)!.then(result => { assert.strictEqual(result.incomplete, undefined); @@ -111,7 +105,7 @@ suite('SnippetsService', function () { test('snippet completions - with prefix', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - const model = disposables.add(createTextModel('bar', 'fooLang')); + const model = disposables.add(instantiateTextModel(instantiationService, 'bar', 'fooLang')); return provider.provideCompletionItems(model, new Position(1, 4), context)!.then(result => { assert.strictEqual(result.incomplete, undefined); @@ -146,7 +140,7 @@ suite('SnippetsService', function () { )]); const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - const model = disposables.add(createTextModel('bar-bar', 'fooLang')); + const model = disposables.add(instantiateTextModel(instantiationService, 'bar-bar', 'fooLang')); await provider.provideCompletionItems(model, new Position(1, 3), context)!.then(result => { assert.strictEqual(result.incomplete, undefined); @@ -217,19 +211,19 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = createTextModel('\t<?php', 'fooLang'); + let model = instantiateTextModel(instantiationService, '\t<?php', 'fooLang'); return provider.provideCompletionItems(model, new Position(1, 7), context)!.then(result => { assert.strictEqual(result.suggestions.length, 1); model.dispose(); - model = createTextModel('\t<?', 'fooLang'); + model = instantiateTextModel(instantiationService, '\t<?', 'fooLang'); return provider.provideCompletionItems(model, new Position(1, 4), context)!; }).then(result => { assert.strictEqual(result.suggestions.length, 1); assert.strictEqual((result.suggestions[0].range as any).insert.startColumn, 2); model.dispose(); - model = createTextModel('a<?', 'fooLang'); + model = instantiateTextModel(instantiationService, 'a<?', 'fooLang'); return provider.provideCompletionItems(model, new Position(1, 4), context)!; }).then(result => { assert.strictEqual(result.suggestions.length, 1); @@ -252,7 +246,7 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = disposables.add(createTextModel('<head>\n\t\n>/head>', 'fooLang')); + let 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)!; @@ -282,7 +276,7 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = disposables.add(createTextModel('', 'fooLang')); + let 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; @@ -309,7 +303,7 @@ suite('SnippetsService', function () { )]); const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = disposables.add(createTextModel('p-', 'fooLang')); + let model = disposables.add(instantiateTextModel(instantiationService, 'p-', 'fooLang')); let result = await provider.provideCompletionItems(model, new Position(1, 2), context)!; assert.strictEqual(result.suggestions.length, 1); @@ -334,7 +328,7 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = disposables.add(createTextModel('Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea b', 'fooLang')); + let model = disposables.add(instantiateTextModel(instantiationService, 'Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea b', 'fooLang')); let result = await provider.provideCompletionItems(model, new Position(1, 158), context)!; assert.strictEqual(result.suggestions.length, 1); @@ -353,7 +347,7 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = disposables.add(createTextModel(':', 'fooLang')); + let model = disposables.add(instantiateTextModel(instantiationService, ':', 'fooLang')); let result = await provider.provideCompletionItems(model, new Position(1, 2), context)!; assert.strictEqual(result.suggestions.length, 0); @@ -372,7 +366,7 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = disposables.add(createTextModel('template', 'fooLang')); + let model = disposables.add(instantiateTextModel(instantiationService, 'template', 'fooLang')); let result = await provider.provideCompletionItems(model, new Position(1, 9), context)!; assert.strictEqual(result.suggestions.length, 1); @@ -395,7 +389,7 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = disposables.add(createTextModel('Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea b text_after_b', 'fooLang')); + let model = disposables.add(instantiateTextModel(instantiationService, 'Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea b text_after_b', 'fooLang')); let result = await provider.provideCompletionItems(model, new Position(1, 158), context)!; assert.strictEqual(result.suggestions.length, 1); @@ -403,7 +397,7 @@ suite('SnippetsService', function () { test('issue #61296: VS code freezes when editing CSS file with emoji', async function () { const languageConfigurationService = new TestLanguageConfigurationService(); - disposableStore.add(languageConfigurationService.register('fooLang', { + disposables.add(languageConfigurationService.register('fooLang', { wordPattern: /(#?-?\d*\.\d\w*%?)|(::?[\w-]*(?=[^,{;]*[,{]))|(([@#.!])?[\w-?]+%?|[@#!.])/g })); @@ -419,7 +413,7 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, languageConfigurationService); - let model = disposables.add(createTextModel('.🐷-a-b', 'fooLang')); + let model = disposables.add(instantiateTextModel(instantiationService, '.🐷-a-b', 'fooLang')); let result = await provider.provideCompletionItems(model, new Position(1, 8), context)!; assert.strictEqual(result.suggestions.length, 1); @@ -438,7 +432,7 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = disposables.add(createTextModel('a ', 'fooLang')); + let model = disposables.add(instantiateTextModel(instantiationService, 'a ', 'fooLang')); let result = await provider.provideCompletionItems(model, new Position(1, 3), context)!; assert.strictEqual(result.suggestions.length, 1); @@ -465,7 +459,7 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = createTextModel(' <', 'fooLang'); + let model = instantiateTextModel(instantiationService, ' <', 'fooLang'); let result = await provider.provideCompletionItems(model, new Position(1, 3), context)!; assert.strictEqual(result.suggestions.length, 1); @@ -473,7 +467,7 @@ suite('SnippetsService', function () { assert.strictEqual((first.range as any).insert.startColumn, 2); model.dispose(); - model = createTextModel('1', 'fooLang'); + model = instantiateTextModel(instantiationService, '1', 'fooLang'); result = await provider.provideCompletionItems(model, new Position(1, 2), context)!; assert.strictEqual(result.suggestions.length, 1); @@ -495,7 +489,7 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = createTextModel('not wordFoo bar', 'fooLang'); + let model = instantiateTextModel(instantiationService, 'not wordFoo bar', 'fooLang'); let result = await provider.provideCompletionItems(model, new Position(1, 3), context)!; assert.strictEqual(result.suggestions.length, 1); @@ -504,7 +498,7 @@ suite('SnippetsService', function () { assert.strictEqual((first.range as any).replace.endColumn, 9); model.dispose(); - model = createTextModel('not woFoo bar', 'fooLang'); + model = instantiateTextModel(instantiationService, 'not woFoo bar', 'fooLang'); result = await provider.provideCompletionItems(model, new Position(1, 3), context)!; assert.strictEqual(result.suggestions.length, 1); @@ -513,7 +507,7 @@ suite('SnippetsService', function () { assert.strictEqual((first.range as any).replace.endColumn, 3); model.dispose(); - model = createTextModel('not word', 'fooLang'); + model = instantiateTextModel(instantiationService, 'not word', 'fooLang'); result = await provider.provideCompletionItems(model, new Position(1, 1), context)!; assert.strictEqual(result.suggestions.length, 1); @@ -537,7 +531,7 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = createTextModel('filler e KEEP ng filler', 'fooLang'); + let model = instantiateTextModel(instantiationService, 'filler e KEEP ng filler', 'fooLang'); let result = await provider.provideCompletionItems(model, new Position(1, 9), context)!; assert.strictEqual(result.suggestions.length, 1); @@ -549,7 +543,7 @@ suite('SnippetsService', function () { test('Snippet will replace auto-closing pair if specified in prefix', async function () { const languageConfigurationService = new TestLanguageConfigurationService(); - disposableStore.add(languageConfigurationService.register('fooLang', { + disposables.add(languageConfigurationService.register('fooLang', { brackets: [ ['{', '}'], ['[', ']'], @@ -569,7 +563,7 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, languageConfigurationService); - let model = createTextModel('[psc]', 'fooLang'); + let model = instantiateTextModel(instantiationService, '[psc]', 'fooLang'); let result = await provider.provideCompletionItems(model, new Position(1, 5), context)!; assert.strictEqual(result.suggestions.length, 1); @@ -594,7 +588,7 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = createTextModel(' ci', 'fooLang'); + let model = instantiateTextModel(instantiationService, ' ci', 'fooLang'); let result = await provider.provideCompletionItems(model, new Position(1, 4), context)!; assert.strictEqual(result.suggestions.length, 1); @@ -615,7 +609,7 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = createTextModel('\'\'', 'fooLang'); + let model = instantiateTextModel(instantiationService, '\'\'', 'fooLang'); let result = await provider.provideCompletionItems( model, new Position(1, 2), @@ -637,7 +631,7 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = createTextModel('\'\'', 'fooLang'); + let model = instantiateTextModel(instantiationService, '\'\'', 'fooLang'); let result = await provider.provideCompletionItems( model, @@ -657,7 +651,7 @@ suite('SnippetsService', function () { ]); const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = createTextModel('\'hellot\'', 'fooLang'); + let model = instantiateTextModel(instantiationService, '\'hellot\'', 'fooLang'); let result = await provider.provideCompletionItems( model, @@ -678,7 +672,7 @@ suite('SnippetsService', function () { ]); const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = createTextModel(')*&^', 'fooLang'); + let model = instantiateTextModel(instantiationService, ')*&^', 'fooLang'); let result = await provider.provideCompletionItems( model, @@ -698,7 +692,7 @@ suite('SnippetsService', function () { ]); const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = createTextModel('foobar', 'fooLang'); + let model = instantiateTextModel(instantiationService, 'foobar', 'fooLang'); let result = await provider.provideCompletionItems( model, @@ -718,7 +712,7 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = createTextModel('function abc(w)', 'fooLang'); + let model = instantiateTextModel(instantiationService, 'function abc(w)', 'fooLang'); let result = await provider.provideCompletionItems( model, new Position(1, 15), @@ -737,7 +731,7 @@ suite('SnippetsService', function () { ]); const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = createTextModel('di', 'fooLang'); + let model = instantiateTextModel(instantiationService, 'di', 'fooLang'); let result = await provider.provideCompletionItems( model, new Position(1, 3), diff --git a/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTags.ts b/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTags.ts index 395145ee8b4..8e263a87401 100644 --- a/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTags.ts +++ b/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTags.ts @@ -36,7 +36,7 @@ export class WorkspaceTags implements IWorkbenchContribution { @IProductService private readonly productService: IProductService, @INativeHostService private readonly nativeHostService: INativeHostService ) { - if (this.telemetryService.telemetryLevel === TelemetryLevel.USAGE) { + if (this.telemetryService.telemetryLevel.value === TelemetryLevel.USAGE) { this.report(); } } diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index 70c269e4a69..5c133362dcb 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -16,6 +16,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import * as Types from 'vs/base/common/types'; import { TerminateResponseCode } from 'vs/base/common/processes'; import { ValidationStatus, ValidationState } from 'vs/base/common/parsers'; +import * as glob from 'vs/base/common/glob'; import * as UUID from 'vs/base/common/uuid'; import * as Platform from 'vs/base/common/platform'; import { LRUCache, Touch } from 'vs/base/common/map'; @@ -41,7 +42,7 @@ import { IConfigurationResolverService } from 'vs/workbench/services/configurati import { IWorkspaceContextService, WorkbenchState, IWorkspaceFolder, IWorkspace, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { IOutputService, IOutputChannel } from 'vs/workbench/contrib/output/common/output'; +import { IOutputService, IOutputChannel } from 'vs/workbench/services/output/common/output'; import { ITerminalGroupService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { ITerminalProfileResolverService } from 'vs/workbench/contrib/terminal/common/terminal'; @@ -69,7 +70,7 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/ import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { toFormattedString } from 'vs/base/common/jsonFormatter'; import { ITextModelService, IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService'; -import { SaveReason } from 'vs/workbench/common/editor'; +import { EditorResourceAccessor, SaveReason } from 'vs/workbench/common/editor'; import { ITextEditorSelection, TextEditorSelectionRevealType } from 'vs/platform/editor/common/editor'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; @@ -2753,11 +2754,21 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer }); } - private splitPerGroupType(tasks: Task[]): { none: Task[]; defaults: Task[] } { + /** + * + * @param tasks - The tasks which need filtering from defaults and non-defaults + * @param defaultType - If there are globs want globs in the default list, otherwise only tasks with true + * @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) { - if ((task.configurationProperties.group as TaskGroup).isDefault) { + // 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); + } else if (!taskGlobsInList && (task.configurationProperties.group as TaskGroup).isDefault === true) { defaults.push(task); } else { none.push(task); @@ -2766,54 +2777,38 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return { none, defaults }; } - private runBuildCommand(): void { + private runTaskGroupCommand(taskGroup: TaskGroup, strings: { + fetching: string; + select: string; + notFoundConfigure: string; + }, configure: () => void, legacyCommand: () => void): void { if (!this.canRunCommand()) { return; } if (this.schemaVersion === JsonSchemaVersion.V0_1_0) { - this.build(); + legacyCommand(); return; } let options: IProgressOptions = { location: ProgressLocation.Window, - title: nls.localize('TaskService.fetchingBuildTasks', 'Fetching build tasks...') + title: strings.fetching }; let promise = (async () => { - const buildTasks = await this._findWorkspaceTasksInGroup(TaskGroup.Build, false); - async function runSingleBuildTask(task: Task | undefined, problemMatcherOptions: ProblemMatcherRunOptions | undefined, that: AbstractTaskService) { + let taskGroupTasks: (Task | ConfiguringTask)[] = []; + + async function runSingleTask(task: Task | undefined, problemMatcherOptions: ProblemMatcherRunOptions | 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 }); } - if (buildTasks.length === 1) { - const buildTask = buildTasks[0]; - if (ConfiguringTask.is(buildTask)) { - return this.tryResolveTask(buildTask).then(resolvedTask => { - runSingleBuildTask(resolvedTask, undefined, this); - }); - } else { - runSingleBuildTask(buildTask, undefined, this); - return; - } - } - - return this.getTasksForGroup(TaskGroup.Build).then((tasks) => { - if (tasks.length > 0) { - let { none, defaults } = this.splitPerGroupType(tasks); - if (defaults.length === 1) { - runSingleBuildTask(defaults[0], undefined, this); - return; - } else if (defaults.length + none.length > 0) { - tasks = defaults.concat(none); - } - } + const chooseAndRunTask = (tasks: Task[]) => { this.showIgnoredFoldersMessage().then(() => { this.showQuickPick(tasks, - nls.localize('TaskService.pickBuildTask', 'Select the build task to run'), + strings.select, { - label: nls.localize('TaskService.noBuildTask', 'No build task to run found. Configure Build Task...'), + label: strings.notFoundConfigure, task: null }, true).then((entry) => { @@ -2822,66 +2817,104 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return; } if (task === null) { - this.runConfigureDefaultBuildTask(); + configure(); return; } - runSingleBuildTask(task, { attachProblemMatcher: true }, this); + runSingleTask(task, { attachProblemMatcher: true }, this); }); }); - }); - })(); - this.progressService.withProgress(options, () => promise); - } + }; - private runTestCommand(): void { - if (!this.canRunCommand()) { - return; - } - if (this.schemaVersion === JsonSchemaVersion.V0_1_0) { - this.runTest(); - return; - } - let options: IProgressOptions = { - location: ProgressLocation.Window, - title: nls.localize('TaskService.fetchingTestTasks', 'Fetching test tasks...') - }; - let promise = this.getTasksForGroup(TaskGroup.Test).then((tasks) => { - if (tasks.length > 0) { - let { none, defaults } = this.splitPerGroupType(tasks); - if (defaults.length === 1) { - this.run(defaults[0], undefined, TaskRunSource.User).then(undefined, reason => { - // eat the error, it has already been surfaced to the user and we don't care about it here - }); - return; - } else if (defaults.length + none.length > 0) { - tasks = defaults.concat(none); - } - } - this.showIgnoredFoldersMessage().then(() => { - this.showQuickPick(tasks, - nls.localize('TaskService.pickTestTask', 'Select the test task to run'), - { - label: nls.localize('TaskService.noTestTaskTerminal', 'No test task to run found. Configure Tasks...'), - task: null - }, true - ).then((entry) => { - let task: Task | undefined | null = entry ? entry.task : undefined; - if (task === undefined) { - return; + // First check for globs before checking for the default tasks of the task group + const absoluteURI = EditorResourceAccessor.getOriginalUri(this.editorService.activeEditor); + if (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; + + taskGroupTasks = await this._findWorkspaceTasks((task) => { + const taskGroup = task.configurationProperties.group; + if (taskGroup && typeof taskGroup !== 'string' && typeof taskGroup.isDefault === 'string') { + return (taskGroup._id === taskGroup._id && glob.match(taskGroup.isDefault, relativePath)); } - if (task === null) { - this.runConfigureTasks(); - return; + + return false; + }); + } + + const handleMultipleTasks = (areGlobTasks: boolean) => { + 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); + if (defaults.length === 1) { + runSingleTask(defaults[0], undefined, this); + return; + } else if (defaults.length + none.length > 0) { + tasks = defaults.concat(none); + } } - this.run(task, undefined, TaskRunSource.User).then(undefined, reason => { - // eat the error, it has already been surfaced to the user and we don't care about it here - }); + + // At this this point there are multiple tasks. + chooseAndRunTask(tasks); }); - }); - }); + }; + + const resolveTaskAndRun = (taskGroupTask: Task | ConfiguringTask) => { + if (ConfiguringTask.is(taskGroupTask)) { + this.tryResolveTask(taskGroupTask).then(resolvedTask => { + runSingleTask(resolvedTask, undefined, this); + }); + } else { + runSingleTask(taskGroupTask, undefined, this); + } + }; + + // A single default glob task was returned, just run it directly + if (taskGroupTasks.length === 1) { + return resolveTaskAndRun(taskGroupTasks[0]); + } + + // If there's multiple globs that match we want to show the quick picker for those tasks + // We will need to call splitPerGroupType putting globs in defaults and the remaining tasks in none. + // We don't need to carry on after here + if (taskGroupTasks.length > 1) { + return handleMultipleTasks(true); + } + + // If no globs are found or matched fallback to checking for default tasks of the task group + if (!taskGroupTasks.length) { + taskGroupTasks = await this._findWorkspaceTasksInGroup(taskGroup, false); + } + + // A single default task was returned, just run it directly + if (taskGroupTasks.length === 1) { + return resolveTaskAndRun(taskGroupTasks[0]); + } + + // Multiple default tasks returned, show the quickPicker + return handleMultipleTasks(false); + })(); this.progressService.withProgress(options, () => promise); } + 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); + } + + 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); + } + private runTerminateCommand(arg?: any): void { if (!this.canRunCommand()) { return; diff --git a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts index c0547a2ae39..992ecb05aec 100644 --- a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts +++ b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts @@ -29,7 +29,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { ITerminalProfileResolverService, TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal'; import { ITerminalService, ITerminalInstance, ITerminalGroupService } from 'vs/workbench/contrib/terminal/browser/terminal'; -import { IOutputService } from 'vs/workbench/contrib/output/common/output'; +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, diff --git a/src/vs/workbench/contrib/tasks/common/jsonSchema_v2.ts b/src/vs/workbench/contrib/tasks/common/jsonSchema_v2.ts index c3d81ff6df0..f0ba5e9fa63 100644 --- a/src/vs/workbench/contrib/tasks/common/jsonSchema_v2.ts +++ b/src/vs/workbench/contrib/tasks/common/jsonSchema_v2.ts @@ -192,9 +192,9 @@ const group: IJSONSchema = { properties: { kind: groupStrings, isDefault: { - type: 'boolean', + type: ['boolean', 'string'], default: false, - description: nls.localize('JsonSchema.tasks.group.isDefault', 'Defines if this task is the default task in the group.') + description: nls.localize('JsonSchema.tasks.group.isDefault', 'Defines if this task is the default task in the group, or a glob to match the file which should trigger this task.') } } }, diff --git a/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts b/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts index 7ab00229e89..9c6f0ba9e34 100644 --- a/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts +++ b/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts @@ -282,7 +282,7 @@ export interface CommandProperties extends BaseCommandProperties { export interface GroupKind { kind?: string; - isDefault?: boolean; + isDefault?: boolean | string; } export interface ConfigurationProperties { @@ -1245,7 +1245,7 @@ export namespace GroupKind { return { _id: external, isDefault: false }; } else if (Types.isString(external.kind) && Tasks.TaskGroup.is(external.kind)) { let group: string = external.kind; - let isDefault: boolean = !!external.isDefault; + let isDefault: boolean | string = Types.isUndefined(external.isDefault) ? false : external.isDefault; return { _id: group, isDefault }; } @@ -1260,7 +1260,7 @@ export namespace GroupKind { } return { kind: group._id, - isDefault: group.isDefault + isDefault: group.isDefault, }; } } diff --git a/src/vs/workbench/contrib/tasks/common/tasks.ts b/src/vs/workbench/contrib/tasks/common/tasks.ts index cb475cae0ee..1e7e42b6c81 100644 --- a/src/vs/workbench/contrib/tasks/common/tasks.ts +++ b/src/vs/workbench/contrib/tasks/common/tasks.ts @@ -393,7 +393,7 @@ export namespace TaskGroup { export interface TaskGroup { _id: string; - isDefault?: boolean; + isDefault?: boolean | string; } export const enum TaskScope { diff --git a/src/vs/workbench/contrib/tasks/electron-sandbox/taskService.ts b/src/vs/workbench/contrib/tasks/electron-sandbox/taskService.ts index 436f0194e19..a17d4185861 100644 --- a/src/vs/workbench/contrib/tasks/electron-sandbox/taskService.ts +++ b/src/vs/workbench/contrib/tasks/electron-sandbox/taskService.ts @@ -30,7 +30,7 @@ import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IViewsService, IViewDescriptorService } from 'vs/workbench/common/views'; -import { IOutputService } from 'vs/workbench/contrib/output/common/output'; +import { IOutputService } from 'vs/workbench/services/output/common/output'; import { ITerminalGroupService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; 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 5b7471bb284..0c5d62458d5 100755 --- a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh +++ b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh @@ -10,6 +10,10 @@ if [ -z "$VSCODE_SHELL_LOGIN" ]; then 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 @@ -21,48 +25,48 @@ else fi if [[ "$PROMPT_COMMAND" =~ .*(' '.*\;)|(\;.*' ').* ]]; then - echo -e "\033[1;33mShell integration cannot be activated due to complex PROMPT_COMMAND: $PROMPT_COMMAND\033[0m" + builtin echo -e "\033[1;33mShell integration cannot be activated due to complex PROMPT_COMMAND: $PROMPT_COMMAND\033[0m" VSCODE_SHELL_HIDE_WELCOME="" - return; + builtin return fi if [ -z "$VSCODE_SHELL_INTEGRATION" ]; then - return + builtin return fi __vsc_in_command_execution="1" __vsc_last_history_id=$(history 1 | awk '{print $1;}') __vsc_prompt_start() { - printf "\033]633;A\007" + builtin printf "\033]633;A\007" } __vsc_prompt_end() { - printf "\033]633;B\007" + builtin printf "\033]633;B\007" } __vsc_update_cwd() { - printf "\033]633;P;Cwd=%s\007" "$PWD" + builtin printf "\033]633;P;Cwd=%s\007" "$PWD" } __vsc_command_output_start() { - printf "\033]633;C\007" + builtin printf "\033]633;C\007" } __vsc_continuation_start() { - printf "\033]633;F\007" + builtin printf "\033]633;F\007" } __vsc_continuation_end() { - printf "\033]633;G\007" + builtin printf "\033]633;G\007" } __vsc_command_complete() { - local __vsc_history_id=$(history 1 | awk '{print $1;}') + local __vsc_history_id=$(builtin history 1 | awk '{print $1;}') if [[ "$__vsc_history_id" == "$__vsc_last_history_id" ]]; then - printf "\033]633;D\007" + builtin printf "\033]633;D\007" else - printf "\033]633;D;%s\007" "$__vsc_status" + builtin printf "\033]633;D;%s\007" "$__vsc_status" __vsc_last_history_id=$__vsc_history_id fi __vsc_update_cwd @@ -95,17 +99,26 @@ __vsc_preexec() { __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 IFS=' ' fi - read -ra ADDR <<<"$__vsc_original_prompt_command" + builtin read -ra ADDR <<<"$__vsc_original_prompt_command" + if [[ ${__vsc_original_ifs+set} ]]; then + IFS="$__vsc_original_ifs" + unset __vsc_original_ifs + else + unset IFS + fi for ((i = 0; i < ${#ADDR[@]}; i++)); do - eval ${ADDR[i]} + # unset IFS + builtin eval ${ADDR[i]} done - IFS='' __vsc_precmd } @@ -130,7 +143,7 @@ fi trap '__vsc_preexec' DEBUG if [ -z "$VSCODE_SHELL_HIDE_WELCOME" ]; then - echo -e "\033[1;32mShell integration activated\033[0m" + builtin echo -e "\033[1;32mShell integration activated\033[0m" else VSCODE_SHELL_HIDE_WELCOME="" fi diff --git a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-env.zsh b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-env.zsh new file mode 100644 index 00000000000..f676a0308bd --- /dev/null +++ b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-env.zsh @@ -0,0 +1,8 @@ +# --------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# --------------------------------------------------------------------------------------------- + +if [[ -f ~/.zshenv ]]; then + . ~/.zshenv +fi diff --git a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-profile.zsh b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-profile.zsh new file mode 100644 index 00000000000..734bb831e11 --- /dev/null +++ b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-profile.zsh @@ -0,0 +1,8 @@ +# --------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# --------------------------------------------------------------------------------------------- + +if [[ $options[norcs] = off && -o "login" && -f ~/.zprofile ]]; then + . ~/.zprofile +fi diff --git a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.zsh b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.zsh index be84dc9674f..2dec005daf4 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.zsh +++ b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.zsh @@ -2,73 +2,68 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. # --------------------------------------------------------------------------------------------- -autoload -Uz add-zsh-hook +builtin autoload -Uz add-zsh-hook # Now that the init script is running, unset ZDOTDIR to ensure ~/.zlogout runs as expected as well # as prevent problems that may occur if the user's init scripts depend on ZDOTDIR not being set. -unset ZDOTDIR +builtin unset ZDOTDIR # 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 [ -f ~/.zshenv ]; then - . ~/.zshenv -fi -if [[ -o "login" && -f ~/.zprofile ]]; then - . ~/.zprofile -fi -if [ -f ~/.zshrc ]; then + +if [[ $options[norcs] = off && -f ~/.zshrc ]]; then . ~/.zshrc fi # Shell integration was disabled by the shell, exit without warning assuming either the shell has # explicitly disabled shell integration as it's incompatible or it implements the protocol. if [ -z "$VSCODE_SHELL_INTEGRATION" ]; then - return + builtin return fi __vsc_in_command_execution="1" __vsc_last_history_id=0 __vsc_prompt_start() { - printf "\033]633;A\007" + builtin printf "\033]633;A\007" } __vsc_prompt_end() { - printf "\033]633;B\007" + builtin printf "\033]633;B\007" } __vsc_update_cwd() { - printf "\033]633;P;Cwd=%s\007" "$PWD" + builtin printf "\033]633;P;Cwd=%s\007" "$PWD" } __vsc_command_output_start() { - printf "\033]633;C\007" + builtin printf "\033]633;C\007" } __vsc_continuation_start() { - printf "\033]633;F\007" + builtin printf "\033]633;F\007" } __vsc_continuation_end() { - printf "\033]633;G\007" + builtin printf "\033]633;G\007" } __vsc_right_prompt_start() { - printf "\033]633;H\007" + builtin printf "\033]633;H\007" } __vsc_right_prompt_end() { - printf "\033]633;I\007" + builtin printf "\033]633;I\007" } __vsc_command_complete() { - local __vsc_history_id=$(history | tail -n1 | awk '{print $1;}') + builtin local __vsc_history_id=$(builtin history | tail -n1 | awk '{print $1;}') if [[ "$__vsc_history_id" == "$__vsc_last_history_id" ]]; then - printf "\033]633;D\007" + builtin printf "\033]633;D\007" else - printf "\033]633;D;%s\007" "$__vsc_status" + builtin printf "\033]633;D;%s\007" "$__vsc_status" __vsc_last_history_id=$__vsc_history_id fi __vsc_update_cwd @@ -114,7 +109,7 @@ add-zsh-hook preexec __vsc_preexec # Show the welcome message if [ -z "${VSCODE_SHELL_HIDE_WELCOME-}" ]; then - echo "\033[1;32mShell integration activated\033[0m" + builtin echo "\033[1;32mShell integration activated\033[0m" else VSCODE_SHELL_HIDE_WELCOME="" fi diff --git a/src/vs/workbench/contrib/terminal/browser/terminalFindWidget.ts b/src/vs/workbench/contrib/terminal/browser/terminalFindWidget.ts index 6168afff93b..4ba93e51fb3 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalFindWidget.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalFindWidget.ts @@ -10,6 +10,9 @@ import { FindReplaceState } from 'vs/editor/contrib/find/browser/findState'; import { ITerminalGroupService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; import { TerminalLocation } from 'vs/platform/terminal/common/terminal'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; export class TerminalFindWidget extends SimpleFindWidget { protected _findInputFocused: IContextKey<boolean>; @@ -19,11 +22,14 @@ export class TerminalFindWidget extends SimpleFindWidget { constructor( findState: FindReplaceState, @IContextViewService _contextViewService: IContextViewService, + @IKeybindingService keybindingService: IKeybindingService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @ITerminalService private readonly _terminalService: ITerminalService, - @ITerminalGroupService private readonly _terminalGroupService: ITerminalGroupService + @ITerminalGroupService private readonly _terminalGroupService: ITerminalGroupService, + @IThemeService private readonly _themeService: IThemeService, + @IConfigurationService private readonly _configurationService: IConfigurationService ) { - super(_contextViewService, _contextKeyService, findState, { showOptionButtons: true, showResultCount: true }); + super(findState, { showOptionButtons: true, showResultCount: true, type: 'Terminal' }, _contextViewService, _contextKeyService, keybindingService); this._register(findState.onFindReplaceStateChange(() => { this.show(); @@ -31,15 +37,25 @@ export class TerminalFindWidget extends SimpleFindWidget { this._findInputFocused = TerminalContextKeys.findInputFocus.bindTo(this._contextKeyService); this._findWidgetFocused = TerminalContextKeys.findFocus.bindTo(this._contextKeyService); this._findWidgetVisible = TerminalContextKeys.findVisible.bindTo(_contextKeyService); + this._register(this._themeService.onDidColorThemeChange(() => { + if (this._findWidgetVisible) { + this.find(true, true); + } + })); + this._register(this._configurationService.onDidChangeConfiguration((e) => { + if (e.affectsConfiguration('workbench.colorCustomizations') && this._findWidgetVisible) { + this.find(true, true); + } + })); } - find(previous: boolean) { + find(previous: boolean, update?: boolean) { const instance = this._terminalService.activeInstance; if (!instance) { return; } if (previous) { - instance.xterm?.findPrevious(this.inputValue, { regex: this._getRegexValue(), wholeWord: this._getWholeWordValue(), caseSensitive: this._getCaseSensitiveValue() }); + instance.xterm?.findPrevious(this.inputValue, { regex: this._getRegexValue(), wholeWord: this._getWholeWordValue(), caseSensitive: this._getCaseSensitiveValue(), incremental: update }); } else { instance.xterm?.findNext(this.inputValue, { regex: this._getRegexValue(), wholeWord: this._getWholeWordValue(), caseSensitive: this._getCaseSensitiveValue() }); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 18be1c0ae4a..9414adfc86a 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -460,6 +460,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // Re-establish the title after reconnect if (this.shellLaunchConfig.attachPersistentProcess) { this.refreshTabLabels(this.shellLaunchConfig.attachPersistentProcess.title, this.shellLaunchConfig.attachPersistentProcess.titleSource); + this.setShellType(this.shellType); } if (this._fixedCols) { @@ -1096,21 +1097,9 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { setTimeout(() => this._refreshSelectionContextKey(), 0); })); - this._register(dom.addDisposableListener(xterm.raw.textarea, 'focus', () => { - this._terminalFocusContextKey.set(true); - if (this.shellType) { - this._terminalShellTypeContextKey.set(this.shellType.toString()); - } else { - this._terminalShellTypeContextKey.reset(); - } - this._onDidFocus.fire(this); - })); - - this._register(dom.addDisposableListener(xterm.raw.textarea, 'blur', () => { - this._terminalFocusContextKey.reset(); - this._onDidBlur.fire(this); - this._refreshSelectionContextKey(); - })); + this._register(dom.addDisposableListener(xterm.raw.textarea, 'focus', () => this._setFocus(true))); + 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); @@ -1136,6 +1125,17 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } } + private _setFocus(focused?: boolean): void { + if (focused) { + this._terminalFocusContextKey.set(true); + this._onDidFocus.fire(this); + } else { + this._terminalFocusContextKey.reset(); + this._onDidBlur.fire(this); + this._refreshSelectionContextKey(); + } + } + private _initDragAndDrop(container: HTMLElement) { this._dndObserver?.dispose(); const dndController = this._instantiationService.createInstance(TerminalInstanceDragAndDropController, container); @@ -1157,6 +1157,9 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { if (asHtml) { const textAsHtml = await xterm.getSelectionAsHtml(command); function listener(e: any) { + if (!e.clipboardData.types.includes('text/plain')) { + e.clipboardData.setData('text/plain', command?.getOutput() ?? ''); + } e.clipboardData.setData('text/html', textAsHtml); e.preventDefault(); } @@ -1882,6 +1885,9 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { setShellType(shellType: TerminalShellType) { this._shellType = shellType; + if (shellType) { + this._terminalShellTypeContextKey.set(shellType?.toString()); + } } private _setAriaLabel(xterm: XTermTerminal | undefined, terminalId: number, title: string | undefined): void { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts index 5e749dcac5b..6c09333437c 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts @@ -388,7 +388,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce baseEnv = await this._terminalProfileResolverService.getEnvironment(this.remoteAuthority); } - const env = terminalEnvironment.createTerminalEnvironment(shellLaunchConfig, envFromConfigValue, variableResolver, this._productService.version, this._configHelper.config.detectLocale, baseEnv); + 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))); @@ -398,7 +398,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce // info widget. While technically these could differ due to the slight change of a race // condition, the chance is minimal plus the impact on the user is also not that great // if it happens - it's not worth adding plumbing to sync back the resolved collection. - this._extEnvironmentVariableCollection.applyToProcessEnvironment(env, variableResolver); + await this._extEnvironmentVariableCollection.applyToProcessEnvironment(env, variableResolver); if (this._extEnvironmentVariableCollection.map.size > 0) { this.environmentVariableInfo = new EnvironmentVariableInfoChangesActive(this._extEnvironmentVariableCollection); this._onEnvironmentVariableInfoChange.fire(this.environmentVariableInfo); @@ -423,7 +423,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce const activeWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot(Schemas.file); - const initialCwd = terminalEnvironment.getCwd( + const initialCwd = await terminalEnvironment.getCwd( shellLaunchConfig, userHome, variableResolver, diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts b/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts index 5885b71b3a3..0849f6676b2 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts @@ -355,25 +355,23 @@ export abstract class BaseTerminalProfileResolverService implements ITerminalPro const env = await this._context.getEnvironment(options.remoteAuthority); const activeWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot(options.remoteAuthority ? Schemas.vscodeRemote : Schemas.file); const lastActiveWorkspace = activeWorkspaceRootUri ? withNullAsUndefined(this._workspaceContextService.getWorkspaceFolder(activeWorkspaceRootUri)) : undefined; - profile.path = this._resolveVariables(profile.path, env, lastActiveWorkspace); + profile.path = await this._resolveVariables(profile.path, env, lastActiveWorkspace); // Resolve args variables if (profile.args) { if (typeof profile.args === 'string') { - profile.args = this._resolveVariables(profile.args, env, lastActiveWorkspace); + profile.args = await this._resolveVariables(profile.args, env, lastActiveWorkspace); } else { - for (let i = 0; i < profile.args.length; i++) { - profile.args[i] = this._resolveVariables(profile.args[i], env, lastActiveWorkspace); - } + profile.args = await Promise.all(profile.args.map(arg => this._resolveVariables(arg, env, lastActiveWorkspace))); } } return profile; } - private _resolveVariables(value: string, env: IProcessEnvironment, lastActiveWorkspace: IWorkspaceFolder | undefined) { + private async _resolveVariables(value: string, env: IProcessEnvironment, lastActiveWorkspace: IWorkspaceFolder | undefined) { try { - value = this._configurationResolverService.resolveWithEnvironment(env, lastActiveWorkspace, value); + value = await this._configurationResolverService.resolveWithEnvironment(env, lastActiveWorkspace, value); } catch (e) { this._logService.error(`Could not resolve shell`, e); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index 8402e0e5362..62d52e82cc9 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -52,6 +52,7 @@ export class TerminalService implements ITerminalService { private _hostActiveTerminals: Map<ITerminalInstanceHost, ITerminalInstance | undefined> = new Map(); private _terminalEditorActive: IContextKey<boolean>; + private readonly _terminalShellTypeContextKey: IContextKey<string>; private _escapeSequenceLoggingEnabled: boolean = false; @@ -187,9 +188,15 @@ export class TerminalService implements ITerminalService { if (!instance && !this._isShuttingDown) { this._terminalGroupService.hidePanel(); } + if (instance?.shellType) { + this._terminalShellTypeContextKey.set(instance.shellType.toString()); + } else if (!instance) { + this._terminalShellTypeContextKey.reset(); + } }); this._handleInstanceContextKeys(); + this._terminalShellTypeContextKey = TerminalContextKeys.shellType.bindTo(this._contextKeyService); this._processSupportContextKey = TerminalContextKeys.processSupported.bindTo(this._contextKeyService); this._processSupportContextKey.set(!isWeb || this._remoteAgentService.getConnection() !== null); this._terminalHasBeenCreated = TerminalContextKeys.terminalHasBeenCreated.bindTo(this._contextKeyService); diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/commandNavigationAddon.ts b/src/vs/workbench/contrib/terminal/browser/xterm/commandNavigationAddon.ts index c7b911c633d..60d82f8436d 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/commandNavigationAddon.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/commandNavigationAddon.ts @@ -34,6 +34,9 @@ export class CommandNavigationAddon extends Disposable implements ICommandTracke activate(terminal: Terminal): void { this._terminal = terminal; + this._terminal.onData(() => { + this._currentMarker = Boundary.Bottom; + }); } constructor( diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts b/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts index 40b75ff1352..3f07b624fa7 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts @@ -73,6 +73,8 @@ export class DecorationAddon extends Disposable implements ITerminalAddon { this._refreshStyles(); } else if (e.affectsConfiguration(TerminalSettingId.FontSize) || e.affectsConfiguration(TerminalSettingId.LineHeight)) { this.refreshLayouts(); + } else if (e.affectsConfiguration('workbench.colorCustomizations')) { + this._refreshStyles(true); } }); this._themeService.onDidColorThemeChange(() => this._refreshStyles(true)); @@ -94,10 +96,10 @@ export class DecorationAddon extends Disposable implements ITerminalAddon { } else { color = ''; } - if (decoration.decoration.overviewRulerOptions) { - decoration.decoration.overviewRulerOptions.color = color; - } else { - decoration.decoration.overviewRulerOptions = { color }; + if (decoration.decoration.options?.overviewRulerOptions) { + decoration.decoration.options.overviewRulerOptions.color = color; + } else if (decoration.decoration.options) { + decoration.decoration.options.overviewRulerOptions = { color }; } } } diff --git a/src/vs/workbench/contrib/terminal/common/environmentVariable.ts b/src/vs/workbench/contrib/terminal/common/environmentVariable.ts index c307e138dd4..1f105bfaa94 100644 --- a/src/vs/workbench/contrib/terminal/common/environmentVariable.ts +++ b/src/vs/workbench/contrib/terminal/common/environmentVariable.ts @@ -7,6 +7,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { Event } from 'vs/base/common/event'; import { IProcessEnvironment } from 'vs/base/common/platform'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { VariableResolver } from 'vs/workbench/contrib/terminal/common/terminalEnvironment'; export const IEnvironmentVariableService = createDecorator<IEnvironmentVariableService>('environmentVariableService'); @@ -51,7 +52,7 @@ export interface IMergedEnvironmentVariableCollection { * @param variableResolver An optional function to use to resolve variables within the * environment values. */ - applyToProcessEnvironment(env: IProcessEnvironment, variableResolver?: (str: string) => string): void; + applyToProcessEnvironment(env: IProcessEnvironment, variableResolver?: VariableResolver): Promise<void>; /** * Generates a diff of this connection against another. Returns undefined if the collections are diff --git a/src/vs/workbench/contrib/terminal/common/environmentVariableCollection.ts b/src/vs/workbench/contrib/terminal/common/environmentVariableCollection.ts index a64ef609d29..e67f607c506 100644 --- a/src/vs/workbench/contrib/terminal/common/environmentVariableCollection.ts +++ b/src/vs/workbench/contrib/terminal/common/environmentVariableCollection.ts @@ -5,6 +5,7 @@ import { IProcessEnvironment, isWindows } from 'vs/base/common/platform'; import { EnvironmentVariableMutatorType, IEnvironmentVariableCollection, IExtensionOwnedEnvironmentVariableMutator, IMergedEnvironmentVariableCollection, IMergedEnvironmentVariableCollectionDiff } from 'vs/workbench/contrib/terminal/common/environmentVariable'; +import { VariableResolver } from 'vs/workbench/contrib/terminal/common/terminalEnvironment'; export class MergedEnvironmentVariableCollection implements IMergedEnvironmentVariableCollection { readonly map: Map<string, IExtensionOwnedEnvironmentVariableMutator[]> = new Map(); @@ -41,16 +42,16 @@ export class MergedEnvironmentVariableCollection implements IMergedEnvironmentVa }); } - applyToProcessEnvironment(env: IProcessEnvironment, variableResolver?: (str: string) => string): void { + async applyToProcessEnvironment(env: IProcessEnvironment, variableResolver?: VariableResolver): Promise<void> { let lowerToActualVariableNames: { [lowerKey: string]: string | undefined } | undefined; if (isWindows) { lowerToActualVariableNames = {}; Object.keys(env).forEach(e => lowerToActualVariableNames![e.toLowerCase()] = e); } - this.map.forEach((mutators, variable) => { + for (const [variable, mutators] of this.map) { const actualVariable = isWindows ? lowerToActualVariableNames![variable.toLowerCase()] || variable : variable; - mutators.forEach(mutator => { - const value = variableResolver ? variableResolver(mutator.value) : mutator.value; + for (const mutator of mutators) { + const value = variableResolver ? await variableResolver(mutator.value) : mutator.value; switch (mutator.type) { case EnvironmentVariableMutatorType.Append: env[actualVariable] = (env[actualVariable] || '') + value; @@ -62,8 +63,8 @@ export class MergedEnvironmentVariableCollection implements IMergedEnvironmentVa env[actualVariable] = value; break; } - }); - }); + } + } } diff(other: IMergedEnvironmentVariableCollection): IMergedEnvironmentVariableCollectionDiff | undefined { diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index 4020b3e6fa0..3ddae516bb0 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -186,7 +186,7 @@ const terminalConfiguration: IConfigurationNode = { default: DEFAULT_LINE_HEIGHT }, [TerminalSettingId.MinimumContrastRatio]: { - markdownDescription: localize('terminal.integrated.minimumContrastRatio', "When set the foreground color of each cell will change to try meet the contrast ratio specified. Example values:\n\n- 1: Do nothing and use the standard theme colors.\n- 4.5: [WCAG AA compliance (minimum)](https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html) (default).\n- 7: [WCAG AAA compliance (enhanced)](https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast7.html).\n- 21: White on black or black on white."), + markdownDescription: localize('terminal.integrated.minimumContrastRatio', "When set, the foreground color of each cell will change to try meet the contrast ratio specified. Note that this will not apply to `powerline` characters per #146406. Example values:\n\n- 1: Do nothing and use the standard theme colors.\n- 4.5: [WCAG AA compliance (minimum)](https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html) (default).\n- 7: [WCAG AAA compliance (enhanced)](https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast7.html).\n- 21: White on black or black on white."), type: 'number', default: 4.5 }, diff --git a/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts b/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts index b1922c5cd52..80041aeb869 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts @@ -78,17 +78,17 @@ function mergeNonNullKeys(env: IProcessEnvironment, other: ITerminalEnvironment } } -function resolveConfigurationVariables(variableResolver: VariableResolver, env: ITerminalEnvironment): ITerminalEnvironment { - Object.keys(env).forEach((key) => { - const value = env[key]; +async function resolveConfigurationVariables(variableResolver: VariableResolver, env: ITerminalEnvironment): Promise<ITerminalEnvironment> { + await Promise.all(Object.entries(env).map(async ([key, value]) => { if (typeof value === 'string') { try { - env[key] = variableResolver(value); + env[key] = await variableResolver(value); } catch (e) { env[key] = value; } } - }); + })); + return env; } @@ -179,17 +179,17 @@ export function getLangEnvVariable(locale?: string): string { return parts.join('_') + '.UTF-8'; } -export function getCwd( +export async function getCwd( shell: IShellLaunchConfig, userHome: string | undefined, variableResolver: VariableResolver | undefined, root: Uri | undefined, customCwd: string | undefined, logService?: ILogService -): string { +): Promise<string> { if (shell.cwd) { const unresolved = (typeof shell.cwd === 'object') ? shell.cwd.fsPath : shell.cwd; - const resolved = _resolveCwd(unresolved, variableResolver); + const resolved = await _resolveCwd(unresolved, variableResolver); return _sanitizeCwd(resolved || unresolved); } @@ -197,7 +197,7 @@ export function getCwd( if (!shell.ignoreConfigurationCwd && customCwd) { if (variableResolver) { - customCwd = _resolveCwd(customCwd, variableResolver, logService); + customCwd = await _resolveCwd(customCwd, variableResolver, logService); } if (customCwd) { if (path.isAbsolute(customCwd)) { @@ -216,10 +216,10 @@ export function getCwd( return _sanitizeCwd(cwd); } -function _resolveCwd(cwd: string, variableResolver: VariableResolver | undefined, logService?: ILogService): string | undefined { +async function _resolveCwd(cwd: string, variableResolver: VariableResolver | undefined, logService?: ILogService): Promise<string | undefined> { if (variableResolver) { try { - return variableResolver(cwd); + return await variableResolver(cwd); } catch (e) { logService?.error('Could not resolve terminal cwd', e); return undefined; @@ -251,7 +251,7 @@ export type TerminalShellArgsSetting = ( | TerminalSettingId.ShellArgsLinux ); -export type VariableResolver = (str: string) => string; +export type VariableResolver = (str: string) => Promise<string>; export function createVariableResolver(lastActiveWorkspace: IWorkspaceFolder | undefined, env: IProcessEnvironment, configurationResolverService: IConfigurationResolverService | undefined): VariableResolver | undefined { if (!configurationResolverService) { @@ -263,7 +263,7 @@ export function createVariableResolver(lastActiveWorkspace: IWorkspaceFolder | u /** * @deprecated Use ITerminalProfileResolverService */ -export function getDefaultShell( +export async function getDefaultShell( fetchSetting: (key: TerminalShellSetting) => string | undefined, defaultShell: string, isWoW64: boolean, @@ -272,7 +272,7 @@ export function getDefaultShell( logService: ILogService, useAutomationShell: boolean, platformOverride: Platform = platform -): string { +): Promise<string> { let maybeExecutable: string | undefined; if (useAutomationShell) { // If automationShell is specified, this should override the normal setting @@ -300,7 +300,7 @@ export function getDefaultShell( if (variableResolver) { try { - executable = variableResolver(executable); + executable = await variableResolver(executable); } catch (e) { logService.error(`Could not resolve shell`, e); } @@ -312,13 +312,13 @@ export function getDefaultShell( /** * @deprecated Use ITerminalProfileResolverService */ -export function getDefaultShellArgs( +export async function getDefaultShellArgs( fetchSetting: (key: TerminalShellSetting | TerminalShellArgsSetting) => string | string[] | undefined, useAutomationShell: boolean, variableResolver: VariableResolver | undefined, logService: ILogService, platformOverride: Platform = platform, -): string | string[] { +): Promise<string | string[]> { if (useAutomationShell) { if (!!getShellSetting(fetchSetting, 'automationShell', platformOverride)) { return []; @@ -331,13 +331,13 @@ export function getDefaultShellArgs( return []; } if (typeof args === 'string' && platformOverride === Platform.Windows) { - return variableResolver ? variableResolver(args) : args; + return variableResolver ? await variableResolver(args) : args; } if (variableResolver) { const resolvedArgs: string[] = []; for (const arg of args) { try { - resolvedArgs.push(variableResolver(arg)); + resolvedArgs.push(await variableResolver(arg)); } catch (e) { logService.error(`Could not resolve ${TerminalSettingPrefix.ShellArgs}${platformKey}`, e); resolvedArgs.push(arg); @@ -357,14 +357,14 @@ function getShellSetting( return fetchSetting(<TerminalShellSetting>`terminal.integrated.${type}.${platformKey}`); } -export function createTerminalEnvironment( +export async function createTerminalEnvironment( shellLaunchConfig: IShellLaunchConfig, envFromConfig: ITerminalEnvironment | undefined, variableResolver: VariableResolver | undefined, version: string | undefined, detectLocale: 'auto' | 'off' | 'on', baseEnv: IProcessEnvironment -): IProcessEnvironment { +): Promise<IProcessEnvironment> { // Create a terminal environment based on settings, launch config and permissions const env: IProcessEnvironment = {}; if (shellLaunchConfig.strictEnv) { @@ -379,10 +379,10 @@ export function createTerminalEnvironment( // Resolve env vars from config and shell if (variableResolver) { if (allowedEnvFromConfig) { - resolveConfigurationVariables(variableResolver, allowedEnvFromConfig); + await resolveConfigurationVariables(variableResolver, allowedEnvFromConfig); } if (shellLaunchConfig.env) { - resolveConfigurationVariables(variableResolver, shellLaunchConfig.env); + await resolveConfigurationVariables(variableResolver, shellLaunchConfig.env); } } diff --git a/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalBackend.ts b/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalBackend.ts index c38b20757e8..b2f01c3e853 100644 --- a/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalBackend.ts +++ b/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalBackend.ts @@ -250,9 +250,9 @@ class LocalTerminalBackend extends BaseTerminalBackend implements ITerminalBacke const platformKey = isWindows ? 'windows' : (isMacintosh ? 'osx' : 'linux'); const envFromConfigValue = this._configurationService.getValue<ITerminalEnvironment | undefined>(`terminal.integrated.env.${platformKey}`); const baseEnv = await (shellLaunchConfig.useShellEnvironment ? this.getShellEnvironment() : this.getEnvironment()); - const env = terminalEnvironment.createTerminalEnvironment(shellLaunchConfig, envFromConfigValue, variableResolver, this._productService.version, this._configurationService.getValue(TerminalSettingId.DetectLocale), baseEnv); + const env = await terminalEnvironment.createTerminalEnvironment(shellLaunchConfig, envFromConfigValue, variableResolver, this._productService.version, this._configurationService.getValue(TerminalSettingId.DetectLocale), baseEnv); if (!shellLaunchConfig.strictEnv && !shellLaunchConfig.hideFromUser) { - this._environmentVariableService.mergedCollection.applyToProcessEnvironment(env, variableResolver); + await this._environmentVariableService.mergedCollection.applyToProcessEnvironment(env, variableResolver); } return env; } diff --git a/src/vs/workbench/contrib/terminal/test/browser/terminalService.test.ts b/src/vs/workbench/contrib/terminal/test/browser/terminalService.test.ts index 9ab2dc7f95f..10ffee5b9d0 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/terminalService.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/terminalService.test.ts @@ -12,7 +12,7 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ContextKeyService } from 'vs/platform/contextkey/browser/contextKeyService'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { TestEditorService, TestLifecycleService, TestTerminalEditorService, TestTerminalGroupService, TestTerminalInstanceService, TestTerminalProfileService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestEditorService, TestLifecycleService, TestRemoteAgentService, TestTerminalEditorService, TestTerminalGroupService, TestTerminalInstanceService, TestTerminalProfileService } from 'vs/workbench/test/browser/workbenchTestServices'; import { ITerminalEditorService, ITerminalGroupService, ITerminalInstance, ITerminalInstanceService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -22,7 +22,6 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; -import { TestRemoteAgentService } from 'vs/workbench/services/remote/test/common/testServices'; suite('Workbench - TerminalService', () => { let instantiationService: TestInstantiationService; diff --git a/src/vs/workbench/contrib/terminal/test/common/environmentVariableCollection.test.ts b/src/vs/workbench/contrib/terminal/test/common/environmentVariableCollection.test.ts index 7b0b6594d4a..40c086a7dcc 100644 --- a/src/vs/workbench/contrib/terminal/test/common/environmentVariableCollection.test.ts +++ b/src/vs/workbench/contrib/terminal/test/common/environmentVariableCollection.test.ts @@ -78,7 +78,7 @@ suite('EnvironmentVariable - MergedEnvironmentVariableCollection', () => { }); suite('applyToProcessEnvironment', () => { - test('should apply the collection to an environment', () => { + test('should apply the collection to an environment', async () => { const merged = new MergedEnvironmentVariableCollection(new Map([ ['ext', { map: deserializeEnvironmentVariableCollection([ @@ -93,7 +93,7 @@ suite('EnvironmentVariable - MergedEnvironmentVariableCollection', () => { B: 'bar', C: 'baz' }; - merged.applyToProcessEnvironment(env); + await merged.applyToProcessEnvironment(env); deepStrictEqual(env, { A: 'a', B: 'barb', @@ -101,7 +101,7 @@ suite('EnvironmentVariable - MergedEnvironmentVariableCollection', () => { }); }); - test('should apply the collection to environment entries with no values', () => { + test('should apply the collection to environment entries with no values', async () => { const merged = new MergedEnvironmentVariableCollection(new Map([ ['ext', { map: deserializeEnvironmentVariableCollection([ @@ -112,7 +112,7 @@ suite('EnvironmentVariable - MergedEnvironmentVariableCollection', () => { }] ])); const env: IProcessEnvironment = {}; - merged.applyToProcessEnvironment(env); + await merged.applyToProcessEnvironment(env); deepStrictEqual(env, { A: 'a', B: 'b', @@ -120,7 +120,7 @@ suite('EnvironmentVariable - MergedEnvironmentVariableCollection', () => { }); }); - test('should apply to variable case insensitively on Windows only', () => { + test('should apply to variable case insensitively on Windows only', async () => { const merged = new MergedEnvironmentVariableCollection(new Map([ ['ext', { map: deserializeEnvironmentVariableCollection([ @@ -135,7 +135,7 @@ suite('EnvironmentVariable - MergedEnvironmentVariableCollection', () => { B: 'B', C: 'C' }; - merged.applyToProcessEnvironment(env); + await merged.applyToProcessEnvironment(env); if (isWindows) { deepStrictEqual(env, { A: 'a', diff --git a/src/vs/workbench/contrib/terminal/test/common/environmentVariableService.test.ts b/src/vs/workbench/contrib/terminal/test/common/environmentVariableService.test.ts index ed3269a6b83..dd391ea487d 100644 --- a/src/vs/workbench/contrib/terminal/test/common/environmentVariableService.test.ts +++ b/src/vs/workbench/contrib/terminal/test/common/environmentVariableService.test.ts @@ -87,7 +87,7 @@ suite('EnvironmentVariable - EnvironmentVariableService', () => { ]); }); - test('should correctly apply the environment values from multiple extension contributions in the correct order', () => { + test('should correctly apply the environment values from multiple extension contributions in the correct order', async () => { const collection1 = new Map<string, IEnvironmentVariableMutator>(); const collection2 = new Map<string, IEnvironmentVariableMutator>(); const collection3 = new Map<string, IEnvironmentVariableMutator>(); @@ -109,7 +109,7 @@ suite('EnvironmentVariable - EnvironmentVariableService', () => { // Verify the entries get applied to the environment as expected const env: IProcessEnvironment = { A: 'foo' }; - environmentVariableService.mergedCollection.applyToProcessEnvironment(env); + await environmentVariableService.mergedCollection.applyToProcessEnvironment(env); deepStrictEqual(env, { A: 'a2:a3:a1' }); }); }); diff --git a/src/vs/workbench/contrib/terminal/test/common/terminalEnvironment.test.ts b/src/vs/workbench/contrib/terminal/test/common/terminalEnvironment.test.ts index ca148531c12..b3918029cd0 100644 --- a/src/vs/workbench/contrib/terminal/test/common/terminalEnvironment.test.ts +++ b/src/vs/workbench/contrib/terminal/test/common/terminalEnvironment.test.ts @@ -180,66 +180,66 @@ suite('Workbench - TerminalEnvironment', () => { strictEqual(Uri.file(a).fsPath, Uri.file(b).fsPath); } - test('should default to userHome for an empty workspace', () => { - assertPathsMatch(getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, undefined, undefined), '/userHome/'); + test('should default to userHome for an empty workspace', async () => { + assertPathsMatch(await getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, undefined, undefined), '/userHome/'); }); - test('should use to the workspace if it exists', () => { - assertPathsMatch(getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, Uri.file('/foo'), undefined), '/foo'); + test('should use to the workspace if it exists', async () => { + assertPathsMatch(await getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, Uri.file('/foo'), undefined), '/foo'); }); - test('should use an absolute custom cwd as is', () => { - assertPathsMatch(getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, undefined, '/foo'), '/foo'); + test('should use an absolute custom cwd as is', async () => { + assertPathsMatch(await getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, undefined, '/foo'), '/foo'); }); - test('should normalize a relative custom cwd against the workspace path', () => { - assertPathsMatch(getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, Uri.file('/bar'), 'foo'), '/bar/foo'); - assertPathsMatch(getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, Uri.file('/bar'), './foo'), '/bar/foo'); - assertPathsMatch(getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, Uri.file('/bar'), '../foo'), '/foo'); + test('should normalize a relative custom cwd against the workspace path', async () => { + assertPathsMatch(await getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, Uri.file('/bar'), 'foo'), '/bar/foo'); + assertPathsMatch(await getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, Uri.file('/bar'), './foo'), '/bar/foo'); + assertPathsMatch(await getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, Uri.file('/bar'), '../foo'), '/foo'); }); - test('should fall back for relative a custom cwd that doesn\'t have a workspace', () => { - assertPathsMatch(getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, undefined, 'foo'), '/userHome/'); - assertPathsMatch(getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, undefined, './foo'), '/userHome/'); - assertPathsMatch(getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, undefined, '../foo'), '/userHome/'); + test('should fall back for relative a custom cwd that doesn\'t have a workspace', async () => { + assertPathsMatch(await getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, undefined, 'foo'), '/userHome/'); + assertPathsMatch(await getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, undefined, './foo'), '/userHome/'); + assertPathsMatch(await getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, undefined, '../foo'), '/userHome/'); }); - test('should ignore custom cwd when told to ignore', () => { - assertPathsMatch(getCwd({ executable: undefined, args: [], ignoreConfigurationCwd: true }, '/userHome/', undefined, Uri.file('/bar'), '/foo'), '/bar'); + test('should ignore custom cwd when told to ignore', async () => { + assertPathsMatch(await getCwd({ executable: undefined, args: [], ignoreConfigurationCwd: true }, '/userHome/', undefined, Uri.file('/bar'), '/foo'), '/bar'); }); }); suite('getDefaultShell', () => { - test('should change Sysnative to System32 in non-WoW64 systems', () => { - const shell = getDefaultShell(key => { + test('should change Sysnative to System32 in non-WoW64 systems', async () => { + const shell = await getDefaultShell(key => { return ({ 'terminal.integrated.shell.windows': 'C:\\Windows\\Sysnative\\cmd.exe' } as any)[key]; }, 'DEFAULT', false, 'C:\\Windows', undefined, {} as any, false, Platform.Windows); strictEqual(shell, 'C:\\Windows\\System32\\cmd.exe'); }); - test('should not change Sysnative to System32 in WoW64 systems', () => { - const shell = getDefaultShell(key => { + test('should not change Sysnative to System32 in WoW64 systems', async () => { + const shell = await getDefaultShell(key => { return ({ 'terminal.integrated.shell.windows': 'C:\\Windows\\Sysnative\\cmd.exe' } as any)[key]; }, 'DEFAULT', true, 'C:\\Windows', undefined, {} as any, false, Platform.Windows); strictEqual(shell, 'C:\\Windows\\Sysnative\\cmd.exe'); }); - test('should use automationShell when specified', () => { - const shell1 = getDefaultShell(key => { + test('should use automationShell when specified', async () => { + const shell1 = await getDefaultShell(key => { return ({ 'terminal.integrated.shell.windows': 'shell', 'terminal.integrated.automationShell.windows': undefined } as any)[key]; }, 'DEFAULT', false, 'C:\\Windows', undefined, {} as any, false, Platform.Windows); strictEqual(shell1, 'shell', 'automationShell was false'); - const shell2 = getDefaultShell(key => { + const shell2 = await getDefaultShell(key => { return ({ 'terminal.integrated.shell.windows': 'shell', 'terminal.integrated.automationShell.windows': undefined } as any)[key]; }, 'DEFAULT', false, 'C:\\Windows', undefined, {} as any, true, Platform.Windows); strictEqual(shell2, 'shell', 'automationShell was true'); - const shell3 = getDefaultShell(key => { + const shell3 = await getDefaultShell(key => { return ({ 'terminal.integrated.shell.windows': 'shell', 'terminal.integrated.automationShell.windows': 'automationShell' diff --git a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts index 9382f47bc67..db687ec08b6 100644 --- a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts +++ b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts @@ -111,6 +111,21 @@ export class UnhideTestAction extends Action2 { } } +export class UnhideAllTestsAction extends Action2 { + constructor() { + super({ + id: TestCommandId.UnhideAllTestsAction, + title: localize('unhideAllTests', 'Unhide All Tests'), + }); + } + + public override run(accessor: ServicesAccessor) { + const service = accessor.get(ITestService); + service.excluded.clear(); + return Promise.resolve(); + } +} + const testItemInlineAndInContext = (order: ActionOrder, when?: ContextKeyExpression) => [ { id: MenuId.TestItem, @@ -1190,4 +1205,5 @@ export const allTestActions = [ TestingViewAsTreeAction, ToggleInlineTestOutput, UnhideTestAction, + UnhideAllTestsAction, ]; diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts index c32661787e3..4d1af363aa7 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts @@ -65,6 +65,11 @@ export class TestingExplorerFilter extends BaseActionViewItem { const wrapper = this.wrapper = dom.$('.testing-filter-wrapper'); container.appendChild(wrapper); + const history = this.history.get([]); + if (history.length) { + this.state.setText(history[history.length - 1]); + } + const input = this.input = this._register(this.instantiationService.createInstance(ContextScopedSuggestEnabledInputWithHistory, { id: 'testing.explorer.filter', ariaLabel: localize('testExplorerFilterLabel', "Filter text for tests in the explorer"), @@ -89,7 +94,7 @@ export class TestingExplorerFilter extends BaseActionViewItem { value: this.state.text.value, placeholderText: localize('testExplorerFilter', "Filter (e.g. text, !exclude, @tag)"), }, - history: this.history.get([]) + history })); this._register(attachSuggestEnabledInputBoxStyler(input, this.themeService)); diff --git a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts index 71b7930768c..979dc41d729 100644 --- a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts +++ b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts @@ -25,7 +25,7 @@ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Lazy } from 'vs/base/common/lazy'; import { Disposable, DisposableStore, IDisposable, IReference, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { clamp } from 'vs/base/common/numbers'; -import { count } from 'vs/base/common/strings'; +import { count, removeAnsiEscapeCodes } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; import { ICodeEditor, IDiffEditorConstructionOptions, isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction2 } from 'vs/editor/browser/editorExtensions'; @@ -1162,7 +1162,7 @@ class TestMessageElement implements ITreeElement { public readonly taskIndex: number, public readonly messageIndex: number, ) { - const { message, location } = test.tasks[taskIndex].messages[messageIndex]; + const { type, message, location } = test.tasks[taskIndex].messages[messageIndex]; this.location = location; this.uri = this.context = buildTestUri({ @@ -1175,7 +1175,9 @@ class TestMessageElement implements ITreeElement { this.id = this.uri.toString(); - const asPlaintext = renderStringAsPlaintext(message); + const asPlaintext = type === TestMessageType.Output + ? removeAnsiEscapeCodes(message) + : renderStringAsPlaintext(message); const lines = count(asPlaintext.trimRight(), '\n'); this.label = firstLine(asPlaintext); if (lines > 0) { diff --git a/src/vs/workbench/contrib/testing/browser/theme.ts b/src/vs/workbench/contrib/testing/browser/theme.ts index dcd6e2bb567..2a8d95b8665 100644 --- a/src/vs/workbench/contrib/testing/browser/theme.ts +++ b/src/vs/workbench/contrib/testing/browser/theme.ts @@ -91,7 +91,7 @@ export const testMessageSeverityColors: { localize('testing.message.error.marginBackground', 'Margin color beside error messages shown inline in the editor.') ), }, - [TestMessageType.Info]: { + [TestMessageType.Output]: { decorationForeground: registerColor( 'testing.message.info.decorationForeground', { dark: transparent(editorForeground, 0.5), light: transparent(editorForeground, 0.5), hcDark: transparent(editorForeground, 0.5), hcLight: transparent(editorForeground, 0.5) }, diff --git a/src/vs/workbench/contrib/testing/common/constants.ts b/src/vs/workbench/contrib/testing/common/constants.ts index c8fc457be2d..224cbd48e90 100644 --- a/src/vs/workbench/contrib/testing/common/constants.ts +++ b/src/vs/workbench/contrib/testing/common/constants.ts @@ -89,4 +89,5 @@ export const enum TestCommandId { ToggleAutoRun = 'testing.toggleautoRun', ToggleInlineTestOutput = 'testing.toggleInlineTestOutput', UnhideTestAction = 'testing.unhideTest', + UnhideAllTestsAction = 'testing.unhideAllTests', } diff --git a/src/vs/workbench/contrib/testing/common/testResult.ts b/src/vs/workbench/contrib/testing/common/testResult.ts index dce023d7d60..368d43988d7 100644 --- a/src/vs/workbench/contrib/testing/common/testResult.ts +++ b/src/vs/workbench/contrib/testing/common/testResult.ts @@ -321,7 +321,7 @@ export class LiveTestResult implements ITestResult { location, message: output.toString(), offset: this.output.offset, - type: TestMessageType.Info, + type: TestMessageType.Output, }; const index = this.mustGetTaskIndex(taskId); diff --git a/src/vs/workbench/contrib/testing/common/testTypes.ts b/src/vs/workbench/contrib/testing/common/testTypes.ts index 6f3e639ef90..29ae1628071 100644 --- a/src/vs/workbench/contrib/testing/common/testTypes.ts +++ b/src/vs/workbench/contrib/testing/common/testTypes.ts @@ -117,7 +117,7 @@ export namespace IRichLocation { export const enum TestMessageType { Error, - Info + Output } export interface ITestErrorMessage { @@ -156,7 +156,7 @@ export namespace ITestErrorMessage { export interface ITestOutputMessage { message: string; - type: TestMessageType.Info; + type: TestMessageType.Output; offset: number; location: IRichLocation | undefined; } @@ -165,20 +165,20 @@ export namespace ITestOutputMessage { export interface Serialized { message: string; offset: number; - type: TestMessageType.Info; + type: TestMessageType.Output; location: IRichLocation.Serialize | undefined; } export const serialize = (message: ITestOutputMessage): Serialized => ({ message: message.message, - type: TestMessageType.Info, + type: TestMessageType.Output, offset: message.offset, location: message.location && IRichLocation.serialize(message.location), }); export const deserialize = (message: Serialized): ITestOutputMessage => ({ message: message.message, - type: TestMessageType.Info, + type: TestMessageType.Output, offset: message.offset, location: message.location && IRichLocation.deserialize(message.location), }); diff --git a/src/vs/workbench/contrib/testing/common/testingContentProvider.ts b/src/vs/workbench/contrib/testing/common/testingContentProvider.ts index c93c0968a07..4ce00913d35 100644 --- a/src/vs/workbench/contrib/testing/common/testingContentProvider.ts +++ b/src/vs/workbench/contrib/testing/common/testingContentProvider.ts @@ -12,6 +12,7 @@ import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { TestMessageType } from 'vs/workbench/contrib/testing/common/testTypes'; import { parseTestUri, TestUriType, TEST_DATA_SCHEME } from 'vs/workbench/contrib/testing/common/testingUri'; import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; +import { removeAnsiEscapeCodes } from 'vs/base/common/strings'; /** * A content provider that returns various outputs for tests. This is used @@ -61,12 +62,14 @@ export class TestingContentProvider implements IWorkbenchContribution, ITextMode break; } case TestUriType.ResultMessage: { - const message = test.tasks[parsed.taskIndex].messages[parsed.messageIndex]?.message; - if (typeof message === 'string') { - text = message; - } else if (message) { - text = message.value; - language = this.languageService.createById('markdown'); + const message = test.tasks[parsed.taskIndex].messages[parsed.messageIndex]; + if (message) { + if (typeof message.message === 'string') { + text = message.type === TestMessageType.Output ? removeAnsiEscapeCodes(message.message) : message.message; + } else { + text = message.message.value; + language = this.languageService.createById('markdown'); + } } break; } 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 397ecffdb59..76104ae81f2 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 @@ -5,7 +5,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IColorRegistry, Extensions, ColorContribution } from 'vs/platform/theme/common/colorRegistry'; -import { asText } from 'vs/platform/request/common/request'; +import { asTextOrError } from 'vs/platform/request/common/request'; import * as pfs from 'vs/base/node/pfs'; import * as path from 'vs/base/common/path'; import * as assert from 'assert'; @@ -39,7 +39,7 @@ suite('Color Registry', function () { const environmentService = new class extends mock<INativeEnvironmentService>() { override args = { _: [] }; }; const reqContext = await new RequestService(new TestConfigurationService(), environmentService, new NullLogService()).request({ url: 'https://raw.githubusercontent.com/microsoft/vscode-docs/vnext/api/references/theme-color.md' }, CancellationToken.None); - const content = (await asText(reqContext))!; + const content = (await asTextOrError(reqContext))!; const expression = /-\s*\`([\w\.]+)\`: (.*)/g; diff --git a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts index 98ed6664ed8..65b9d91beca 100644 --- a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts +++ b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts @@ -66,6 +66,7 @@ function isTimelineItem(item: TreeElement | undefined): item is TimelineItem { function updateRelativeTime(item: TimelineItem, lastRelativeTime: string | undefined): string | undefined { item.relativeTime = isTimelineItem(item) ? fromNow(item.timestamp) : undefined; + item.relativeTimeFullWord = isTimelineItem(item) ? fromNow(item.timestamp, false, true) : undefined; if (lastRelativeTime === undefined || item.relativeTime !== lastRelativeTime) { lastRelativeTime = item.relativeTime; item.hideRelativeTime = false; @@ -196,6 +197,7 @@ class LoadMoreCommand { readonly iconDark = undefined; readonly source = undefined; readonly relativeTime = undefined; + readonly relativeTimeFullWord = undefined; readonly hideRelativeTime = undefined; constructor(loading: boolean) { @@ -350,7 +352,7 @@ export class TimelinePane extends ViewPane { } private onActiveEditorChanged() { - if (!this.followActiveEditor) { + if (!this.followActiveEditor || !this.isExpanded()) { return; } @@ -569,9 +571,10 @@ export class TimelinePane extends ViewPane { } } request?.tokenSource.dispose(true); - + options.cacheResults = true; + options.resetCache = reset; request = this.timelineService.getTimeline( - source, uri, options, new CancellationTokenSource(), { cacheResults: true, resetCache: reset } + source, uri, options, new CancellationTokenSource() ); if (request === undefined) { @@ -885,7 +888,7 @@ export class TimelinePane extends ViewPane { if (isLoadMoreCommand(element)) { return element.ariaLabel; } - return element.accessibilityInformation ? element.accessibilityInformation.label : localize('timeline.aria.item', "{0}: {1}", element.relativeTime ?? '', element.label); + return element.accessibilityInformation ? element.accessibilityInformation.label : localize('timeline.aria.item', "{0}: {1}", element.relativeTimeFullWord ?? '', element.label); }, getRole(element: TreeElement): string { if (isLoadMoreCommand(element)) { @@ -1172,6 +1175,7 @@ class TimelineTreeRenderer implements ITreeRenderer<TreeElement, FuzzyScore, Tim }); template.timestamp.textContent = item.relativeTime ?? ''; + template.timestamp.ariaLabel = item.relativeTimeFullWord ?? ''; template.timestamp.parentElement!.classList.toggle('timeline-timestamp--duplicate', isTimelineItem(item) && item.hideRelativeTime); template.actionBar.context = { uri: this.uri, item: item } as TimelineActionContext; diff --git a/src/vs/workbench/contrib/timeline/common/timeline.ts b/src/vs/workbench/contrib/timeline/common/timeline.ts index edf2a850a02..5185f65703e 100644 --- a/src/vs/workbench/contrib/timeline/common/timeline.ts +++ b/src/vs/workbench/contrib/timeline/common/timeline.ts @@ -51,6 +51,7 @@ export interface TimelineItem { contextValue?: string; relativeTime?: string; + relativeTimeFullWord?: string; hideRelativeTime?: boolean; } @@ -76,11 +77,8 @@ export interface TimelineChangeEvent { export interface TimelineOptions { cursor?: string; limit?: number | { timestamp: number; id?: string }; -} - -export interface InternalTimelineOptions { - cacheResults: boolean; - resetCache: boolean; + resetCache?: boolean; + cacheResults?: boolean; } export interface Timeline { @@ -100,7 +98,7 @@ export interface Timeline { export interface TimelineProvider extends TimelineProviderDescriptor, IDisposable { onDidChange?: Event<TimelineChangeEvent>; - provideTimeline(uri: URI, options: TimelineOptions, token: CancellationToken, internalOptions?: InternalTimelineOptions): Promise<Timeline | undefined>; + provideTimeline(uri: URI, options: TimelineOptions, token: CancellationToken): Promise<Timeline | undefined>; } export interface TimelineSource { @@ -151,7 +149,7 @@ export interface ITimelineService { getSources(): TimelineSource[]; - getTimeline(id: string, uri: URI, options: TimelineOptions, tokenSource: CancellationTokenSource, internalOptions?: InternalTimelineOptions): TimelineRequest | undefined; + getTimeline(id: string, uri: URI, options: TimelineOptions, tokenSource: CancellationTokenSource): TimelineRequest | undefined; setUri(uri: URI): void; } diff --git a/src/vs/workbench/contrib/timeline/common/timelineService.ts b/src/vs/workbench/contrib/timeline/common/timelineService.ts index 52bc586f90d..190dfa2501c 100644 --- a/src/vs/workbench/contrib/timeline/common/timelineService.ts +++ b/src/vs/workbench/contrib/timeline/common/timelineService.ts @@ -8,7 +8,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { ILogService } from 'vs/platform/log/common/log'; -import { ITimelineService, TimelineChangeEvent, TimelineOptions, TimelineProvidersChangeEvent, TimelineProvider, InternalTimelineOptions, TimelinePaneId } from './timeline'; +import { ITimelineService, TimelineChangeEvent, TimelineOptions, TimelineProvidersChangeEvent, TimelineProvider, TimelinePaneId } from './timeline'; import { IViewsService } from 'vs/workbench/common/views'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; @@ -44,7 +44,7 @@ export class TimelineService implements ITimelineService { return [...this.providers.values()].map(p => ({ id: p.id, label: p.label })); } - getTimeline(id: string, uri: URI, options: TimelineOptions, tokenSource: CancellationTokenSource, internalOptions?: InternalTimelineOptions) { + getTimeline(id: string, uri: URI, options: TimelineOptions, tokenSource: CancellationTokenSource) { this.logService.trace(`TimelineService#getTimeline(${id}): uri=${uri.toString()}`); const provider = this.providers.get(id); @@ -61,7 +61,7 @@ export class TimelineService implements ITimelineService { } return { - result: provider.provideTimeline(uri, options, tokenSource.token, internalOptions) + result: provider.provideTimeline(uri, options, tokenSource.token) .then(result => { if (result === undefined) { return undefined; diff --git a/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts b/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts index f7379a4d721..df20df258da 100644 --- a/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts +++ b/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts @@ -21,7 +21,7 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IProductService } from 'vs/platform/product/common/productService'; -import { asText, IRequestService } from 'vs/platform/request/common/request'; +import { asTextOrError, IRequestService } from 'vs/platform/request/common/request'; import { DEFAULT_MARKDOWN_STYLES, renderMarkdownDocument } from 'vs/workbench/contrib/markdown/browser/markdownDocumentRenderer'; import { WebviewInput } from 'vs/workbench/contrib/webviewPanel/browser/webviewEditorInput'; import { IWebviewWorkbenchService } from 'vs/workbench/contrib/webviewPanel/browser/webviewWorkbenchService'; @@ -163,7 +163,7 @@ export class ReleaseNotesManager { const fetchReleaseNotes = async () => { let text; try { - text = await asText(await this._requestService.request({ url }, CancellationToken.None)); + text = await asTextOrError(await this._requestService.request({ url }, CancellationToken.None)); } catch { throw new Error('Failed to fetch release notes'); } diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index 07d50613857..4a7fe8b6202 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -37,7 +37,7 @@ import { EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/ed import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import * as Constants from 'vs/workbench/contrib/logs/common/logConstants'; -import { IOutputService } from 'vs/workbench/contrib/output/common/output'; +import { IOutputService } from 'vs/workbench/services/output/common/output'; import { IActivityService, IBadge, NumberBadge, ProgressBadge } from 'vs/workbench/services/activity/common/activity'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; diff --git a/src/vs/workbench/contrib/webview/browser/overlayWebview.ts b/src/vs/workbench/contrib/webview/browser/overlayWebview.ts index bb25dfdc171..112efec0ac3 100644 --- a/src/vs/workbench/contrib/webview/browser/overlayWebview.ts +++ b/src/vs/workbench/contrib/webview/browser/overlayWebview.ts @@ -21,7 +21,8 @@ export class OverlayWebview extends Disposable implements IOverlayWebview { private readonly _onDidWheel = this._register(new Emitter<IMouseWheelEvent>()); public readonly onDidWheel = this._onDidWheel.event; - private readonly _pendingMessages = new Set<{ readonly message: any; readonly transfer?: readonly ArrayBuffer[] }>(); + private _isFirstLoad = true; + private readonly _firstLoadPendingMessages = new Set<{ readonly message: any; readonly transfer?: readonly ArrayBuffer[]; readonly resolve: (value: boolean) => void }>(); private readonly _webview = this._register(new MutableDisposable<IWebviewElement>()); private readonly _webviewEvents = this._register(new DisposableStore()); @@ -70,6 +71,11 @@ export class OverlayWebview extends Disposable implements IOverlayWebview { this._container?.remove(); this._container = undefined; + for (const msg of this._firstLoadPendingMessages) { + msg.resolve(false); + } + this._firstLoadPendingMessages.clear(); + this._onDidDispose.fire(); super.dispose(); @@ -200,8 +206,13 @@ export class OverlayWebview extends Disposable implements IOverlayWebview { this._onDidUpdateState.fire(state); })); - this._pendingMessages.forEach(msg => webview.postMessage(msg.message, msg.transfer)); - this._pendingMessages.clear(); + if (this._isFirstLoad) { + this._firstLoadPendingMessages.forEach(async msg => { + msg.resolve(await webview.postMessage(msg.message, msg.transfer)); + }); + } + this._isFirstLoad = false; + this._firstLoadPendingMessages.clear(); } this.container.style.visibility = 'visible'; @@ -268,12 +279,19 @@ export class OverlayWebview extends Disposable implements IOverlayWebview { private readonly _onMissingCsp = this._register(new Emitter<ExtensionIdentifier>()); public readonly onMissingCsp: Event<any> = this._onMissingCsp.event; - public postMessage(message: any, transfer?: readonly ArrayBuffer[]): void { + public async postMessage(message: any, transfer?: readonly ArrayBuffer[]): Promise<boolean> { if (this._webview.value) { - this._webview.value.postMessage(message, transfer); - } else { - this._pendingMessages.add({ message, transfer }); + return this._webview.value.postMessage(message, transfer); } + + if (this._isFirstLoad) { + let resolve: (x: boolean) => void; + const p = new Promise<boolean>(r => resolve = r); + this._firstLoadPendingMessages.add({ message, transfer, resolve: resolve! }); + return p; + } + + return false; } focus(): void { this._webview.value?.focus(); } diff --git a/src/vs/workbench/contrib/webview/browser/pre/main.js b/src/vs/workbench/contrib/webview/browser/pre/main.js index 48beccb020a..d2c57f4aa10 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/main.js +++ b/src/vs/workbench/contrib/webview/browser/pre/main.js @@ -368,16 +368,14 @@ const unloadMonitor = new class { } switch (this.confirmBeforeClose) { - case 'always': - { - event.preventDefault(); - event.returnValue = ''; - return ''; - } - case 'never': - { - break; - } + case 'always': { + event.preventDefault(); + event.returnValue = ''; + return ''; + } + case 'never': { + break; + } case 'keyboardOnly': default: { if (this.isModifierKeyDown) { @@ -680,6 +678,22 @@ const handleInnerScroll = (event) => { }); }; +function handleInnerDragStartEvent(/** @type {DragEvent} */ e) { + if (e.defaultPrevented) { + // Extension code has already handled this event + return; + } + + if (!e.dataTransfer || e.shiftKey) { + return; + } + + // Only handle drags from outside editor for now + if (e.dataTransfer.items.length && Array.prototype.every.call(e.dataTransfer.items, item => item.kind === 'file')) { + hostMessaging.postMessage('drag-start'); + } +} + /** * @param {() => void} callback */ @@ -1021,6 +1035,9 @@ onDomReady(() => { }); }); + contentWindow.addEventListener('dragenter', handleInnerDragStartEvent); + contentWindow.addEventListener('dragover', handleInnerDragStartEvent); + unloadMonitor.onIframeLoaded(newFrame); } }); @@ -1107,5 +1124,10 @@ onDomReady(() => { } }; + // Also forward events before the contents of the webview have loaded + window.addEventListener('keydown', handleInnerKeydown); + window.addEventListener('dragenter', handleInnerDragStartEvent); + window.addEventListener('dragover', handleInnerDragStartEvent); + hostMessaging.signalReady(); }); diff --git a/src/vs/workbench/contrib/webview/browser/pre/service-worker.js b/src/vs/workbench/contrib/webview/browser/pre/service-worker.js index 70534ee470d..81a45ab6ca7 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/service-worker.js +++ b/src/vs/workbench/contrib/webview/browser/pre/service-worker.js @@ -189,8 +189,10 @@ sw.addEventListener('fetch', (event) => { } // If we're making a request against the remote authority, we want to go - // back through VS Code itself so that we are authenticated properly - if (requestUrl.host === remoteAuthority) { + // through VS Code itself so that we are authenticated properly. If the + // service worker is hosted on the same origin we will have cookies and + // authentication will not be an issue. + if (requestUrl.origin !== sw.origin && requestUrl.host === remoteAuthority) { switch (event.request.method) { case 'GET': case 'HEAD': diff --git a/src/vs/workbench/contrib/webview/browser/webview.ts b/src/vs/workbench/contrib/webview/browser/webview.ts index 0db26467cba..284a3d7f897 100644 --- a/src/vs/workbench/contrib/webview/browser/webview.ts +++ b/src/vs/workbench/contrib/webview/browser/webview.ts @@ -183,7 +183,7 @@ export interface IWebview extends IDisposable { readonly onMessage: Event<WebviewMessageReceivedEvent>; readonly onMissingCsp: Event<ExtensionIdentifier>; - postMessage(message: any, transfer?: readonly ArrayBuffer[]): void; + postMessage(message: any, transfer?: readonly ArrayBuffer[]): Promise<boolean>; focus(): void; reload(): void; diff --git a/src/vs/workbench/contrib/webview/browser/webviewElement.ts b/src/vs/workbench/contrib/webview/browser/webviewElement.ts index 1c312d73bdd..6cd87f63f28 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewElement.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { isFirefox } from 'vs/base/browser/browser'; -import { addDisposableListener } from 'vs/base/browser/dom'; +import { addDisposableListener, EventType } from 'vs/base/browser/dom'; import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; import { IAction } from 'vs/base/common/actions'; import { ThrottledDelayer } from 'vs/base/common/async'; @@ -59,6 +59,7 @@ export const enum WebviewMessageChannels { didKeydown = 'did-keydown', didKeyup = 'did-keyup', didContextMenu = 'did-context-menu', + dragStart = 'drag-start', } interface IKeydownEvent { @@ -85,10 +86,11 @@ namespace WebviewState { readonly type = Type.Initializing; constructor( - public readonly pendingMessages: Array<{ + public pendingMessages: Array<{ readonly channel: string; readonly data?: any; readonly transferable: Transferable[]; + readonly resolve: (posted: boolean) => void; }> ) { } } @@ -349,6 +351,10 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD } })); + this._register(this.on(WebviewMessageChannels.dragStart, () => { + this.startBlockingIframeDragEvents(); + })); + if (options.enableFindWidget) { this._webviewFindWidget = this._register(instantiationService.createInstance(WebviewFindWidget, this)); this.styledFindWidget(); @@ -369,6 +375,13 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD this.messagePort = undefined; + if (this._state.type === WebviewState.Type.Initializing) { + for (const message of this._state.pendingMessages) { + message.resolve(false); + } + this._state.pendingMessages = []; + } + this._onDidDispose.fire(); this._resourceLoadingCts.dispose(true); @@ -410,15 +423,18 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD private readonly _onDidDispose = this._register(new Emitter<void>()); public readonly onDidDispose = this._onDidDispose.event; - public postMessage(message: any, transfer?: ArrayBuffer[]): void { - this._send('message', { message, transfer }); + public postMessage(message: any, transfer?: ArrayBuffer[]): Promise<boolean> { + return this._send('message', { message, transfer }); } - protected _send(channel: string, data?: any, transferable: Transferable[] = []): void { + protected async _send(channel: string, data?: any, transferable: Transferable[] = []): Promise<boolean> { if (this._state.type === WebviewState.Type.Initializing) { - this._state.pendingMessages.push({ channel, data, transferable }); + let resolve: (x: boolean) => void; + const promise = new Promise<boolean>(r => resolve = r); + this._state.pendingMessages.push({ channel, data, transferable, resolve: resolve! }); + return promise; } else { - this.doPostMessage(channel, data, transferable); + return this.doPostMessage(channel, data, transferable); } } @@ -478,9 +494,32 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD if (this._webviewFindWidget) { parent.appendChild(this._webviewFindWidget.getDomNode()); } + + [EventType.MOUSE_DOWN, EventType.MOUSE_MOVE, EventType.DROP].forEach(eventName => { + this._register(addDisposableListener(parent, eventName, () => { + this.stopBlockingIframeDragEvents(); + })); + }); + + [parent, window].forEach(node => this._register(addDisposableListener(node as HTMLElement, EventType.DRAG_END, () => { + this.stopBlockingIframeDragEvents(); + }))); + parent.appendChild(this.element); } + private startBlockingIframeDragEvents() { + if (this.element) { + this.element.style.pointerEvents = 'none'; + } + } + + private stopBlockingIframeDragEvents() { + if (this.element) { + this.element.style.pointerEvents = 'auto'; + } + } + protected webviewContentEndpoint(encodedWebviewOrigin: string): string { const endpoint = this._environmentService.webviewExternalEndpoint!.replace('{{uuid}}', encodedWebviewOrigin); if (endpoint[endpoint.length - 1] === '/') { @@ -494,10 +533,12 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD return uri.scheme + '://' + uri.authority.toLowerCase(); } - private doPostMessage(channel: string, data?: any, transferable: Transferable[] = []): void { + private doPostMessage(channel: string, data?: any, transferable: Transferable[] = []): boolean { if (this.element && this.messagePort) { this.messagePort.postMessage({ channel, args: data }, transferable); + return true; } + return false; } protected on<T = unknown>(channel: WebviewMessageChannels, handler: (data: T, e: MessageEvent) => void): IDisposable { @@ -530,7 +571,9 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD } as const; type Classification = { - extension: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'mjbvz'; comment: 'The id of the extension that created the webview.' }; + extension: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The id of the extension that created the webview.' }; + owner: 'mjbz'; + comment: 'Helps find which extensions are contributing webviews with invalid CSPs'; }; this._telemetryService.publicLog2<typeof payload, Classification>('webviewMissingCsp', payload); @@ -670,18 +713,14 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD } windowDidDragStart(): void { - // Webview break drag and droping around the main window (no events are generated when you are over them) + // Webview break drag and dropping around the main window (no events are generated when you are over them) // Work around this by disabling pointer events during the drag. // https://github.com/electron/electron/issues/18226 - if (this.element) { - this.element.style.pointerEvents = 'none'; - } + this.startBlockingIframeDragEvents(); } windowDidDragEnd(): void { - if (this.element) { - this.element.style.pointerEvents = ''; - } + this.stopBlockingIframeDragEvents(); } public selectAll() { diff --git a/src/vs/workbench/contrib/webview/browser/webviewFindWidget.ts b/src/vs/workbench/contrib/webview/browser/webviewFindWidget.ts index e7bad42e252..58841297621 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewFindWidget.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewFindWidget.ts @@ -6,6 +6,7 @@ import { Event } from 'vs/base/common/event'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { SimpleFindWidget } from 'vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget'; import { KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED } from 'vs/workbench/contrib/webview/browser/webview'; @@ -29,9 +30,10 @@ export class WebviewFindWidget extends SimpleFindWidget { constructor( private readonly _delegate: WebviewFindDelegate, @IContextViewService contextViewService: IContextViewService, - @IContextKeyService contextKeyService: IContextKeyService + @IContextKeyService contextKeyService: IContextKeyService, + @IKeybindingService keybindingService: IKeybindingService ) { - super(contextViewService, contextKeyService, undefined, { showOptionButtons: false, checkImeCompletionState: _delegate.checkImeCompletionState }); + super(undefined, { showOptionButtons: false, checkImeCompletionState: _delegate.checkImeCompletionState }, contextViewService, contextKeyService, keybindingService); this._findWidgetFocused = KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED.bindTo(contextKeyService); this._register(_delegate.hasFindResult(hasResult => { diff --git a/src/vs/workbench/contrib/webviewPanel/browser/webviewEditorInput.ts b/src/vs/workbench/contrib/webviewPanel/browser/webviewEditorInput.ts index 1caa0e68f25..63e3f1cc124 100644 --- a/src/vs/workbench/contrib/webviewPanel/browser/webviewEditorInput.ts +++ b/src/vs/workbench/contrib/webviewPanel/browser/webviewEditorInput.ts @@ -23,7 +23,7 @@ export class WebviewInput extends EditorInput { } public override get capabilities(): EditorInputCapabilities { - return EditorInputCapabilities.Readonly | EditorInputCapabilities.Singleton; + return EditorInputCapabilities.Readonly | EditorInputCapabilities.Singleton | EditorInputCapabilities.CanDropIntoEditor; } private _name: string; diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts index 9784e9f9fd2..7015fa5e7b4 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts @@ -99,12 +99,16 @@ const parsedStartEntries: IWelcomePageStartEntry[] = startEntries.map((e, i) => })); type GettingStartedActionClassification = { - command: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; owner: 'JacksonKearl'; comment: 'Help understand what actions are most commonly taken on the getting started page' }; - argument: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; owner: 'JacksonKearl'; comment: 'As above' }; + command: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'The command being executed on the getting started page.' }; + walkthroughId: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'The walkthrough which the command is in' }; + argument: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'The arguments being passed to the command' }; + owner: 'lramos15'; + comment: 'Help understand what actions are most commonly taken on the getting started page'; }; type GettingStartedActionEvent = { command: string; + walkthroughId: string | undefined; argument: string | undefined; }; @@ -343,7 +347,7 @@ export class GettingStartedPage extends EditorPane { private async runDispatchCommand(command: string, argument: string) { this.commandService.executeCommand('workbench.action.keepEditor'); - this.telemetryService.publicLog2<GettingStartedActionEvent, GettingStartedActionClassification>('gettingStarted.ActionExecuted', { command, argument }); + this.telemetryService.publicLog2<GettingStartedActionEvent, GettingStartedActionClassification>('gettingStarted.ActionExecuted', { command, argument, walkthroughId: this.currentWalkthrough?.id }); switch (command) { case 'scrollPrev': { this.scrollPrev(); @@ -511,7 +515,7 @@ export class GettingStartedPage extends EditorPane { if (hrefs.length === 1) { const href = hrefs[0]; if (href.startsWith('http')) { - this.telemetryService.publicLog2<GettingStartedActionEvent, GettingStartedActionClassification>('gettingStarted.ActionExecuted', { command: 'runStepAction', argument: href }); + this.telemetryService.publicLog2<GettingStartedActionEvent, GettingStartedActionClassification>('gettingStarted.ActionExecuted', { command: 'runStepAction', argument: href, walkthroughId: this.currentWalkthrough?.id }); this.openerService.open(href); } } @@ -546,7 +550,7 @@ export class GettingStartedPage extends EditorPane { if (hrefs.length === 1) { const href = hrefs[0]; if (href.startsWith('http')) { - this.telemetryService.publicLog2<GettingStartedActionEvent, GettingStartedActionClassification>('gettingStarted.ActionExecuted', { command: 'runStepAction', argument: href }); + this.telemetryService.publicLog2<GettingStartedActionEvent, GettingStartedActionClassification>('gettingStarted.ActionExecuted', { command: 'runStepAction', argument: href, walkthroughId: this.currentWalkthrough?.id }); this.openerService.open(href); } } @@ -723,10 +727,10 @@ export class GettingStartedPage extends EditorPane { const showOnStartupLabel = $('label.caption', { for: 'showOnStartup' }, localize('welcomePage.showOnStartup', "Show welcome page on startup")); const onShowOnStartupChanged = () => { if (showOnStartupCheckbox.checked) { - this.telemetryService.publicLog2<GettingStartedActionEvent, GettingStartedActionClassification>('gettingStarted.ActionExecuted', { command: 'showOnStartupChecked', argument: undefined }); + this.telemetryService.publicLog2<GettingStartedActionEvent, GettingStartedActionClassification>('gettingStarted.ActionExecuted', { command: 'showOnStartupChecked', argument: undefined, walkthroughId: this.currentWalkthrough?.id }); this.configurationService.updateValue(configurationKey, 'welcomePage'); } else { - this.telemetryService.publicLog2<GettingStartedActionEvent, GettingStartedActionClassification>('gettingStarted.ActionExecuted', { command: 'showOnStartupUnchecked', argument: undefined }); + this.telemetryService.publicLog2<GettingStartedActionEvent, GettingStartedActionClassification>('gettingStarted.ActionExecuted', { command: 'showOnStartupUnchecked', argument: undefined, walkthroughId: this.currentWalkthrough?.id }); this.configurationService.updateValue(configurationKey, 'none'); } }; @@ -852,7 +856,7 @@ export class GettingStartedPage extends EditorPane { link.title = fullPath; link.setAttribute('aria-label', localize('welcomePage.openFolderWithPath', "Open folder {0} with path {1}", name, parentPath)); link.addEventListener('click', e => { - this.telemetryService.publicLog2<GettingStartedActionEvent, GettingStartedActionClassification>('gettingStarted.ActionExecuted', { command: 'openRecent', argument: undefined }); + this.telemetryService.publicLog2<GettingStartedActionEvent, GettingStartedActionClassification>('gettingStarted.ActionExecuted', { command: 'openRecent', argument: undefined, walkthroughId: this.currentWalkthrough?.id }); this.hostService.openWindow([windowOpenable], { forceNewWindow: e.ctrlKey || e.metaKey, remoteAuthority: recent.remoteAuthority || null // local window if remoteAuthority is not set or can not be deducted from the openable @@ -1092,7 +1096,7 @@ export class GettingStartedPage extends EditorPane { const toSide = href.startsWith('command:toSide:'); const command = href.replace(/command:(toSide:)?/, 'command:'); - this.telemetryService.publicLog2<GettingStartedActionEvent, GettingStartedActionClassification>('gettingStarted.ActionExecuted', { command: 'runStepAction', argument: href }); + this.telemetryService.publicLog2<GettingStartedActionEvent, GettingStartedActionClassification>('gettingStarted.ActionExecuted', { command: 'runStepAction', argument: href, walkthroughId: this.currentWalkthrough?.id }); const fullSize = this.groupsService.contentDimension; diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts index 75b3891db8d..4be26d8aedc 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts @@ -340,7 +340,7 @@ export class WalkthroughsService extends Disposable implements IWalkthroughsServ } if (step.media.image) { - const altText = (step.media as any).altText; + const altText = step.media.altText; if (altText === undefined) { console.error('Walkthrough item:', fullyQualifiedID, 'is missing altText for its media element.'); } @@ -362,7 +362,7 @@ export class WalkthroughsService extends Disposable implements IWalkthroughsServ }; } - // Legacy media config + // Legacy media config (only in use by remote-wsl at the moment) else { const legacyMedia = step.media as unknown as { path: string; altText: string }; if (typeof legacyMedia.path === 'string' && legacyMedia.path.endsWith('.md')) { diff --git a/src/vs/workbench/contrib/welcomeViews/common/newFile.contribution.ts b/src/vs/workbench/contrib/welcomeViews/common/newFile.contribution.ts index b78599440db..8b87c52e229 100644 --- a/src/vs/workbench/contrib/welcomeViews/common/newFile.contribution.ts +++ b/src/vs/workbench/contrib/welcomeViews/common/newFile.contribution.ts @@ -9,7 +9,7 @@ import { assertIsDefined } from 'vs/base/common/types'; import { localize } from 'vs/nls'; import { Action2, IMenuService, MenuId, registerAction2, IMenu, MenuRegistry, MenuItemAction } from 'vs/platform/actions/common/actions'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; @@ -18,11 +18,9 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; - +const builtInSource = localize('Built-In', "Built-In"); const category = localize('Create', "Create"); -export const HasMultipleNewFileEntries = new RawContextKey<boolean>('hasMultipleNewFileEntries', false); - registerAction2(class extends Action2 { constructor() { super({ @@ -36,15 +34,14 @@ registerAction2(class extends Action2 { }, menu: { id: MenuId.MenubarFileMenu, - when: HasMultipleNewFileEntries, group: '1_new', order: 2 } }); } - run(accessor: ServicesAccessor) { - assertIsDefined(NewFileTemplatesManager.Instance).run(); + async run(accessor: ServicesAccessor): Promise<boolean> { + return assertIsDefined(NewFileTemplatesManager.Instance).run(); } }); @@ -68,8 +65,6 @@ class NewFileTemplatesManager extends Disposable { this._register({ dispose() { if (NewFileTemplatesManager.Instance === this) { NewFileTemplatesManager.Instance = undefined; } } }); this.menu = menuService.createMenu(MenuId.NewFile, contextKeyService); - this.updateContextKeys(); - this._register(this.menu.onDidChange(() => { this.updateContextKeys(); })); } private allEntries(): NewFileItem[] { @@ -77,43 +72,53 @@ class NewFileTemplatesManager extends Disposable { for (const [groupName, group] of this.menu.getActions({ renderShortTitle: true })) { for (const action of group) { if (action instanceof MenuItemAction) { - items.push({ commandID: action.item.id, from: action.item.source ?? localize('Built-In', "Built-In"), title: action.label, group: groupName }); + items.push({ commandID: action.item.id, from: action.item.source ?? builtInSource, title: action.label, group: groupName }); } } } return items; } - private updateContextKeys() { - HasMultipleNewFileEntries.bindTo(this.contextKeyService).set(this.allEntries().length > 1); - } - - run() { + async run(): Promise<boolean> { const entries = this.allEntries(); if (entries.length === 0) { throw Error('Unexpected empty new items list'); } else if (entries.length === 1) { this.commandService.executeCommand(entries[0].commandID); + return true; } else { - this.selectNewEntry(entries); + return this.selectNewEntry(entries); } } - private async selectNewEntry(entries: NewFileItem[]) { + private async selectNewEntry(entries: NewFileItem[]): Promise<boolean> { + let resolveResult: (res: boolean) => void; + const resultPromise = new Promise<boolean>(resolve => { + resolveResult = resolve; + }); + const disposables = new DisposableStore(); const qp = this.quickInputService.createQuickPick(); qp.title = localize('createNew', "Create New..."); qp.matchOnDetail = true; qp.matchOnDescription = true; - const sortCategories = (a: string, b: string): number => { + const sortCategories = (a: NewFileItem, b: NewFileItem): number => { const categoryPriority: Record<string, number> = { 'file': 1, 'notebook': 2 }; - if (categoryPriority[a] && categoryPriority[b]) { return categoryPriority[b] - categoryPriority[a]; } - if (categoryPriority[a]) { return 1; } - if (categoryPriority[b]) { return -1; } - return a.localeCompare(b); + if (categoryPriority[a.group] && categoryPriority[b.group]) { + if (categoryPriority[a.group] !== categoryPriority[b.group]) { + return categoryPriority[b.group] - categoryPriority[a.group]; + } + } + else if (categoryPriority[a.group]) { return 1; } + else if (categoryPriority[b.group]) { return -1; } + + if (a.from === builtInSource) { return 1; } + if (b.from === builtInSource) { return -1; } + + return a.from.localeCompare(b.from); }; const displayCategory: Record<string, string> = { @@ -125,7 +130,7 @@ class NewFileTemplatesManager extends Disposable { const items: (((IQuickPickItem & NewFileItem) | IQuickPickSeparator))[] = []; let lastSeparator: string | undefined; entries - .sort((a, b) => -sortCategories(a.group, b.group)) + .sort((a, b) => -sortCategories(a, b)) .forEach((entry) => { const command = entry.commandID; const keybinding = this.keybindingService.lookupKeybinding(command || '', this.contextKeyService); @@ -159,6 +164,8 @@ class NewFileTemplatesManager extends Disposable { disposables.add(qp.onDidAccept(async e => { const selected = qp.selectedItems[0] as (IQuickPickItem & NewFileItem); + resolveResult(!!selected); + qp.hide(); if (selected) { await this.commandService.executeCommand(selected.commandID); } })); @@ -166,23 +173,26 @@ class NewFileTemplatesManager extends Disposable { disposables.add(qp.onDidHide(() => { qp.dispose(); disposables.dispose(); + resolveResult(false); })); disposables.add(qp.onDidTriggerItemButton(e => { qp.hide(); this.commandService.executeCommand('workbench.action.openGlobalKeybindings', (e.item as (IQuickPickItem & NewFileItem)).commandID); + resolveResult(false); })); qp.show(); - } + return resultPromise; + } } Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench) .registerWorkbenchContribution(NewFileTemplatesManager, LifecyclePhase.Restored); MenuRegistry.appendMenuItem(MenuId.NewFile, { - group: 'File', + group: 'file', command: { id: 'workbench.action.files.newUntitledFile', title: localize('miNewFile2', "Text File") diff --git a/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts b/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts index 714d83dc166..1575fbf2118 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts +++ b/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts @@ -761,7 +761,9 @@ class WorkspaceTrustTelemetryContribution extends Disposable implements IWorkben const disabledByCliFlag = this.environmentService.disableWorkspaceTrust; type WorkspaceTrustDisabledEventClassification = { - reason: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; + owner: 'sbatten'; + comment: 'Logged when workspace trust is disabled'; + reason: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The reason workspace trust is disabled. e.g. cli or setting' }; }; type WorkspaceTrustDisabledEvent = { @@ -775,7 +777,9 @@ class WorkspaceTrustTelemetryContribution extends Disposable implements IWorkben } type WorkspaceTrustInfoEventClassification = { - trustedFoldersCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; + owner: 'sbatten'; + comment: 'Information about the workspaces trusted on the machine'; + trustedFoldersCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The number of trusted folders on the machine' }; }; type WorkspaceTrustInfoEvent = { @@ -798,8 +802,10 @@ class WorkspaceTrustTelemetryContribution extends Disposable implements IWorkben }; type WorkspaceTrustStateChangedEventClassification = { - workspaceId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; - isTrusted: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; + owner: 'sbatten'; + comment: 'Logged when the workspace transitions between trusted and restricted modes'; + workspaceId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'An id of the workspace' }; + isTrusted: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'true if the workspace is trusted' }; }; this.telemetryService.publicLog2<WorkspaceTrustStateChangedEvent, WorkspaceTrustStateChangedEventClassification>('workspaceTrustStateChanged', { @@ -809,9 +815,11 @@ class WorkspaceTrustTelemetryContribution extends Disposable implements IWorkben if (isTrusted) { type WorkspaceTrustFolderInfoEventClassification = { - trustedFolderDepth: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; - workspaceFolderDepth: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; - delta: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; + owner: 'sbatten'; + comment: 'Some metrics on the trusted workspaces folder structure'; + trustedFolderDepth: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The number of directories deep of the trusted path' }; + workspaceFolderDepth: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The number of directories deep of the workspace path' }; + delta: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The difference between the trusted path and the workspace path directories depth' }; }; type WorkspaceTrustFolderInfoEvent = { diff --git a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts index 5aae1567d83..679be9104d5 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts +++ b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts @@ -50,7 +50,8 @@ import { WorkspaceTrustEditorInput } from 'vs/workbench/services/workspaces/brow import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { getExtensionDependencies } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { EnablementState, IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; -import { posix } from 'vs/base/common/path'; +import { posix, win32 } from 'vs/base/common/path'; +import { hasDriveLetter, toSlashes } from 'vs/base/common/extpath'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { IProductService } from 'vs/platform/product/common/productService'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; @@ -352,7 +353,6 @@ class WorkspaceTrustedUrisTable extends Disposable { canSelectMany: false, defaultUri: item.uri, openLabel: localize('trustUri', "Trust Folder"), - title: localize('selectTrustedUri', "Select Folder To Trust") }); @@ -530,8 +530,10 @@ class TrustedUriPathColumnRenderer implements ITableRenderer<ITrustedUriItem, IT const accept = () => { hideInputBox(); - const uri = item.uri.with({ path: templateData.pathInput.value }); - templateData.pathLabel.innerText = templateData.pathInput.value; + + const pathToUse = templateData.pathInput.value; + const uri = hasDriveLetter(pathToUse) ? item.uri.with({ path: posix.sep + toSlashes(pathToUse) }) : item.uri.with({ path: pathToUse }); + templateData.pathLabel.innerText = this.formatPath(uri); if (uri) { this.table.acceptEdit(item, uri); @@ -563,12 +565,10 @@ class TrustedUriPathColumnRenderer implements ITableRenderer<ITrustedUriItem, IT reject(); }))); - const stringValue = item.uri.scheme === Schemas.file ? URI.revive(item.uri).fsPath : item.uri.path; + const stringValue = this.formatPath(item.uri); templateData.pathInput.value = stringValue; templateData.pathLabel.innerText = stringValue; templateData.element.classList.toggle('current-workspace-parent', item.parentOfWorkspaceItem); - - // templateData.pathLabel.style.display = ''; } disposeTemplate(templateData: ITrustedUriPathColumnTemplateData): void { @@ -576,6 +576,24 @@ class TrustedUriPathColumnRenderer implements ITableRenderer<ITrustedUriItem, IT templateData.renderDisposables.dispose(); } + private formatPath(uri: URI): string { + if (uri.scheme === Schemas.file) { + return uri.fsPath; + } + + // If the path is not a file uri, but points to a windows remote, we should create windows fs path + // e.g. /c:/user/directory => C:\user\directory + if (uri.path.startsWith(posix.sep)) { + const pathWithoutLeadingSeparator = uri.path.substring(1); + const isWindowsPath = hasDriveLetter(pathWithoutLeadingSeparator, true); + if (isWindowsPath) { + return win32.normalize(pathWithoutLeadingSeparator); + } + } + + return uri.path; + } + } diff --git a/src/vs/workbench/electron-sandbox/actions/windowActions.ts b/src/vs/workbench/electron-sandbox/actions/windowActions.ts index 8ab7286bcfe..a89681fa292 100644 --- a/src/vs/workbench/electron-sandbox/actions/windowActions.ts +++ b/src/vs/workbench/electron-sandbox/actions/windowActions.ts @@ -227,6 +227,7 @@ abstract class BaseSwitchWindow extends Action2 { activeItem: picks[autoFocusIndex], placeHolder, quickNavigate: this.isQuickNavigate() ? { keybindings: keybindingService.lookupKeybindings(this.desc.id) } : undefined, + hideInput: this.isQuickNavigate(), onDidTriggerItemButton: async context => { await nativeHostService.closeWindowById(context.item.payload); context.removeItem(); diff --git a/src/vs/workbench/electron-sandbox/desktop.contribution.ts b/src/vs/workbench/electron-sandbox/desktop.contribution.ts index c939e4c6c80..8c88a219e24 100644 --- a/src/vs/workbench/electron-sandbox/desktop.contribution.ts +++ b/src/vs/workbench/electron-sandbox/desktop.contribution.ts @@ -22,6 +22,10 @@ import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { InstallShellScriptAction, UninstallShellScriptAction } from 'vs/workbench/electron-sandbox/actions/installActions'; import { EditorsVisibleContext, SingleEditorGroupsContext } from 'vs/workbench/common/contextkeys'; import { TELEMETRY_SETTING_ID } from 'vs/platform/telemetry/common/telemetry'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ShutdownReason } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { NativeWindow } from 'vs/workbench/electron-sandbox/window'; +import { ModifierKeyEmitter } from 'vs/base/browser/dom'; // Actions (function registerActions(): void { @@ -59,8 +63,18 @@ import { TELEMETRY_SETTING_ID } from 'vs/platform/telemetry/common/telemetry'; KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'workbench.action.quit', weight: KeybindingWeight.WorkbenchContrib, - handler(accessor: ServicesAccessor) { + async handler(accessor: ServicesAccessor) { const nativeHostService = accessor.get(INativeHostService); + const configurationService = accessor.get(IConfigurationService); + + const confirmBeforeClose = configurationService.getValue<'always' | 'never' | 'keyboardOnly'>('window.confirmBeforeClose'); + if (confirmBeforeClose === 'always' || (confirmBeforeClose === 'keyboardOnly' && ModifierKeyEmitter.getInstance().isModifierPressed)) { + const confirmed = await NativeWindow.confirmOnShutdown(accessor, ShutdownReason.QUIT); + if (!confirmed) { + return; // quit prevented by user + } + } + nativeHostService.quit(); }, when: undefined, @@ -70,21 +84,21 @@ import { TELEMETRY_SETTING_ID } from 'vs/platform/telemetry/common/telemetry'; // Actions: macOS Native Tabs if (isMacintosh) { - [ + for (const command of [ { handler: NewWindowTabHandler, id: 'workbench.action.newWindowTab', title: { value: localize('newTab', "New Window Tab"), original: 'New Window Tab' } }, { handler: ShowPreviousWindowTabHandler, id: 'workbench.action.showPreviousWindowTab', title: { value: localize('showPreviousTab', "Show Previous Window Tab"), original: 'Show Previous Window Tab' } }, { handler: ShowNextWindowTabHandler, id: 'workbench.action.showNextWindowTab', title: { value: localize('showNextWindowTab', "Show Next Window Tab"), original: 'Show Next Window Tab' } }, { handler: MoveWindowTabToNewWindowHandler, id: 'workbench.action.moveWindowTabToNewWindow', title: { value: localize('moveWindowTabToNewWindow', "Move Window Tab to New Window"), original: 'Move Window Tab to New Window' } }, { handler: MergeWindowTabsHandlerHandler, id: 'workbench.action.mergeAllWindowTabs', title: { value: localize('mergeAllWindowTabs', "Merge All Windows"), original: 'Merge All Windows' } }, { handler: ToggleWindowTabsBarHandler, id: 'workbench.action.toggleWindowTabsBar', title: { value: localize('toggleWindowTabsBar', "Toggle Window Tabs Bar"), original: 'Toggle Window Tabs Bar' } } - ].forEach(command => { + ]) { CommandsRegistry.registerCommand(command.id, command.handler); MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command, when: ContextKeyExpr.equals('config.window.nativeTabs', true) }); - }); + } } // Actions: Developer diff --git a/src/vs/workbench/electron-sandbox/desktop.main.ts b/src/vs/workbench/electron-sandbox/desktop.main.ts index 6a78e65d27c..7e658a10cdc 100644 --- a/src/vs/workbench/electron-sandbox/desktop.main.ts +++ b/src/vs/workbench/electron-sandbox/desktop.main.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize } from 'vs/nls'; import product from 'vs/platform/product/common/product'; import { INativeWindowConfiguration, zoomLevelToZoomFactor } from 'vs/platform/window/common/window'; import { Workbench } from 'vs/workbench/browser/workbench'; @@ -83,15 +84,15 @@ export class DesktopMain extends Disposable { // Files const filesToWait = this.configuration.filesToWait; const filesToWaitPaths = filesToWait?.paths; - [filesToWaitPaths, this.configuration.filesToOpenOrCreate, this.configuration.filesToDiff].forEach(paths => { + for (const paths of [filesToWaitPaths, this.configuration.filesToOpenOrCreate, this.configuration.filesToDiff]) { if (Array.isArray(paths)) { - paths.forEach(path => { + for (const path of paths) { if (path.fileUri) { path.fileUri = URI.revive(path.fileUri); } - }); + } } - }); + } if (filesToWait) { filesToWait.waitMarkerFileUri = URI.revive(filesToWait.waitMarkerFileUri); @@ -129,7 +130,7 @@ export class DesktopMain extends Disposable { private registerListeners(workbench: Workbench, storageService: NativeStorageService): void { // Workbench Lifecycle - this._register(workbench.onWillShutdown(event => event.join(storageService.close(), 'join.closeStorage'))); + 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())); } @@ -278,7 +279,7 @@ export class DesktopMain extends Disposable { const workspaceTrustEnablementService = new WorkspaceTrustEnablementService(configurationService, environmentService); serviceCollection.set(IWorkspaceTrustEnablementService, workspaceTrustEnablementService); - const workspaceTrustManagementService = new WorkspaceTrustManagementService(configurationService, remoteAuthorityResolverService, storageService, uriIdentityService, environmentService, configurationService, workspaceTrustEnablementService, logService); + const workspaceTrustManagementService = new WorkspaceTrustManagementService(configurationService, remoteAuthorityResolverService, storageService, uriIdentityService, environmentService, configurationService, workspaceTrustEnablementService); serviceCollection.set(IWorkspaceTrustManagementService, workspaceTrustManagementService); // Update workspace trust so that configuration is updated accordingly diff --git a/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts b/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts index 0afcfd8ade7..7a7858fd9c4 100644 --- a/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { getZoomFactor } from 'vs/base/browser/browser'; -import { $, addDisposableListener, append, Dimension, EventType, hide, prepend, runAtThisOrScheduleAtNextAnimationFrame, show } from 'vs/base/browser/dom'; +import { $, addDisposableListener, append, EventType, hide, prepend, show } from 'vs/base/browser/dom'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { ILabelService } from 'vs/platform/label/common/label'; @@ -25,11 +25,13 @@ import { getTitleBarStyle } 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 { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; 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 getMacTitlebarSize() { const osVersion = this.environmentService.os.release; @@ -60,9 +62,10 @@ export class TitlebarPart extends BrowserTitleBarPart { @IContextKeyService contextKeyService: IContextKeyService, @IHostService hostService: IHostService, @IProductService productService: IProductService, - @INativeHostService private readonly nativeHostService: INativeHostService + @INativeHostService private readonly nativeHostService: INativeHostService, + @IKeybindingService keybindingService: IKeybindingService, ) { - super(contextMenuService, configurationService, editorService, environmentService, contextService, instantiationService, themeService, labelService, storageService, layoutService, menuService, contextKeyService, hostService, productService); + super(contextMenuService, configurationService, editorService, environmentService, contextService, instantiationService, themeService, labelService, storageService, layoutService, menuService, contextKeyService, hostService, productService, keybindingService); this.environmentService = environmentService; } @@ -131,28 +134,6 @@ export class TitlebarPart extends BrowserTitleBarPart { } } - protected override adjustTitleMarginToCenter(): void { - if (this.customMenubar && this.menubar) { - const leftMarker = (this.appIcon ? this.appIcon.clientWidth : 0) + this.menubar.clientWidth + 10; - const rightMarker = this.element.clientWidth - (this.windowControls ? this.windowControls.clientWidth : 0) - 10; - - // Not enough space to center the titlebar within window, - // Center between menu and window controls - if (leftMarker > (this.element.clientWidth - this.title.clientWidth) / 2 || - rightMarker < (this.element.clientWidth + this.title.clientWidth) / 2) { - this.title.style.position = ''; - this.title.style.left = ''; - this.title.style.transform = ''; - return; - } - } - - this.title.style.position = 'absolute'; - this.title.style.left = '50%'; - this.title.style.transform = 'translate(-50%, 0)'; - this.title.style.maxWidth = `calc(100vw - ${2 * ((this.windowControls?.clientWidth || 70) + 10)}px)`; - } - protected override installMenubar(): void { super.installMenubar(); @@ -186,7 +167,8 @@ export class TitlebarPart extends BrowserTitleBarPart { this.dragRegion = prepend(this.rootContainer, $('div.titlebar-drag-region')); // Window Controls (Native Windows/Linux) - if (!isMacintosh && this.windowControls) { + const hasWindowControlsOverlay = typeof (navigator as any).windowControlsOverlay !== 'undefined'; + if (!isMacintosh && getTitleBarStyle(this.configurationService) !== 'native' && !hasWindowControlsOverlay && this.windowControls) { // Minimize const minimizeIcon = append(this.windowControls, $('div.window-icon.window-minimize' + Codicon.chromeMinimize.cssSelector)); this._register(addDisposableListener(minimizeIcon, EventType.CLICK, e => { @@ -220,25 +202,15 @@ export class TitlebarPart extends BrowserTitleBarPart { return ret; } - override updateLayout(dimension: Dimension): void { - this.lastLayoutDimensions = dimension; - - if (getTitleBarStyle(this.configurationService) === 'custom') { - if (isMacintosh || this.currentMenubarVisibility === 'hidden') { - this.rootContainer.style.height = `${100.0 * getZoomFactor()}%`; - this.rootContainer.style.width = `${100.0 * getZoomFactor()}%`; - this.rootContainer.style.transform = `scale(${1 / getZoomFactor()})`; - } else { - this.rootContainer.style.height = `100%`; - this.rootContainer.style.width = `100%`; - this.rootContainer.style.transform = ''; - } - - runAtThisOrScheduleAtNextAnimationFrame(() => this.adjustTitleMarginToCenter()); + override updateStyles(): void { + super.updateStyles(); - if (this.customMenubar) { - const menubarDimension = new Dimension(0, dimension.height); - this.customMenubar.layout(menubarDimension); + // WCO styles only supported on Windows currently + if (isWindows) { + 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); } } } diff --git a/src/vs/workbench/electron-sandbox/window.ts b/src/vs/workbench/electron-sandbox/window.ts index 389c1218e67..231433ee322 100644 --- a/src/vs/workbench/electron-sandbox/window.ts +++ b/src/vs/workbench/electron-sandbox/window.ts @@ -7,7 +7,7 @@ import { localize } from 'vs/nls'; import { URI } from 'vs/base/common/uri'; import { onUnexpectedError } from 'vs/base/common/errors'; import { equals } from 'vs/base/common/objects'; -import { EventType, EventHelper, addDisposableListener, scheduleAtNextAnimationFrame } from 'vs/base/browser/dom'; +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'; @@ -44,8 +44,7 @@ import { assertIsDefined, isArray } from 'vs/base/common/types'; import { IOpenerService, OpenOptions } from 'vs/platform/opener/common/opener'; import { Schemas } from 'vs/base/common/network'; import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; -import { posix, dirname } from 'vs/base/common/path'; -import { getBaseLabel } from 'vs/base/common/labels'; +import { posix } from 'vs/base/common/path'; import { ITunnelService, extractLocalHostUriMetaDataForPortMapping } from 'vs/platform/tunnel/common/tunnel'; import { IWorkbenchLayoutService, Parts, positionFromString, Position } from 'vs/workbench/services/layout/browser/layoutService'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; @@ -58,12 +57,14 @@ import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editor import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { AuthInfo } from 'vs/base/parts/sandbox/electron-sandbox/electronTypes'; import { ILogService } from 'vs/platform/log/common/log'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { whenEditorClosed } from 'vs/workbench/browser/editor'; import { ISharedProcessService } from 'vs/platform/ipc/electron-sandbox/services'; import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; import { toErrorMessage } from 'vs/base/common/errorMessage'; -import { registerLegacyWindowDriver, registerWindowDriver } from 'vs/platform/driver/electron-sandbox/driver'; +import { registerWindowDriver } from 'vs/platform/driver/electron-sandbox/driver'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { dirname } from 'vs/base/common/resources'; export class NativeWindow extends Disposable { @@ -115,7 +116,8 @@ export class NativeWindow extends Disposable { @ILogService private readonly logService: ILogService, @IInstantiationService private readonly instantiationService: IInstantiationService, @ISharedProcessService private readonly sharedProcessService: ISharedProcessService, - @IProgressService private readonly progressService: IProgressService + @IProgressService private readonly progressService: IProgressService, + @ILabelService private readonly labelService: ILabelService ) { super(); @@ -132,11 +134,11 @@ export class NativeWindow extends Disposable { this._register(this.editorService.onDidActiveEditorChange(() => this.updateTouchbarMenu())); // prevent opening a real URL inside the window - [EventType.DRAG_OVER, EventType.DROP].forEach(event => { + for (const event of [EventType.DRAG_OVER, EventType.DROP]) { window.document.body.addEventListener(event, (e: DragEvent) => { EventHelper.stop(e); }); - }); + } // Support runAction event ipcRenderer.on('vscode:runAction', async (event: unknown, request: INativeRunActionInWindowRequest) => { @@ -332,7 +334,46 @@ export class NativeWindow extends Disposable { this._register(this.lifecycleService.onWillShutdown(e => this.onWillShutdown(e))); } - private onBeforeShutdown({ reason }: BeforeShutdownEvent): void { + private onBeforeShutdown({ veto, reason }: BeforeShutdownEvent): void { + if (reason === ShutdownReason.CLOSE) { + const confirmBeforeCloseSetting = this.configurationService.getValue<'always' | 'never' | 'keyboardOnly'>('window.confirmBeforeClose'); + + const confirmBeforeClose = confirmBeforeCloseSetting === 'always' || (confirmBeforeCloseSetting === 'keyboardOnly' && ModifierKeyEmitter.getInstance().isModifierPressed); + if (confirmBeforeClose) { + + // When we need to confirm on close or quit, veto the shutdown + // with a long running promise to figure out whether shutdown + // can proceed or not. + + return veto((async () => { + let actualReason: ShutdownReason = reason; + if (reason === ShutdownReason.CLOSE && !isMacintosh) { + const windowCount = await this.nativeHostService.getWindowCount(); + if (windowCount === 1) { + actualReason = ShutdownReason.QUIT; // Windows/Linux: closing last window means to QUIT + } + } + + let confirmed = true; + if (confirmBeforeClose) { + confirmed = await this.instantiationService.invokeFunction(accessor => NativeWindow.confirmOnShutdown(accessor, actualReason)); + } + + // Progress for long running shutdown + if (confirmed) { + this.progressOnBeforeShutdown(reason); + } + + return !confirmed; + })(), 'veto.confirmBeforeClose'); + } + } + + // Progress for long running shutdown + this.progressOnBeforeShutdown(reason); + } + + private progressOnBeforeShutdown(reason: ShutdownReason): void { this.progressService.withProgress({ location: ProgressLocation.Window, // use window progress to not be too annoying about this operation delay: 800, // delay so that it only appears when operation takes a long time @@ -346,25 +387,63 @@ export class NativeWindow extends Disposable { }); } + static async confirmOnShutdown(accessor: ServicesAccessor, reason: ShutdownReason): Promise<boolean> { + const dialogService = accessor.get(IDialogService); + const configurationService = accessor.get(IConfigurationService); + + const message = reason === ShutdownReason.QUIT ? + (isMacintosh ? localize('quitMessageMac', "Are you sure you want to quit?") : localize('quitMessage', "Are you sure you want to exit?")) : + localize('closeWindowMessage', "Are you sure you want to close the window?"); + const primaryButton = reason === ShutdownReason.QUIT ? + (isMacintosh ? localize({ key: 'quitButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Quit") : localize({ key: 'exitButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Exit")) : + localize({ key: 'closeWindowButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Close Window"); + + const res = await dialogService.confirm({ + type: 'question', + message, + primaryButton, + checkbox: { + label: localize('doNotAskAgain', "Do not ask me again") + } + }); + + // Update setting if checkbox checked + if (res.checkboxChecked) { + await configurationService.updateValue('window.confirmBeforeClose', 'never'); + } + + return res.confirmed; + } + private onBeforeShutdownError({ error, reason }: BeforeShutdownErrorEvent): void { this.dialogService.show(Severity.Error, this.toShutdownLabel(reason, true), undefined, { detail: localize('shutdownErrorDetail', "Error: {0}", toErrorMessage(error)) }); } - private onWillShutdown({ reason, force }: WillShutdownEvent): void { - this.progressService.withProgress({ - location: ProgressLocation.Dialog, // use a dialog to prevent the user from making any more interactions now - buttons: [this.toForceShutdownLabel(reason)], // allow to force shutdown anyway - delay: 800, // delay so that it only appears when operation takes a long time - cancellable: false, // do not allow to cancel - sticky: true, // do not allow to dismiss - title: this.toShutdownLabel(reason, false) - }, () => { - return Event.toPromise(this.lifecycleService.onDidShutdown); // dismiss this dialog when we actually shutdown - }, () => { - force(); - }); + private onWillShutdown({ reason, force, joiners }: WillShutdownEvent): void { + + // Delay so that the dialog only appears after timeout + const shutdownDialogScheduler = new RunOnceScheduler(() => { + const pendingJoiners = joiners(); + + this.progressService.withProgress({ + location: ProgressLocation.Dialog, // use a dialog to prevent the user from making any more interactions now + buttons: [this.toForceShutdownLabel(reason)], // allow to force shutdown anyway + cancellable: false, // do not allow to cancel + sticky: true, // do not allow to dismiss + title: this.toShutdownLabel(reason, false), + detail: pendingJoiners.length > 0 ? localize('willShutdownDetail', "The following operations are still running: \n{0}", pendingJoiners.map(joiner => `- ${joiner.label}`).join('\n')) : undefined + }, () => { + return Event.toPromise(this.lifecycleService.onDidShutdown); // dismiss this dialog when we actually shutdown + }, () => { + force(); + }); + }, 1200); + shutdownDialogScheduler.schedule(); + + // Dispose scheduler when we actually shutdown + Event.once(this.lifecycleService.onDidShutdown)(() => shutdownDialogScheduler.dispose()); } private toShutdownLabel(reason: ShutdownReason, isError: boolean): string { @@ -518,17 +597,17 @@ export class NativeWindow extends Disposable { pathOffset++; // for segments which are not the file name we want to open the folder } - const path = segments.slice(0, pathOffset).join(posix.sep); + const path = URI.file(segments.slice(0, pathOffset).join(posix.sep)); let label: string; if (!isFile) { - label = getBaseLabel(dirname(path)); + label = this.labelService.getUriBasenameLabel(dirname(path)); } else { - label = getBaseLabel(path); + label = this.labelService.getUriBasenameLabel(path); } const commandId = `workbench.action.revealPathInFinder${i}`; - this.customTitleContextMenuDisposable.add(CommandsRegistry.registerCommand(commandId, () => this.nativeHostService.showItemInFolder(path))); + this.customTitleContextMenuDisposable.add(CommandsRegistry.registerCommand(commandId, () => this.nativeHostService.showItemInFolder(path.fsPath))); this.customTitleContextMenuDisposable.add(MenuRegistry.appendMenuItem(MenuId.TitleBarContext, { command: { id: commandId, title: label || posix.sep }, order: -i })); } } @@ -573,27 +652,18 @@ export class NativeWindow extends Disposable { } // Smoke Test Driver - this.setupDriver(); + if (this.environmentService.enableSmokeTestDriver) { + this.setupDriver(); + } } private setupDriver(): void { - - // Browser Driver - if (this.environmentService.args['enable-smoke-test-driver']) { - const that = this; - registerWindowDriver({ - async exitApplication(): Promise<number> { - that.nativeHostService.quit(); - - return that.environmentService.mainPid; - } - }); - } - - // Legacy Driver (TODO@bpasero remove me eventually) - else if (this.environmentService.args.driver) { - this.instantiationService.invokeFunction(async accessor => this._register(await registerLegacyWindowDriver(accessor, this.nativeHostService.windowId))); - } + const that = this; + registerWindowDriver({ + async exitApplication(): Promise<void> { + return that.nativeHostService.quit(); + } + }); } private setupOpenHandlers(): void { @@ -744,9 +814,9 @@ export class NativeWindow extends Disposable { private doAddFolders(): void { const foldersToAdd: IWorkspaceFolderCreationData[] = []; - this.pendingFoldersToAdd.forEach(folder => { + for (const folder of this.pendingFoldersToAdd) { foldersToAdd.push(({ uri: folder })); - }); + } this.pendingFoldersToAdd = []; diff --git a/src/vs/workbench/services/assignment/common/assignmentService.ts b/src/vs/workbench/services/assignment/common/assignmentService.ts index d04be1ca7ed..455a4faab6b 100644 --- a/src/vs/workbench/services/assignment/common/assignmentService.ts +++ b/src/vs/workbench/services/assignment/common/assignmentService.ts @@ -67,7 +67,9 @@ class WorkbenchAssignmentServiceTelemetry implements IExperimentationTelemetry { /* __GDPR__ "query-expfeature" : { - "ABExp.queriedFeature": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + "owner": "sbatten", + "comment": "Logs queries to the experiment service by feature for metric calculations", + "ABExp.queriedFeature": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The experimental feature being queried" } } */ this.telemetryService.publicLog(eventName, data); @@ -103,8 +105,10 @@ export class WorkbenchAssignmentService extends BaseAssignmentService { }; type TASClientReadTreatmentClassification = { - treatmentValue: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; - treatmentName: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; + owner: 'sbatten'; + comment: 'Logged when a treatment value is read from the experiment service'; + treatmentValue: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The value of the read treatment' }; + treatmentName: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The name of the treatment that was read' }; }; this.telemetryService.publicLog2<TASClientReadTreatmentData, TASClientReadTreatmentClassification>('tasClientReadTreatmentComplete', diff --git a/src/vs/workbench/services/authentication/browser/authenticationService.ts b/src/vs/workbench/services/authentication/browser/authenticationService.ts index 5f7431dafe9..f543021d924 100644 --- a/src/vs/workbench/services/authentication/browser/authenticationService.ts +++ b/src/vs/workbench/services/authentication/browser/authenticationService.ts @@ -200,7 +200,7 @@ export class AuthenticationService extends Disposable implements IAuthentication this._placeholderMenuItem = MenuRegistry.appendMenuItem(MenuId.AccountsContext, { command: { id: 'noAuthenticationProviders', - title: nls.localize('loading', "Loading..."), + title: nls.localize('authentication.Placeholder', "No accounts requested yet..."), precondition: ContextKeyExpr.false() }, }); diff --git a/src/vs/workbench/services/configuration/common/configurationEditingService.ts b/src/vs/workbench/services/configuration/common/configurationEditingService.ts index 771c51dbe48..3e4e805c231 100644 --- a/src/vs/workbench/services/configuration/common/configurationEditingService.ts +++ b/src/vs/workbench/services/configuration/common/configurationEditingService.ts @@ -366,7 +366,7 @@ export class ConfigurationEditingService { case ConfigurationEditingErrorCode.ERROR_INVALID_USER_TARGET: return nls.localize('errorInvalidUserTarget', "Unable to write to User Settings because {0} does not support for global scope.", operation.key); case ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_TARGET: return nls.localize('errorInvalidWorkspaceTarget', "Unable to write to Workspace Settings because {0} does not support for workspace scope in a multi folder workspace.", operation.key); case ConfigurationEditingErrorCode.ERROR_INVALID_FOLDER_TARGET: return nls.localize('errorInvalidFolderTarget', "Unable to write to Folder Settings because no resource is provided."); - case ConfigurationEditingErrorCode.ERROR_INVALID_RESOURCE_LANGUAGE_CONFIGURATION: return nls.localize('errorInvalidResourceLanguageConfiguraiton', "Unable to write to Language Settings because {0} is not a resource language setting.", operation.key); + case ConfigurationEditingErrorCode.ERROR_INVALID_RESOURCE_LANGUAGE_CONFIGURATION: return nls.localize('errorInvalidResourceLanguageConfiguration', "Unable to write to Language Settings because {0} is not a resource language setting.", operation.key); case ConfigurationEditingErrorCode.ERROR_NO_WORKSPACE_OPENED: return nls.localize('errorNoWorkspaceOpened', "Unable to write to {0} because no workspace is opened. Please open a workspace first and try again.", this.stringifyTarget(target)); // User issues 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 61cc2fc1b51..db9b1eeff33 100644 --- a/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts +++ b/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts @@ -105,6 +105,47 @@ suite('WorkspaceContextService - Folder', () => { assert.strictEqual(actual, testObject.getWorkspace().folders[0]); }); + test('getWorkspaceFolder() - queries in workspace folder', async () => { + + const logService = new NullLogService(); + const fileService = disposables.add(new FileService(logService)); + const fileSystemProvider = disposables.add(new InMemoryFileSystemProvider()); + fileService.registerProvider(ROOT.scheme, fileSystemProvider); + + const folder = joinPath(ROOT, folderName).with({ query: 'myquery=1' }); + await fileService.createFolder(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(undefined, undefined), new SignService(undefined), new NullLogService()), new UriIdentityService(fileService), new NullLogService())); + await (<WorkspaceService>testObject).initialize(convertToWorkspacePayload(folder)); + + const actual = testObject.getWorkspaceFolder(joinPath(folder, 'a')); + + assert.strictEqual(actual, testObject.getWorkspace().folders[0]); + }); + + test('getWorkspaceFolder() - queries in resource', async () => { + + const logService = new NullLogService(); + const fileService = disposables.add(new FileService(logService)); + const fileSystemProvider = disposables.add(new InMemoryFileSystemProvider()); + fileService.registerProvider(ROOT.scheme, fileSystemProvider); + + const folder = joinPath(ROOT, folderName); + await fileService.createFolder(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(undefined, undefined), new SignService(undefined), new NullLogService()), new UriIdentityService(fileService), new NullLogService())); + await (<WorkspaceService>testObject).initialize(convertToWorkspacePayload(folder)); + + + const actual = testObject.getWorkspaceFolder(joinPath(folder, 'a').with({ query: 'myquery=1' })); + + assert.strictEqual(actual, testObject.getWorkspace().folders[0]); + }); + test('isCurrentWorkspace() => true', () => { assert.ok(testObject.isCurrentWorkspace(folder)); }); diff --git a/src/vs/workbench/services/configurationResolver/browser/baseConfigurationResolverService.ts b/src/vs/workbench/services/configurationResolver/browser/baseConfigurationResolverService.ts new file mode 100644 index 00000000000..b4f3c050e7e --- /dev/null +++ b/src/vs/workbench/services/configurationResolver/browser/baseConfigurationResolverService.ts @@ -0,0 +1,374 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { URI as uri } from 'vs/base/common/uri'; +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 { 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'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { AbstractVariableResolverService } from 'vs/workbench/services/configurationResolver/common/variableResolver'; +import { ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; +import { IQuickInputService, IInputOptions, IQuickPickItem, IPickOptions } from 'vs/platform/quickinput/common/quickInput'; +import { ConfiguredInput } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; +import { IProcessEnvironment } from 'vs/base/common/platform'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { IPathService } from 'vs/workbench/services/path/common/pathService'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; + +export abstract class BaseConfigurationResolverService extends AbstractVariableResolverService { + + static readonly INPUT_OR_COMMAND_VARIABLES_PATTERN = /\${((input|command):(.*?))}/g; + + constructor( + context: { + getAppRoot: () => string | undefined; + getExecPath: () => string | undefined; + }, + envVariablesPromise: Promise<IProcessEnvironment>, + editorService: IEditorService, + private readonly configurationService: IConfigurationService, + private readonly commandService: ICommandService, + private readonly workspaceContextService: IWorkspaceContextService, + private readonly quickInputService: IQuickInputService, + private readonly labelService: ILabelService, + private readonly pathService: IPathService, + extensionService: IExtensionService, + ) { + super({ + getFolderUri: (folderName: string): uri | undefined => { + const folder = workspaceContextService.getWorkspace().folders.filter(f => f.name === folderName).pop(); + return folder ? folder.uri : undefined; + }, + getWorkspaceFolderCount: (): number => { + return workspaceContextService.getWorkspace().folders.length; + }, + getConfigurationValue: (folderUri: uri | undefined, suffix: string): string | undefined => { + return configurationService.getValue<string>(suffix, folderUri ? { resource: folderUri } : {}); + }, + getAppRoot: (): string | undefined => { + return context.getAppRoot(); + }, + getExecPath: (): string | undefined => { + return context.getExecPath(); + }, + getFilePath: (): string | undefined => { + const fileResource = EditorResourceAccessor.getOriginalUri(editorService.activeEditor, { + supportSideBySide: SideBySideEditor.PRIMARY, + filterByScheme: [Schemas.file, Schemas.vscodeUserData, this.pathService.defaultUriScheme] + }); + if (!fileResource) { + return undefined; + } + return this.labelService.getUriLabel(fileResource, { noPrefix: true }); + }, + getWorkspaceFolderPathForFile: (): string | undefined => { + const fileResource = EditorResourceAccessor.getOriginalUri(editorService.activeEditor, { + supportSideBySide: SideBySideEditor.PRIMARY, + filterByScheme: [Schemas.file, Schemas.vscodeUserData, this.pathService.defaultUriScheme] + }); + if (!fileResource) { + return undefined; + } + const wsFolder = workspaceContextService.getWorkspaceFolder(fileResource); + if (!wsFolder) { + return undefined; + } + return this.labelService.getUriLabel(wsFolder.uri, { noPrefix: true }); + }, + getSelectedText: (): string | undefined => { + const activeTextEditorControl = editorService.activeTextEditorControl; + + let activeControl: ICodeEditor | null = null; + + if (isCodeEditor(activeTextEditorControl)) { + activeControl = activeTextEditorControl; + } else if (isDiffEditor(activeTextEditorControl)) { + const original = activeTextEditorControl.getOriginalEditor(); + const modified = activeTextEditorControl.getModifiedEditor(); + activeControl = original.hasWidgetFocus() ? original : modified; + } + + const activeModel = activeControl?.getModel(); + const activeSelection = activeControl?.getSelection(); + if (activeModel && activeSelection) { + return activeModel.getValueInRange(activeSelection); + } + return undefined; + }, + getLineNumber: (): string | undefined => { + const activeTextEditorControl = editorService.activeTextEditorControl; + if (isCodeEditor(activeTextEditorControl)) { + const selection = activeTextEditorControl.getSelection(); + if (selection) { + const lineNumber = selection.positionLineNumber; + return String(lineNumber); + } + } + return undefined; + }, + getExtension: id => { + return extensionService.getExtension(id); + }, + }, labelService, pathService.userHome().then(home => home.path), envVariablesPromise); + } + + public override async resolveWithInteractionReplace(folder: IWorkspaceFolder | undefined, config: any, section?: string, variables?: IStringDictionary<string>, target?: ConfigurationTarget): Promise<any> { + // resolve any non-interactive variables and any contributed variables + config = await this.resolveAnyAsync(folder, config); + + // resolve input variables in the order in which they are encountered + return this.resolveWithInteraction(folder, config, section, variables, target).then(mapping => { + // finally substitute evaluated command variables (if there are any) + if (!mapping) { + return null; + } else if (mapping.size > 0) { + return this.resolveAnyAsync(folder, config, fromMap(mapping)); + } else { + return config; + } + }); + } + + public override async resolveWithInteraction(folder: IWorkspaceFolder | undefined, config: any, section?: string, variables?: IStringDictionary<string>, target?: ConfigurationTarget): Promise<Map<string, string> | undefined> { + // resolve any non-interactive variables and any contributed variables + const resolved = await this.resolveAnyMap(folder, config); + config = resolved.newConfig; + const allVariableMapping: Map<string, string> = resolved.resolvedVariables; + + // resolve input and command variables in the order in which they are encountered + return this.resolveWithInputAndCommands(folder, config, variables, section, target).then(inputOrCommandMapping => { + if (this.updateMapping(inputOrCommandMapping, allVariableMapping)) { + return allVariableMapping; + } + return undefined; + }); + } + + /** + * Add all items from newMapping to fullMapping. Returns false if newMapping is undefined. + */ + private updateMapping(newMapping: IStringDictionary<string> | undefined, fullMapping: Map<string, string>): boolean { + if (!newMapping) { + return false; + } + forEach(newMapping, (entry) => { + fullMapping.set(entry.key, entry.value); + }); + return true; + } + + /** + * Finds and executes all input and command variables in the given configuration and returns their values as a dictionary. + * Please note: this method does not substitute the input or command variables (so the configuration is not modified). + * The returned dictionary can be passed to "resolvePlatform" for the actual substitution. + * See #6569. + * + * @param variableToCommandMap Aliases for commands + */ + private async resolveWithInputAndCommands(folder: IWorkspaceFolder | undefined, configuration: any, variableToCommandMap?: IStringDictionary<string>, section?: string, target?: ConfigurationTarget): Promise<IStringDictionary<string> | undefined> { + + if (!configuration) { + return Promise.resolve(undefined); + } + + // get all "inputs" + let inputs: ConfiguredInput[] = []; + if (this.workspaceContextService.getWorkbenchState() !== WorkbenchState.EMPTY && section) { + const overrides: IConfigurationOverrides = folder ? { resource: folder.uri } : {}; + let 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; + case ConfigurationTarget.WORKSPACE: inputs = (<any>result.workspaceValue)?.inputs; break; + default: inputs = (<any>result.workspaceFolderValue)?.inputs; + } + } else { + const valueResult = this.configurationService.getValue<any>(section, overrides); + if (valueResult) { + inputs = valueResult.inputs; + } + } + } + + // extract and dedupe all "input" and "command" variables and preserve their order in an array + const variables: string[] = []; + this.findVariables(configuration, variables); + + const variableValues: IStringDictionary<string> = Object.create(null); + + for (const variable of variables) { + + const [type, name] = variable.split(':', 2); + + let result: string | undefined; + + switch (type) { + + case 'input': + result = await this.showUserInput(name, inputs); + break; + + case 'command': { + // use the name as a command ID #12735 + const commandId = (variableToCommandMap ? variableToCommandMap[name] : undefined) || name; + result = await this.commandService.executeCommand(commandId, configuration); + if (typeof result !== 'string' && !Types.isUndefinedOrNull(result)) { + throw new Error(nls.localize('commandVariable.noStringType', "Cannot substitute command variable '{0}' because command did not return a result of type string.", commandId)); + } + break; + } + default: + // Try to resolve it as a contributed variable + if (this._contributedVariables.has(variable)) { + result = await this._contributedVariables.get(variable)!(); + } + } + + if (typeof result === 'string') { + variableValues[variable] = result; + } else { + return undefined; + } + } + + return variableValues; + } + + /** + * Recursively finds all command or input variables in object and pushes them into variables. + * @param object object is searched for variables. + * @param variables All found variables are returned in variables. + */ + private findVariables(object: any, variables: string[]) { + if (typeof object === 'string') { + let matches; + while ((matches = BaseConfigurationResolverService.INPUT_OR_COMMAND_VARIABLES_PATTERN.exec(object)) !== null) { + if (matches.length === 4) { + const command = matches[1]; + if (variables.indexOf(command) < 0) { + variables.push(command); + } + } + } + this._contributedVariables.forEach((value, contributed: string) => { + if ((variables.indexOf(contributed) < 0) && (object.indexOf('${' + contributed + '}') >= 0)) { + variables.push(contributed); + } + }); + } else if (Types.isArray(object)) { + object.forEach(value => { + this.findVariables(value, variables); + }); + } else if (object) { + Object.keys(object).forEach(key => { + const value = object[key]; + this.findVariables(value, variables); + }); + } + } + + /** + * Takes the provided input info and shows the quick pick so the user can provide the value for the input + * @param variable Name of the input variable. + * @param inputInfos Information about each possible input variable. + */ + private showUserInput(variable: string, inputInfos: ConfiguredInput[]): Promise<string | undefined> { + + if (!inputInfos) { + return Promise.reject(new Error(nls.localize('inputVariable.noInputSection', "Variable '{0}' must be defined in an '{1}' section of the debug or task configuration.", variable, 'input'))); + } + + // find info for the given input variable + const info = inputInfos.filter(item => item.id === variable).pop(); + if (info) { + + const missingAttribute = (attrName: string) => { + throw new Error(nls.localize('inputVariable.missingAttribute', "Input variable '{0}' is of type '{1}' and must include '{2}'.", variable, info.type, attrName)); + }; + + switch (info.type) { + + case 'promptString': { + if (!Types.isString(info.description)) { + missingAttribute('description'); + } + const inputOptions: IInputOptions = { prompt: info.description, ignoreFocusLost: true }; + if (info.default) { + inputOptions.value = info.default; + } + if (info.password) { + inputOptions.password = info.password; + } + return this.quickInputService.input(inputOptions).then(resolvedInput => { + return resolvedInput; + }); + } + + case 'pickString': { + if (!Types.isString(info.description)) { + missingAttribute('description'); + } + if (Types.isArray(info.options)) { + info.options.forEach(pickOption => { + if (!Types.isString(pickOption) && !Types.isString(pickOption.value)) { + missingAttribute('value'); + } + }); + } else { + missingAttribute('options'); + } + interface PickStringItem extends IQuickPickItem { + value: string; + } + const picks = new Array<PickStringItem>(); + info.options.forEach(pickOption => { + const value = Types.isString(pickOption) ? pickOption : pickOption.value; + const label = Types.isString(pickOption) ? undefined : pickOption.label; + + // If there is no label defined, use value as label + const item: PickStringItem = { + label: label ? `${label}: ${value}` : value, + value: value + }; + + if (value === info.default) { + item.description = nls.localize('inputVariable.defaultInputValue', "(Default)"); + picks.unshift(item); + } else { + picks.push(item); + } + }); + const pickOptions: IPickOptions<PickStringItem> = { placeHolder: info.description, matchOnDetail: true, ignoreFocusLost: true }; + return this.quickInputService.pick(picks, pickOptions, undefined).then(resolvedInput => { + if (resolvedInput) { + return resolvedInput.value; + } + return undefined; + }); + } + + case 'command': { + if (!Types.isString(info.command)) { + missingAttribute('command'); + } + return this.commandService.executeCommand<string>(info.command, info.args).then(result => { + if (typeof result === 'string' || Types.isUndefinedOrNull(result)) { + return result; + } + throw new Error(nls.localize('inputVariable.command.noStringType', "Cannot substitute input variable '{0}' because command '{1}' did not return a result of type string.", variable, info.command)); + }); + } + + default: + throw new Error(nls.localize('inputVariable.unknownType', "Input variable '{0}' can only be of type 'promptString', 'pickString', or 'command'.", variable)); + } + } + return Promise.reject(new Error(nls.localize('inputVariable.undefinedVariable', "Undefined input variable '{0}' encountered. Remove or define '{0}' to continue.", variable))); + } +} diff --git a/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts b/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts index acbac82cf51..4a7d00a5b4d 100644 --- a/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts +++ b/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts @@ -3,372 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { URI as uri } from 'vs/base/common/uri'; -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 { 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'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { AbstractVariableResolverService } from 'vs/workbench/services/configurationResolver/common/variableResolver'; -import { ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; -import { IQuickInputService, IInputOptions, IQuickPickItem, IPickOptions } from 'vs/platform/quickinput/common/quickInput'; -import { ConfiguredInput } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; -import { IProcessEnvironment } from 'vs/base/common/platform'; +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 { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { BaseConfigurationResolverService } from 'vs/workbench/services/configurationResolver/browser/baseConfigurationResolverService'; +import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; -export abstract class BaseConfigurationResolverService extends AbstractVariableResolverService { - - static readonly INPUT_OR_COMMAND_VARIABLES_PATTERN = /\${((input|command):(.*?))}/g; - - constructor( - context: { - getAppRoot: () => string | undefined; - getExecPath: () => string | undefined; - }, - envVariablesPromise: Promise<IProcessEnvironment>, - editorService: IEditorService, - private readonly configurationService: IConfigurationService, - private readonly commandService: ICommandService, - private readonly workspaceContextService: IWorkspaceContextService, - private readonly quickInputService: IQuickInputService, - private readonly labelService: ILabelService, - private readonly pathService: IPathService - ) { - super({ - getFolderUri: (folderName: string): uri | undefined => { - const folder = workspaceContextService.getWorkspace().folders.filter(f => f.name === folderName).pop(); - return folder ? folder.uri : undefined; - }, - getWorkspaceFolderCount: (): number => { - return workspaceContextService.getWorkspace().folders.length; - }, - getConfigurationValue: (folderUri: uri | undefined, suffix: string): string | undefined => { - return configurationService.getValue<string>(suffix, folderUri ? { resource: folderUri } : {}); - }, - getAppRoot: (): string | undefined => { - return context.getAppRoot(); - }, - getExecPath: (): string | undefined => { - return context.getExecPath(); - }, - getFilePath: (): string | undefined => { - const fileResource = EditorResourceAccessor.getOriginalUri(editorService.activeEditor, { - supportSideBySide: SideBySideEditor.PRIMARY, - filterByScheme: [Schemas.file, Schemas.vscodeUserData, this.pathService.defaultUriScheme] - }); - if (!fileResource) { - return undefined; - } - return this.labelService.getUriLabel(fileResource, { noPrefix: true }); - }, - getWorkspaceFolderPathForFile: (): string | undefined => { - const fileResource = EditorResourceAccessor.getOriginalUri(editorService.activeEditor, { - supportSideBySide: SideBySideEditor.PRIMARY, - filterByScheme: [Schemas.file, Schemas.vscodeUserData, this.pathService.defaultUriScheme] - }); - if (!fileResource) { - return undefined; - } - const wsFolder = workspaceContextService.getWorkspaceFolder(fileResource); - if (!wsFolder) { - return undefined; - } - return this.labelService.getUriLabel(wsFolder.uri, { noPrefix: true }); - }, - getSelectedText: (): string | undefined => { - const activeTextEditorControl = editorService.activeTextEditorControl; - - let activeControl: ICodeEditor | null = null; - - if (isCodeEditor(activeTextEditorControl)) { - activeControl = activeTextEditorControl; - } else if (isDiffEditor(activeTextEditorControl)) { - const original = activeTextEditorControl.getOriginalEditor(); - const modified = activeTextEditorControl.getModifiedEditor(); - activeControl = original.hasWidgetFocus() ? original : modified; - } - - const activeModel = activeControl?.getModel(); - const activeSelection = activeControl?.getSelection(); - if (activeModel && activeSelection) { - return activeModel.getValueInRange(activeSelection); - } - return undefined; - }, - getLineNumber: (): string | undefined => { - const activeTextEditorControl = editorService.activeTextEditorControl; - if (isCodeEditor(activeTextEditorControl)) { - const selection = activeTextEditorControl.getSelection(); - if (selection) { - const lineNumber = selection.positionLineNumber; - return String(lineNumber); - } - } - return undefined; - } - }, labelService, pathService.userHome().then(home => home.path), envVariablesPromise); - } - - public override async resolveWithInteractionReplace(folder: IWorkspaceFolder | undefined, config: any, section?: string, variables?: IStringDictionary<string>, target?: ConfigurationTarget): Promise<any> { - // resolve any non-interactive variables and any contributed variables - config = await this.resolveAnyAsync(folder, config); - - // resolve input variables in the order in which they are encountered - return this.resolveWithInteraction(folder, config, section, variables, target).then(mapping => { - // finally substitute evaluated command variables (if there are any) - if (!mapping) { - return null; - } else if (mapping.size > 0) { - return this.resolveAnyAsync(folder, config, fromMap(mapping)); - } else { - return config; - } - }); - } - - public override async resolveWithInteraction(folder: IWorkspaceFolder | undefined, config: any, section?: string, variables?: IStringDictionary<string>, target?: ConfigurationTarget): Promise<Map<string, string> | undefined> { - // resolve any non-interactive variables and any contributed variables - const resolved = await this.resolveAnyMap(folder, config); - config = resolved.newConfig; - const allVariableMapping: Map<string, string> = resolved.resolvedVariables; - - // resolve input and command variables in the order in which they are encountered - return this.resolveWithInputAndCommands(folder, config, variables, section, target).then(inputOrCommandMapping => { - if (this.updateMapping(inputOrCommandMapping, allVariableMapping)) { - return allVariableMapping; - } - return undefined; - }); - } - - /** - * Add all items from newMapping to fullMapping. Returns false if newMapping is undefined. - */ - private updateMapping(newMapping: IStringDictionary<string> | undefined, fullMapping: Map<string, string>): boolean { - if (!newMapping) { - return false; - } - forEach(newMapping, (entry) => { - fullMapping.set(entry.key, entry.value); - }); - return true; - } - - /** - * Finds and executes all input and command variables in the given configuration and returns their values as a dictionary. - * Please note: this method does not substitute the input or command variables (so the configuration is not modified). - * The returned dictionary can be passed to "resolvePlatform" for the actual substitution. - * See #6569. - * - * @param variableToCommandMap Aliases for commands - */ - private async resolveWithInputAndCommands(folder: IWorkspaceFolder | undefined, configuration: any, variableToCommandMap?: IStringDictionary<string>, section?: string, target?: ConfigurationTarget): Promise<IStringDictionary<string> | undefined> { - - if (!configuration) { - return Promise.resolve(undefined); - } - - // get all "inputs" - let inputs: ConfiguredInput[] = []; - if (this.workspaceContextService.getWorkbenchState() !== WorkbenchState.EMPTY && section) { - const overrides: IConfigurationOverrides = folder ? { resource: folder.uri } : {}; - let 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; - case ConfigurationTarget.WORKSPACE: inputs = (<any>result.workspaceValue)?.inputs; break; - default: inputs = (<any>result.workspaceFolderValue)?.inputs; - } - } else { - const valueResult = this.configurationService.getValue<any>(section, overrides); - if (valueResult) { - inputs = valueResult.inputs; - } - } - } - - // extract and dedupe all "input" and "command" variables and preserve their order in an array - const variables: string[] = []; - this.findVariables(configuration, variables); - - const variableValues: IStringDictionary<string> = Object.create(null); - - for (const variable of variables) { - - const [type, name] = variable.split(':', 2); - - let result: string | undefined; - - switch (type) { - - case 'input': - result = await this.showUserInput(name, inputs); - break; - - case 'command': { - // use the name as a command ID #12735 - const commandId = (variableToCommandMap ? variableToCommandMap[name] : undefined) || name; - result = await this.commandService.executeCommand(commandId, configuration); - if (typeof result !== 'string' && !Types.isUndefinedOrNull(result)) { - throw new Error(nls.localize('commandVariable.noStringType', "Cannot substitute command variable '{0}' because command did not return a result of type string.", commandId)); - } - break; - } - default: - // Try to resolve it as a contributed variable - if (this._contributedVariables.has(variable)) { - result = await this._contributedVariables.get(variable)!(); - } - } - - if (typeof result === 'string') { - variableValues[variable] = result; - } else { - return undefined; - } - } - - return variableValues; - } - - /** - * Recursively finds all command or input variables in object and pushes them into variables. - * @param object object is searched for variables. - * @param variables All found variables are returned in variables. - */ - private findVariables(object: any, variables: string[]) { - if (typeof object === 'string') { - let matches; - while ((matches = BaseConfigurationResolverService.INPUT_OR_COMMAND_VARIABLES_PATTERN.exec(object)) !== null) { - if (matches.length === 4) { - const command = matches[1]; - if (variables.indexOf(command) < 0) { - variables.push(command); - } - } - } - this._contributedVariables.forEach((value, contributed: string) => { - if ((variables.indexOf(contributed) < 0) && (object.indexOf('${' + contributed + '}') >= 0)) { - variables.push(contributed); - } - }); - } else if (Types.isArray(object)) { - object.forEach(value => { - this.findVariables(value, variables); - }); - } else if (object) { - Object.keys(object).forEach(key => { - const value = object[key]; - this.findVariables(value, variables); - }); - } - } - - /** - * Takes the provided input info and shows the quick pick so the user can provide the value for the input - * @param variable Name of the input variable. - * @param inputInfos Information about each possible input variable. - */ - private showUserInput(variable: string, inputInfos: ConfiguredInput[]): Promise<string | undefined> { - - if (!inputInfos) { - return Promise.reject(new Error(nls.localize('inputVariable.noInputSection', "Variable '{0}' must be defined in an '{1}' section of the debug or task configuration.", variable, 'input'))); - } - - // find info for the given input variable - const info = inputInfos.filter(item => item.id === variable).pop(); - if (info) { - - const missingAttribute = (attrName: string) => { - throw new Error(nls.localize('inputVariable.missingAttribute', "Input variable '{0}' is of type '{1}' and must include '{2}'.", variable, info.type, attrName)); - }; - - switch (info.type) { - - case 'promptString': { - if (!Types.isString(info.description)) { - missingAttribute('description'); - } - const inputOptions: IInputOptions = { prompt: info.description, ignoreFocusLost: true }; - if (info.default) { - inputOptions.value = info.default; - } - if (info.password) { - inputOptions.password = info.password; - } - return this.quickInputService.input(inputOptions).then(resolvedInput => { - return resolvedInput; - }); - } - - case 'pickString': { - if (!Types.isString(info.description)) { - missingAttribute('description'); - } - if (Types.isArray(info.options)) { - info.options.forEach(pickOption => { - if (!Types.isString(pickOption) && !Types.isString(pickOption.value)) { - missingAttribute('value'); - } - }); - } else { - missingAttribute('options'); - } - interface PickStringItem extends IQuickPickItem { - value: string; - } - const picks = new Array<PickStringItem>(); - info.options.forEach(pickOption => { - const value = Types.isString(pickOption) ? pickOption : pickOption.value; - const label = Types.isString(pickOption) ? undefined : pickOption.label; - - // If there is no label defined, use value as label - const item: PickStringItem = { - label: label ? `${label}: ${value}` : value, - value: value - }; - - if (value === info.default) { - item.description = nls.localize('inputVariable.defaultInputValue', "(Default)"); - picks.unshift(item); - } else { - picks.push(item); - } - }); - const pickOptions: IPickOptions<PickStringItem> = { placeHolder: info.description, matchOnDetail: true, ignoreFocusLost: true }; - return this.quickInputService.pick(picks, pickOptions, undefined).then(resolvedInput => { - if (resolvedInput) { - return resolvedInput.value; - } - return undefined; - }); - } - - case 'command': { - if (!Types.isString(info.command)) { - missingAttribute('command'); - } - return this.commandService.executeCommand<string>(info.command, info.args).then(result => { - if (typeof result === 'string' || Types.isUndefinedOrNull(result)) { - return result; - } - throw new Error(nls.localize('inputVariable.command.noStringType', "Cannot substitute input variable '{0}' because command '{1}' did not return a result of type string.", variable, info.command)); - }); - } - - default: - throw new Error(nls.localize('inputVariable.unknownType', "Input variable '{0}' can only be of type 'promptString', 'pickString', or 'command'.", variable)); - } - } - return Promise.reject(new Error(nls.localize('inputVariable.undefinedVariable', "Undefined input variable '{0}' encountered. Remove or define '{0}' to continue.", variable))); - } -} - export class ConfigurationResolverService extends BaseConfigurationResolverService { constructor( @@ -378,10 +24,13 @@ export class ConfigurationResolverService extends BaseConfigurationResolverServi @IWorkspaceContextService workspaceContextService: IWorkspaceContextService, @IQuickInputService quickInputService: IQuickInputService, @ILabelService labelService: ILabelService, - @IPathService pathService: IPathService + @IPathService pathService: IPathService, + @IExtensionService extensionService: IExtensionService, ) { super({ getAppRoot: () => undefined, getExecPath: () => undefined }, Promise.resolve(Object.create(null)), editorService, configurationService, - commandService, workspaceContextService, quickInputService, labelService, pathService); + commandService, workspaceContextService, quickInputService, labelService, pathService, extensionService); } } + +registerSingleton(IConfigurationResolverService, ConfigurationResolverService, true); diff --git a/src/vs/workbench/services/configurationResolver/common/configurationResolver.ts b/src/vs/workbench/services/configurationResolver/common/configurationResolver.ts index d8ba161425b..37a44d15196 100644 --- a/src/vs/workbench/services/configurationResolver/common/configurationResolver.ts +++ b/src/vs/workbench/services/configurationResolver/common/configurationResolver.ts @@ -14,7 +14,7 @@ export const IConfigurationResolverService = createDecorator<IConfigurationResol export interface IConfigurationResolverService { readonly _serviceBrand: undefined; - resolveWithEnvironment(environment: IProcessEnvironment, folder: IWorkspaceFolder | undefined, value: string): string; + resolveWithEnvironment(environment: IProcessEnvironment, folder: IWorkspaceFolder | undefined, value: string): Promise<string>; resolveAsync(folder: IWorkspaceFolder | undefined, value: string): Promise<string>; resolveAsync(folder: IWorkspaceFolder | undefined, value: string[]): Promise<string[]>; diff --git a/src/vs/workbench/services/configurationResolver/common/variableResolver.ts b/src/vs/workbench/services/configurationResolver/common/variableResolver.ts index a75a3c77ef7..7e557d33120 100644 --- a/src/vs/workbench/services/configurationResolver/common/variableResolver.ts +++ b/src/vs/workbench/services/configurationResolver/common/variableResolver.ts @@ -15,6 +15,7 @@ import { URI as uri } from 'vs/base/common/uri'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { ILabelService } from 'vs/platform/label/common/label'; +import { replaceAsync } from 'vs/base/common/strings'; export interface IVariableResolveContext { getFolderUri(folderName: string): uri | undefined; @@ -26,6 +27,7 @@ export interface IVariableResolveContext { getWorkspaceFolderPathForFile?(): string | undefined; getSelectedText(): string | undefined; getLineNumber(): string | undefined; + getExtension(id: string): Promise<{ readonly extensionLocation: uri } | undefined>; } type Environment = { env: IProcessEnvironment | undefined; userHome: string | undefined }; @@ -66,7 +68,7 @@ export class AbstractVariableResolverService implements IConfigurationResolverSe return envVariables; } - public resolveWithEnvironment(environment: IProcessEnvironment, root: IWorkspaceFolder | undefined, value: string): string { + public resolveWithEnvironment(environment: IProcessEnvironment, root: IWorkspaceFolder | undefined, value: string): Promise<string> { return this.recursiveResolve({ env: this.prepareEnv(environment), userHome: undefined }, root ? root.uri : undefined, value); } @@ -133,52 +135,53 @@ export class AbstractVariableResolverService implements IConfigurationResolverSe } } - private recursiveResolve(environment: Environment, folderUri: uri | undefined, value: any, commandValueMapping?: IStringDictionary<string>, resolvedVariables?: Map<string, string>): any { + private async recursiveResolve(environment: Environment, folderUri: uri | undefined, value: any, commandValueMapping?: IStringDictionary<string>, resolvedVariables?: Map<string, string>): Promise<any> { if (types.isString(value)) { return this.resolveString(environment, folderUri, value, commandValueMapping, resolvedVariables); } else if (types.isArray(value)) { - return value.map(s => this.recursiveResolve(environment, folderUri, s, commandValueMapping, resolvedVariables)); + 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); - Object.keys(value).forEach(key => { - const replaced = this.resolveString(environment, folderUri, key, commandValueMapping, resolvedVariables); - result[replaced] = this.recursiveResolve(environment, folderUri, value[key], commandValueMapping, resolvedVariables); - }); + 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; + })); + // two step process to preserve object key order + for (const [key, value] of replaced) { + result[key] = value; + } return result; } return value; } - private resolveString(environment: Environment, folderUri: uri | undefined, value: string, commandValueMapping: IStringDictionary<string> | undefined, resolvedVariables?: Map<string, string>): string { - + private resolveString(environment: Environment, folderUri: uri | undefined, value: string, commandValueMapping: IStringDictionary<string> | undefined, resolvedVariables?: Map<string, string>): Promise<string> { // loop through all variables occurrences in 'value' - const replaced = value.replace(AbstractVariableResolverService.VARIABLE_REGEXP, (match: string, variable: string) => { + return replaceAsync(value, AbstractVariableResolverService.VARIABLE_REGEXP, async (match: string, variable: string) => { // disallow attempted nesting, see #77289. This doesn't exclude variables that resolve to other variables. if (variable.includes(AbstractVariableResolverService.VARIABLE_LHS)) { return match; } - let resolvedValue = this.evaluateSingleVariable(environment, match, variable, folderUri, commandValueMapping); + let resolvedValue = await this.evaluateSingleVariable(environment, match, variable, folderUri, commandValueMapping); if (resolvedVariables) { resolvedVariables.set(variable, resolvedValue); } if ((resolvedValue !== match) && types.isString(resolvedValue) && resolvedValue.match(AbstractVariableResolverService.VARIABLE_REGEXP)) { - resolvedValue = this.resolveString(environment, folderUri, resolvedValue, commandValueMapping, resolvedVariables); + resolvedValue = await this.resolveString(environment, folderUri, resolvedValue, commandValueMapping, resolvedVariables); } return resolvedValue; }); - - return replaced; } private fsPath(displayUri: uri): string { return this._labelService ? this._labelService.getUriLabel(displayUri, { noPrefix: true }) : displayUri.fsPath; } - private evaluateSingleVariable(environment: Environment, match: string, variable: string, folderUri: uri | undefined, commandValueMapping: IStringDictionary<string> | undefined): string { + private async evaluateSingleVariable(environment: Environment, match: string, variable: string, folderUri: uri | undefined, commandValueMapping: IStringDictionary<string> | undefined): Promise<string> { // try to separate variable arguments from variable name let argument: string | undefined; @@ -268,6 +271,16 @@ export class AbstractVariableResolverService implements IConfigurationResolverSe case 'input': return this.resolveFromMap(match, argument, commandValueMapping, 'input'); + case 'extensionInstallFolder': + if (argument) { + const ext = await this._context.getExtension(argument); + if (!ext) { + throw new Error(localize('extensionNotInstalled', "Variable {0} can not be resolved because the extension {1} is not installed.", match, argument)); + } + return this.fsPath(ext.extensionLocation); + } + throw new Error(localize('missingExtensionName', "Variable {0} can not be resolved because no extension name is given.", match)); + default: { switch (variable) { diff --git a/src/vs/workbench/services/configurationResolver/electron-sandbox/configurationResolverService.ts b/src/vs/workbench/services/configurationResolver/electron-sandbox/configurationResolverService.ts index b98d3a68357..14a385e3e06 100644 --- a/src/vs/workbench/services/configurationResolver/electron-sandbox/configurationResolverService.ts +++ b/src/vs/workbench/services/configurationResolver/electron-sandbox/configurationResolverService.ts @@ -11,10 +11,11 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { BaseConfigurationResolverService } from 'vs/workbench/services/configurationResolver/browser/configurationResolverService'; +import { BaseConfigurationResolverService } from 'vs/workbench/services/configurationResolver/browser/baseConfigurationResolverService'; import { ILabelService } from 'vs/platform/label/common/label'; import { IShellEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/shellEnvironmentService'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; export class ConfigurationResolverService extends BaseConfigurationResolverService { @@ -27,7 +28,8 @@ export class ConfigurationResolverService extends BaseConfigurationResolverServi @IQuickInputService quickInputService: IQuickInputService, @ILabelService labelService: ILabelService, @IShellEnvironmentService shellEnvironmentService: IShellEnvironmentService, - @IPathService pathService: IPathService + @IPathService pathService: IPathService, + @IExtensionService extensionService: IExtensionService, ) { super({ getAppRoot: (): string | undefined => { @@ -35,9 +37,9 @@ export class ConfigurationResolverService extends BaseConfigurationResolverServi }, getExecPath: (): string | undefined => { return environmentService.execPath; - } + }, }, shellEnvironmentService.getShellEnv(), editorService, configurationService, commandService, - workspaceContextService, quickInputService, labelService, pathService); + workspaceContextService, quickInputService, labelService, pathService, extensionService); } } 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 014d9f42c43..330be4494f0 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 @@ -4,27 +4,30 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import { stub } from 'sinon'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { IPath, normalize } from 'vs/base/common/path'; import * as platform from 'vs/base/common/platform'; import { isObject } from 'vs/base/common/types'; -import { URI as uri } from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { Selection } from 'vs/editor/common/core/selection'; import { EditorType } from 'vs/editor/common/editorCommon'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { IFormatterChangeEvent, ILabelService, ResourceLabelFormatter } from 'vs/platform/label/common/label'; import { IWorkspace, IWorkspaceFolder, IWorkspaceIdentifier, Workspace } from 'vs/platform/workspace/common/workspace'; import { testWorkspace } from 'vs/platform/workspace/test/common/testWorkspace'; -import { BaseConfigurationResolverService } from 'vs/workbench/services/configurationResolver/browser/configurationResolverService'; +import { BaseConfigurationResolverService } from 'vs/workbench/services/configurationResolver/browser/baseConfigurationResolverService'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { NativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { TestEditorService, TestQuickInputService } from 'vs/workbench/test/browser/workbenchTestServices'; -import { TestContextService, TestProductService } from 'vs/workbench/test/common/workbenchTestServices'; +import { TestContextService, TestExtensionService, TestProductService } from 'vs/workbench/test/common/workbenchTestServices'; import { TestNativeWindowConfiguration } from 'vs/workbench/test/electron-browser/workbenchTestServices'; const mockLineNumber = 10; @@ -42,7 +45,7 @@ class TestEditorServiceWithActiveEditor extends TestEditorService { override get activeEditor(): any { return { get resource(): any { - return uri.parse('file:///VSCode/workspaceLocation/file'); + return URI.parse('file:///VSCode/workspaceLocation/file'); } }; } @@ -68,6 +71,7 @@ suite('Configuration Resolver Service', () => { let quickInputService: TestQuickInputService; let labelService: MockLabelService; let pathService: MockPathService; + let extensionService: IExtensionService; setup(() => { mockCommandService = new MockCommandService(); @@ -76,9 +80,10 @@ suite('Configuration Resolver Service', () => { environmentService = new MockWorkbenchEnvironmentService(envVariables); labelService = new MockLabelService(); pathService = new MockPathService(); - containingWorkspace = testWorkspace(uri.parse('file:///VSCode/workspaceLocation')); + extensionService = new TestExtensionService(); + containingWorkspace = testWorkspace(URI.parse('file:///VSCode/workspaceLocation')); workspace = containingWorkspace.folders[0]; - configurationResolverService = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), editorService, new MockInputsConfigurationService(), mockCommandService, new TestContextService(containingWorkspace), quickInputService, labelService, pathService); + configurationResolverService = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), editorService, new MockInputsConfigurationService(), mockCommandService, new TestContextService(containingWorkspace), quickInputService, labelService, pathService, extensionService); }); teardown(() => { @@ -185,6 +190,13 @@ suite('Configuration Resolver Service', () => { assert.strictEqual(await configurationResolverService!.resolveAsync(workspace, '${env:key1} ${env:key1${env:key2}}'), 'Value for key1 ${env:key1${env:key2}}'); }); + test('supports extensionDir', async () => { + const getExtension = stub(extensionService, 'getExtension'); + getExtension.withArgs('publisher.extId').returns(Promise.resolve({ extensionLocation: URI.file('/some/path') } as IExtensionDescription)); + + assert.strictEqual(await configurationResolverService!.resolveAsync(workspace, '${extensionInstallFolder:publisher.extId}'), URI.file('/some/path').fsPath); + }); + // test('substitute keys and values in object', () => { // const myObject = { // '${workspaceRootFolderName}': '${lineNumber}', @@ -217,7 +229,7 @@ suite('Configuration Resolver Service', () => { } }); - let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService); + let 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'); }); @@ -228,7 +240,7 @@ suite('Configuration Resolver Service', () => { } }); - let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService); + let 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'); }); @@ -245,7 +257,7 @@ suite('Configuration Resolver Service', () => { } }); - let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService); + let 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'); }); @@ -262,7 +274,7 @@ suite('Configuration Resolver Service', () => { } }); - let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService); + let 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,7 +295,7 @@ suite('Configuration Resolver Service', () => { } }); - let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService); + let 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 { @@ -317,7 +329,7 @@ suite('Configuration Resolver Service', () => { } }); - let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService); + let 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'); }); @@ -327,7 +339,7 @@ suite('Configuration Resolver Service', () => { editor: {} }); - let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService); + let 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'); }); @@ -340,7 +352,7 @@ suite('Configuration Resolver Service', () => { } }); - let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService); + let 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')); @@ -632,13 +644,13 @@ suite('Configuration Resolver Service', () => { }); }); - test('resolveWithEnvironment', () => { + test('resolveWithEnvironment', async () => { const env = { 'VAR_1': 'VAL_1', 'VAR_2': 'VAL_2' }; const configuration = 'echo ${env:VAR_1}${env:VAR_2}'; - const resolvedResult = configurationResolverService!.resolveWithEnvironment({ ...env }, undefined, configuration); + const resolvedResult = await configurationResolverService!.resolveWithEnvironment({ ...env }, undefined, configuration); assert.deepStrictEqual(resolvedResult, 'echo VAL_1VAL_2'); }); }); @@ -667,13 +679,13 @@ class MockCommandService implements ICommandService { class MockLabelService implements ILabelService { _serviceBrand: undefined; - getUriLabel(resource: uri, options?: { relative?: boolean | undefined; noPrefix?: boolean | undefined; endWithSeparator?: boolean | undefined }): string { + getUriLabel(resource: URI, options?: { relative?: boolean | undefined; noPrefix?: boolean | undefined }): string { return normalize(resource.fsPath); } - getUriBasenameLabel(resource: uri): string { + getUriBasenameLabel(resource: URI): string { throw new Error('Method not implemented.'); } - getWorkspaceLabel(workspace: uri | IWorkspaceIdentifier | IWorkspace, options?: { verbose: boolean }): string { + getWorkspaceLabel(workspace: URI | IWorkspaceIdentifier | IWorkspace, options?: { verbose: boolean }): string { throw new Error('Method not implemented.'); } getHostLabel(scheme: string, authority?: string): string { @@ -697,18 +709,21 @@ class MockPathService implements IPathService { throw new Error('Property not implemented'); } defaultUriScheme: string = Schemas.file; - fileURI(path: string): Promise<uri> { + fileURI(path: string): Promise<URI> { throw new Error('Method not implemented.'); } - async userHome(options?: { preferLocal: boolean }): Promise<uri> { - return uri.file('c:\\users\\username'); + userHome(options?: { preferLocal: boolean }): Promise<URI>; + userHome(options: { preferLocal: true }): URI; + userHome(options?: { preferLocal: boolean }): Promise<URI> | URI { + const uri = URI.file('c:\\users\\username'); + return options?.preferLocal ? uri : Promise.resolve(uri); } - hasValidBasename(resource: uri, basename?: string): Promise<boolean>; - hasValidBasename(resource: uri, os: platform.OperatingSystem, basename?: string): boolean; - hasValidBasename(resource: uri, arg2?: string | platform.OperatingSystem, name?: string): boolean | Promise<boolean> { + hasValidBasename(resource: URI, basename?: string): Promise<boolean>; + hasValidBasename(resource: URI, os: platform.OperatingSystem, basename?: string): boolean; + hasValidBasename(resource: URI, arg2?: string | platform.OperatingSystem, name?: string): boolean | Promise<boolean> { throw new Error('Method not implemented.'); } - resolvedUserHome: uri | undefined; + resolvedUserHome: URI | undefined; } class MockInputsConfigurationService extends TestConfigurationService { diff --git a/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts b/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts index 464e02e1d57..b491ce7079e 100644 --- a/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts +++ b/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts @@ -97,10 +97,24 @@ class NativeContextMenuService extends Disposable implements IContextMenuService let x: number; let y: number; - const zoom = getZoomFactor(); + let zoom = getZoomFactor(); if (dom.isHTMLElement(anchor)) { const elementPosition = dom.getDomNodePagePosition(anchor); + // When drawing context menus, we adjust the pixel position for native menus using zoom level + // In areas where zoom is applied to the element or its ancestors, we need to adjust accordingly + // e.g. The title bar has counter zoom behavior meaning it applies the inverse of zoom level. + // Window Zoom Level: 1.5, Title Bar Zoom: 1/1.5, Coordinate Multiplier: 1.5 * 1.0 / 1.5 = 1.0 + let testElement: HTMLElement | null = anchor; + do { + const elementZoomLevel = (dom.getComputedStyle(testElement) as any).zoom; + if (elementZoomLevel !== null && elementZoomLevel !== undefined && elementZoomLevel !== '1') { + zoom *= elementZoomLevel; + } + + testElement = testElement.parentElement; + } while (testElement !== null && testElement !== document.documentElement); + x = elementPosition.left; y = elementPosition.top + elementPosition.height; diff --git a/src/vs/workbench/services/credentials/test/browser/credentialsService.test.ts b/src/vs/workbench/services/credentials/test/browser/credentialsService.test.ts index 18ba51382bb..8d1e35b4997 100644 --- a/src/vs/workbench/services/credentials/test/browser/credentialsService.test.ts +++ b/src/vs/workbench/services/credentials/test/browser/credentialsService.test.ts @@ -6,8 +6,7 @@ import * as assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { BrowserCredentialsService } from 'vs/workbench/services/credentials/browser/credentialsService'; -import { TestRemoteAgentService } from 'vs/workbench/services/remote/test/common/testServices'; -import { TestEnvironmentService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestEnvironmentService, TestRemoteAgentService } from 'vs/workbench/test/browser/workbenchTestServices'; import { TestProductService } from 'vs/workbench/test/common/workbenchTestServices'; suite('CredentialsService - web', () => { diff --git a/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts b/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts index eb895e4de5e..1cb9e2a9af9 100644 --- a/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts +++ b/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts @@ -6,7 +6,7 @@ import * as nls from 'vs/nls'; import { IWindowOpenable, isWorkspaceToOpen, isFileToOpen } from 'vs/platform/window/common/window'; import { IPickAndOpenOptions, ISaveDialogOptions, IOpenDialogOptions, FileFilter, IFileDialogService, IDialogService, ConfirmResult, getFileNamesMessage } from 'vs/platform/dialogs/common/dialogs'; -import { isSavedWorkspace, IWorkspaceContextService, WorkbenchState, WORKSPACE_EXTENSION } from 'vs/platform/workspace/common/workspace'; +import { isSavedWorkspace, isTemporaryWorkspace, IWorkspaceContextService, WorkbenchState, WORKSPACE_EXTENSION } from 'vs/platform/workspace/common/workspace'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { URI } from 'vs/base/common/uri'; @@ -30,6 +30,7 @@ import { ICommandService } from 'vs/platform/commands/common/commands'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { EditorOpenSource } from 'vs/platform/editor/common/editor'; +import { ILogService } from 'vs/platform/log/common/log'; export abstract class AbstractFileDialogService implements IFileDialogService { @@ -51,7 +52,8 @@ export abstract class AbstractFileDialogService implements IFileDialogService { @IPathService private readonly pathService: IPathService, @ICommandService protected readonly commandService: ICommandService, @IEditorService protected readonly editorService: IEditorService, - @ICodeEditorService protected readonly codeEditorService: ICodeEditorService + @ICodeEditorService protected readonly codeEditorService: ICodeEditorService, + @ILogService private readonly logService: ILogService ) { } async defaultFilePath(schemeFilter = this.getSchemeFilterForWindow()): Promise<URI> { @@ -63,7 +65,7 @@ export abstract class AbstractFileDialogService implements IFileDialogService { if (!candidate) { candidate = this.historyService.getLastActiveWorkspaceRoot(schemeFilter); } else { - candidate = candidate && resources.dirname(candidate); + candidate = resources.dirname(candidate); } if (!candidate) { @@ -85,18 +87,19 @@ export abstract class AbstractFileDialogService implements IFileDialogService { if (!candidate) { return this.pathService.userHome({ preferLocal: schemeFilter === Schemas.file }); - } else { - return resources.dirname(candidate); } + + return resources.dirname(candidate); } - async defaultWorkspacePath(schemeFilter = this.getSchemeFilterForWindow(), filename?: string): Promise<URI> { + async defaultWorkspacePath(schemeFilter = this.getSchemeFilterForWindow()): Promise<URI> { let defaultWorkspacePath: URI | undefined; + // Check for current workspace config file first... if (this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) { const configuration = this.contextService.getWorkspace().configuration; - if (configuration && configuration.scheme === schemeFilter && isSavedWorkspace(configuration, this.environmentService)) { - defaultWorkspacePath = resources.dirname(configuration) || undefined; + if (configuration?.scheme === schemeFilter && isSavedWorkspace(configuration, this.environmentService) && !isTemporaryWorkspace(configuration)) { + defaultWorkspacePath = resources.dirname(configuration); } } @@ -105,21 +108,28 @@ export abstract class AbstractFileDialogService implements IFileDialogService { defaultWorkspacePath = await this.defaultFilePath(schemeFilter); } - if (defaultWorkspacePath && filename) { - defaultWorkspacePath = resources.joinPath(defaultWorkspacePath, filename); - } - return defaultWorkspacePath; } async showSaveConfirm(fileNamesOrResources: (string | URI)[]): Promise<ConfirmResult> { - if (this.environmentService.isExtensionDevelopment && this.environmentService.extensionTestsLocationURI) { - return ConfirmResult.DONT_SAVE; // no veto when we are in extension dev testing mode because we cannot assume we run interactive + if (this.skipDialogs()) { + this.logService.trace('FileDialogService: refused to show save confirmation dialog in tests.'); + + // no veto when we are in extension dev testing mode because we cannot assume we run interactive + return ConfirmResult.DONT_SAVE; } return this.doShowSaveConfirm(fileNamesOrResources); } + private skipDialogs(): boolean { + if (this.environmentService.isExtensionDevelopment && this.environmentService.extensionTestsLocationURI) { + return true; // integration tests + } + + return !!this.environmentService.enableSmokeTestDriver; // smoke tests + } + private async doShowSaveConfirm(fileNamesOrResources: (string | URI)[]): Promise<ConfirmResult> { if (fileNamesOrResources.length === 0) { return ConfirmResult.DONT_SAVE; @@ -195,8 +205,10 @@ export abstract class AbstractFileDialogService implements IFileDialogService { } protected addFileToRecentlyOpened(uri: URI): void { + // add the picked file into the list of recently opened // only if it is outside the currently opened workspace + if (!this.contextService.isInsideWorkspace(uri)) { this.workspacesService.addRecentlyOpened([{ fileUri: uri, label: this.labelService.getUriLabel(uri) }]); } diff --git a/src/vs/workbench/services/dialogs/browser/fileDialogService.ts b/src/vs/workbench/services/dialogs/browser/fileDialogService.ts index cbb7a21bcba..1fc0ccc3570 100644 --- a/src/vs/workbench/services/dialogs/browser/fileDialogService.ts +++ b/src/vs/workbench/services/dialogs/browser/fileDialogService.ts @@ -223,29 +223,28 @@ export class FileDialogService extends AbstractFileDialogService implements IFil // Otherwise inform the user about options const buttons = context === 'open' ? - [localize('openRemote', "Open Remote..."), localize('openFiles', "Open Files..."), localize('learnMore', "Learn More")] : + [localize('openRemote', "Open Remote..."), localize('learnMore', "Learn More"), localize('openFiles', "Open Files...")] : [localize('openRemote', "Open Remote..."), localize('learnMore', "Learn More")]; const res = await this.dialogService.show( Severity.Warning, - localize('unsupportedBrowserMessage', "Local File System Access is Unsupported"), + localize('unsupportedBrowserMessage', "Opening Local Folders is Unsupported"), buttons, { - detail: localize('unsupportedBrowserDetail', "Your current browser doesn't support local file system access.\nYou can either open single files or open a remote repository."), + detail: localize('unsupportedBrowserDetail', "Your browser doesn't support opening local folders.\nYou can either open single files or open a remote repository."), cancelId: -1 // no "Cancel" button offered } ); switch (res.choice) { - - // Open Remote... case 0: this.commandService.executeCommand('workbench.action.remote.showMenu'); break; - - // Open Files... (context === 'open') case 1: - if (context === 'open') { + this.openerService.open('https://aka.ms/VSCodeWebLocalFileSystemAccess'); + break; + case 2: + { const files = await triggerUpload(); if (files) { const filesData = (await this.instantiationService.invokeFunction(accessor => extractFileListData(accessor, files))).filter(fileData => !fileData.isDirectory); @@ -259,14 +258,7 @@ export class FileDialogService extends AbstractFileDialogService implements IFil })); } } - break; - } else { - // Fallthrough for "Learn More" } - - // Learn More - case 2: - this.openerService.open('https://aka.ms/VSCodeWebLocalFileSystemAccess'); break; } diff --git a/src/vs/workbench/services/dialogs/common/dialogService.ts b/src/vs/workbench/services/dialogs/common/dialogService.ts index d17f9fbbf5c..7360fe7d875 100644 --- a/src/vs/workbench/services/dialogs/common/dialogService.ts +++ b/src/vs/workbench/services/dialogs/common/dialogService.ts @@ -8,6 +8,8 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { IConfirmation, IConfirmationResult, IDialogOptions, IDialogService, IInput, IInputResult, IShowResult } from 'vs/platform/dialogs/common/dialogs'; import { DialogsModel } from 'vs/workbench/common/dialogs'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { ILogService } from 'vs/platform/log/common/log'; export class DialogService extends Disposable implements IDialogService { @@ -19,25 +21,58 @@ export class DialogService extends Disposable implements IDialogService { readonly onDidShowDialog = this.model.onDidShowDialog; + constructor( + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @ILogService private readonly logService: ILogService + ) { + super(); + } + + private skipDialogs(): boolean { + if (this.environmentService.isExtensionDevelopment && this.environmentService.extensionTestsLocationURI) { + return true; // integration tests + } + + return !!this.environmentService.enableSmokeTestDriver; // smoke tests + } + async confirm(confirmation: IConfirmation): Promise<IConfirmationResult> { + if (this.skipDialogs()) { + this.logService.trace('DialogService: refused to show confirmation dialog in tests.'); + + return { confirmed: true }; + } + const handle = this.model.show({ confirmArgs: { confirmation } }); return await handle.result as IConfirmationResult; } async show(severity: Severity, message: string, buttons?: string[], options?: IDialogOptions): Promise<IShowResult> { + if (this.skipDialogs()) { + throw new Error('DialogService: refused to show dialog in tests.'); + } + const handle = this.model.show({ showArgs: { severity, message, buttons, options } }); return await handle.result as IShowResult; } async input(severity: Severity, message: string, buttons: string[], inputs: IInput[], options?: IDialogOptions): Promise<IInputResult> { + if (this.skipDialogs()) { + throw new Error('DialogService: refused to show input dialog in tests.'); + } + const handle = this.model.show({ inputArgs: { severity, message, buttons, inputs, options } }); return await handle.result as IInputResult; } async about(): Promise<void> { + if (this.skipDialogs()) { + throw new Error('DialogService: refused to show about dialog in tests.'); + } + const handle = this.model.show({}); await handle.result; } diff --git a/src/vs/workbench/services/dialogs/electron-sandbox/fileDialogService.ts b/src/vs/workbench/services/dialogs/electron-sandbox/fileDialogService.ts index e4468a73200..dbd0f777b07 100644 --- a/src/vs/workbench/services/dialogs/electron-sandbox/fileDialogService.ts +++ b/src/vs/workbench/services/dialogs/electron-sandbox/fileDialogService.ts @@ -25,6 +25,7 @@ import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { ILogService } from 'vs/platform/log/common/log'; export class FileDialogService extends AbstractFileDialogService implements IFileDialogService { @@ -45,17 +46,18 @@ export class FileDialogService extends AbstractFileDialogService implements IFil @IPathService pathService: IPathService, @ICommandService commandService: ICommandService, @IEditorService editorService: IEditorService, - @ICodeEditorService codeEditorService: ICodeEditorService + @ICodeEditorService codeEditorService: ICodeEditorService, + @ILogService logService: ILogService ) { super(hostService, contextService, historyService, environmentService, instantiationService, - configurationService, fileService, openerService, dialogService, languageService, workspacesService, labelService, pathService, commandService, editorService, codeEditorService); + configurationService, fileService, openerService, dialogService, languageService, workspacesService, labelService, pathService, commandService, editorService, codeEditorService, logService); } private toNativeOpenDialogOptions(options: IPickAndOpenOptions): INativeOpenDialogOptions { return { forceNewWindow: options.forceNewWindow, telemetryExtraData: options.telemetryExtraData, - defaultPath: options.defaultUri && options.defaultUri.fsPath + defaultPath: options.defaultUri?.fsPath }; } @@ -140,7 +142,7 @@ export class FileDialogService extends AbstractFileDialogService implements IFil private toNativeSaveDialogOptions(options: ISaveDialogOptions): SaveDialogOptions { options.defaultUri = options.defaultUri ? URI.file(options.defaultUri.path) : undefined; return { - defaultPath: options.defaultUri && options.defaultUri.fsPath, + defaultPath: options.defaultUri?.fsPath, buttonLabel: options.saveLabel, filters: options.filters, title: options.title @@ -167,11 +169,9 @@ export class FileDialogService extends AbstractFileDialogService implements IFil return this.showOpenDialogSimplified(schema, options); } - const defaultUri = options.defaultUri; - const newOptions: OpenDialogOptions & { properties: string[] } = { title: options.title, - defaultPath: defaultUri && defaultUri.fsPath, + defaultPath: options.defaultUri?.fsPath, buttonLabel: options.openLabel, filters: options.filters, properties: [] diff --git a/src/vs/workbench/services/dialogs/test/electron-sandbox/fileDialogService.test.ts b/src/vs/workbench/services/dialogs/test/electron-sandbox/fileDialogService.test.ts index fbe3a518f48..d8ab94017c1 100644 --- a/src/vs/workbench/services/dialogs/test/electron-sandbox/fileDialogService.test.ts +++ b/src/vs/workbench/services/dialogs/test/electron-sandbox/fileDialogService.test.ts @@ -33,6 +33,7 @@ import { ICommandService } from 'vs/platform/commands/common/commands'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ILogService } from 'vs/platform/log/common/log'; class TestFileDialogService extends FileDialogService { constructor( @@ -53,10 +54,11 @@ class TestFileDialogService extends FileDialogService { @IPathService pathService: IPathService, @ICommandService commandService: ICommandService, @IEditorService editorService: IEditorService, - @ICodeEditorService codeEditorService: ICodeEditorService + @ICodeEditorService codeEditorService: ICodeEditorService, + @ILogService logService: ILogService ) { super(hostService, contextService, historyService, environmentService, instantiationService, configurationService, fileService, - openerService, nativeHostService, dialogService, languageService, workspacesService, labelService, pathService, commandService, editorService, codeEditorService); + openerService, nativeHostService, dialogService, languageService, workspacesService, labelService, pathService, commandService, editorService, codeEditorService, logService); } protected override getSimpleFileDialog() { @@ -125,7 +127,7 @@ suite('FileDialogService', function () { instantiationService.stub(IPathService, new class { defaultUriScheme: string = 'vscode-virtual-test'; userHome = async () => URI.file('/user/home'); - }); + } as IPathService); const dialogService = instantiationService.createInstance(TestFileDialogService, new TestSimpleFileDialog()); instantiationService.set(IFileDialogService, dialogService); const workspaceService: IWorkspaceEditingService = instantiationService.createInstance(BrowserWorkspaceEditingService); @@ -157,7 +159,7 @@ suite('FileDialogService', function () { instantiationService.stub(IPathService, new class { defaultUriScheme: string = Schemas.vscodeRemote; userHome = async () => URI.file('/user/home'); - }); + } as IPathService); const dialogService = instantiationService.createInstance(TestFileDialogService, new TestSimpleFileDialog()); instantiationService.set(IFileDialogService, dialogService); const workspaceService: IWorkspaceEditingService = instantiationService.createInstance(BrowserWorkspaceEditingService); diff --git a/src/vs/workbench/services/editor/browser/editorResolverService.ts b/src/vs/workbench/services/editor/browser/editorResolverService.ts index 9edd90ea440..481d60b1b3f 100644 --- a/src/vs/workbench/services/editor/browser/editorResolverService.ts +++ b/src/vs/workbench/services/editor/browser/editorResolverService.ts @@ -746,7 +746,9 @@ export class EditorResolverService extends Disposable implements IEditorResolver private sendEditorResolutionTelemetry(chosenInput: EditorInput): void { type editorResolutionClassification = { - viewType: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; owner: 'lramos15'; 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 undertsanding of what editors are most popular' }; + owner: 'lramos15'; + comment: 'An event that fires when an editor type is picked'; }; type editorResolutionEvent = { viewType: string; diff --git a/src/vs/workbench/services/editor/browser/editorService.ts b/src/vs/workbench/services/editor/browser/editorService.ts index 51015fde85a..4b5689f2e11 100644 --- a/src/vs/workbench/services/editor/browser/editorService.ts +++ b/src/vs/workbench/services/editor/browser/editorService.ts @@ -148,7 +148,7 @@ export class EditorService extends Disposable implements EditorServiceImpl { const groupDisposables = new DisposableStore(); groupDisposables.add(group.onDidModelChange(e => { - this._onDidEditorsChange.fire({ groupId: group.id, ...e }); + this._onDidEditorsChange.fire({ groupId: group.id, event: e }); })); groupDisposables.add(group.onDidActiveEditorChange(() => { diff --git a/src/vs/workbench/services/editor/common/editorGroupsService.ts b/src/vs/workbench/services/editor/common/editorGroupsService.ts index bdea82e86be..0802cbd5acf 100644 --- a/src/vs/workbench/services/editor/common/editorGroupsService.ts +++ b/src/vs/workbench/services/editor/common/editorGroupsService.ts @@ -652,7 +652,7 @@ export interface IEditorGroup { * * @returns a promise when all editors are closed. */ - closeAllEditors(options?: ICloseAllEditorsOptions): Promise<void>; + closeAllEditors(options?: ICloseAllEditorsOptions): Promise<boolean>; /** * Replaces editors in this group with the provided replacement. diff --git a/src/vs/workbench/services/editor/common/editorService.ts b/src/vs/workbench/services/editor/common/editorService.ts index 601636e9c48..32f2c303e1e 100644 --- a/src/vs/workbench/services/editor/common/editorService.ts +++ b/src/vs/workbench/services/editor/common/editorService.ts @@ -88,8 +88,15 @@ export interface IOpenEditorsOptions { readonly validateTrust?: boolean; } -export interface IEditorsChangeEvent extends IGroupModelChangeEvent { +export interface IEditorsChangeEvent { + /** + * The group which had the editor change + */ groupId: GroupIdentifier; + /* + * The event fired from the model + */ + event: IGroupModelChangeEvent; } export interface IEditorService { 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 c52507b6659..83497325fa8 100644 --- a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts @@ -894,6 +894,7 @@ suite('EditorGroupsService', () => { test('closeAllEditors - dirty editor handling', async () => { const [part, instantiationService] = await createPart(); + let closeResult = true; const accessor = instantiationService.createInstance(TestServiceAccessor); accessor.fileDialogService.setConfirmResult(ConfirmResult.DONT_SAVE); @@ -909,14 +910,16 @@ suite('EditorGroupsService', () => { await group.openEditor(input2); accessor.fileDialogService.setConfirmResult(ConfirmResult.CANCEL); - await group.closeAllEditors(); + closeResult = await group.closeAllEditors(); + assert.strictEqual(closeResult, false); assert.ok(!input1.gotDisposed); assert.ok(!input2.gotDisposed); accessor.fileDialogService.setConfirmResult(ConfirmResult.DONT_SAVE); - await group.closeAllEditors(); + closeResult = await group.closeAllEditors(); + assert.strictEqual(closeResult, true); assert.ok(input1.gotDisposed); assert.ok(input2.gotDisposed); }); 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 478a9c0fa50..12042e67927 100644 --- a/src/vs/workbench/services/editor/test/browser/editorService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorService.test.ts @@ -24,7 +24,7 @@ import { IWorkspaceTrustRequestService, WorkspaceTrustUriResponse } from 'vs/pla import { TestWorkspaceTrustRequestService } from 'vs/workbench/services/workspaces/test/common/testWorkspaceTrustService'; import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; -import { UnknownErrorEditor } from 'vs/workbench/browser/parts/editor/editorPlaceholder'; +import { ErrorPlaceholderEditor } from 'vs/workbench/browser/parts/editor/editorPlaceholder'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; @@ -2018,7 +2018,7 @@ suite('EditorService', () => { failingInput.setFailToOpen(); let failingEditor = await service.openEditor(failingInput); - assert.ok(failingEditor instanceof UnknownErrorEditor); + assert.ok(failingEditor instanceof ErrorPlaceholderEditor); }); test('openEditor shows placeholder when restoring fails', async function () { @@ -2032,7 +2032,7 @@ suite('EditorService', () => { failingInput.setFailToOpen(); let failingEditor = await service.openEditor(failingInput); - assert.ok(failingEditor instanceof UnknownErrorEditor); + assert.ok(failingEditor instanceof ErrorPlaceholderEditor); }); test('save, saveAll, revertAll', async function () { diff --git a/src/vs/workbench/services/environment/browser/environmentService.ts b/src/vs/workbench/services/environment/browser/environmentService.ts index aec5f09e2ec..d41839b87ba 100644 --- a/src/vs/workbench/services/environment/browser/environmentService.ts +++ b/src/vs/workbench/services/environment/browser/environmentService.ts @@ -17,6 +17,7 @@ import { parseLineAndColumnAware } from 'vs/base/common/extpath'; import { LogLevelToString } from 'vs/platform/log/common/log'; import { isUndefined } from 'vs/base/common/types'; import { refineServiceDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; export const IBrowserWorkbenchEnvironmentService = refineServiceDecorator<IEnvironmentService, IBrowserWorkbenchEnvironmentService>(IEnvironmentService); @@ -172,6 +173,9 @@ export class BrowserWorkbenchEnvironmentService implements IBrowserWorkbenchEnvi } @memoize + get enableSmokeTestDriver() { return this.options.developmentOptions?.enableSmokeTestDriver; } + + @memoize get disableExtensions() { return this.payload?.get('disableExtensions') === 'true'; } @memoize @@ -202,7 +206,7 @@ export class BrowserWorkbenchEnvironmentService implements IBrowserWorkbenchEnvi get logExtensionHostCommunication(): boolean { return this.payload?.get('logExtensionHostCommunication') === 'true'; } @memoize - get skipReleaseNotes(): boolean { return false; } + get skipReleaseNotes(): boolean { return this.payload?.get('skipReleaseNotes') === 'true'; } @memoize get skipWelcome(): boolean { return this.payload?.get('skipWelcome') === 'true'; } @@ -292,7 +296,7 @@ export class BrowserWorkbenchEnvironmentService implements IBrowserWorkbenchEnvi } @memoize - get filesToOpenOrCreate(): IPath[] | undefined { + get filesToOpenOrCreate(): IPath<ITextEditorOptions>[] | undefined { if (this.payload) { const fileToOpen = this.payload.get('openFile'); if (fileToOpen) { @@ -304,7 +308,9 @@ export class BrowserWorkbenchEnvironmentService implements IBrowserWorkbenchEnvi return [{ fileUri: fileUri.with({ path: pathColumnAware.path }), - selection: !isUndefined(pathColumnAware.line) ? { startLineNumber: pathColumnAware.line, startColumn: pathColumnAware.column || 1 } : undefined + options: { + selection: !isUndefined(pathColumnAware.line) ? { startLineNumber: pathColumnAware.line, startColumn: pathColumnAware.column || 1 } : undefined + } }]; } diff --git a/src/vs/workbench/services/environment/common/environmentService.ts b/src/vs/workbench/services/environment/common/environmentService.ts index 15e8bac2238..d9e2e3ba65e 100644 --- a/src/vs/workbench/services/environment/common/environmentService.ts +++ b/src/vs/workbench/services/environment/common/environmentService.ts @@ -39,6 +39,7 @@ export interface IWorkbenchEnvironmentService extends IEnvironmentService { // --- Development readonly debugRenderer: boolean; readonly logExtensionHostCommunication?: boolean; + readonly enableSmokeTestDriver?: boolean; // --- Editors to open readonly filesToOpenOrCreate?: IPath[] | undefined; diff --git a/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts b/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts index ab7626e36cb..f675b6800ed 100644 --- a/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts +++ b/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts @@ -103,6 +103,9 @@ export class NativeWorkbenchEnvironmentService extends AbstractNativeEnvironment get logExtensionHostCommunication(): boolean { return !!this.args.logExtensionHostCommunication; } @memoize + get enableSmokeTestDriver(): boolean { return !!this.args['enable-smoke-test-driver']; } + + @memoize get extensionEnabledProposedApi(): string[] | undefined { if (Array.isArray(this.args['enable-proposed-api'])) { return this.args['enable-proposed-api']; diff --git a/src/vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.ts b/src/vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.ts index c88dede88ba..089434c2a84 100644 --- a/src/vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.ts +++ b/src/vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.ts @@ -58,6 +58,8 @@ export class BuiltinExtensionsScannerService implements IBuiltinExtensionsScanne 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 })); } } diff --git a/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts b/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts index f57e3cb5db6..1040b35beff 100644 --- a/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts +++ b/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts @@ -182,6 +182,11 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench } async setEnablement(extensions: IExtension[], newState: EnablementState): Promise<boolean[]> { + await this.extensionsManager.whenInitialized(); + + if (newState === EnablementState.EnabledGlobally || newState === EnablementState.EnabledWorkspace) { + extensions.push(...this.getExtensionsToEnableRecursively(extensions, this.extensionsManager.extensions, newState, { dependencies: true, pack: true })); + } const workspace = newState === EnablementState.DisabledWorkspace || newState === EnablementState.EnabledWorkspace; for (const extension of extensions) { @@ -213,6 +218,33 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench return result; } + private getExtensionsToEnableRecursively(extensions: IExtension[], allExtensions: ReadonlyArray<IExtension>, enablementState: EnablementState, options: { dependencies: boolean; pack: boolean }, checked: IExtension[] = []): IExtension[] { + const toCheck = extensions.filter(e => checked.indexOf(e) === -1); + if (toCheck.length) { + for (const extension of toCheck) { + checked.push(extension); + } + const extensionsToDisable = allExtensions.filter(i => { + if (checked.indexOf(i) !== -1) { + return false; + } + if (this.getEnablementState(i) === enablementState) { + return false; + } + 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 []; + } + private _setUserEnablementState(extension: IExtension, newState: EnablementState): Promise<boolean> { const currentState = this._getUserEnablementState(extension.identifier); diff --git a/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts b/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts index 92ec5bd6f8d..8438e2f3377 100644 --- a/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts +++ b/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts @@ -36,8 +36,9 @@ import { IExtensionStorageService } from 'vs/platform/extensionManagement/common import { isNonEmptyArray } from 'vs/base/common/arrays'; import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { ExtensionManifestValidator } from 'vs/workbench/services/extensions/common/extensionPoints'; import { IProductService } from 'vs/platform/product/common/productService'; +import { validateExtensionManifest } from 'vs/platform/extensions/common/extensionValidator'; +import Severity from 'vs/base/common/severity'; type GalleryExtensionInfo = { readonly id: string; preRelease?: boolean; migrateStorageFrom?: string }; type ExtensionInfo = { readonly id: string; preRelease: boolean }; @@ -196,8 +197,6 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten const extension = await this.toScannedExtension(webExtension, true); if (extension.isValid || !scanOptions?.skipInvalidExtensions) { result.push(extension); - } else { - this.logService.info(`Ignoring additional builtin extension ${webExtension.identifier.id} because it is not valid.`, extension.validationMessages); } } catch (error) { this.logService.info(`Error while fetching the additional builtin extension ${location.toString()}.`, getErrorMessage(error)); @@ -225,8 +224,6 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten const extension = await this.toScannedExtension(webExtension, true); if (extension.isValid || !scanOptions?.skipInvalidExtensions) { result.push(extension); - } else { - this.logService.info(`Ignoring additional builtin extension ${webExtension.identifier.id} because it is not valid.`, extension.validationMessages); } } catch (error) { this.logService.info(`Ignoring additional builtin extension ${webExtension.identifier.id} because there is an error while converting it into scanned extension`, getErrorMessage(error)); @@ -493,19 +490,9 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten if (existing && semver.gt(existing.manifest.version, webExtension.version)) { continue; } - try { - const extension = await this.toScannedExtension(webExtension, false); - if (extension.isValid || !scanOptions?.skipInvalidExtensions) { - result.set(extension.identifier.id.toLowerCase(), extension); - } else { - this.logService.info(`Skipping user installed extension ${webExtension.identifier.id} because it is not valid.`, extension.validationMessages); - } - } catch (error) { - if (scanOptions?.bailOut) { - throw error; - } else { - this.logService.error(error, 'Error while scanning user extension', webExtension.identifier.id); - } + const extension = await this.toScannedExtension(webExtension, false); + if (extension.isValid || !scanOptions?.skipInvalidExtensions) { + result.set(extension.identifier.id.toLowerCase(), extension); } } return [...result.values()]; @@ -562,26 +549,50 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten private async toScannedExtension(webExtension: IWebExtension, isBuiltin: boolean, type: ExtensionType = ExtensionType.User): Promise<IScannedExtension> { const url = joinPath(webExtension.location, 'package.json'); - let content; + const validations: [Severity, string][] = []; + let content: string | undefined; try { content = await this.extensionResourceLoaderService.readExtensionResource(url); + if (!content) { + validations.push([Severity.Error, `Error while fetching package.json from the location '${url}'. Server returned no content`]); + } } catch (error) { - throw new Error(`Error while fetching package.json for extension '${webExtension.identifier.id}' from the location '${url}'. ${getErrorMessage(error)}`); + validations.push([Severity.Error, `Error while fetching package.json from the location '${url}'. ${getErrorMessage(error)}`]); } - if (!content) { - throw new Error(`Error while fetching package.json for extension '${webExtension.identifier.id}'. Server returned no content for the request '${url}'`); + let manifest: IExtensionManifest | null = null; + if (content) { + try { + manifest = JSON.parse(content); + } catch (error) { + validations.push([Severity.Error, `Error while parsing package.json. ${getErrorMessage(error)}`]); + } + } + + if (!manifest) { + const [publisher, name] = webExtension.identifier.id.split('.'); + manifest = { + name, + publisher, + version: webExtension.version, + engines: { vscode: '*' }, + }; } - let manifest: IExtensionManifest = JSON.parse(content); if (webExtension.packageNLSUri) { manifest = await this.translateManifest(manifest, webExtension.packageNLSUri); } const uuid = (<IGalleryMetadata | undefined>webExtension.metadata)?.id; - const validationMessages: string[] = []; - const isValid = ExtensionManifestValidator.isValidExtensionManifest(this.productService.version, this.productService.date, webExtension.location, manifest, false, validationMessages); + validations.push(...validateExtensionManifest(this.productService.version, this.productService.date, webExtension.location, manifest, false)); + let isValid = true; + for (const [severity, message] of validations) { + if (severity === Severity.Error) { + isValid = false; + this.logService.error(message); + } + } return { identifier: { id: webExtension.identifier.id, uuid: webExtension.identifier.uuid || uuid }, @@ -593,7 +604,7 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten changelogUrl: webExtension.changelogUri, metadata: webExtension.metadata, targetPlatform: TargetPlatform.WEB, - validationMessages, + validations, isValid }; } diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts index efb907e741c..c3ab212906b 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts @@ -140,11 +140,9 @@ export interface IWorkbenchExtensionEnablementService { export interface IScannedExtension extends IExtension { readonly metadata?: Metadata; - readonly isValid: boolean; - readonly validationMessages: readonly string[]; } -export type ScanOptions = { readonly bailOut?: boolean; readonly skipInvalidExtensions?: boolean }; +export type ScanOptions = { readonly skipInvalidExtensions?: boolean }; export const IWebExtensionsScannerService = createDecorator<IWebExtensionsScannerService>('IWebExtensionsScannerService'); export interface IWebExtensionsScannerService { diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index 1b34c6a0604..3c2475d48c0 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -21,7 +21,6 @@ import { IDownloadService } from 'vs/platform/download/common/download'; import { flatten } from 'vs/base/common/arrays'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import Severity from 'vs/base/common/severity'; -import { canceled } from 'vs/base/common/errors'; import { IUserDataSyncEnablementService, SyncResource } from 'vs/platform/userDataSync/common/userDataSync'; import { Promises } from 'vs/base/common/async'; import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; @@ -31,6 +30,7 @@ import { ICommandService } from 'vs/platform/commands/common/commands'; import { isUndefined } from 'vs/base/common/types'; import { IFileService } from 'vs/platform/files/common/files'; import { ILogService } from 'vs/platform/log/common/log'; +import { CancellationError } from 'vs/base/common/errors'; export class ExtensionManagementService extends Disposable implements IWorkbenchExtensionManagementService { @@ -74,8 +74,8 @@ export class ExtensionManagementService extends Disposable implements IWorkbench 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; } - async getInstalled(type?: ExtensionType, donotIgnoreInvalidExtensions?: boolean): Promise<ILocalExtension[]> { - const result = await Promise.all(this.servers.map(({ extensionManagementService }) => extensionManagementService.getInstalled(type, donotIgnoreInvalidExtensions))); + async getInstalled(type?: ExtensionType): Promise<ILocalExtension[]> { + const result = await Promise.all(this.servers.map(({ extensionManagementService }) => extensionManagementService.getInstalled(type))); return flatten(result); } @@ -368,7 +368,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench case 1: return true; } - throw canceled(); + throw new CancellationError(); } return false; } @@ -402,7 +402,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench }); if (trustState === undefined) { - throw canceled(); + throw new CancellationError(); } } } @@ -462,7 +462,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench // Unfortunately ICommandService cannot be used directly due to cyclic dependencies this.instantiationService.invokeFunction(accessor => accessor.get(ICommandService).executeCommand('extension.open', extension.identifier.id, 'extensionPack')); } - throw canceled(); + throw new CancellationError(); } diff --git a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts index c1380b91d5f..28a5b281506 100644 --- a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts @@ -45,14 +45,14 @@ export class WebExtensionManagementService extends AbstractExtensionManagementSe return false; } - async getInstalled(type?: ExtensionType, bailOut?: boolean): Promise<ILocalExtension[]> { + async getInstalled(type?: ExtensionType): Promise<ILocalExtension[]> { const extensions = []; if (type === undefined || type === ExtensionType.System) { const systemExtensions = await this.webExtensionsScannerService.scanSystemExtensions(); extensions.push(...systemExtensions); } if (type === undefined || type === ExtensionType.User) { - const userExtensions = await this.webExtensionsScannerService.scanUserExtensions({ bailOut }); + const userExtensions = await this.webExtensionsScannerService.scanUserExtensions(); extensions.push(...userExtensions); } return Promise.all(extensions.map(e => toLocalExtension(e))); diff --git a/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts index 6fd1cff558a..9888caf5571 100644 --- a/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts @@ -107,6 +107,10 @@ export class NativeRemoteExtensionManagementService extends ExtensionManagementC 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 { + /** If no compatible release version is found, check if the extension has a release version or not and throw relevant error */ + if (!includePreRelease && extension.properties.isPreReleaseVersion && (await this.galleryService.getExtensions([extension.identifier], CancellationToken.None))[0]) { + throw new ExtensionManagementError(localize('notFoundReleaseExtension', "Can't install release version of '{0}' extension because it has no release version.", extension.identifier.id), ExtensionManagementErrorCode.ReleaseVersionNotFound); + } 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); } 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 894fc37142d..660c2fcd2e7 100644 --- a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts +++ b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts @@ -386,6 +386,31 @@ suite('ExtensionEnablementService Test', () => { assert.strictEqual(testObject.getEnablementState(extension), EnablementState.EnabledGlobally); }); + test('test enable an extension also enables dependencies', async () => { + installed.push(...[aLocalExtension2('pub.a', { extensionDependencies: ['pub.b'] }), aLocalExtension('pub.b')]); + const target = installed[0]; + const dep = installed[1]; + await (<TestExtensionEnablementService>testObject).waitUntilInitialized(); + await testObject.setEnablement([dep, target], EnablementState.DisabledGlobally); + await testObject.setEnablement([target], EnablementState.EnabledGlobally); + assert.ok(testObject.isEnabled(target)); + assert.ok(testObject.isEnabled(dep)); + assert.strictEqual(testObject.getEnablementState(target), EnablementState.EnabledGlobally); + assert.strictEqual(testObject.getEnablementState(dep), EnablementState.EnabledGlobally); + }); + + test('test enable an extension also enables packed extensions', async () => { + installed.push(...[aLocalExtension2('pub.a', { extensionPack: ['pub.b'] }), aLocalExtension('pub.b')]); + const target = installed[0]; + const dep = installed[1]; + await testObject.setEnablement([dep, target], EnablementState.DisabledGlobally); + await testObject.setEnablement([target], EnablementState.EnabledGlobally); + assert.ok(testObject.isEnabled(target)); + assert.ok(testObject.isEnabled(dep)); + assert.strictEqual(testObject.getEnablementState(target), EnablementState.EnabledGlobally); + assert.strictEqual(testObject.getEnablementState(dep), EnablementState.EnabledGlobally); + }); + test('test remove an extension from disablement list when uninstalled', async () => { const extension = aLocalExtension('pub.a'); installed.push(extension); @@ -513,7 +538,7 @@ suite('ExtensionEnablementService Test', () => { const extension = aLocalExtension('pub.a'); installed.push(extension); - testObject.setEnablement([extension], EnablementState.EnabledWorkspace); + await testObject.setEnablement([extension], EnablementState.EnabledWorkspace); instantiationService.stub(IWorkbenchEnvironmentService, { enableExtensions: <readonly string[]>['pub.a'] } as IWorkbenchEnvironmentService); testObject = new TestExtensionEnablementService(instantiationService); @@ -525,7 +550,7 @@ suite('ExtensionEnablementService Test', () => { const extension = aLocalExtension('pub.a'); installed.push(extension); - testObject.setEnablement([extension], EnablementState.DisabledGlobally); + await testObject.setEnablement([extension], EnablementState.DisabledGlobally); instantiationService.stub(IWorkbenchEnvironmentService, { enableExtensions: <readonly string[]>['pub.a'] } as IWorkbenchEnvironmentService); testObject = new TestExtensionEnablementService(instantiationService); @@ -537,7 +562,7 @@ suite('ExtensionEnablementService Test', () => { const extension = aLocalExtension('pub.a'); installed.push(extension); - testObject.setEnablement([extension], EnablementState.DisabledWorkspace); + await testObject.setEnablement([extension], EnablementState.DisabledWorkspace); instantiationService.stub(IWorkbenchEnvironmentService, { enableExtensions: <readonly string[]>['pub.a'] } as IWorkbenchEnvironmentService); testObject = new TestExtensionEnablementService(instantiationService); diff --git a/src/vs/workbench/services/extensionResourceLoader/electron-sandbox/extensionResourceLoaderService.ts b/src/vs/workbench/services/extensionResourceLoader/electron-sandbox/extensionResourceLoaderService.ts index 471f36a8d0a..ee604f29b31 100644 --- a/src/vs/workbench/services/extensionResourceLoader/electron-sandbox/extensionResourceLoaderService.ts +++ b/src/vs/workbench/services/extensionResourceLoader/electron-sandbox/extensionResourceLoaderService.ts @@ -8,7 +8,7 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IFileService } from 'vs/platform/files/common/files'; import { AbstractExtensionResourceLoaderService, IExtensionResourceLoaderService } from 'vs/workbench/services/extensionResourceLoader/common/extensionResourceLoader'; import { IProductService } from 'vs/platform/product/common/productService'; -import { asText, IRequestService } from 'vs/platform/request/common/request'; +import { asTextOrError, IRequestService } from 'vs/platform/request/common/request'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -31,7 +31,7 @@ export class ExtensionResourceLoaderService extends AbstractExtensionResourceLoa if (this.isExtensionGalleryResource(uri)) { const headers = await this.getExtensionGalleryRequestHeaders(); const requestContext = await this._requestService.request({ url: uri.toString(), headers }, CancellationToken.None); - return (await asText(requestContext)) || ''; + return (await asTextOrError(requestContext)) || ''; } const result = await this._fileService.readFile(uri); return result.value.toString(); diff --git a/src/vs/workbench/services/extensions/browser/extensionService.ts b/src/vs/workbench/services/extensions/browser/extensionService.ts index 42a05f2344a..1384f29e5ac 100644 --- a/src/vs/workbench/services/extensions/browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/browser/extensionService.ts @@ -16,7 +16,7 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { AbstractExtensionService, ExtensionRunningPreference, extensionRunningPreferenceToString } from 'vs/workbench/services/extensions/common/abstractExtensionService'; import { RemoteExtensionHost, IRemoteExtensionHostDataProvider, IRemoteExtensionHostInitData } from 'vs/workbench/services/extensions/common/remoteExtensionHost'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; -import { IWebWorkerExtensionHostDataProvider, WebWorkerExtensionHost } from 'vs/workbench/services/extensions/browser/webWorkerExtensionHost'; +import { IWebWorkerExtensionHostDataProvider, IWebWorkerExtensionHostInitData, WebWorkerExtensionHost } from 'vs/workbench/services/extensions/browser/webWorkerExtensionHost'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ExtensionIdentifier, IExtensionDescription, IExtension, ExtensionType } from 'vs/platform/extensions/common/extensions'; import { ExtensionKind } from 'vs/platform/environment/common/environment'; @@ -108,12 +108,13 @@ export class ExtensionService extends AbstractExtensionService implements IExten private _createLocalExtensionHostDataProvider(desiredRunningLocation: ExtensionRunningLocation): IWebWorkerExtensionHostDataProvider { return { - getInitData: async () => { + getInitData: async (): Promise<IWebWorkerExtensionHostInitData> => { const allExtensions = await this.getExtensions(); const localWebWorkerExtensions = this._filterByRunningLocation(allExtensions, desiredRunningLocation); return { autoStart: true, - extensions: localWebWorkerExtensions + allExtensions: allExtensions, + myExtensions: localWebWorkerExtensions.map(extension => extension.identifier) }; } }; @@ -232,8 +233,8 @@ export class ExtensionService extends AbstractExtensionService implements IExten extensionHostLogsPath: remoteEnv.extensionHostLogsPath, globalStorageHome: remoteEnv.globalStorageHome, workspaceStorageHome: remoteEnv.workspaceStorageHome, - extensions: remoteExtensions, - allExtensions: this._registry.getAllExtensionDescriptions() + allExtensions: this._registry.getAllExtensionDescriptions(), + myExtensions: remoteExtensions.map(extension => extension.identifier), }; } diff --git a/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts b/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts index 85bff25d857..31b1091efce 100644 --- a/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts +++ b/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts @@ -12,11 +12,11 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { ILabelService } from 'vs/platform/label/common/label'; import { ILogService } from 'vs/platform/log/common/log'; -import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import * as platform from 'vs/base/common/platform'; import * as dom from 'vs/base/browser/dom'; import { URI } from 'vs/base/common/uri'; -import { IExtensionHost, ExtensionHostLogFileName, LocalWebWorkerRunningLocation } from 'vs/workbench/services/extensions/common/extensions'; +import { IExtensionHost, ExtensionHostLogFileName, LocalWebWorkerRunningLocation, ExtensionHostExtensions } from 'vs/workbench/services/extensions/common/extensions'; import { IProductService } from 'vs/platform/product/common/productService'; import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; import { joinPath } from 'vs/base/common/resources'; @@ -30,11 +30,11 @@ 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 { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; export interface IWebWorkerExtensionHostInitData { readonly autoStart: boolean; - readonly extensions: IExtensionDescription[]; + readonly allExtensions: IExtensionDescription[]; + readonly myExtensions: ExtensionIdentifier[]; } export interface IWebWorkerExtensionHostDataProvider { @@ -45,7 +45,7 @@ export class WebWorkerExtensionHost extends Disposable implements IExtensionHost public readonly remoteAuthority = null; public readonly lazyStart: boolean; - public readonly extensions = new ExtensionDescriptionRegistry([]); + public readonly extensions = new ExtensionHostExtensions(); private readonly _onDidExit = this._register(new Emitter<[number, string | null]>()); public readonly onExit: Event<[number, string | null]> = this._onDidExit.event; @@ -130,6 +130,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('aria-hidden', 'true'); iframe.style.display = 'none'; const vscodeWebWorkerExtHostId = generateUuid(); @@ -267,7 +268,7 @@ export class WebWorkerExtensionHost extends Disposable implements IExtensionHost private async _createExtHostInitData(): Promise<IExtensionHostInitData> { const [telemetryInfo, initData] = await Promise.all([this._telemetryService.getTelemetryInfo(), this._initDataProvider.getInitData()]); const workspace = this._contextService.getWorkspace(); - this.extensions.deltaExtensions(initData.extensions, []); + const deltaExtensions = this.extensions.set(initData.allExtensions, initData.myExtensions); return { commit: this._productService.commit, version: this._productService.version, @@ -289,9 +290,8 @@ export class WebWorkerExtensionHost extends Disposable implements IExtensionHost name: this._labelService.getWorkspaceLabel(workspace), transient: workspace.transient }, - resolvedExtensions: [], - hostExtensions: [], - extensions: this.extensions.getAllExtensionDescriptions(), + allExtensions: deltaExtensions.toAdd, + myExtensions: deltaExtensions.myToAdd, telemetryInfo, logLevel: this._logService.getLevel(), logsLocation: this._extensionHostLogsLocation, diff --git a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts index 86784926919..fc7e02bdcfc 100644 --- a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts +++ b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts @@ -32,7 +32,6 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; -import { ILog } from 'vs/workbench/services/extensions/common/extensionPoints'; import { dedupExtensions } from 'vs/workbench/services/extensions/common/extensionsUtil'; import { ApiProposalName, allApiProposals } from 'vs/workbench/services/extensions/common/extensionsApiProposals'; import { forEach } from 'vs/base/common/collections'; @@ -629,12 +628,10 @@ export abstract class AbstractExtensionService extends Disposable implements IEx await Promise.all(promises); } - private async _updateExtensionsOnExtHost(extensionHostManager: IExtensionHostManager, _toAdd: IExtensionDescription[], _toRemove: ExtensionIdentifier[], removedRunningLocation: Map<string, ExtensionRunningLocation | null>): Promise<void> { - const toAdd = filterByExtensionHostManager(_toAdd, this._runningLocation, extensionHostManager); - const toRemove = _filterByExtensionHostManager(_toRemove, extId => extId, removedRunningLocation, extensionHostManager); - if (toRemove.length > 0 || toAdd.length > 0) { - await extensionHostManager.deltaExtensions(toAdd, toRemove); - } + private async _updateExtensionsOnExtHost(extensionHostManager: IExtensionHostManager, toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[], removedRunningLocation: Map<string, ExtensionRunningLocation | null>): Promise<void> { + const myToAdd = filterByExtensionHostManager(toAdd, this._runningLocation, extensionHostManager); + const myToRemove = _filterByExtensionHostManager(toRemove, extId => extId, removedRunningLocation, extensionHostManager); + await extensionHostManager.deltaExtensions({ toRemove, toAdd, myToRemove, myToAdd: myToAdd.map(extension => extension.identifier) }); } public canAddExtension(extension: IExtensionDescription): boolean { @@ -1245,26 +1242,6 @@ export abstract class AbstractExtensionService extends Disposable implements IEx //#region Called by extension host - protected _createLogger(): ILog { - return { - error: (message: string | Error): void => { - if (this._isDev) { - this._notificationService.notify({ severity: Severity.Error, message }); - } - this._logService.error(message); - }, - warn: (message: string): void => { - if (this._isDev) { - this._notificationService.notify({ severity: Severity.Warning, message }); - } - this._logService.warn(message); - }, - info: (message: string): void => { - this._logService.info(message); - } - }; - } - private _acquireInternalAPI(): IInternalExtensionService { return { _activateById: (extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise<void> => { @@ -1329,7 +1306,6 @@ export abstract class AbstractExtensionService extends Disposable implements IEx } protected async _scanWebExtensions(): Promise<IExtensionDescription[]> { - const log = this._createLogger(); const system: IExtensionDescription[] = [], user: IExtensionDescription[] = [], development: IExtensionDescription[] = []; try { await Promise.all([ @@ -1338,9 +1314,9 @@ export abstract class AbstractExtensionService extends Disposable implements IEx this._webExtensionsScannerService.scanExtensionsUnderDevelopment().then(extensions => development.push(...extensions.map(e => toExtensionDescription(e, true)))) ]); } catch (error) { - log.error(error); + this._logService.error(error); } - return dedupExtensions(system, user, development, log); + return dedupExtensions(system, user, development, this._logService); } //#endregion diff --git a/src/vs/workbench/services/extensions/common/extensionDescriptionRegistry.ts b/src/vs/workbench/services/extensions/common/extensionDescriptionRegistry.ts index b3cb9b17813..274bc9c396e 100644 --- a/src/vs/workbench/services/extensions/common/extensionDescriptionRegistry.ts +++ b/src/vs/workbench/services/extensions/common/extensionDescriptionRegistry.ts @@ -61,10 +61,8 @@ export class ExtensionDescriptionRegistry { } } - public keepOnly(extensionIds: ExtensionIdentifier[]): void { - const toKeep = new Set<string>(); - extensionIds.forEach(extensionId => toKeep.add(ExtensionIdentifier.toKey(extensionId))); - this._extensionDescriptions = this._extensionDescriptions.filter(extension => toKeep.has(ExtensionIdentifier.toKey(extension.identifier))); + public set(extensionDescriptions: IExtensionDescription[]) { + this._extensionDescriptions = extensionDescriptions; this._initialize(); this._onDidChange.fire(undefined); } diff --git a/src/vs/workbench/services/extensions/common/extensionHostManager.ts b/src/vs/workbench/services/extensions/common/extensionHostManager.ts index e51f3d09cc6..eb4c4437b6a 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostManager.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostManager.ts @@ -19,13 +19,14 @@ import { registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { StopWatch } from 'vs/base/common/stopwatch'; import { VSBuffer } from 'vs/base/common/buffer'; -import { IExtensionHost, ExtensionHostKind, ActivationKind, extensionHostKindToString, ExtensionActivationReason, IInternalExtensionService, ExtensionRunningLocation } from 'vs/workbench/services/extensions/common/extensions'; +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 { URI } from 'vs/base/common/uri'; import { ILogService } from 'vs/platform/log/common/log'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IExtensionHostProxy, IResolveAuthorityResult } from 'vs/workbench/services/extensions/common/extensionHostProxy'; +import { IExtensionDescriptionDelta } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; // Enable to see detailed message communication between window and extension host const LOG_EXTENSION_HOST_COMMUNICATION = false; @@ -39,7 +40,7 @@ export interface IExtensionHostManager { dispose(): void; ready(): Promise<void>; representsRunningLocation(runningLocation: ExtensionRunningLocation): boolean; - deltaExtensions(toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[]): Promise<void>; + deltaExtensions(extensionsDelta: IExtensionDescriptionDelta): Promise<void>; containsExtension(extensionId: ExtensionIdentifier): boolean; activate(extension: ExtensionIdentifier, reason: ExtensionActivationReason): Promise<boolean>; activateByEvent(activationEvent: string, activationKind: ActivationKind): Promise<void>; @@ -50,7 +51,7 @@ export interface IExtensionHostManager { * Returns `null` if no resolver for `remoteAuthority` is found. */ getCanonicalURI(remoteAuthority: string, uri: URI): Promise<URI | null>; - start(enabledExtensionIds: ExtensionIdentifier[]): Promise<void>; + start(allExtensions: IExtensionDescription[], myExtensions: ExtensionIdentifier[]): Promise<void>; extensionTestsExecute(): Promise<number>; extensionTestsSendExit(exitCode: number): Promise<void>; setRemoteEnvironment(env: { [key: string]: string | null }): Promise<void>; @@ -272,6 +273,7 @@ class ExtensionHostManager extends Disposable implements IExtensionHostManager { extensionHostKind: this.kind, getProxy: <T>(identifier: ProxyIdentifier<T>): Proxied<T> => this._rpcProtocol!.getProxy(identifier), set: <T, R extends T>(identifier: ProxyIdentifier<T>, instance: R): R => this._rpcProtocol!.set(identifier, instance), + dispose: (): void => this._rpcProtocol!.dispose(), assertRegistered: (identifiers: ProxyIdentifier<any>[]): void => this._rpcProtocol!.assertRegistered(identifiers), drain: (): Promise<void> => this._rpcProtocol!.drain(), @@ -397,13 +399,13 @@ class ExtensionHostManager extends Disposable implements IExtensionHostManager { return proxy.getCanonicalURI(remoteAuthority, uri); } - public async start(enabledExtensionIds: ExtensionIdentifier[]): Promise<void> { + public async start(allExtensions: IExtensionDescription[], myExtensions: ExtensionIdentifier[]): Promise<void> { const proxy = await this._proxy; if (!proxy) { return; } - this._extensionHost.extensions.keepOnly(enabledExtensionIds); - return proxy.startExtensionHost(enabledExtensionIds); + const deltaExtensions = this._extensionHost.extensions.set(allExtensions, myExtensions); + return proxy.startExtensionHost(deltaExtensions); } public async extensionTestsExecute(): Promise<number> { @@ -432,13 +434,13 @@ class ExtensionHostManager extends Disposable implements IExtensionHostManager { return this._extensionHost.runningLocation.equals(runningLocation); } - public async deltaExtensions(toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[]): Promise<void> { + public async deltaExtensions(extensionsDelta: IExtensionDescriptionDelta): Promise<void> { const proxy = await this._proxy; if (!proxy) { return; } - this._extensionHost.extensions.deltaExtensions(toAdd, toRemove); - return proxy.deltaExtensions(toAdd, toRemove); + this._extensionHost.extensions.delta(extensionsDelta); + return proxy.deltaExtensions(extensionsDelta); } public containsExtension(extensionId: ExtensionIdentifier): boolean { @@ -467,6 +469,7 @@ class LazyStartExtensionHostManager extends Disposable implements IExtensionHost private readonly _extensionHost: IExtensionHost; private _startCalled: Barrier; private _actual: ExtensionHostManager | null; + private _lazyStartExtensions: ExtensionHostExtensions | null; public get kind(): ExtensionHostKind { return this._extensionHost.runningLocation.kind; @@ -484,6 +487,7 @@ class LazyStartExtensionHostManager extends Disposable implements IExtensionHost this.onDidExit = extensionHost.onExit; this._startCalled = new Barrier(); this._actual = null; + this._lazyStartExtensions = null; } private _createActual(reason: string): ExtensionHostManager { @@ -499,7 +503,7 @@ class LazyStartExtensionHostManager extends Disposable implements IExtensionHost return this._actual; } const actual = this._createActual(reason); - await actual.start([]); + await actual.start([], []); return actual; } @@ -512,13 +516,17 @@ class LazyStartExtensionHostManager extends Disposable implements IExtensionHost public representsRunningLocation(runningLocation: ExtensionRunningLocation): boolean { return this._extensionHost.runningLocation.equals(runningLocation); } - public async deltaExtensions(toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[]): Promise<void> { + public async deltaExtensions(extensionsDelta: IExtensionDescriptionDelta): Promise<void> { await this._startCalled.wait(); - const extensionHostAlreadyStarted = Boolean(this._actual); - const shouldStartExtensionHost = (toAdd.length > 0); - if (extensionHostAlreadyStarted || shouldStartExtensionHost) { - const actual = await this._getOrCreateActualAndStart(`contains ${toAdd.length} new extension(s) (installed or enabled): ${toAdd.map(ext => ext.identifier.value)}`); - return actual.deltaExtensions(toAdd, toRemove); + if (this._actual) { + return this._actual.deltaExtensions(extensionsDelta); + } + this._lazyStartExtensions!.delta(extensionsDelta); + if (extensionsDelta.myToAdd.length > 0) { + const actual = this._createActual(`contains ${extensionsDelta.myToAdd.length} new extension(s) (installed or enabled): ${extensionsDelta.myToAdd.map(extId => extId.value)}`); + const { toAdd, myToAdd } = this._lazyStartExtensions!.toDelta(); + actual.start(toAdd, myToAdd); + return; } } public containsExtension(extensionId: ExtensionIdentifier): boolean { @@ -581,15 +589,17 @@ class LazyStartExtensionHostManager extends Disposable implements IExtensionHost } throw new Error(`Cannot resolve canonical URI`); } - public async start(enabledExtensionIds: ExtensionIdentifier[]): Promise<void> { - if (enabledExtensionIds.length > 0) { + public async start(allExtensions: IExtensionDescription[], myExtensions: ExtensionIdentifier[]): Promise<void> { + if (myExtensions.length > 0) { // there are actual extensions, so let's launch the extension host - const actual = this._createActual(`contains ${enabledExtensionIds.length} extension(s): ${enabledExtensionIds.map(extId => extId.value)}.`); - const result = actual.start(enabledExtensionIds); + const actual = this._createActual(`contains ${myExtensions.length} extension(s): ${myExtensions.map(extId => extId.value)}.`); + const result = actual.start(allExtensions, myExtensions); this._startCalled.open(); return result; } - // there are no actual extensions + // there are no actual extensions running, store extensions in `this._lazyStartExtensions` + this._lazyStartExtensions = new ExtensionHostExtensions(); + this._lazyStartExtensions.set(allExtensions, myExtensions); this._startCalled.open(); } public async extensionTestsExecute(): Promise<number> { diff --git a/src/vs/workbench/services/extensions/common/extensionHostProtocol.ts b/src/vs/workbench/services/extensions/common/extensionHostProtocol.ts index a090dced1df..ca72df6f92f 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostProtocol.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostProtocol.ts @@ -10,15 +10,21 @@ import { LogLevel } from 'vs/platform/log/common/log'; import { IRemoteConnectionData } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { ITelemetryInfo } from 'vs/platform/telemetry/common/telemetry'; +export interface IExtensionDescriptionDelta { + readonly toRemove: ExtensionIdentifier[]; + readonly toAdd: IExtensionDescription[]; + readonly myToRemove: ExtensionIdentifier[]; + readonly myToAdd: ExtensionIdentifier[]; +} + export interface IExtensionHostInitData { version: string; commit?: string; parentPid: number; environment: IEnvironment; workspace?: IStaticWorkspaceData | null; - resolvedExtensions: ExtensionIdentifier[]; - hostExtensions: ExtensionIdentifier[]; - extensions: IExtensionDescription[]; + allExtensions: IExtensionDescription[]; + myExtensions: ExtensionIdentifier[]; telemetryInfo: ITelemetryInfo; logLevel: LogLevel; logsLocation: URI; diff --git a/src/vs/workbench/services/extensions/common/extensionHostProxy.ts b/src/vs/workbench/services/extensions/common/extensionHostProxy.ts index d021dda0f0d..b528989a727 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostProxy.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostProxy.ts @@ -5,8 +5,9 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { URI } from 'vs/base/common/uri'; -import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { IRemoteConnectionData, RemoteAuthorityResolverErrorCode, ResolverResult } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { IExtensionDescriptionDelta } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; import { ActivationKind, ExtensionActivationReason } from 'vs/workbench/services/extensions/common/extensions'; export interface IResolveAuthorityErrorResult { @@ -31,14 +32,14 @@ export interface IExtensionHostProxy { * Returns `null` if no resolver for `remoteAuthority` is found. */ getCanonicalURI(remoteAuthority: string, uri: URI): Promise<URI | null>; - startExtensionHost(enabledExtensionIds: ExtensionIdentifier[]): Promise<void>; + 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>; updateRemoteConnectionData(connectionData: IRemoteConnectionData): Promise<void>; - deltaExtensions(toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[]): Promise<void>; + deltaExtensions(extensionsDelta: IExtensionDescriptionDelta): Promise<void>; test_latency(n: number): Promise<number>; test_up(b: VSBuffer): Promise<number>; test_down(size: number): Promise<VSBuffer>; diff --git a/src/vs/workbench/services/extensions/common/extensionPoints.ts b/src/vs/workbench/services/extensions/common/extensionPoints.ts deleted file mode 100644 index 892afa14c54..00000000000 --- a/src/vs/workbench/services/extensions/common/extensionPoints.ts +++ /dev/null @@ -1,750 +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 nls from 'vs/nls'; -import * as path from 'vs/base/common/path'; -import * as resources from 'vs/base/common/resources'; -import * as semver from 'vs/base/common/semver/semver'; -import * as json from 'vs/base/common/json'; -import * as arrays from 'vs/base/common/arrays'; -import { getParseErrorMessage } from 'vs/base/common/jsonErrorMessages'; -import * as types from 'vs/base/common/types'; -import { URI } from 'vs/base/common/uri'; -import { getGalleryExtensionId, getExtensionId, ExtensionKey } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { isValidExtensionVersion } from 'vs/platform/extensions/common/extensionValidator'; -import { ExtensionIdentifier, IExtensionDescription, IExtensionManifest, IRelaxedExtensionDescription, TargetPlatform, UNDEFINED_PUBLISHER } from 'vs/platform/extensions/common/extensions'; -import { Metadata } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { FileOperationResult, IFileService, toFileOperationResult } from 'vs/platform/files/common/files'; - -const MANIFEST_FILE = 'package.json'; - -export interface Translations { - [id: string]: string; -} - -export namespace Translations { - export function equals(a: Translations, b: Translations): boolean { - if (a === b) { - return true; - } - let aKeys = Object.keys(a); - let bKeys: Set<string> = new Set<string>(); - for (let key of Object.keys(b)) { - bKeys.add(key); - } - if (aKeys.length !== bKeys.size) { - return false; - } - - for (let key of aKeys) { - if (a[key] !== b[key]) { - return false; - } - bKeys.delete(key); - } - return bKeys.size === 0; - } -} - -export interface ILog { - error(message: string | Error): void; - warn(message: string): void; - info(message: string): void; -} - -export interface NlsConfiguration { - readonly devMode: boolean; - readonly locale: string | undefined; - readonly pseudo: boolean; - readonly translations: Translations; -} - -abstract class ExtensionManifestHandler { - - protected readonly _absoluteManifestPath: string; - - constructor( - protected readonly _ourVersion: string, - protected readonly _ourProductDate: string | undefined, - protected readonly _absoluteFolderPath: string, - protected readonly _isBuiltin: boolean, - protected readonly _isUnderDevelopment: boolean, - protected readonly _log: ILog, - protected readonly _fileService: IFileService - ) { - this._absoluteManifestPath = path.join(this._absoluteFolderPath, MANIFEST_FILE); - } - - protected _error(source: string, message: string): void { - this._log.error(`[${source}]: ${message}`); - } - - protected _warn(source: string, message: string): void { - this._log.warn(`[${source}]: ${message}`); - } - -} - -class ExtensionManifestParser extends ExtensionManifestHandler { - - private static _fastParseJSON<T>(text: string, errors: json.ParseError[]): T { - try { - return JSON.parse(text); - } catch (err) { - // invalid JSON, let's get good errors - return json.parse(text, errors); - } - } - - public parse(): Promise<IExtensionDescription | null> { - return readFile(this._fileService, this._absoluteManifestPath).then((manifestContents) => { - const errors: json.ParseError[] = []; - const manifest = ExtensionManifestParser._fastParseJSON<IRelaxedExtensionDescription & { __metadata?: Metadata }>(manifestContents, errors); - if (json.getNodeType(manifest) !== 'object') { - this._error(this._absoluteFolderPath, nls.localize('jsonParseInvalidType', "Invalid manifest file {0}: Not an JSON object.", this._absoluteManifestPath)); - } else if (errors.length === 0) { - manifest.uuid = manifest.__metadata?.id; - manifest.targetPlatform = manifest.__metadata?.targetPlatform ?? TargetPlatform.UNDEFINED; - manifest.isUserBuiltin = !!manifest.__metadata?.isBuiltin; - delete manifest.__metadata; - return manifest; - } else { - errors.forEach(e => { - this._error(this._absoluteFolderPath, nls.localize('jsonParseFail', "Failed to parse {0}: [{1}, {2}] {3}.", this._absoluteManifestPath, e.offset, e.length, getParseErrorMessage(e.error))); - }); - } - return null; - }, (err) => { - if (err.code === 'ENOENT') { - return null; - } - - this._error(this._absoluteFolderPath, nls.localize('fileReadFail', "Cannot read file {0}: {1}.", this._absoluteManifestPath, err.message)); - return null; - }); - } -} - -interface MessageBag { - [key: string]: string | { message: string; comment: string[] }; -} - -interface TranslationBundle { - contents: { - package: MessageBag; - }; -} - -interface LocalizedMessages { - values: MessageBag | undefined; - default: string | null; -} - -class ExtensionManifestNLSReplacer extends ExtensionManifestHandler { - - private readonly _nlsConfig: NlsConfiguration; - - constructor( - ourVersion: string, - ourProductDate: string | undefined, - absoluteFolderPath: string, - isBuiltin: boolean, - isUnderDevelopment: boolean, - nlsConfig: NlsConfiguration, - log: ILog, - fileService: IFileService - ) { - super(ourVersion, ourProductDate, absoluteFolderPath, isBuiltin, isUnderDevelopment, log, fileService); - this._nlsConfig = nlsConfig; - } - - public replaceNLS(extensionDescription: IExtensionDescription): Promise<IExtensionDescription> { - const reportErrors = (localized: string | null, errors: json.ParseError[]): void => { - errors.forEach((error) => { - this._error(this._absoluteFolderPath, nls.localize('jsonsParseReportErrors', "Failed to parse {0}: {1}.", localized, getParseErrorMessage(error.error))); - }); - }; - const reportInvalidFormat = (localized: string | null): void => { - this._error(this._absoluteFolderPath, nls.localize('jsonInvalidFormat', "Invalid format {0}: JSON object expected.", localized)); - }; - - let extension = path.extname(this._absoluteManifestPath); - let basename = this._absoluteManifestPath.substr(0, this._absoluteManifestPath.length - extension.length); - - const translationId = `${extensionDescription.publisher}.${extensionDescription.name}`; - let translationPath = this._nlsConfig.translations[translationId]; - let localizedMessages: Promise<LocalizedMessages | undefined>; - if (translationPath) { - localizedMessages = readFile(this._fileService, translationPath).then<LocalizedMessages, LocalizedMessages>((content) => { - let errors: json.ParseError[] = []; - let translationBundle: TranslationBundle = json.parse(content, errors); - if (errors.length > 0) { - reportErrors(translationPath, errors); - return { values: undefined, default: `${basename}.nls.json` }; - } else if (json.getNodeType(translationBundle) !== 'object') { - reportInvalidFormat(translationPath); - return { values: undefined, default: `${basename}.nls.json` }; - } else { - let values = translationBundle.contents ? translationBundle.contents.package : undefined; - return { values: values, default: `${basename}.nls.json` }; - } - }, (error) => { - return { values: undefined, default: `${basename}.nls.json` }; - }); - } else { - localizedMessages = existsFile(this._fileService, basename + '.nls' + extension).then<LocalizedMessages | undefined, LocalizedMessages | undefined>(exists => { - if (!exists) { - return undefined; - } - return ExtensionManifestNLSReplacer.findMessageBundles(this._nlsConfig, basename, this._fileService).then((messageBundle) => { - if (!messageBundle.localized) { - return { values: undefined, default: messageBundle.original }; - } - return readFile(this._fileService, messageBundle.localized).then(messageBundleContent => { - let errors: json.ParseError[] = []; - let messages: MessageBag = json.parse(messageBundleContent, errors); - if (errors.length > 0) { - reportErrors(messageBundle.localized, errors); - return { values: undefined, default: messageBundle.original }; - } else if (json.getNodeType(messages) !== 'object') { - reportInvalidFormat(messageBundle.localized); - return { values: undefined, default: messageBundle.original }; - } - return { values: messages, default: messageBundle.original }; - }, (err) => { - return { values: undefined, default: messageBundle.original }; - }); - }, (err) => { - return undefined; - }); - }); - } - - return localizedMessages.then((localizedMessages) => { - if (localizedMessages === undefined) { - return extensionDescription; - } - let errors: json.ParseError[] = []; - // resolveOriginalMessageBundle returns null if localizedMessages.default === undefined; - return this.resolveOriginalMessageBundle(localizedMessages.default, errors).then((defaults) => { - if (errors.length > 0) { - reportErrors(localizedMessages.default, errors); - return extensionDescription; - } else if (json.getNodeType(localizedMessages) !== 'object') { - reportInvalidFormat(localizedMessages.default); - return extensionDescription; - } - const localized = localizedMessages.values || Object.create(null); - ExtensionManifestNLSReplacer._replaceNLStrings(this._nlsConfig, extensionDescription, localized, defaults, this._absoluteFolderPath, this._log); - return extensionDescription; - }); - }, (err) => { - return extensionDescription; - }); - } - - /** - * Parses original message bundle, returns null if the original message bundle is null. - */ - private resolveOriginalMessageBundle(originalMessageBundle: string | null, errors: json.ParseError[]) { - return new Promise<{ [key: string]: string } | null>((c, e) => { - if (originalMessageBundle) { - readFile(this._fileService, originalMessageBundle).then(originalBundleContent => { - c(json.parse(originalBundleContent, errors)); - }, (err) => { - c(null); - }); - } else { - c(null); - } - }); - } - - /** - * Finds localized message bundle and the original (unlocalized) one. - * If the localized file is not present, returns null for the original and marks original as localized. - */ - private static findMessageBundles(nlsConfig: NlsConfiguration, basename: string, fileService: IFileService): Promise<{ localized: string; original: string | null }> { - return new Promise<{ localized: string; original: string | null }>((c, e) => { - function loop(basename: string, locale: string): void { - let toCheck = `${basename}.nls.${locale}.json`; - existsFile(fileService, toCheck).then(exists => { - if (exists) { - c({ localized: toCheck, original: `${basename}.nls.json` }); - } - let index = locale.lastIndexOf('-'); - if (index === -1) { - c({ localized: `${basename}.nls.json`, original: null }); - } else { - locale = locale.substring(0, index); - loop(basename, locale); - } - }); - } - - if (nlsConfig.devMode || nlsConfig.pseudo || !nlsConfig.locale) { - return c({ localized: basename + '.nls.json', original: null }); - } - loop(basename, nlsConfig.locale); - }); - } - - /** - * This routine makes the following assumptions: - * The root element is an object literal - */ - private static _replaceNLStrings<T extends object>(nlsConfig: NlsConfiguration, literal: T, messages: MessageBag, originalMessages: MessageBag | null, messageScope: string, log: ILog): void { - function processEntry(obj: any, key: string | number, command?: boolean) { - const value = obj[key]; - if (types.isString(value)) { - const str = <string>value; - const length = str.length; - if (length > 1 && str[0] === '%' && str[length - 1] === '%') { - const messageKey = str.substr(1, length - 2); - let translated = messages[messageKey]; - // If the messages come from a language pack they might miss some keys - // Fill them from the original messages. - if (translated === undefined && originalMessages) { - translated = originalMessages[messageKey]; - } - let message: string | undefined = typeof translated === 'string' ? translated : (typeof translated?.message === 'string' ? translated.message : undefined); - if (message !== undefined) { - if (nlsConfig.pseudo) { - // FF3B and FF3D is the Unicode zenkaku representation for [ and ] - message = '\uFF3B' + message.replace(/[aouei]/g, '$&$&') + '\uFF3D'; - } - obj[key] = command && (key === 'title' || key === 'category') && originalMessages ? { value: message, original: originalMessages[messageKey] } : message; - } else { - const message = nls.localize('missingNLSKey', "Couldn't find message for key {0}.", messageKey); - log.warn(`[${messageScope}]: ${message}`); - } - } - } else if (types.isObject(value)) { - for (let k in value) { - if (value.hasOwnProperty(k)) { - k === 'commands' ? processEntry(value, k, true) : processEntry(value, k, command); - } - } - } else if (types.isArray(value)) { - for (let i = 0; i < value.length; i++) { - processEntry(value, i, command); - } - } - } - - for (let key in literal) { - if (literal.hasOwnProperty(key)) { - processEntry(literal, key); - } - } - } -} - -export class ExtensionManifestValidator extends ExtensionManifestHandler { - validate(_extensionDescription: IExtensionDescription): IExtensionDescription | null { - let extensionDescription = <IRelaxedExtensionDescription>_extensionDescription; - extensionDescription.isBuiltin = this._isBuiltin; - extensionDescription.isUserBuiltin = !this._isBuiltin && !!extensionDescription.isUserBuiltin; - extensionDescription.isUnderDevelopment = this._isUnderDevelopment; - - let notices: string[] = []; - if (!ExtensionManifestValidator.isValidExtensionManifest(this._ourVersion, this._ourProductDate, URI.file(this._absoluteFolderPath), extensionDescription, extensionDescription.isBuiltin, notices)) { - notices.forEach((error) => { - this._error(this._absoluteFolderPath, error); - }); - return null; - } - - // in this case the notices are warnings - notices.forEach((error) => { - this._warn(this._absoluteFolderPath, error); - }); - - // allow publisher to be undefined to make the initial extension authoring experience smoother - if (!extensionDescription.publisher) { - extensionDescription.publisher = UNDEFINED_PUBLISHER; - } - - // id := `publisher.name` - extensionDescription.id = getExtensionId(extensionDescription.publisher, extensionDescription.name); - extensionDescription.identifier = new ExtensionIdentifier(extensionDescription.id); - - extensionDescription.extensionLocation = URI.file(this._absoluteFolderPath); - - return extensionDescription; - } - - public static isValidExtensionManifest(productVersion: string, productDate: string | undefined, extensionLocation: URI, extensionManifest: IExtensionManifest, extensionIsBuiltin: boolean, notices: string[]): boolean { - - if (!ExtensionManifestValidator.baseIsValidExtensionManifest(extensionLocation, extensionManifest, notices)) { - return false; - } - - if (!semver.valid(extensionManifest.version)) { - notices.push(nls.localize('notSemver', "Extension version is not semver compatible.")); - return false; - } - - return isValidExtensionVersion(productVersion, productDate, extensionManifest, extensionIsBuiltin, notices); - } - - private static baseIsValidExtensionManifest(extensionLocation: URI, extensionDescription: IExtensionManifest, notices: string[]): boolean { - if (!extensionDescription) { - notices.push(nls.localize('extensionDescription.empty', "Got empty extension description")); - return false; - } - if (typeof extensionDescription.publisher !== 'undefined' && typeof extensionDescription.publisher !== 'string') { - notices.push(nls.localize('extensionDescription.publisher', "property publisher must be of type `string`.")); - return false; - } - if (typeof extensionDescription.name !== 'string') { - notices.push(nls.localize('extensionDescription.name', "property `{0}` is mandatory and must be of type `string`", 'name')); - return false; - } - if (typeof extensionDescription.version !== 'string') { - notices.push(nls.localize('extensionDescription.version', "property `{0}` is mandatory and must be of type `string`", 'version')); - return false; - } - if (!extensionDescription.engines) { - notices.push(nls.localize('extensionDescription.engines', "property `{0}` is mandatory and must be of type `object`", 'engines')); - return false; - } - if (typeof extensionDescription.engines.vscode !== 'string') { - notices.push(nls.localize('extensionDescription.engines.vscode', "property `{0}` is mandatory and must be of type `string`", 'engines.vscode')); - return false; - } - if (typeof extensionDescription.extensionDependencies !== 'undefined') { - if (!ExtensionManifestValidator._isStringArray(extensionDescription.extensionDependencies)) { - notices.push(nls.localize('extensionDescription.extensionDependencies', "property `{0}` can be omitted or must be of type `string[]`", 'extensionDependencies')); - return false; - } - } - if (typeof extensionDescription.activationEvents !== 'undefined') { - if (!ExtensionManifestValidator._isStringArray(extensionDescription.activationEvents)) { - notices.push(nls.localize('extensionDescription.activationEvents1', "property `{0}` can be omitted or must be of type `string[]`", 'activationEvents')); - return false; - } - if (typeof extensionDescription.main === 'undefined' && typeof extensionDescription.browser === 'undefined') { - notices.push(nls.localize('extensionDescription.activationEvents2', "properties `{0}` and `{1}` must both be specified or must both be omitted", 'activationEvents', 'main')); - return false; - } - } - if (typeof extensionDescription.extensionKind !== 'undefined') { - if (typeof extensionDescription.main === 'undefined') { - notices.push(nls.localize('extensionDescription.extensionKind', "property `{0}` can be defined only if property `main` is also defined.", 'extensionKind')); - // not a failure case - } - } - if (typeof extensionDescription.main !== 'undefined') { - if (typeof extensionDescription.main !== 'string') { - notices.push(nls.localize('extensionDescription.main1', "property `{0}` can be omitted or must be of type `string`", 'main')); - return false; - } else { - const mainLocation = resources.joinPath(extensionLocation, extensionDescription.main); - if (!resources.isEqualOrParent(mainLocation, extensionLocation)) { - notices.push(nls.localize('extensionDescription.main2', "Expected `main` ({0}) to be included inside extension's folder ({1}). This might make the extension non-portable.", mainLocation.path, extensionLocation.path)); - // not a failure case - } - } - if (typeof extensionDescription.activationEvents === 'undefined') { - notices.push(nls.localize('extensionDescription.main3', "properties `{0}` and `{1}` must both be specified or must both be omitted", 'activationEvents', 'main')); - return false; - } - } - if (typeof extensionDescription.browser !== 'undefined') { - if (typeof extensionDescription.browser !== 'string') { - notices.push(nls.localize('extensionDescription.browser1', "property `{0}` can be omitted or must be of type `string`", 'browser')); - return false; - } else { - const browserLocation = resources.joinPath(extensionLocation, extensionDescription.browser); - if (!resources.isEqualOrParent(browserLocation, extensionLocation)) { - notices.push(nls.localize('extensionDescription.browser2', "Expected `browser` ({0}) to be included inside extension's folder ({1}). This might make the extension non-portable.", browserLocation.path, extensionLocation.path)); - // not a failure case - } - } - if (typeof extensionDescription.activationEvents === 'undefined') { - notices.push(nls.localize('extensionDescription.browser3', "properties `{0}` and `{1}` must both be specified or must both be omitted", 'activationEvents', 'browser')); - return false; - } - } - return true; - } - - private static _isStringArray(arr: string[]): boolean { - if (!Array.isArray(arr)) { - return false; - } - for (let i = 0, len = arr.length; i < len; i++) { - if (typeof arr[i] !== 'string') { - return false; - } - } - return true; - } -} - -export class ExtensionScannerInput { - - public mtime: number | undefined; - - constructor( - public readonly ourVersion: string, - public readonly ourProductDate: string | undefined, - public readonly commit: string | undefined, - public readonly locale: string | undefined, - public readonly devMode: boolean, - public readonly absoluteFolderPath: string, - public readonly isBuiltin: boolean, - public readonly isUnderDevelopment: boolean, - public readonly targetPlatform: TargetPlatform, - public readonly translations: Translations - ) { - // Keep empty!! (JSON.parse) - } - - public static createNLSConfig(input: ExtensionScannerInput): NlsConfiguration { - return { - devMode: input.devMode, - locale: input.locale, - pseudo: input.locale === 'pseudo', - translations: input.translations - }; - } - - public static equals(a: ExtensionScannerInput, b: ExtensionScannerInput): boolean { - return ( - a.ourVersion === b.ourVersion - && a.ourProductDate === b.ourProductDate - && a.commit === b.commit - && a.locale === b.locale - && a.devMode === b.devMode - && a.absoluteFolderPath === b.absoluteFolderPath - && a.isBuiltin === b.isBuiltin - && a.isUnderDevelopment === b.isUnderDevelopment - && a.mtime === b.mtime - && a.targetPlatform === b.targetPlatform - && Translations.equals(a.translations, b.translations) - ); - } -} - -export interface IExtensionReference { - name: string; - path: string; -} - -export interface IExtensionResolver { - resolveExtensions(): Promise<IExtensionReference[]>; -} - -class DefaultExtensionResolver implements IExtensionResolver { - - constructor( - private readonly root: string, - private readonly _fileService: IFileService - ) { - } - - resolveExtensions(): Promise<IExtensionReference[]> { - return readDirsInDir(this._fileService, this.root) - .then(folders => folders.map(name => ({ name, path: path.join(this.root, name) }))); - } -} - -export class ExtensionScanner { - - /** - * Read the extension defined in `absoluteFolderPath` - */ - private static scanExtension(version: string, productDate: string | undefined, absoluteFolderPath: string, isBuiltin: boolean, isUnderDevelopment: boolean, nlsConfig: NlsConfiguration, log: ILog, fileService: IFileService): Promise<IExtensionDescription | null> { - absoluteFolderPath = path.normalize(absoluteFolderPath); - - let parser = new ExtensionManifestParser(version, productDate, absoluteFolderPath, isBuiltin, isUnderDevelopment, log, fileService); - return parser.parse().then<IExtensionDescription | null>((extensionDescription) => { - if (extensionDescription === null) { - return null; - } - - let nlsReplacer = new ExtensionManifestNLSReplacer(version, productDate, absoluteFolderPath, isBuiltin, isUnderDevelopment, nlsConfig, log, fileService); - return nlsReplacer.replaceNLS(extensionDescription); - }).then((extensionDescription) => { - if (extensionDescription === null) { - return null; - } - - let validator = new ExtensionManifestValidator(version, productDate, absoluteFolderPath, isBuiltin, isUnderDevelopment, log, fileService); - return validator.validate(extensionDescription); - }); - } - - /** - * Scan a list of extensions defined in `absoluteFolderPath` - */ - public static async scanExtensions(input: ExtensionScannerInput, log: ILog, fileService: IFileService, resolver: IExtensionResolver | null = null): Promise<IExtensionDescription[]> { - const absoluteFolderPath = input.absoluteFolderPath; - const isBuiltin = input.isBuiltin; - const isUnderDevelopment = input.isUnderDevelopment; - - if (!resolver) { - resolver = new DefaultExtensionResolver(absoluteFolderPath, fileService); - } - - try { - let obsolete: { [folderName: string]: boolean } = {}; - if (!isBuiltin) { - try { - const obsoleteFileContents = await readFile(fileService, path.join(absoluteFolderPath, '.obsolete')); - obsolete = JSON.parse(obsoleteFileContents); - } catch (err) { - // Don't care - } - } - - let refs = await resolver.resolveExtensions(); - - // Ensure the same extension order - refs.sort((a, b) => a.name < b.name ? -1 : 1); - - if (!isBuiltin) { - refs = refs.filter(ref => ref.name.indexOf('.') !== 0); // Do not consider user extension folder starting with `.` - } - - const nlsConfig = ExtensionScannerInput.createNLSConfig(input); - let _extensionDescriptions = await Promise.all(refs.map(r => this.scanExtension(input.ourVersion, input.ourProductDate, r.path, isBuiltin, isUnderDevelopment, nlsConfig, log, fileService))); - let extensionDescriptions = arrays.coalesce(_extensionDescriptions); - extensionDescriptions = extensionDescriptions.filter(item => item !== null && !obsolete[new ExtensionKey({ id: getGalleryExtensionId(item.publisher, item.name) }, item.version, item.targetPlatform).toString()]); - - if (!isBuiltin) { - extensionDescriptions = this.filterOutdatedExtensions(extensionDescriptions, input.targetPlatform); - } - - extensionDescriptions.sort((a, b) => { - if (a.extensionLocation.fsPath < b.extensionLocation.fsPath) { - return -1; - } - return 1; - }); - return extensionDescriptions; - } catch (err) { - log.error(`Error scanning extensions at ${absoluteFolderPath}:`); - log.error(err); - return []; - } - } - - /** - * Combination of scanExtension and scanExtensions: If an extension manifest is found at root, we load just this extension, - * otherwise we assume the folder contains multiple extensions. - */ - public static scanOneOrMultipleExtensions(input: ExtensionScannerInput, log: ILog, fileService: IFileService): Promise<IExtensionDescription[]> { - const absoluteFolderPath = input.absoluteFolderPath; - const isBuiltin = input.isBuiltin; - const isUnderDevelopment = input.isUnderDevelopment; - - return existsFile(fileService, path.join(absoluteFolderPath, MANIFEST_FILE)).then((exists) => { - if (exists) { - const nlsConfig = ExtensionScannerInput.createNLSConfig(input); - return this.scanExtension(input.ourVersion, input.ourProductDate, absoluteFolderPath, isBuiltin, isUnderDevelopment, nlsConfig, log, fileService).then((extensionDescription) => { - if (extensionDescription === null) { - return []; - } - return [extensionDescription]; - }); - } - return this.scanExtensions(input, log, fileService); - }, (err) => { - log.error(`Error scanning extensions at ${absoluteFolderPath}:`); - log.error(err); - return []; - }); - } - - public static scanSingleExtension(input: ExtensionScannerInput, log: ILog, fileService: IFileService): Promise<IExtensionDescription | null> { - const absoluteFolderPath = input.absoluteFolderPath; - const isBuiltin = input.isBuiltin; - const isUnderDevelopment = input.isUnderDevelopment; - const nlsConfig = ExtensionScannerInput.createNLSConfig(input); - return this.scanExtension(input.ourVersion, input.ourProductDate, absoluteFolderPath, isBuiltin, isUnderDevelopment, nlsConfig, log, fileService); - } - - public static mergeBuiltinExtensions(builtinExtensions: Promise<IExtensionDescription[]>, extraBuiltinExtensions: Promise<IExtensionDescription[]>): Promise<IExtensionDescription[]> { - return Promise.all([builtinExtensions, extraBuiltinExtensions]).then(([builtinExtensions, extraBuiltinExtensions]) => { - let resultMap: { [id: string]: IExtensionDescription } = Object.create(null); - for (let i = 0, len = builtinExtensions.length; i < len; i++) { - resultMap[ExtensionIdentifier.toKey(builtinExtensions[i].identifier)] = builtinExtensions[i]; - } - // Overwrite with extensions found in extra - for (let i = 0, len = extraBuiltinExtensions.length; i < len; i++) { - resultMap[ExtensionIdentifier.toKey(extraBuiltinExtensions[i].identifier)] = extraBuiltinExtensions[i]; - } - - let resultArr = Object.keys(resultMap).map((id) => resultMap[id]); - resultArr.sort((a, b) => { - const aLastSegment = path.basename(a.extensionLocation.fsPath); - const bLastSegment = path.basename(b.extensionLocation.fsPath); - if (aLastSegment < bLastSegment) { - return -1; - } - if (aLastSegment > bLastSegment) { - return 1; - } - return 0; - }); - return resultArr; - }); - } - - private static filterOutdatedExtensions(extensions: IExtensionDescription[], targetPlatform: TargetPlatform): IExtensionDescription[] { - const result = new Map<string, IExtensionDescription>(); - for (const extension of extensions) { - const extensionKey = extension.identifier.value; - const existing = result.get(extensionKey); - if (existing) { - if (semver.gt(existing.version, extension.version)) { - continue; - } - if (semver.eq(existing.version, extension.version) && existing.targetPlatform === targetPlatform) { - continue; - } - } - result.set(extensionKey, extension); - } - return [...result.values()]; - } -} - -async function readFile(fileService: IFileService, filename: string): Promise<string> { - try { - const contents = await fileService.readFile(URI.file(filename), { atomic: true }); - return contents.value.toString(); - } catch (err) { - if (toFileOperationResult(err) === FileOperationResult.FILE_NOT_FOUND) { - const nodeLikeError = new Error(`File not found`); - (<any>nodeLikeError).code = 'ENOENT'; - throw nodeLikeError; - } - throw err; - } -} - -async function existsFile(fileService: IFileService, filename: string): Promise<boolean> { - try { - const stat = await fileService.resolve(URI.file(filename)); - return stat.isFile; - } catch (err) { - return false; - } -} - -async function readDirsInDir(fileService: IFileService, dirPath: string): Promise<string[]> { - const stat = await fileService.resolve(URI.file(dirPath)); - const result: string[] = []; - for (const child of (stat.children || [])) { - if (child.isDirectory) { - result.push(child.name); - } - } - return result; -} diff --git a/src/vs/workbench/services/extensions/common/extensions.ts b/src/vs/workbench/services/extensions/common/extensions.ts index 5ab3abcb921..722dba8dd6b 100644 --- a/src/vs/workbench/services/extensions/common/extensions.ts +++ b/src/vs/workbench/services/extensions/common/extensions.ts @@ -13,7 +13,7 @@ import { getExtensionId, getGalleryExtensionId } from 'vs/platform/extensionMana import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; import { ApiProposalName } from 'vs/workbench/services/extensions/common/extensionsApiProposals'; import { IV8Profile } from 'vs/platform/profiling/common/profiling'; -import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; +import { IExtensionDescriptionDelta } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; export const nullExtensionDescription = Object.freeze<IExtensionDescription>({ identifier: new ExtensionIdentifier('nullExtensionDescription'), @@ -150,10 +150,11 @@ export interface IExtensionHost { readonly remoteAuthority: string | null; readonly lazyStart: boolean; /** - * A collection of extensions that will execute or are executing on this extension host. + * A collection of extensions which includes information about which + * extension will execute or is executing on this extension host. * **NOTE**: this will reflect extensions correctly only after `start()` resolves. */ - readonly extensions: ExtensionDescriptionRegistry; + readonly extensions: ExtensionHostExtensions; readonly onExit: Event<[number, string | null]>; start(): Promise<IMessagePassingProtocol> | null; @@ -162,6 +163,201 @@ export interface IExtensionHost { dispose(): void; } +export class ExtensionHostExtensions { + + private _allExtensions: IExtensionDescription[]; + private _myExtensions: ExtensionIdentifier[]; + + constructor() { + this._allExtensions = []; + this._myExtensions = []; + } + + public toDelta(): IExtensionDescriptionDelta { + return { + toRemove: [], + toAdd: this._allExtensions, + myToRemove: [], + myToAdd: this._myExtensions + }; + } + + public set(allExtensions: IExtensionDescription[], myExtensions: ExtensionIdentifier[]): IExtensionDescriptionDelta { + const toRemove: ExtensionIdentifier[] = []; + const toAdd: IExtensionDescription[] = []; + const myToRemove: ExtensionIdentifier[] = []; + const myToAdd: ExtensionIdentifier[] = []; + + const oldExtensionsMap = extensionDescriptionArrayToMap(this._allExtensions); + const newExtensionsMap = extensionDescriptionArrayToMap(allExtensions); + const extensionsAreTheSame = (a: IExtensionDescription, b: IExtensionDescription) => { + return ( + (a.extensionLocation.toString() === b.extensionLocation.toString()) + || (a.isBuiltin === b.isBuiltin) + || (a.isUserBuiltin === b.isUserBuiltin) + || (a.isUnderDevelopment === b.isUnderDevelopment) + ); + }; + + for (const oldExtension of this._allExtensions) { + const newExtension = newExtensionsMap.get(ExtensionIdentifier.toKey(oldExtension.identifier)); + if (!newExtension) { + toRemove.push(oldExtension.identifier); + oldExtensionsMap.delete(ExtensionIdentifier.toKey(oldExtension.identifier)); + continue; + } + if (!extensionsAreTheSame(oldExtension, newExtension)) { + // The new extension is different than the old one + // (e.g. maybe it executes in a different location) + toRemove.push(oldExtension.identifier); + oldExtensionsMap.delete(ExtensionIdentifier.toKey(oldExtension.identifier)); + continue; + } + } + for (const newExtension of allExtensions) { + const oldExtension = oldExtensionsMap.get(ExtensionIdentifier.toKey(newExtension.identifier)); + if (!oldExtension) { + toAdd.push(newExtension); + continue; + } + if (!extensionsAreTheSame(oldExtension, newExtension)) { + // The new extension is different than the old one + // (e.g. maybe it executes in a different location) + toRemove.push(oldExtension.identifier); + oldExtensionsMap.delete(ExtensionIdentifier.toKey(oldExtension.identifier)); + continue; + } + } + + const myOldExtensionsSet = extensionIdentifiersArrayToSet(this._myExtensions); + const myNewExtensionsSet = extensionIdentifiersArrayToSet(myExtensions); + for (const oldExtensionId of this._myExtensions) { + if (!myNewExtensionsSet.has(ExtensionIdentifier.toKey(oldExtensionId))) { + myToRemove.push(oldExtensionId); + } + } + for (const newExtensionId of myExtensions) { + if (!myOldExtensionsSet.has(ExtensionIdentifier.toKey(newExtensionId))) { + myToAdd.push(newExtensionId); + } + } + + const delta = { toRemove, toAdd, myToRemove, myToAdd }; + this.delta(delta); + return delta; + } + + public delta(extensionsDelta: IExtensionDescriptionDelta): void { + const { toRemove, toAdd, myToRemove, myToAdd } = extensionsDelta; + // First handle removals + const toRemoveSet = extensionIdentifiersArrayToSet(toRemove); + const myToRemoveSet = extensionIdentifiersArrayToSet(myToRemove); + for (let i = 0; i < this._allExtensions.length; i++) { + if (toRemoveSet.has(ExtensionIdentifier.toKey(this._allExtensions[i].identifier))) { + this._allExtensions.splice(i, 1); + i--; + } + } + for (let i = 0; i < this._myExtensions.length; i++) { + if (myToRemoveSet.has(ExtensionIdentifier.toKey(this._myExtensions[i]))) { + this._myExtensions.splice(i, 1); + i--; + } + } + // Then handle additions + for (const extension of toAdd) { + this._allExtensions.push(extension); + } + for (const extensionId of myToAdd) { + this._myExtensions.push(extensionId); + } + } + + public containsExtension(extensionId: ExtensionIdentifier): boolean { + for (const myExtensionId of this._myExtensions) { + if (ExtensionIdentifier.equals(myExtensionId, extensionId)) { + return true; + } + } + return false; + } +} + +export class ExtensionIdentifierSet implements Set<ExtensionIdentifier> { + + readonly [Symbol.toStringTag]: string = 'ExtensionIdentifierSet'; + + private readonly _map = new Map<string, ExtensionIdentifier>(); + private readonly _toKey = ExtensionIdentifier.toKey; + + constructor(values?: Iterable<ExtensionIdentifier>) { + if (values) { + for (const value of values) { + this.add(value); + } + } + } + + get size(): number { + return this._map.size; + } + + add(value: ExtensionIdentifier): this { + this._map.set(this._toKey(value), value); + return this; + } + + clear(): void { + this._map.clear(); + } + + delete(value: ExtensionIdentifier): boolean { + return this._map.delete(this._toKey(value)); + } + + has(value: ExtensionIdentifier): boolean { + return this._map.has(this._toKey(value)); + } + + forEach(callbackfn: (value: ExtensionIdentifier, value2: ExtensionIdentifier, set: Set<ExtensionIdentifier>) => void, thisArg?: any): void { + this._map.forEach(value => callbackfn.call(thisArg, value, value, this)); + } + + *entries(): IterableIterator<[ExtensionIdentifier, ExtensionIdentifier]> { + for (let [_key, value] of this._map) { + yield [value, value]; + } + } + + keys(): IterableIterator<ExtensionIdentifier> { + return this._map.values(); + } + + values(): IterableIterator<ExtensionIdentifier> { + return this._map.values(); + } + + [Symbol.iterator](): IterableIterator<ExtensionIdentifier> { + return this._map.values(); + } +} + +export function extensionIdentifiersArrayToSet(extensionIds: ExtensionIdentifier[]): Set<string> { + const result = new Set<string>(); + for (const extensionId of extensionIds) { + result.add(ExtensionIdentifier.toKey(extensionId)); + } + return result; +} + +function extensionDescriptionArrayToMap(extensions: IExtensionDescription[]): Map<string, IExtensionDescription> { + const result = new Map<string, IExtensionDescription>(); + for (const extension of extensions) { + result.set(ExtensionIdentifier.toKey(extension.identifier), extension); + } + return result; +} + export function isProposedApiEnabled(extension: IExtensionDescription, proposal: ApiProposalName): boolean { if (!extension.enabledApiProposals) { return false; @@ -377,6 +573,8 @@ export function toExtension(extensionDescription: IExtensionDescription): IExten manifest: extensionDescription, location: extensionDescription.extensionLocation, targetPlatform: extensionDescription.targetPlatform, + validations: [], + isValid: true }; } diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index 0a1e93cded5..62beb7707f3 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -19,6 +19,7 @@ export const allApiProposals = Object.freeze({ documentFiltersExclusive: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.documentFiltersExclusive.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', externalUriOpener: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.externalUriOpener.d.ts', fileSearchProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.fileSearchProvider.d.ts', findTextInFiles: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.findTextInFiles.d.ts', @@ -30,25 +31,23 @@ export const allApiProposals = Object.freeze({ inputBoxSeverity: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.inputBoxSeverity.d.ts', ipc: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.ipc.d.ts', notebookCellExecutionState: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookCellExecutionState.d.ts', - notebookConcatTextDocument: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookConcatTextDocument.d.ts', notebookContentProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookContentProvider.d.ts', notebookControllerKind: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookControllerKind.d.ts', 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', - notebookDocumentEvents: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookDocumentEvents.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', 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', 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', resolvers: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.resolvers.d.ts', 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', - tabs: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.tabs.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/extensionsUtil.ts b/src/vs/workbench/services/extensions/common/extensionsUtil.ts index 4050fa6a460..977052ebcfc 100644 --- a/src/vs/workbench/services/extensions/common/extensionsUtil.ts +++ b/src/vs/workbench/services/extensions/common/extensionsUtil.ts @@ -4,16 +4,16 @@ *--------------------------------------------------------------------------------------------*/ import { ExtensionIdentifier, IExtensionDescription, IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { ILog } from 'vs/workbench/services/extensions/common/extensionPoints'; import { localize } from 'vs/nls'; +import { ILogService } from 'vs/platform/log/common/log'; -export function dedupExtensions(system: IExtensionDescription[], user: IExtensionDescription[], development: IExtensionDescription[], log: ILog): IExtensionDescription[] { +export function dedupExtensions(system: IExtensionDescription[], user: IExtensionDescription[], development: IExtensionDescription[], logService: ILogService): IExtensionDescription[] { let result = new Map<string, IExtensionDescription>(); system.forEach((systemExtension) => { const extensionKey = ExtensionIdentifier.toKey(systemExtension.identifier); const extension = result.get(extensionKey); if (extension) { - log.warn(localize('overwritingExtension', "Overwriting extension {0} with {1}.", extension.extensionLocation.fsPath, systemExtension.extensionLocation.fsPath)); + logService.warn(localize('overwritingExtension', "Overwriting extension {0} with {1}.", extension.extensionLocation.fsPath, systemExtension.extensionLocation.fsPath)); } result.set(extensionKey, systemExtension); }); @@ -25,13 +25,13 @@ export function dedupExtensions(system: IExtensionDescription[], user: IExtensio // Overwriting a builtin extension inherits the `isBuiltin` property and it doesn't show a warning (<IRelaxedExtensionDescription>userExtension).isBuiltin = true; } else { - log.warn(localize('overwritingExtension', "Overwriting extension {0} with {1}.", extension.extensionLocation.fsPath, userExtension.extensionLocation.fsPath)); + logService.warn(localize('overwritingExtension', "Overwriting extension {0} with {1}.", extension.extensionLocation.fsPath, userExtension.extensionLocation.fsPath)); } } result.set(extensionKey, userExtension); }); development.forEach(developedExtension => { - log.info(localize('extensionUnderDevelopment', "Loading development extension at {0}", developedExtension.extensionLocation.fsPath)); + logService.info(localize('extensionUnderDevelopment', "Loading development extension at {0}", developedExtension.extensionLocation.fsPath)); const extensionKey = ExtensionIdentifier.toKey(developedExtension.identifier); const extension = result.get(extensionKey); if (extension) { diff --git a/src/vs/workbench/services/extensions/common/proxyIdentifier.ts b/src/vs/workbench/services/extensions/common/proxyIdentifier.ts index 6a02adb6cf1..42573f28c93 100644 --- a/src/vs/workbench/services/extensions/common/proxyIdentifier.ts +++ b/src/vs/workbench/services/extensions/common/proxyIdentifier.ts @@ -26,6 +26,8 @@ export interface IRPCProtocol { * Wait for the write buffer (if applicable) to become empty. */ drain(): Promise<void>; + + dispose(): void; } export class ProxyIdentifier<T> { diff --git a/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts b/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts index bd382d7f168..b6eb37647f8 100644 --- a/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts +++ b/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts @@ -25,10 +25,9 @@ import { ISignService } from 'vs/platform/sign/common/sign'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; import { parseExtensionDevOptions } from 'vs/workbench/services/extensions/common/extensionDevOptions'; import { createMessageOfType, isMessageOfType, MessageType, IExtensionHostInitData, UIKind } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; -import { ExtensionHostLogFileName, IExtensionHost, RemoteRunningLocation } from 'vs/workbench/services/extensions/common/extensions'; +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'; @@ -39,8 +38,8 @@ export interface IRemoteExtensionHostInitData { readonly extensionHostLogsPath: URI; readonly globalStorageHome: URI; readonly workspaceStorageHome: URI; - readonly extensions: IExtensionDescription[]; readonly allExtensions: IExtensionDescription[]; + readonly myExtensions: ExtensionIdentifier[]; } export interface IRemoteExtensionHostDataProvider { @@ -52,7 +51,7 @@ export class RemoteExtensionHost extends Disposable implements IExtensionHost { public readonly remoteAuthority: string; public readonly lazyStart = false; - public readonly extensions = new ExtensionDescriptionRegistry([]); + public readonly extensions = new ExtensionHostExtensions(); private _onExit: Emitter<[number, string | null]> = this._register(new Emitter<[number, string | null]>()); public readonly onExit: Event<[number, string | null]> = this._onExit.event; @@ -213,19 +212,8 @@ export class RemoteExtensionHost extends Disposable implements IExtensionHost { private async _createExtHostInitData(isExtensionDevelopmentDebug: boolean): Promise<IExtensionHostInitData> { const [telemetryInfo, remoteInitData] = await Promise.all([this._telemetryService.getTelemetryInfo(), this._initDataProvider.getInitData()]); - - // Collect all identifiers for extension ids which can be considered "resolved" - const remoteExtensions = new Set<string>(); - remoteInitData.extensions.forEach((extension) => remoteExtensions.add(ExtensionIdentifier.toKey(extension.identifier.value))); - - const resolvedExtensions = remoteInitData.allExtensions.filter(extension => !extension.main && !extension.browser).map(extension => extension.identifier); - const hostExtensions = ( - remoteInitData.allExtensions - .filter(extension => !remoteExtensions.has(ExtensionIdentifier.toKey(extension.identifier.value))) - .filter(extension => (extension.main || extension.browser) && extension.api === 'none').map(extension => extension.identifier) - ); const workspace = this._contextService.getWorkspace(); - this.extensions.deltaExtensions(remoteInitData.extensions, []); + const deltaExtensions = this.extensions.set(remoteInitData.allExtensions, remoteInitData.myExtensions); return { commit: this._productService.commit, version: this._productService.version, @@ -253,9 +241,8 @@ export class RemoteExtensionHost extends Disposable implements IExtensionHost { authority: this._initDataProvider.remoteAuthority, connectionData: remoteInitData.connectionData }, - resolvedExtensions: resolvedExtensions, - hostExtensions: hostExtensions, - extensions: this.extensions.getAllExtensionDescriptions(), + allExtensions: deltaExtensions.toAdd, + myExtensions: deltaExtensions.myToAdd, telemetryInfo, logLevel: this._logService.getLevel(), logsLocation: remoteInitData.extensionHostLogsPath, diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts index cfa6569db24..047e66bc6f5 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ILocalProcessExtensionHostDataProvider, LocalProcessExtensionHost } from 'vs/workbench/services/extensions/electron-browser/localProcessExtensionHost'; +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'; @@ -35,7 +35,7 @@ import { IRemoteExplorerService } from 'vs/workbench/services/remote/common/remo import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { getRemoteName } from 'vs/platform/remote/common/remoteHosts'; import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment'; -import { IWebWorkerExtensionHostDataProvider, WebWorkerExtensionHost } from 'vs/workbench/services/extensions/browser/webWorkerExtensionHost'; +import { IWebWorkerExtensionHostDataProvider, IWebWorkerExtensionHostInitData, WebWorkerExtensionHost } from 'vs/workbench/services/extensions/browser/webWorkerExtensionHost'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { ILogService } from 'vs/platform/log/common/log'; import { CATEGORIES } from 'vs/workbench/common/actions'; @@ -145,7 +145,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten return this._remoteAgentService.scanSingleExtension(extension.location, extension.type === ExtensionType.System); } - return this._extensionScanner.scanSingleExtension(extension.location.fsPath, extension.type === ExtensionType.System, this._createLogger()); + return this._extensionScanner.scanSingleExtension(extension.location.fsPath, extension.type === ExtensionType.System); } private async _scanAllLocalExtensions(): Promise<IExtensionDescription[]> { @@ -157,23 +157,25 @@ export class ExtensionService extends AbstractExtensionService implements IExten private _createLocalExtensionHostDataProvider(isInitialStart: boolean, desiredRunningLocation: ExtensionRunningLocation): ILocalProcessExtensionHostDataProvider & IWebWorkerExtensionHostDataProvider { return { - getInitData: async () => { + getInitData: async (): Promise<ILocalProcessExtensionHostInitData & IWebWorkerExtensionHostInitData> => { if (isInitialStart) { // Here we load even extensions that would be disabled by workspace trust const localExtensions = this._checkEnabledAndProposedAPI(await this._scanAllLocalExtensions(), /* ignore workspace trust */true); const runningLocation = this._determineRunningLocation(localExtensions); - const localProcessExtensions = filterByRunningLocation(localExtensions, runningLocation, desiredRunningLocation); + const myExtensions = filterByRunningLocation(localExtensions, runningLocation, desiredRunningLocation); return { autoStart: false, - extensions: localProcessExtensions + allExtensions: localExtensions, + myExtensions: myExtensions.map(extension => extension.identifier) }; } else { // restart case const allExtensions = await this.getExtensions(); - const localProcessExtensions = this._filterByRunningLocation(allExtensions, desiredRunningLocation); + const myExtensions = this._filterByRunningLocation(allExtensions, desiredRunningLocation); return { autoStart: true, - extensions: localProcessExtensions + allExtensions: allExtensions, + myExtensions: myExtensions.map(extension => extension.identifier) }; } } @@ -431,7 +433,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten } protected async _scanAndHandleExtensions(): Promise<void> { - this._extensionScanner.startScanningExtensions(this._createLogger()); + this._extensionScanner.startScanningExtensions(); const remoteAuthority = this._environmentService.remoteAuthority; @@ -561,8 +563,8 @@ export class ExtensionService extends AbstractExtensionService implements IExten extensionHostLogsPath: remoteEnv.extensionHostLogsPath, globalStorageHome: remoteEnv.globalStorageHome, workspaceStorageHome: remoteEnv.workspaceStorageHome, - extensions: remoteExtensions, allExtensions: this._registry.getAllExtensionDescriptions(), + myExtensions: remoteExtensions.map(extension => extension.identifier), }); } @@ -583,7 +585,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten private _startExtensionHost(extensionHostManager: IExtensionHostManager, _extensions: IExtensionDescription[]): void { const extensions = this._filterByExtensionHostManager(_extensions, extensionHostManager); - extensionHostManager.start(extensions.map(extension => extension.identifier)); + extensionHostManager.start(this._registry.getAllExtensionDescriptions(), extensions.map(extension => extension.identifier)); } public _onExtensionHostExit(code: number): void { diff --git a/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts b/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts index 2f7e9b944f6..0f7f60bbc7d 100644 --- a/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts +++ b/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts @@ -4,14 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import { Server, Socket, createServer } from 'net'; -import { findFreePort } from 'vs/base/node/ports'; 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 { toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { DisposableStore } 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'; @@ -30,11 +29,11 @@ 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 { withNullAsUndefined } from 'vs/base/common/types'; -import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { parseExtensionDevOptions } from '../common/extensionDevOptions'; import { VSBuffer } from 'vs/base/common/buffer'; import { IExtensionHostDebugService } from 'vs/platform/debug/common/extensionHostDebug'; -import { IExtensionHost, ExtensionHostLogFileName, LocalProcessRunningLocation } from 'vs/workbench/services/extensions/common/extensions'; +import { IExtensionHost, ExtensionHostLogFileName, LocalProcessRunningLocation, ExtensionHostExtensions } from 'vs/workbench/services/extensions/common/extensions'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { joinPath } from 'vs/base/common/resources'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -42,13 +41,14 @@ import { IOutputChannelRegistry, Extensions } from 'vs/workbench/services/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 { removeDangerousEnvVariables } from 'vs/base/node/processes'; +import { removeDangerousEnvVariables } from 'vs/base/common/processes'; import { StopWatch } from 'vs/base/common/stopwatch'; -import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; +import { process } from 'vs/base/parts/sandbox/electron-sandbox/globals'; export interface ILocalProcessExtensionHostInitData { readonly autoStart: boolean; - readonly extensions: IExtensionDescription[]; + readonly allExtensions: IExtensionDescription[]; + readonly myExtensions: ExtensionIdentifier[]; } export interface ILocalProcessExtensionHostDataProvider { @@ -108,7 +108,7 @@ export class LocalProcessExtensionHost implements IExtensionHost { public readonly remoteAuthority = null; public readonly lazyStart = false; - public readonly extensions = new ExtensionDescriptionRegistry([]); + public readonly extensions = new ExtensionHostExtensions(); private readonly _onExit: Emitter<[number, string]> = new Emitter<[number, string]>(); public readonly onExit: Event<[number, string]> = this._onExit.event; @@ -171,7 +171,7 @@ export class LocalProcessExtensionHost implements IExtensionHost { this._toDispose.add(this._onExit); this._toDispose.add(this._lifecycleService.onWillShutdown(e => this._onWillShutdown(e))); - this._toDispose.add(this._lifecycleService.onDidShutdown(reason => this.terminate())); + 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(); @@ -182,12 +182,6 @@ export class LocalProcessExtensionHost implements IExtensionHost { this._hostService.reload(); } })); - - const globalExitListener = () => this.terminate(); - process.once('exit', globalExitListener); - this._toDispose.add(toDisposable(() => { - process.removeListener('exit' as 'loaded', globalExitListener); // https://github.com/electron/electron/issues/21475 - })); } public dispose(): void { @@ -380,7 +374,7 @@ export class LocalProcessExtensionHost implements IExtensionHost { } const expected = this._environmentService.debugExtensionHost.port; - const port = await findFreePort(expected, 10 /* try 10 ports */, 5000 /* try up to 5 seconds */, 2048 /* skip 2048 ports between attempts */); + const port = await this._nativeHostService.findFreePort(expected, 10 /* try 10 ports */, 5000 /* try up to 5 seconds */, 2048 /* skip 2048 ports between attempts */); if (!this._isExtensionDevTestFromCli) { if (!port) { @@ -504,7 +498,7 @@ export class LocalProcessExtensionHost implements IExtensionHost { private async _createExtHostInitData(): Promise<IExtensionHostInitData> { const [telemetryInfo, initData] = await Promise.all([this._telemetryService.getTelemetryInfo(), this._initDataProvider.getInitData()]); const workspace = this._contextService.getWorkspace(); - this.extensions.deltaExtensions(initData.extensions, []); + const deltaExtensions = this.extensions.set(initData.allExtensions, initData.myExtensions); return { commit: this._productService.commit, version: this._productService.version, @@ -533,9 +527,8 @@ export class LocalProcessExtensionHost implements IExtensionHost { connectionData: null, isRemote: false }, - resolvedExtensions: [], - hostExtensions: [], - extensions: this.extensions.getAllExtensionDescriptions(), + allExtensions: deltaExtensions.toAdd, + myExtensions: deltaExtensions.myToAdd, telemetryInfo, logLevel: this._logService.getLevel(), logsLocation: this._environmentService.extHostLogsPath, @@ -639,7 +632,7 @@ export class LocalProcessExtensionHost implements IExtensionHost { return withNullAsUndefined(this._inspectPort); } - public terminate(): void { + private terminate(): void { if (this._terminating) { return; } @@ -695,7 +688,7 @@ export class LocalProcessExtensionHost implements IExtensionHost { // 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 */), 'join.extensionDevelopment'); + event.join(timeout(100 /* wait a bit for IPC to get delivered */), { id: 'join.extensionDevelopment', label: nls.localize('join.extensionDevelopment', "Terminating extension debug session") }); } } } diff --git a/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts b/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts index c487b74c2b5..852ea7702e9 100644 --- a/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts +++ b/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts @@ -3,355 +3,80 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; import * as path from 'vs/base/common/path'; -import * as errors from 'vs/base/common/errors'; -import { FileAccess, Schemas } from 'vs/base/common/network'; -import * as objects from 'vs/base/common/objects'; import * as platform from 'vs/base/common/platform'; -import { joinPath, originalFSPath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; -import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; -import { BUILTIN_MANIFEST_CACHE_FILE, MANIFEST_CACHE_FOLDER, USER_MANIFEST_CACHE_FILE, IExtensionDescription, IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { IProductService } from 'vs/platform/product/common/productService'; -import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; -import { IHostService } from 'vs/workbench/services/host/browser/host'; -import { Translations, ILog, ExtensionScanner, ExtensionScannerInput, IExtensionReference, IExtensionResolver } from 'vs/workbench/services/extensions/common/extensionPoints'; +import { IExtensionDescription, ExtensionType } from 'vs/platform/extensions/common/extensions'; import { dedupExtensions } from 'vs/workbench/services/extensions/common/extensionsUtil'; -import { IFileService } from 'vs/platform/files/common/files'; -import { VSBuffer } from 'vs/base/common/buffer'; -import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; - -interface IExtensionCacheData { - input: ExtensionScannerInput; - result: IExtensionDescription[]; -} - -let _SystemExtensionsRoot: string | null = null; -function getSystemExtensionsRoot(): string { - if (!_SystemExtensionsRoot) { - _SystemExtensionsRoot = path.normalize(path.join(FileAccess.asFileUri('', require).fsPath, '..', 'extensions')); - } - return _SystemExtensionsRoot; -} - -let _ExtraDevSystemExtensionsRoot: string | null = null; -function getExtraDevSystemExtensionsRoot(): string { - if (!_ExtraDevSystemExtensionsRoot) { - _ExtraDevSystemExtensionsRoot = path.normalize(path.join(FileAccess.asFileUri('', require).fsPath, '..', '.build', 'builtInExtensions')); - } - return _ExtraDevSystemExtensionsRoot; -} +import { IExtensionsScannerService, toExtensionDescription } from 'vs/platform/extensionManagement/common/extensionsScannerService'; +import { ILogService } from 'vs/platform/log/common/log'; +import Severity from 'vs/base/common/severity'; +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'; export class CachedExtensionScanner { public readonly scannedExtensions: Promise<IExtensionDescription[]>; private _scannedExtensionsResolve!: (result: IExtensionDescription[]) => void; private _scannedExtensionsReject!: (err: any) => void; - public readonly translationConfig: Promise<Translations>; constructor( @INotificationService private readonly _notificationService: INotificationService, - @INativeWorkbenchEnvironmentService private readonly _environmentService: INativeWorkbenchEnvironmentService, @IHostService private readonly _hostService: IHostService, - @IProductService private readonly _productService: IProductService, - @IFileService private readonly _fileService: IFileService, - @IExtensionManagementService private readonly _extensionManagementService: IExtensionManagementService + @IExtensionsScannerService private readonly _extensionsScannerService: IExtensionsScannerService, + @ILogService private readonly _logService: ILogService, ) { this.scannedExtensions = new Promise<IExtensionDescription[]>((resolve, reject) => { this._scannedExtensionsResolve = resolve; this._scannedExtensionsReject = reject; }); - this.translationConfig = this._readTranslationConfig(); } - public async scanSingleExtension(path: string, isBuiltin: boolean, log: ILog): Promise<IExtensionDescription | null> { - const translations = await this.translationConfig; - - const version = this._productService.version; - const commit = this._productService.commit; - const date = this._productService.date; - const devMode = !this._environmentService.isBuilt; - const locale = platform.language; - const targetPlatform = await this._extensionManagementService.getTargetPlatform(); - const input = new ExtensionScannerInput(version, date, commit, locale, devMode, path, isBuiltin, false, targetPlatform, translations); - return ExtensionScanner.scanSingleExtension(input, log, this._fileService); + public async scanSingleExtension(extensionPath: string, isBuiltin: boolean): Promise<IExtensionDescription | null> { + const scannedExtension = await this._extensionsScannerService.scanExistingExtension(URI.file(path.resolve(extensionPath)), isBuiltin ? ExtensionType.System : ExtensionType.User, { language: platform.language }); + return scannedExtension ? toExtensionDescription(scannedExtension, false) : null; } - public async startScanningExtensions(log: ILog): Promise<void> { + public async startScanningExtensions(): Promise<void> { try { - const translations = await this.translationConfig; - const { system, user, development } = await this._scanInstalledExtensions(log, translations); - const r = dedupExtensions(system, user, development, log); + const { system, user, development } = await this._scanInstalledExtensions(); + const r = dedupExtensions(system, user, development, this._logService); this._scannedExtensionsResolve(r); } catch (err) { this._scannedExtensionsReject(err); } } - private async _validateExtensionsCache(cacheKey: string, input: ExtensionScannerInput): Promise<void> { - const cacheFolder = path.join(this._environmentService.userDataPath, MANIFEST_CACHE_FOLDER); - const cacheFile = path.join(cacheFolder, cacheKey); - - const expected = JSON.parse(JSON.stringify(await ExtensionScanner.scanExtensions(input, new NullLogger(), this._fileService))); - - const cacheContents = await this._readExtensionCache(cacheKey); - if (!cacheContents) { - // Cache has been deleted by someone else, which is perfectly fine... - return; - } - const actual = cacheContents.result; - - if (objects.equals(expected, actual)) { - // Cache is valid and running with it is perfectly fine... - return; - } - - try { - await this._fileService.del(URI.file(cacheFile)); - } catch (err) { - errors.onUnexpectedError(err); - console.error(err); - } - - this._notificationService.prompt( - Severity.Error, - nls.localize('extensionCache.invalid', "Extensions have been modified on disk. Please reload the window."), - [{ - label: nls.localize('reloadWindow', "Reload Window"), - run: () => this._hostService.reload() - }] - ); - } - - private async _readExtensionCache(cacheKey: string): Promise<IExtensionCacheData | null> { - const cacheFolder = path.join(this._environmentService.userDataPath, MANIFEST_CACHE_FOLDER); - const cacheFile = path.join(cacheFolder, cacheKey); - + private async _scanInstalledExtensions(): Promise<{ system: IExtensionDescription[]; user: IExtensionDescription[]; development: IExtensionDescription[] }> { try { - const cacheRawContents = await this._fileService.readFile(URI.file(cacheFile)); - return JSON.parse(cacheRawContents.value.toString()); - } catch (err) { - // That's ok... - } - - return null; - } - - private async _writeExtensionCache(cacheKey: string, cacheContents: IExtensionCacheData): Promise<void> { - const cacheFolder = path.join(this._environmentService.userDataPath, MANIFEST_CACHE_FOLDER); - const cacheFile = path.join(cacheFolder, cacheKey); - - try { - await this._fileService.createFolder(URI.file(cacheFolder)); - } catch (err) { - // That's ok... - } - - try { - await this._fileService.writeFile(URI.file(cacheFile), VSBuffer.fromString(JSON.stringify(cacheContents))); - } catch (err) { - // That's ok... - } - } - - private async _scanExtensionsWithCache(cacheKey: string, input: ExtensionScannerInput, log: ILog): Promise<IExtensionDescription[]> { - if (input.devMode) { - // Do not cache when running out of sources... - return ExtensionScanner.scanExtensions(input, log, this._fileService); - } - - try { - const folderStat = await this._fileService.stat(URI.file(input.absoluteFolderPath)); - if (typeof folderStat.mtime === 'number') { - input.mtime = folderStat.mtime; - } - } catch (err) { - // That's ok... - } - - const cacheContents = await this._readExtensionCache(cacheKey); - if (cacheContents && cacheContents.input && ExtensionScannerInput.equals(cacheContents.input, input)) { - // Validate the cache asynchronously after 5s - setTimeout(async () => { - try { - await this._validateExtensionsCache(cacheKey, input); - } catch (err) { - errors.onUnexpectedError(err); - } - }, 5000); - return cacheContents.result.map((extensionDescription) => { - // revive URI object - (<IRelaxedExtensionDescription>extensionDescription).extensionLocation = URI.revive(extensionDescription.extensionLocation); - return extensionDescription; - }); - } - - const counterLogger = new CounterLogger(log); - const result = await ExtensionScanner.scanExtensions(input, counterLogger, this._fileService); - if (counterLogger.errorCnt === 0) { - // Nothing bad happened => cache the result - const cacheContents: IExtensionCacheData = { - input: input, - result: result - }; - await this._writeExtensionCache(cacheKey, cacheContents); - } - - return result; - } - - private async _readTranslationConfig(): Promise<Translations> { - if (platform.translationsConfigFile) { - try { - const content = await this._fileService.readFile(URI.file(platform.translationsConfigFile)); - return JSON.parse(content.value.toString()) as Translations; - } catch (err) { - // no problemo - } - } - return Object.create(null); - } - - private async _scanInstalledExtensions( - log: ILog, - translations: Translations - ): Promise<{ system: IExtensionDescription[]; user: IExtensionDescription[]; development: IExtensionDescription[] }> { - - const version = this._productService.version; - const commit = this._productService.commit; - const date = this._productService.date; - const devMode = !this._environmentService.isBuilt; - const locale = platform.language; - const targetPlatform = await this._extensionManagementService.getTargetPlatform(); - - const builtinExtensions = this._scanExtensionsWithCache( - BUILTIN_MANIFEST_CACHE_FILE, - new ExtensionScannerInput(version, date, commit, locale, devMode, getSystemExtensionsRoot(), true, false, targetPlatform, translations), - log - ); - - let finalBuiltinExtensions: Promise<IExtensionDescription[]> = builtinExtensions; - - if (devMode) { - const builtInExtensions = Promise.resolve<IBuiltInExtension[]>(this._productService.builtInExtensions || []); - - const controlFilePath = joinPath(this._environmentService.userHome, '.vscode-oss-dev', 'extensions', 'control.json').fsPath; - const controlFile = this._fileService.readFile(URI.file(controlFilePath)) - .then<IBuiltInExtensionControl>(raw => JSON.parse(raw.value.toString()), () => ({} as any)); - - const input = new ExtensionScannerInput(version, date, commit, locale, devMode, getExtraDevSystemExtensionsRoot(), true, false, targetPlatform, translations); - const extraBuiltinExtensions = Promise.all([builtInExtensions, controlFile]) - .then(([builtInExtensions, control]) => new ExtraBuiltInExtensionResolver(builtInExtensions, control)) - .then(resolver => ExtensionScanner.scanExtensions(input, log, this._fileService, resolver)); - - finalBuiltinExtensions = ExtensionScanner.mergeBuiltinExtensions(builtinExtensions, extraBuiltinExtensions); - } - - const userExtensions = (this._scanExtensionsWithCache( - USER_MANIFEST_CACHE_FILE, - new ExtensionScannerInput(version, date, commit, locale, devMode, this._environmentService.extensionsPath, false, false, targetPlatform, translations), - log - )); - - // Always load developed extensions while extensions development - let developedExtensions: Promise<IExtensionDescription[]> = Promise.resolve([]); - if (this._environmentService.isExtensionDevelopment && this._environmentService.extensionDevelopmentLocationURI) { - const extDescsP = this._environmentService.extensionDevelopmentLocationURI.filter(extLoc => extLoc.scheme === Schemas.file).map(extLoc => { - return ExtensionScanner.scanOneOrMultipleExtensions( - new ExtensionScannerInput(version, date, commit, locale, devMode, originalFSPath(extLoc), false, true, targetPlatform, translations), - log, - this._fileService + const language = platform.language; + const [scannedSystemExtensions, scannedUserExtensions] = await Promise.all([ + this._extensionsScannerService.scanSystemExtensions({ language, useCache: true, checkControlFile: true }), + this._extensionsScannerService.scanUserExtensions({ language, 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 disposable = this._extensionsScannerService.onDidChangeCache(() => { + disposable.dispose(); + this._notificationService.prompt( + Severity.Error, + localize('extensionCache.invalid', "Extensions have been modified on disk. Please reload the window."), + [{ + label: localize('reloadWindow', "Reload Window"), + run: () => this._hostService.reload() + }] ); }); - developedExtensions = Promise.all(extDescsP).then((extDescArrays: IExtensionDescription[][]) => { - let extDesc: IExtensionDescription[] = []; - for (let eds of extDescArrays) { - extDesc = extDesc.concat(eds); - } - return extDesc; - }); - } - - return Promise.all([finalBuiltinExtensions, userExtensions, developedExtensions]).then((extensionDescriptions: IExtensionDescription[][]) => { - const system = extensionDescriptions[0]; - const user = extensionDescriptions[1]; - const development = extensionDescriptions[2]; + timeout(5000).then(() => disposable.dispose()); return { system, user, development }; - }).then(undefined, err => { - log.error(`Error scanning installed extensions:`); - log.error(err); + } catch (err) { + this._logService.error(`Error scanning installed extensions:`); + this._logService.error(err); return { system: [], user: [], development: [] }; - }); - } -} - -interface IBuiltInExtension { - name: string; - version: string; - repo: string; -} - -interface IBuiltInExtensionControl { - [name: string]: 'marketplace' | 'disabled' | string; -} - -class ExtraBuiltInExtensionResolver implements IExtensionResolver { - - constructor(private builtInExtensions: IBuiltInExtension[], private control: IBuiltInExtensionControl) { } - - resolveExtensions(): Promise<IExtensionReference[]> { - const result: IExtensionReference[] = []; - - for (const ext of this.builtInExtensions) { - const controlState = this.control[ext.name] || 'marketplace'; - - switch (controlState) { - case 'disabled': - break; - case 'marketplace': - result.push({ name: ext.name, path: path.join(getExtraDevSystemExtensionsRoot(), ext.name) }); - break; - default: - result.push({ name: ext.name, path: controlState }); - break; - } } - - return Promise.resolve(result); - } -} - -class CounterLogger implements ILog { - - public errorCnt = 0; - public warnCnt = 0; - public infoCnt = 0; - - constructor(private readonly _actual: ILog) { - } - - public error(message: string | Error): void { - this.errorCnt++; - this._actual.error(message); - } - - public warn(message: string): void { - this.warnCnt++; - this._actual.warn(message); } - public info(message: string): void { - this.infoCnt++; - this._actual.info(message); - } -} - -class NullLogger implements ILog { - public error(message: string | Error): void { - } - public warn(message: string): void { - } - public info(message: string): void { - } } diff --git a/src/vs/workbench/services/host/browser/browserHostService.ts b/src/vs/workbench/services/host/browser/browserHostService.ts index cdc63895e27..955ba20bddf 100644 --- a/src/vs/workbench/services/host/browser/browserHostService.ts +++ b/src/vs/workbench/services/host/browser/browserHostService.ts @@ -34,6 +34,7 @@ import { isUndefined } from 'vs/base/common/types'; import { isTemporaryWorkspace, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { Schemas } from 'vs/base/common/network'; +import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; /** * A workspace to open in the workbench can either be: @@ -285,14 +286,16 @@ export class BrowserHostService extends Disposable implements IHostService { // Same Window: open via editor service in current window if (this.shouldReuse(options, true /* file */)) { - let openables: IPathData[] = []; + let openables: IPathData<ITextEditorOptions>[] = []; // Support: --goto parameter to open on line/col if (options?.gotoLineMode) { const pathColumnAware = parseLineAndColumnAware(openable.fileUri.path); openables = [{ fileUri: openable.fileUri.with({ path: pathColumnAware.path }), - selection: !isUndefined(pathColumnAware.line) ? { startLineNumber: pathColumnAware.line, startColumn: pathColumnAware.column || 1 } : undefined + options: { + selection: !isUndefined(pathColumnAware.line) ? { startLineNumber: pathColumnAware.line, startColumn: pathColumnAware.column || 1 } : undefined + } }]; } else { openables = [openable]; diff --git a/src/vs/workbench/services/hover/browser/hover.ts b/src/vs/workbench/services/hover/browser/hover.ts index c22832f9755..074b8203b5e 100644 --- a/src/vs/workbench/services/hover/browser/hover.ts +++ b/src/vs/workbench/services/hover/browser/hover.ts @@ -33,7 +33,8 @@ export interface IHoverService { showHover(options: IHoverOptions, focus?: boolean): IHoverWidget | undefined; /** - * Hides the hover if it was visible. + * Hides the hover if it was visible. This call will be ignored if the the hover is currently + * "locked" via the alt/option key. */ hideHover(): void; } diff --git a/src/vs/workbench/services/hover/browser/hoverService.ts b/src/vs/workbench/services/hover/browser/hoverService.ts index 7d5331927d9..f0a3c2487f4 100644 --- a/src/vs/workbench/services/hover/browser/hoverService.ts +++ b/src/vs/workbench/services/hover/browser/hoverService.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/hover'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { editorHoverBackground, editorHoverBorder, textLinkForeground, editorHoverForeground, editorHoverStatusBarBackground, textCodeBlockBackground, widgetShadow, textLinkActiveForeground } from 'vs/platform/theme/common/colorRegistry'; +import { editorHoverBackground, editorHoverBorder, textLinkForeground, editorHoverForeground, editorHoverStatusBarBackground, textCodeBlockBackground, widgetShadow, textLinkActiveForeground, focusBorder, toolbarHoverBackground } from 'vs/platform/theme/common/colorRegistry'; import { IHoverService, IHoverOptions, IHoverWidget } from 'vs/workbench/services/hover/browser/hover'; import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -19,6 +19,7 @@ export class HoverService implements IHoverService { declare readonly _serviceBrand: undefined; private _currentHoverOptions: IHoverOptions | undefined; + private _currentHover: HoverWidget | undefined; constructor( @IInstantiationService private readonly _instantiationService: IInstantiationService, @@ -50,6 +51,13 @@ export class HoverService implements IHoverService { } else { hoverDisposables.add(addDisposableListener(options.target, EventType.CLICK, () => this.hideHover())); } + const focusedElement = <HTMLElement | null>document.activeElement; + if (focusedElement) { + hoverDisposables.add(addDisposableListener(focusedElement, EventType.KEY_DOWN, e => this._keyDown(e, hover))); + hoverDisposables.add(addDisposableListener(document, EventType.KEY_DOWN, e => this._keyDown(e, hover))); + hoverDisposables.add(addDisposableListener(focusedElement, EventType.KEY_UP, e => this._keyUp(e, hover))); + hoverDisposables.add(addDisposableListener(document, EventType.KEY_UP, e => this._keyUp(e, hover))); + } if (options.hideOnKeyDown) { const focusedElement = document.activeElement; if (focusedElement) { @@ -64,13 +72,16 @@ export class HoverService implements IHoverService { hoverDisposables.add(toDisposable(() => observer.disconnect())); } + this._currentHover = hover; + return hover; } hideHover(): void { - if (!this._currentHoverOptions) { + if (this._currentHover?.isLocked || !this._currentHoverOptions) { return; } + this._currentHover = undefined; this._currentHoverOptions = undefined; this._contextViewService.hideContextView(); } @@ -81,6 +92,24 @@ export class HoverService implements IHoverService { hover.dispose(); } } + + private _keyDown(e: KeyboardEvent, hover: HoverWidget) { + if (e.key === 'Alt') { + hover.isLocked = true; + return; + } + this.hideHover(); + } + + private _keyUp(e: KeyboardEvent, hover: HoverWidget) { + if (e.key === 'Alt') { + hover.isLocked = false; + // Hide if alt is released while the mouse os not over hover/target + if (!hover.isMouseIn) { + this.hideHover(); + } + } + } } class HoverContextViewDelegate implements IDelegate { @@ -126,6 +155,8 @@ registerThemingParticipant((theme, collector) => { const hoverBorder = theme.getColor(editorHoverBorder); if (hoverBorder) { collector.addRule(`.monaco-workbench .workbench-hover { border: 1px solid ${hoverBorder}; }`); + collector.addRule(`.monaco-workbench .workbench-hover-container.locked .workbench-hover { outline: 1px solid ${hoverBorder}; }`); + collector.addRule(`.monaco-workbench .workbench-hover .hover-row:not(:first-child):not(:empty) { border-top: 1px solid ${hoverBorder.transparent(0.5)}; }`); collector.addRule(`.monaco-workbench .workbench-hover hr { border-top: 1px solid ${hoverBorder.transparent(0.5)}; }`); collector.addRule(`.monaco-workbench .workbench-hover hr { border-bottom: 0px solid ${hoverBorder.transparent(0.5)}; }`); @@ -133,6 +164,15 @@ registerThemingParticipant((theme, collector) => { collector.addRule(`.monaco-workbench .workbench-hover-pointer:after { border-right: 1px solid ${hoverBorder}; }`); collector.addRule(`.monaco-workbench .workbench-hover-pointer:after { border-bottom: 1px solid ${hoverBorder}; }`); } + const focus = theme.getColor(focusBorder); + if (focus) { + collector.addRule(`.monaco-workbench .workbench-hover-container.locked .workbench-hover:focus { outline-color: ${focus}; }`); + collector.addRule(`.monaco-workbench .workbench-hover-lock:focus { outline: 1px solid ${focus}; }`); + } + const toolbarHoverBackgroundColor = theme.getColor(toolbarHoverBackground); + if (toolbarHoverBackgroundColor) { + collector.addRule(`.monaco-workbench .workbench-hover-container.locked .workbench-hover-lock:hover { background-color: ${toolbarHoverBackgroundColor}; }`); + } const link = theme.getColor(textLinkForeground); if (link) { collector.addRule(`.monaco-workbench .workbench-hover a { color: ${link}; }`); diff --git a/src/vs/workbench/services/hover/browser/hoverWidget.ts b/src/vs/workbench/services/hover/browser/hoverWidget.ts index 7856afda3a1..837a4bdfd52 100644 --- a/src/vs/workbench/services/hover/browser/hoverWidget.ts +++ b/src/vs/workbench/services/hover/browser/hoverWidget.ts @@ -38,7 +38,7 @@ const enum Constants { export class HoverWidget extends Widget { private readonly _messageListeners = new DisposableStore(); - private readonly _mouseTracker: CompositeMouseTracker; + private readonly _lockMouseTracker: CompositeMouseTracker; private readonly _hover: BaseHoverWidget; private readonly _hoverPointer: HTMLElement | undefined; @@ -51,8 +51,10 @@ export class HoverWidget extends Widget { private _forcePosition: boolean = false; private _x: number = 0; private _y: number = 0; + private _isLocked: boolean = false; get isDisposed(): boolean { return this._isDisposed; } + get isMouseIn(): boolean { return this._lockMouseTracker.isMouseIn; } get domNode(): HTMLElement { return this._hover.containerDomNode; } private readonly _onDispose = this._register(new Emitter<void>()); @@ -64,6 +66,19 @@ export class HoverWidget extends Widget { get x(): number { return this._x; } get y(): number { return this._y; } + /** + * Whether the hover is "locked" by holding the alt/option key. When locked, the hover will not + * hide and can be hovered regardless of whether the `hideOnHover` hover option is set. + */ + get isLocked(): boolean { return this._isLocked; } + set isLocked(value: boolean) { + if (this._isLocked === value) { + return; + } + this._isLocked = value; + this._hoverContainer.classList.toggle('locked', this._isLocked); + } + constructor( options: IHoverOptions, @IKeybindingService private readonly _keybindingService: IKeybindingService, @@ -165,7 +180,6 @@ export class HoverWidget extends Widget { } this._hoverContainer.appendChild(this._hover.containerDomNode); - const mouseTrackerTargets = [...this._target.targetElements]; let hideOnHover: boolean; if (options.actions && options.actions.length > 0) { // If there are actions, require hover so they can be accessed @@ -179,12 +193,31 @@ export class HoverWidget extends Widget { hideOnHover = options.hideOnHover; } } + const mouseTrackerTargets = [...this._target.targetElements]; if (!hideOnHover) { mouseTrackerTargets.push(this._hoverContainer); } - this._mouseTracker = new CompositeMouseTracker(mouseTrackerTargets); - this._register(this._mouseTracker.onMouseOut(() => this.dispose())); - this._register(this._mouseTracker); + const mouseTracker = this._register(new CompositeMouseTracker(mouseTrackerTargets)); + this._register(mouseTracker.onMouseOut(() => { + if (!this._isLocked) { + this.dispose(); + } + })); + + // Setup another mouse tracker when hideOnHover is set in order to track the hover as well + // when it is locked. This ensures the hover will hide on mouseout after alt has been + // released to unlock the element. + if (hideOnHover) { + const mouseTracker2Targets = [...this._target.targetElements, this._hoverContainer]; + this._lockMouseTracker = this._register(new CompositeMouseTracker(mouseTracker2Targets)); + this._register(this._lockMouseTracker.onMouseOut(() => { + if (!this._isLocked) { + this.dispose(); + } + })); + } else { + this._lockMouseTracker = mouseTracker; + } } public render(container: HTMLElement): void { @@ -468,6 +501,8 @@ class CompositeMouseTracker extends Widget { private readonly _onMouseOut = this._register(new Emitter<void>()); get onMouseOut(): Event<void> { return this._onMouseOut.event; } + get isMouseIn(): boolean { return this._isMouseIn; } + constructor( private _elements: HTMLElement[] ) { diff --git a/src/vs/workbench/services/hover/browser/media/hover.css b/src/vs/workbench/services/hover/browser/media/hover.css index 215cb9b9fcc..d560a4a3a96 100644 --- a/src/vs/workbench/services/hover/browser/media/hover.css +++ b/src/vs/workbench/services/hover/browser/media/hover.css @@ -38,6 +38,12 @@ width: 5px; height: 5px; } +.monaco-workbench .locked .workbench-hover-pointer:after { + width: 4px; + height: 4px; + border-right-width: 2px; + border-bottom-width: 2px; +} .monaco-workbench .workbench-hover-pointer.left { left: -3px; } .monaco-workbench .workbench-hover-pointer.right { right: 3px; } diff --git a/src/vs/workbench/services/label/common/labelService.ts b/src/vs/workbench/services/label/common/labelService.ts index dafc93d58a3..1f6860f0050 100644 --- a/src/vs/workbench/services/label/common/labelService.ts +++ b/src/vs/workbench/services/label/common/labelService.ts @@ -6,11 +6,11 @@ import { localize } from 'vs/nls'; import { URI } from 'vs/base/common/uri'; import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; -import * as paths from 'vs/base/common/path'; +import { posix, win32 } from 'vs/base/common/path'; import { Emitter } from 'vs/base/common/event'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry, IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { Registry } from 'vs/platform/registry/common/platform'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IWorkspaceContextService, IWorkspace, isWorkspace, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IWorkspaceIdentifier, toWorkspaceIdentifier, WORKSPACE_EXTENSION, isUntitledWorkspace, isTemporaryWorkspace } from 'vs/platform/workspace/common/workspace'; import { basenameOrAuthority, basename, joinPath, dirname } from 'vs/base/common/resources'; import { tildify, getPathLabel } from 'vs/base/common/labels'; @@ -21,6 +21,9 @@ import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; +import { OperatingSystem, OS } from 'vs/base/common/platform'; +import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { Schemas } from 'vs/base/common/network'; const resourceLabelFormattersExtPoint = ExtensionsRegistry.registerExtensionPoint<ResourceLabelFormatter[]>({ extensionPoint: 'resourceLabelFormatters', @@ -78,17 +81,19 @@ function hasDriveLetterIgnorePlatform(path: string): boolean { } class ResourceLabelFormattersHandler implements IWorkbenchContribution { - private formattersDisposables = new Map<ResourceLabelFormatter, IDisposable>(); + + private readonly formattersDisposables = new Map<ResourceLabelFormatter, IDisposable>(); constructor(@ILabelService labelService: ILabelService) { resourceLabelFormattersExtPoint.setHandler((extensions, delta) => { delta.added.forEach(added => added.value.forEach(formatter => { if (!isProposedApiEnabled(added.description, 'contribLabelFormatterWorkspaceTooltip') && formatter.formatting.workspaceTooltip) { - // workspaceTooltip is only proposed - formatter.formatting.workspaceTooltip = undefined; + formatter.formatting.workspaceTooltip = undefined; // workspaceTooltip is only proposed } + this.formattersDisposables.set(formatter, labelService.registerFormatter(formatter)); })); + delta.removed.forEach(removed => removed.value.forEach(formatter => { this.formattersDisposables.get(formatter)!.dispose(); })); @@ -106,37 +111,70 @@ export class LabelService extends Disposable implements ILabelService { private readonly _onDidChangeFormatters = this._register(new Emitter<IFormatterChangeEvent>({ leakWarningThreshold: 400 })); readonly onDidChangeFormatters = this._onDidChangeFormatters.event; + private os: OperatingSystem; + private userHome: URI | undefined; + constructor( - @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, - @IPathService private readonly pathService: IPathService + @IPathService private readonly pathService: IPathService, + @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService ) { super(); + + // Find some meaningful defaults until the remote environment + // is resolved, by taking the current OS we are running in + // and by taking the local `userHome` if we run on a local + // file scheme. + this.os = OS; + this.userHome = pathService.defaultUriScheme === Schemas.file ? this.pathService.userHome({ preferLocal: true }) : undefined; + + // Remote environment is potentially long running + this.resolveRemoteEnvironment(); + } + + private async resolveRemoteEnvironment(): Promise<void> { + + // OS + const env = await this.remoteAgentService.getEnvironment(); + this.os = env?.os ?? OS; + + // User home + this.userHome = await this.pathService.userHome(); } findFormatting(resource: URI): ResourceLabelFormatting | undefined { let bestResult: ResourceLabelFormatter | undefined; - this.formatters.forEach(formatter => { + for (const formatter of this.formatters) { if (formatter.scheme === resource.scheme) { if (!formatter.authority && (!bestResult || formatter.priority)) { bestResult = formatter; - return; + continue; } + if (!formatter.authority) { - return; + continue; } - if (match(formatter.authority.toLowerCase(), resource.authority.toLowerCase()) && (!bestResult || !bestResult.authority || formatter.authority.length > bestResult.authority.length || ((formatter.authority.length === bestResult.authority.length) && formatter.priority))) { + if ( + match(formatter.authority.toLowerCase(), resource.authority.toLowerCase()) && + ( + !bestResult || + !bestResult.authority || + formatter.authority.length > bestResult.authority.length || + ((formatter.authority.length === bestResult.authority.length) && formatter.priority) + ) + ) { bestResult = formatter; } } - }); + } return bestResult ? bestResult.formatting : undefined; } - getUriLabel(resource: URI, options: { relative?: boolean; noPrefix?: boolean; endWithSeparator?: boolean; separator?: '/' | '\\' } = {}): string { + getUriLabel(resource: URI, options: { relative?: boolean; noPrefix?: boolean; separator?: '/' | '\\' } = {}): string { let formatting = this.findFormatting(resource); if (formatting && options.separator) { // mixin separator if defined from the outside @@ -154,51 +192,66 @@ export class LabelService extends Disposable implements ILabelService { return label; } - private doGetUriLabel(resource: URI, formatting?: ResourceLabelFormatting, options: { relative?: boolean; noPrefix?: boolean; endWithSeparator?: boolean } = {}): string { + private doGetUriLabel(resource: URI, formatting?: ResourceLabelFormatting, options: { relative?: boolean; noPrefix?: boolean } = {}): string { if (!formatting) { - return getPathLabel(resource.path, { userHome: this.pathService.resolvedUserHome }, options.relative ? this.contextService : undefined); + return getPathLabel(resource, { + os: this.os, + tildify: this.userHome ? { userHome: this.userHome } : undefined, + relative: options.relative ? { + noPrefix: options.noPrefix, + getWorkspace: () => this.contextService.getWorkspace(), + getWorkspaceFolder: resource => this.contextService.getWorkspaceFolder(resource) + } : undefined + }); } - let label: string | undefined; - const baseResource = this.contextService?.getWorkspaceFolder(resource); + // Relative label + if (options.relative) { + const folder = this.contextService?.getWorkspaceFolder(resource); + if (folder) { + const folderLabel = this.formatUri(folder.uri, formatting, options.noPrefix); - if (options.relative && baseResource) { - const baseResourceLabel = this.formatUri(baseResource.uri, formatting, options.noPrefix); - let relativeLabel = this.formatUri(resource, formatting, options.noPrefix); + let relativeLabel = this.formatUri(resource, formatting, options.noPrefix); + let overlap = 0; + while (relativeLabel[overlap] && relativeLabel[overlap] === folderLabel[overlap]) { + overlap++; + } - let overlap = 0; - while (relativeLabel[overlap] && relativeLabel[overlap] === baseResourceLabel[overlap]) { overlap++; } - if (!relativeLabel[overlap] || relativeLabel[overlap] === formatting.separator) { - relativeLabel = relativeLabel.substring(1 + overlap); - } else if (overlap === baseResourceLabel.length && baseResource.uri.path === '/') { - relativeLabel = relativeLabel.substring(overlap); - } + if (!relativeLabel[overlap] || relativeLabel[overlap] === formatting.separator) { + relativeLabel = relativeLabel.substring(1 + overlap); + } else if (overlap === folderLabel.length && folder.uri.path === posix.sep) { + relativeLabel = relativeLabel.substring(overlap); + } - const hasMultipleRoots = this.contextService.getWorkspace().folders.length > 1; - if (hasMultipleRoots && !options.noPrefix) { - const rootName = baseResource?.name ?? basenameOrAuthority(baseResource.uri); - relativeLabel = relativeLabel ? (rootName + ' • ' + relativeLabel) : rootName; // always show root basename if there are multiple - } + // always show root basename if there are multiple folders + const hasMultipleRoots = this.contextService.getWorkspace().folders.length > 1; + if (hasMultipleRoots && !options.noPrefix) { + const rootName = folder?.name ?? basenameOrAuthority(folder.uri); + relativeLabel = relativeLabel ? `${rootName} • ${relativeLabel}` : rootName; + } - label = relativeLabel; - } else { - label = this.formatUri(resource, formatting, options.noPrefix); + return relativeLabel; + } } - return options.endWithSeparator ? this.appendSeparatorIfMissing(label, formatting) : label; + // Absolute label + return this.formatUri(resource, formatting, options.noPrefix); } getUriBasenameLabel(resource: URI): string { const formatting = this.findFormatting(resource); const label = this.doGetUriLabel(resource, formatting); - if (formatting) { - switch (formatting.separator) { - case paths.win32.sep: return paths.win32.basename(label); - case paths.posix.sep: return paths.posix.basename(label); - } + + let pathLib: typeof win32 | typeof posix; + if (formatting?.separator === win32.sep) { + pathLib = win32; + } else if (formatting?.separator === posix.sep) { + pathLib = posix; + } else { + pathLib = (this.os === OperatingSystem.Windows) ? win32 : posix; } - return paths.basename(label); + return pathLib.basename(label); } getWorkspaceLabel(workspace: IWorkspace | IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | URI, options?: { verbose: boolean }): string { @@ -258,22 +311,26 @@ export class LabelService extends Disposable implements ILabelService { } private doGetSingleFolderWorkspaceLabel(folderUri: URI, options?: { verbose: boolean }): string { - const label = options?.verbose ? this.getUriLabel(folderUri) : basename(folderUri) || '/'; + const label = options?.verbose ? this.getUriLabel(folderUri) : basename(folderUri) || posix.sep; + return this.appendWorkspaceSuffix(label, folderUri); } getSeparator(scheme: string, authority?: string): '/' | '\\' { const formatter = this.findFormatting(URI.from({ scheme, authority })); - return formatter?.separator || '/'; + + return formatter?.separator || posix.sep; } getHostLabel(scheme: string, authority?: string): string { const formatter = this.findFormatting(URI.from({ scheme, authority })); + return formatter?.workspaceSuffix || authority || ''; } getHostTooltip(scheme: string, authority?: string): string | undefined { const formatter = this.findFormatting(URI.from({ scheme, authority })); + return formatter?.workspaceTooltip; } @@ -304,10 +361,10 @@ export class LabelService extends Disposable implements ILabelService { if (query && query[0] === '{' && query[query.length - 1] === '}') { try { return JSON.parse(query)[qsValue] || ''; - } - catch { } + } catch { } } } + return ''; } } @@ -319,11 +376,11 @@ export class LabelService extends Disposable implements ILabelService { } if (formatting.tildify && !forceNoTildify) { - const userHome = this.pathService.resolvedUserHome; - if (userHome) { - label = tildify(label, userHome.fsPath); + if (this.userHome) { + label = tildify(label, this.userHome.fsPath, this.os); } } + if (formatting.authorityPrefix && resource.authority) { label = formatting.authorityPrefix + label; } @@ -331,17 +388,10 @@ export class LabelService extends Disposable implements ILabelService { return label.replace(sepRegexp, formatting.separator); } - private appendSeparatorIfMissing(label: string, formatting: ResourceLabelFormatting): string { - let appendedLabel = label; - if (!label.endsWith(formatting.separator)) { - appendedLabel += formatting.separator; - } - return appendedLabel; - } - private appendWorkspaceSuffix(label: string, uri: URI): string { const formatting = this.findFormatting(uri); const suffix = formatting && (typeof formatting.workspaceSuffix === 'string') ? formatting.workspaceSuffix : undefined; + return suffix ? `${label} [${suffix}]` : label; } } 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 9a91b17f7c9..ab94051a7e6 100644 --- a/src/vs/workbench/services/label/test/browser/label.test.ts +++ b/src/vs/workbench/services/label/test/browser/label.test.ts @@ -5,18 +5,19 @@ import * as resources from 'vs/base/common/resources'; import * as assert from 'assert'; -import { TestEnvironmentService, TestPathService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestEnvironmentService, TestPathService, TestRemoteAgentService } from 'vs/workbench/test/browser/workbenchTestServices'; import { URI } from 'vs/base/common/uri'; import { LabelService } from 'vs/workbench/services/label/common/labelService'; import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices'; import { WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { Workspace } from 'vs/platform/workspace/test/common/testWorkspace'; +import { isWindows } from 'vs/base/common/platform'; suite('URI Label', () => { let labelService: LabelService; setup(() => { - labelService = new LabelService(TestEnvironmentService, new TestContextService(), new TestPathService()); + labelService = new LabelService(TestEnvironmentService, new TestContextService(), new TestPathService(), new TestRemoteAgentService()); }); test('custom scheme', function () { @@ -176,7 +177,9 @@ suite('multi-root workspace', () => { new WorkspaceFolder({ uri: tests, index: 1, name: 'Tests' }), new WorkspaceFolder({ uri: other, index: 2, name: resources.basename(other) }), ])), - new TestPathService()); + new TestPathService(), + new TestRemoteAgentService() + ); }); test('labels of files in multiroot workspaces are the foldername followed by offset from the folder', () => { @@ -249,6 +252,27 @@ suite('multi-root workspace', () => { assert.strictEqual(generated, label, path); }); }); + + test('relative label without formatter', () => { + const rootFolder = URI.parse('myscheme://myauthority/'); + + labelService = new LabelService( + TestEnvironmentService, + new TestContextService( + new Workspace('test-workspace', [ + new WorkspaceFolder({ uri: rootFolder, index: 0, name: 'FSProotFolder' }), + ])), + new TestPathService(undefined, rootFolder.scheme), + new TestRemoteAgentService() + ); + + const generated = labelService.getUriLabel(URI.parse('myscheme://myauthority/some/folder/test.txt'), { relative: true }); + if (isWindows) { + assert.strictEqual(generated, 'some\\folder\\test.txt'); + } else { + assert.strictEqual(generated, 'some/folder/test.txt'); + } + }); }); suite('workspace at FSP root', () => { @@ -263,7 +287,9 @@ suite('workspace at FSP root', () => { new Workspace('test-workspace', [ new WorkspaceFolder({ uri: rootFolder, index: 0, name: 'FSProotFolder' }), ])), - new TestPathService()); + new TestPathService(), + new TestRemoteAgentService() + ); labelService.registerFormatter({ scheme: 'myscheme', formatting: { diff --git a/src/vs/workbench/services/label/test/electron-browser/label.test.ts b/src/vs/workbench/services/label/test/electron-browser/label.test.ts index 24c474bddda..ad2976d6bb7 100644 --- a/src/vs/workbench/services/label/test/electron-browser/label.test.ts +++ b/src/vs/workbench/services/label/test/electron-browser/label.test.ts @@ -11,13 +11,14 @@ import { isWindows } from 'vs/base/common/platform'; import { LabelService } from 'vs/workbench/services/label/common/labelService'; import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices'; import { TestNativePathService, TestEnvironmentService } from 'vs/workbench/test/electron-browser/workbenchTestServices'; +import { TestRemoteAgentService } from 'vs/workbench/test/browser/workbenchTestServices'; suite('URI Label', () => { let labelService: LabelService; setup(() => { - labelService = new LabelService(TestEnvironmentService, new TestContextService(), new TestNativePathService()); + labelService = new LabelService(TestEnvironmentService, new TestContextService(), new TestNativePathService(), new TestRemoteAgentService()); }); test('file scheme', function () { diff --git a/src/vs/workbench/services/language/common/languageService.ts b/src/vs/workbench/services/language/common/languageService.ts index 90c599c26ed..3b60d31507a 100644 --- a/src/vs/workbench/services/language/common/languageService.ts +++ b/src/vs/workbench/services/language/common/languageService.ts @@ -16,6 +16,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { ExtensionMessageCollector, ExtensionsRegistry, IExtensionPoint, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ILogService } from 'vs/platform/log/common/log'; export interface IRawLanguageExtensionPoint { id: string; @@ -113,7 +114,8 @@ export class WorkbenchLanguageService extends LanguageService { constructor( @IExtensionService extensionService: IExtensionService, @IConfigurationService configurationService: IConfigurationService, - @IEnvironmentService environmentService: IEnvironmentService + @IEnvironmentService environmentService: IEnvironmentService, + @ILogService private readonly logService: ILogService ) { super(environmentService.verbose || environmentService.isExtensionDevelopment || !environmentService.isBuilt); this._configurationService = configurationService; @@ -184,6 +186,12 @@ export class WorkbenchLanguageService extends LanguageService { if (configuration.files?.associations) { Object.keys(configuration.files.associations).forEach(pattern => { const langId = configuration.files.associations[pattern]; + if (typeof langId !== 'string') { + this.logService.warn(`Ingnoing configured 'files.associations' for '${pattern}' because its type is not a string but '${typeof langId}'`); + + return; // https://github.com/microsoft/vscode/issues/147284 + } + const mimeType = this.getMimeType(langId) || `text/x-${langId}`; registerConfiguredLanguageAssociation({ id: langId, mime: mimeType, filepattern: pattern }); diff --git a/src/vs/workbench/services/languageDetection/browser/languageDetectionSimpleWorker.ts b/src/vs/workbench/services/languageDetection/browser/languageDetectionSimpleWorker.ts index 2d9166e9ed5..59d8bf7ef8b 100644 --- a/src/vs/workbench/services/languageDetection/browser/languageDetectionSimpleWorker.ts +++ b/src/vs/workbench/services/languageDetection/browser/languageDetectionSimpleWorker.ts @@ -9,7 +9,7 @@ import { IRequestHandler } from 'vs/base/common/worker/simpleWorker'; import { EditorSimpleWorker } from 'vs/editor/common/services/editorSimpleWorker'; import { IEditorWorkerHost } from 'vs/editor/common/services/editorWorkerHost'; -type RegexpModel = { detect: (inp: string, langBiases: Record<string, number>) => string | undefined }; +type RegexpModel = { detect: (inp: string, langBiases: Record<string, number>, supportedLangs?: string[]) => string | undefined }; /** * Called on the worker side @@ -34,7 +34,9 @@ export class LanguageDetectionSimpleWorker extends EditorSimpleWorker { private _modelOperations: ModelOperations | undefined; private _loadFailed: boolean = false; - public async detectLanguage(uri: string, langBiases: Record<string, number> | undefined, preferHistory: boolean): Promise<string | undefined> { + private modelIdToCoreId = new Map<string, string>(); + + public async detectLanguage(uri: string, langBiases: Record<string, number> | undefined, preferHistory: boolean, supportedLangs?: string[]): Promise<string | undefined> { const languages: string[] = []; const confidences: number[] = []; const stopWatch = new StopWatch(true); @@ -43,8 +45,14 @@ export class LanguageDetectionSimpleWorker extends EditorSimpleWorker { const neuralResolver = async () => { for await (const language of this.detectLanguagesImpl(documentTextSample)) { - languages.push(language.languageId); - confidences.push(language.confidence); + if (!this.modelIdToCoreId.has(language.languageId)) { + this.modelIdToCoreId.set(language.languageId, await this._host.fhr('getLanguageId', [language.languageId])); + } + const coreId = this.modelIdToCoreId.get(language.languageId); + if (coreId && (!supportedLangs?.length || supportedLangs.includes(coreId))) { + languages.push(coreId); + confidences.push(language.confidence); + } } stopWatch.stop(); @@ -55,15 +63,7 @@ export class LanguageDetectionSimpleWorker extends EditorSimpleWorker { return undefined; }; - const historicalResolver = async () => { - if (langBiases) { - const regexpDetection = await this.runRegexpModel(documentTextSample, langBiases); - if (regexpDetection) { - return regexpDetection; - } - } - return undefined; - }; + const historicalResolver = async () => this.runRegexpModel(documentTextSample, langBiases ?? {}, supportedLangs); if (preferHistory) { const history = await historicalResolver(); @@ -112,11 +112,22 @@ export class LanguageDetectionSimpleWorker extends EditorSimpleWorker { } } - private async runRegexpModel(content: string, langBiases: Record<string, number>): Promise<string | undefined> { + private async runRegexpModel(content: string, langBiases: Record<string, number>, supportedLangs?: string[]): Promise<string | undefined> { const regexpModel = await this.getRegexpModel(); if (!regexpModel) { return; } - const detected = regexpModel.detect(content, langBiases); + if (supportedLangs?.length) { + // When using supportedLangs, normally computed biases are too extreme. Just use a "bitmask" of sorts. + for (const lang of Object.keys(langBiases)) { + if (supportedLangs.includes(lang)) { + langBiases[lang] = 1; + } else { + langBiases[lang] = 0; + } + } + } + + const detected = regexpModel.detect(content, langBiases, supportedLangs); return detected; } @@ -156,21 +167,21 @@ export class LanguageDetectionSimpleWorker extends EditorSimpleWorker { // For the following languages, we increase the confidence because // these are commonly used languages in VS Code and supported // by the model. - case 'javascript': + case 'js': case 'html': case 'json': - case 'typescript': + case 'ts': case 'css': - case 'python': + case 'py': case 'xml': case 'php': modelResult.confidence += LanguageDetectionSimpleWorker.positiveConfidenceCorrectionBucket1; break; // case 'yaml': // YAML has been know to cause incorrect language detection because the language is pretty simple. We don't want to increase the confidence for this. case 'cpp': - case 'shellscript': + case 'sh': case 'java': - case 'csharp': + case 'cs': case 'c': modelResult.confidence += LanguageDetectionSimpleWorker.positiveConfidenceCorrectionBucket2; break; diff --git a/src/vs/workbench/services/languageDetection/browser/languageDetectionWorkerServiceImpl.ts b/src/vs/workbench/services/languageDetection/browser/languageDetectionWorkerServiceImpl.ts index eaa283b67d3..d3428409896 100644 --- a/src/vs/workbench/services/languageDetection/browser/languageDetectionWorkerServiceImpl.ts +++ b/src/vs/workbench/services/languageDetection/browser/languageDetectionWorkerServiceImpl.ts @@ -53,7 +53,7 @@ export class LanguageDetectionService extends Disposable implements ILanguageDet constructor( @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, - @ILanguageService private readonly _languageService: ILanguageService, + @ILanguageService languageService: ILanguageService, @IConfigurationService private readonly _configurationService: IConfigurationService, @IDiagnosticsService private readonly _diagnosticsService: IDiagnosticsService, @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, @@ -68,6 +68,7 @@ export class LanguageDetectionService extends Disposable implements ILanguageDet this._languageDetectionWorkerClient = new LanguageDetectionWorkerClient( modelService, + languageService, telemetryService, // TODO: See if it's possible to bundle vscode-languagedetection this._environmentService.isBuilt && !isWeb @@ -95,7 +96,7 @@ export class LanguageDetectionService extends Disposable implements ILanguageDet let count = 0; for (const ext of fileExtensions.extensions) { - const langId = this.getLanguageId(ext); + const langId = this._languageDetectionWorkerClient.getLanguageId(ext); if (langId && count < TOP_LANG_COUNTS) { this.workspaceLanguageIds.add(langId); count++; @@ -109,15 +110,6 @@ export class LanguageDetectionService extends Disposable implements ILanguageDet return !!languageId && this._configurationService.getValue<boolean>(LanguageDetectionService.enablementSettingKey, { overrideIdentifier: languageId }); } - private getLanguageId(language: string | undefined): string | undefined { - if (!language) { - return undefined; - } - if (this._languageService.isRegisteredLanguageId(language)) { - return language; - } - return this._languageService.guessLanguageIdByFilepathOrFirstLine(URI.file(`file.${language}`)) ?? undefined; - } private getLanguageBiases(): Record<string, number> { if (!this.dirtyBiases) { return this.langBiases; } @@ -147,19 +139,14 @@ export class LanguageDetectionService extends Disposable implements ILanguageDet return biases; } - async detectLanguage(resource: URI): Promise<string | undefined> { + async detectLanguage(resource: URI, supportedLangs?: string[]): Promise<string | undefined> { const useHistory = this._configurationService.getValue<string[]>(LanguageDetectionService.historyBasedEnablementConfig); const preferHistory = this._configurationService.getValue<boolean>(LanguageDetectionService.preferHistoryConfig); if (useHistory) { await this.resolveWorkspaceLanguageIds(); } const biases = useHistory ? this.getLanguageBiases() : undefined; - const language = await this._languageDetectionWorkerClient.detectLanguage(resource, biases, preferHistory); - - if (language) { - return this.getLanguageId(language); - } - return undefined; + return this._languageDetectionWorkerClient.detectLanguage(resource, biases, preferHistory, supportedLangs); } private initEditorOpenedListeners(storageService: IStorageService) { @@ -234,6 +221,7 @@ export class LanguageDetectionWorkerClient extends EditorWorkerClient { constructor( modelService: IModelService, + private readonly _languageService: ILanguageService, private readonly _telemetryService: ITelemetryService, private readonly _indexJsUri: string, private readonly _modelJsonUri: string, @@ -260,6 +248,14 @@ export class LanguageDetectionWorkerClient extends EditorWorkerClient { return this.workerPromise; } + private _guessLanguageIdByUri(uri: URI): string | undefined { + const guess = this._languageService.guessLanguageIdByFilepathOrFirstLine(uri); + if (guess && guess !== 'unknown') { + return guess; + } + return undefined; + } + override async _getProxy(): Promise<LanguageDetectionSimpleWorker> { return (await this._getOrCreateLanguageDetectionWorker()).getProxyObject(); } @@ -275,6 +271,8 @@ export class LanguageDetectionWorkerClient extends EditorWorkerClient { return this.getWeightsUri(); case 'getRegexpModelUri': return this.getRegexpModelUri(); + case 'getLanguageId': + return this.getLanguageId(args[0]); case 'sendTelemetryEvent': return this.sendTelemetryEvent(args[0], args[1], args[2]); default: @@ -286,6 +284,20 @@ export class LanguageDetectionWorkerClient extends EditorWorkerClient { return this._indexJsUri; } + getLanguageId(languageIdOrExt: string | undefined) { + if (!languageIdOrExt) { + return undefined; + } + if (this._languageService.isRegisteredLanguageId(languageIdOrExt)) { + return languageIdOrExt; + } + const guessed = this._guessLanguageIdByUri(URI.file(`file.${languageIdOrExt}`)); + if (!guessed || guessed === 'unknown') { + return undefined; + } + return guessed; + } + async getModelJsonUri() { return this._modelJsonUri; } @@ -306,9 +318,35 @@ export class LanguageDetectionWorkerClient extends EditorWorkerClient { }); } - public async detectLanguage(resource: URI, langBiases: Record<string, number> | undefined, preferHistory: boolean): Promise<string | undefined> { + public async detectLanguage(resource: URI, langBiases: Record<string, number> | undefined, preferHistory: boolean, supportedLangs?: string[]): Promise<string | undefined> { + const startTime = Date.now(); + const quickGuess = this._guessLanguageIdByUri(resource); + if (quickGuess) { + return quickGuess; + } + await this._withSyncedResources([resource]); - return (await this._getProxy()).detectLanguage(resource.toString(), langBiases, preferHistory); + const modelId = await (await this._getProxy()).detectLanguage(resource.toString(), langBiases, preferHistory, supportedLangs); + const langaugeId = this.getLanguageId(modelId); + + const LanguageDetectionStatsId = 'automaticlanguagedetection.perf'; + + interface ILanguageDetectionPerf { + timeSpent: number; + detection: string; + } + + type LanguageDetectionPerfClassification = { + timeSpent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; + detection: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; + }; + + this._telemetryService.publicLog2<ILanguageDetectionPerf, LanguageDetectionPerfClassification>(LanguageDetectionStatsId, { + timeSpent: Date.now() - startTime, + detection: langaugeId || 'unknown', + }); + + return langaugeId; } } diff --git a/src/vs/workbench/services/languageDetection/common/languageDetectionWorkerService.ts b/src/vs/workbench/services/languageDetection/common/languageDetectionWorkerService.ts index 2b725466178..0fe24c9c6fd 100644 --- a/src/vs/workbench/services/languageDetection/common/languageDetectionWorkerService.ts +++ b/src/vs/workbench/services/languageDetection/common/languageDetectionWorkerService.ts @@ -19,9 +19,10 @@ export interface ILanguageDetectionService { /** * @param resource The resource to detect the language for. + * @param supportedLangs Optional. When populated, the model will only return languages from the provided list * @returns the language id for the given resource or undefined if the model is not confident enough. */ - detectLanguage(resource: URI): Promise<string | undefined>; + detectLanguage(resource: URI, supportedLangs?: string[]): Promise<string | undefined>; } //#region Telemetry events diff --git a/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts b/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts index 6915f6f035d..96c7fcbff3f 100644 --- a/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts +++ b/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts @@ -168,9 +168,10 @@ export class BrowserLifecycleService extends AbstractLifecycleService { const logService = this.logService; this._onWillShutdown.fire({ reason: ShutdownReason.QUIT, - token: CancellationToken.None, // Unsupported in web - join(promise, id) { - logService.error(`[lifecycle] Long running operations during shutdown are unsupported in the web (id: ${id})`); + joiners: () => [], // Unsupported in web + token: CancellationToken.None, // Unsupported in web + join(promise, joiner) { + logService.error(`[lifecycle] Long running operations during shutdown are unsupported in the web (id: ${joiner.id})`); }, force: () => { /* No-Op in web */ }, }); diff --git a/src/vs/workbench/services/lifecycle/common/lifecycle.ts b/src/vs/workbench/services/lifecycle/common/lifecycle.ts index 199577ebfde..a8c2c9e2cd6 100644 --- a/src/vs/workbench/services/lifecycle/common/lifecycle.ts +++ b/src/vs/workbench/services/lifecycle/common/lifecycle.ts @@ -65,6 +65,11 @@ export interface BeforeShutdownErrorEvent { readonly error: Error; } +export interface IWillShutdownEventJoiner { + id: string; + label: string; +} + /** * An event that is send out when the window closes. Clients have a chance to join the closing * by providing a promise from the join method. Returning a promise is useful in cases of long @@ -90,10 +95,15 @@ export interface WillShutdownEvent { * Allows to join the shutdown. The promise can be a long running operation but it * will block the application from closing. * - * @param id to identify the join operation in case it takes very long or never + * @param joiner to identify the join operation in case it takes very long or never * completes. */ - join(promise: Promise<void>, id: string): void; + join(promise: Promise<void>, joiner: IWillShutdownEventJoiner): void; + + /** + * Allows to access the joiners that have not finished joining this event. + */ + joiners(): IWillShutdownEventJoiner[]; /** * Allows to enforce the shutdown, even when there are @@ -170,7 +180,7 @@ export const enum LifecyclePhase { Eventually = 4 } -export function LifecyclePhaseToString(phase: LifecyclePhase) { +export function LifecyclePhaseToString(phase: LifecyclePhase): string { switch (phase) { case LifecyclePhase.Starting: return 'Starting'; case LifecyclePhase.Ready: return 'Ready'; diff --git a/src/vs/workbench/services/lifecycle/electron-sandbox/lifecycleService.ts b/src/vs/workbench/services/lifecycle/electron-sandbox/lifecycleService.ts index a02dbc84a53..9ad9e92efe9 100644 --- a/src/vs/workbench/services/lifecycle/electron-sandbox/lifecycleService.ts +++ b/src/vs/workbench/services/lifecycle/electron-sandbox/lifecycleService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { handleVetos } from 'vs/platform/lifecycle/common/lifecycle'; -import { ShutdownReason, ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { ShutdownReason, ILifecycleService, IWillShutdownEventJoiner } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals'; import { ILogService } from 'vs/platform/log/common/log'; @@ -155,18 +155,19 @@ export class NativeLifecycleService extends AbstractLifecycleService { protected async handleWillShutdown(reason: ShutdownReason): Promise<void> { const joiners: Promise<void>[] = []; - const pendingJoiners = new Set<string>(); + const pendingJoiners = new Set<IWillShutdownEventJoiner>(); const cts = new CancellationTokenSource(); this._onWillShutdown.fire({ reason, token: cts.token, - join(promise, id) { + joiners: () => Array.from(pendingJoiners.values()), + join(promise, joiner) { joiners.push(promise); // Track promise completion - pendingJoiners.add(id); - promise.finally(() => pendingJoiners.delete(id)); + pendingJoiners.add(joiner); + promise.finally(() => pendingJoiners.delete(joiner)); }, force: () => { cts.dispose(true); @@ -174,7 +175,7 @@ export class NativeLifecycleService extends AbstractLifecycleService { }); const longRunningWillShutdownWarning = disposableTimeout(() => { - this.logService.warn(`[lifecycle] onWillShutdown is taking a long time, pending operations: ${Array.from(pendingJoiners).join(', ')}`); + this.logService.warn(`[lifecycle] onWillShutdown is taking a long time, pending operations: ${Array.from(pendingJoiners).map(joiner => joiner.id).join(', ')}`); }, NativeLifecycleService.WILL_SHUTDOWN_WARNING_DELAY); try { diff --git a/src/vs/workbench/services/lifecycle/test/electron-browser/lifecycleService.test.ts b/src/vs/workbench/services/lifecycle/test/electron-browser/lifecycleService.test.ts index 63cd2106537..4edcf36737f 100644 --- a/src/vs/workbench/services/lifecycle/test/electron-browser/lifecycleService.test.ts +++ b/src/vs/workbench/services/lifecycle/test/electron-browser/lifecycleService.test.ts @@ -132,7 +132,7 @@ suite('Lifecycleservice', function () { joinCalled = true; resolve(); - }), 'test'); + }), { id: 'test', label: 'test' }); }); await lifecycleService.handleWillShutdown(ShutdownReason.QUIT); @@ -148,7 +148,7 @@ suite('Lifecycleservice', function () { joinCalled = true; reject(new Error('Fail')); - }), 'test'); + }), { id: 'test', label: 'test' }); }); await lifecycleService.handleWillShutdown(ShutdownReason.QUIT); diff --git a/src/vs/workbench/services/output/common/output.ts b/src/vs/workbench/services/output/common/output.ts index f12fa430d7a..1d96dcd198a 100644 --- a/src/vs/workbench/services/output/common/output.ts +++ b/src/vs/workbench/services/output/common/output.ts @@ -6,6 +6,149 @@ import { Event, Emitter } from 'vs/base/common/event'; import { Registry } from 'vs/platform/registry/common/platform'; import { URI } from 'vs/base/common/uri'; +import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IFileService, whenProviderRegistered } from 'vs/platform/files/common/files'; +import { ILogService } from 'vs/platform/log/common/log'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { CancellationError, getErrorMessage, isCancellationError } from 'vs/base/common/errors'; +import { CancelablePromise, createCancelablePromise, timeout } from 'vs/base/common/async'; + +/** + * Mime type used by the output editor. + */ +export const OUTPUT_MIME = 'text/x-code-output'; + +/** + * Output resource scheme. + */ +export const OUTPUT_SCHEME = 'output'; + +/** + * Id used by the output editor. + */ +export const OUTPUT_MODE_ID = 'Log'; + +/** + * Mime type used by the log output editor. + */ +export const LOG_MIME = 'text/x-code-log-output'; + +/** + * Log resource scheme. + */ +export const LOG_SCHEME = 'log'; + +/** + * Id used by the log output editor. + */ +export const LOG_MODE_ID = 'log'; + +/** + * Output view id + */ +export const OUTPUT_VIEW_ID = 'workbench.panel.output'; + +export const OUTPUT_SERVICE_ID = 'outputService'; + +export const MAX_OUTPUT_LENGTH = 10000 /* Max. number of output lines to show in output */ * 100 /* Guestimated chars per line */; + +export const CONTEXT_IN_OUTPUT = new RawContextKey<boolean>('inOutput', false); + +export const CONTEXT_ACTIVE_LOG_OUTPUT = new RawContextKey<boolean>('activeLogOutput', false); + +export const CONTEXT_OUTPUT_SCROLL_LOCK = new RawContextKey<boolean>(`outputView.scrollLock`, false); + +export const IOutputService = createDecorator<IOutputService>(OUTPUT_SERVICE_ID); + +/** + * The output service to manage output from the various processes running. + */ +export interface IOutputService { + readonly _serviceBrand: undefined; + + /** + * Given the channel id returns the output channel instance. + * Channel should be first registered via OutputChannelRegistry. + */ + getChannel(id: string): IOutputChannel | undefined; + + /** + * Given the channel id returns the registered output channel descriptor. + */ + getChannelDescriptor(id: string): IOutputChannelDescriptor | undefined; + + /** + * Returns an array of all known output channels descriptors. + */ + getChannelDescriptors(): IOutputChannelDescriptor[]; + + /** + * Returns the currently active channel. + * Only one channel can be active at a given moment. + */ + getActiveChannel(): IOutputChannel | undefined; + + /** + * Show the channel with the passed id. + */ + showChannel(id: string, preserveFocus?: boolean): Promise<void>; + + /** + * Allows to register on active output channel change. + */ + onActiveOutputChannel: Event<string>; +} + +export enum OutputChannelUpdateMode { + Append = 1, + Replace, + Clear +} + +export interface IOutputChannel { + + /** + * Identifier of the output channel. + */ + id: string; + + /** + * Label of the output channel to be displayed to the user. + */ + label: string; + + /** + * URI of the output channel. + */ + uri: URI; + + /** + * Appends output to the channel. + */ + append(output: string): void; + + /** + * Clears all received output for this channel. + */ + clear(): void; + + /** + * Replaces the content of the channel with given output + */ + replace(output: string): void; + + /** + * Update the channel. + */ + update(mode: OutputChannelUpdateMode.Append): void; + update(mode: OutputChannelUpdateMode, till: number): void; + + /** + * Disposes the output channel. + */ + dispose(): void; +} export const Extensions = { OutputChannels: 'workbench.contributions.outputChannels' @@ -82,3 +225,34 @@ class OutputChannelRegistry implements IOutputChannelRegistry { } Registry.add(Extensions.OutputChannels, new OutputChannelRegistry()); + +export function registerLogChannel(id: string, label: string, file: URI, fileService: IFileService, logService: ILogService): CancelablePromise<void> { + return createCancelablePromise(async token => { + await whenProviderRegistered(file, fileService); + const outputChannelRegistry = Registry.as<IOutputChannelRegistry>(Extensions.OutputChannels); + try { + await whenFileExists(file, 1, fileService, logService, token); + outputChannelRegistry.registerChannel({ id, label, file, log: true }); + } catch (error) { + if (!isCancellationError(error)) { + logService.error('Error while registering log channel', file.toString(), getErrorMessage(error)); + } + } + }); +} + +async function whenFileExists(file: URI, trial: number, fileService: IFileService, logService: ILogService, token: CancellationToken): Promise<void> { + const exists = await fileService.exists(file); + if (exists) { + return; + } + if (token.isCancellationRequested) { + throw new CancellationError(); + } + if (trial > 10) { + throw new Error(`Timed out while waiting for file to be created`); + } + logService.debug(`[Registering Log Channel] File does not exist. Waiting for 1s to retry.`, file.toString()); + await timeout(1000, token); + await whenFileExists(file, trial + 1, fileService, logService, token); +} diff --git a/src/vs/workbench/services/path/browser/pathService.ts b/src/vs/workbench/services/path/browser/pathService.ts index 85f31c841fb..215f1ec36ad 100644 --- a/src/vs/workbench/services/path/browser/pathService.ts +++ b/src/vs/workbench/services/path/browser/pathService.ts @@ -9,6 +9,8 @@ import { IPathService, AbstractPathService } from 'vs/workbench/services/path/co import { URI } from 'vs/base/common/uri'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { firstOrDefault } from 'vs/base/common/arrays'; +import { dirname } from 'vs/base/common/resources'; export class BrowserPathService extends AbstractPathService { @@ -17,11 +19,8 @@ export class BrowserPathService extends AbstractPathService { @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, @IWorkspaceContextService contextService: IWorkspaceContextService ) { - super(URI.from({ - scheme: AbstractPathService.findDefaultUriScheme(environmentService, contextService), - authority: environmentService.remoteAuthority, - path: '/' - }), + super( + guessLocalUserHome(environmentService, contextService), remoteAgentService, environmentService, contextService @@ -29,4 +28,33 @@ export class BrowserPathService extends AbstractPathService { } } +function guessLocalUserHome(environmentService: IWorkbenchEnvironmentService, contextService: IWorkspaceContextService): URI { + + // In web we do not really have the concept of a "local" user home + // but we still require it in many places as a fallback. As such, + // we have to come up with a synthetic location derived from the + // environment. + + const workspace = contextService.getWorkspace(); + + const firstFolder = firstOrDefault(workspace.folders); + if (firstFolder) { + return firstFolder.uri; + } + + if (workspace.configuration) { + return dirname(workspace.configuration); + } + + // This is not ideal because with a user home location of `/`, all paths + // will potentially appear with `~/...`, but at this point we really do + // not have any other good alternative. + + return URI.from({ + scheme: AbstractPathService.findDefaultUriScheme(environmentService, contextService), + authority: environmentService.remoteAuthority, + path: '/' + }); +} + registerSingleton(IPathService, BrowserPathService, true); diff --git a/src/vs/workbench/services/path/common/pathService.ts b/src/vs/workbench/services/path/common/pathService.ts index b00c949dc3f..83baa4126b0 100644 --- a/src/vs/workbench/services/path/common/pathService.ts +++ b/src/vs/workbench/services/path/common/pathService.ts @@ -57,6 +57,7 @@ export interface IPathService { * remote's user home directory, otherwise the local one unless * `preferLocal` is set to `true`. */ + userHome(options: { preferLocal: true }): URI; userHome(options?: { preferLocal: boolean }): Promise<URI>; /** @@ -103,7 +104,7 @@ export abstract class AbstractPathService implements IPathService { // User Home this.resolveUserHome = (async () => { const env = await this.remoteAgentService.getEnvironment(); - const userHome = this.maybeUnresolvedUserHome = env?.userHome || localUserHome; + const userHome = this.maybeUnresolvedUserHome = env?.userHome ?? localUserHome; return userHome; })(); @@ -138,7 +139,7 @@ export abstract class AbstractPathService implements IPathService { return AbstractPathService.findDefaultUriScheme(this.environmentService, this.contextService); } - protected static findDefaultUriScheme(environmentService: IWorkbenchEnvironmentService, contextService: IWorkspaceContextService): string { + static findDefaultUriScheme(environmentService: IWorkbenchEnvironmentService, contextService: IWorkspaceContextService): string { if (environmentService.remoteAuthority) { return Schemas.vscodeRemote; } @@ -161,7 +162,9 @@ export abstract class AbstractPathService implements IPathService { return Schemas.file; } - async userHome(options?: { preferLocal: boolean }): Promise<URI> { + userHome(options?: { preferLocal: boolean }): Promise<URI>; + userHome(options: { preferLocal: true }): URI; + userHome(options?: { preferLocal: boolean }): Promise<URI> | URI { return options?.preferLocal ? this.localUserHome : this.resolveUserHome; } diff --git a/src/vs/workbench/services/preferences/browser/preferencesService.ts b/src/vs/workbench/services/preferences/browser/preferencesService.ts index eb5737fa9af..555a3a7ee20 100644 --- a/src/vs/workbench/services/preferences/browser/preferencesService.ts +++ b/src/vs/workbench/services/preferences/browser/preferencesService.ts @@ -17,7 +17,6 @@ import { IModelService } from 'vs/editor/common/services/model'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import * as nls from 'vs/nls'; -import { ICommandService } from 'vs/platform/commands/common/commands'; 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'; @@ -46,6 +45,7 @@ import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteA import { ITextEditorService } from 'vs/workbench/services/textfile/common/textEditorService'; 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'; const emptyEditableSettingsContent = '{\n}'; @@ -76,7 +76,6 @@ export class PreferencesService extends Disposable implements IPreferencesServic @ILanguageService private readonly languageService: ILanguageService, @ILabelService private readonly labelService: ILabelService, @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService, - @ICommandService private readonly commandService: ICommandService, @ITextEditorService private readonly textEditorService: ITextEditorService ) { super(); @@ -542,7 +541,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic codeEditor.revealPositionNearTop(position); codeEditor.focus(); if (edit) { - await this.commandService.executeCommand('editor.action.triggerSuggest'); + SuggestController.get(codeEditor)?.triggerSuggest(); } } } diff --git a/src/vs/workbench/services/preferences/common/preferencesModels.ts b/src/vs/workbench/services/preferences/common/preferencesModels.ts index df0250c0d61..000a74b77ca 100644 --- a/src/vs/workbench/services/preferences/common/preferencesModels.ts +++ b/src/vs/workbench/services/preferences/common/preferencesModels.ts @@ -903,7 +903,7 @@ export class DefaultSettingsEditorModel extends AbstractSettingsModel implements // Force tokenization now - otherwise it may be slightly delayed, causing a flash of white text const tokenizeTo = Math.min(startLine + 60, this._model.getLineCount()); - this._model.forceTokenization(tokenizeTo); + this._model.tokenization.forceTokenization(tokenizeTo); return { matches, settingsGroups }; } diff --git a/src/vs/workbench/services/preferences/test/browser/preferencesService.test.ts b/src/vs/workbench/services/preferences/test/browser/preferencesService.test.ts index 4f5a7658424..b83421feb0b 100644 --- a/src/vs/workbench/services/preferences/test/browser/preferencesService.test.ts +++ b/src/vs/workbench/services/preferences/test/browser/preferencesService.test.ts @@ -15,8 +15,7 @@ import { TestJSONEditingService } from 'vs/workbench/services/configuration/test import { PreferencesService } from 'vs/workbench/services/preferences/browser/preferencesService'; import { IPreferencesService, ISettingsEditorOptions } from 'vs/workbench/services/preferences/common/preferences'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; -import { TestRemoteAgentService } from 'vs/workbench/services/remote/test/common/testServices'; -import { ITestInstantiationService, TestEditorService, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestRemoteAgentService, ITestInstantiationService, TestEditorService, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; suite('PreferencesService', () => { diff --git a/src/vs/workbench/services/profiles/common/extensionsProfile.ts b/src/vs/workbench/services/profiles/common/extensionsProfile.ts new file mode 100644 index 00000000000..3e10dd0f9f5 --- /dev/null +++ b/src/vs/workbench/services/profiles/common/extensionsProfile.ts @@ -0,0 +1,102 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementService, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +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'; + +interface IProfileExtension { + identifier: IExtensionIdentifier; + preRelease?: boolean; + disabled?: boolean; +} + +export class ExtensionsProfile implements IResourceProfile { + + constructor( + @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, + @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, + @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, + @ILogService private readonly logService: ILogService, + ) { + } + + async getProfileContent(): Promise<string> { + const extensions = await this.getLocalExtensions(); + return JSON.stringify(extensions); + } + + async applyProfile(content: string): Promise<void> { + const profileExtensions: IProfileExtension[] = JSON.parse(content); + const installedExtensions = await this.extensionManagementService.getInstalled(); + const extensionsToEnableOrDisable: { extension: ILocalExtension; enablementState: EnablementState }[] = []; + const extensionsToInstall: IProfileExtension[] = []; + for (const e of profileExtensions) { + const installedExtension = installedExtensions.find(installed => areSameExtensions(installed.identifier, e.identifier)); + if (!installedExtension || installedExtension.preRelease !== e.preRelease) { + extensionsToInstall.push(e); + } + if (installedExtension && this.extensionEnablementService.isEnabled(installedExtension) !== !e.disabled) { + extensionsToEnableOrDisable.push({ extension: installedExtension, enablementState: e.disabled ? EnablementState.DisabledGlobally : EnablementState.EnabledGlobally }); + } + } + const extensionsToUninstall: ILocalExtension[] = installedExtensions.filter(extension => extension.type === ExtensionType.User && !profileExtensions.some(({ identifier }) => areSameExtensions(identifier, extension.identifier))); + for (const { extension, enablementState } of extensionsToEnableOrDisable) { + this.logService.trace(`Profile: Updating extension enablement...`, extension.identifier.id); + await this.extensionEnablementService.setEnablement([extension], enablementState); + this.logService.info(`Profile: Updated extension enablement`, extension.identifier.id); + } + if (extensionsToInstall.length) { + const galleryExtensions = await this.extensionGalleryService.getExtensions(extensionsToInstall.map(e => ({ ...e.identifier, hasPreRelease: e.preRelease })), CancellationToken.None); + await Promise.all(extensionsToInstall.map(async e => { + const extension = galleryExtensions.find(galleryExtension => areSameExtensions(galleryExtension.identifier, e.identifier)); + if (!extension) { + return; + } + if (await this.extensionManagementService.canInstall(extension)) { + this.logService.trace(`Profile: Installing extension...`, e.identifier.id, extension.version); + await this.extensionManagementService.installFromGallery(extension, { isMachineScoped: false, donotIncludePackAndDependencies: true, installPreReleaseVersion: e.preRelease } /* set isMachineScoped value to prevent install and sync dialog in web */); + this.logService.info(`Profile: Installed extension.`, e.identifier.id, extension.version); + } else { + this.logService.info(`Profile: Skipped installing extension because it cannot be installed.`, extension.displayName || extension.identifier.id); + } + })); + } + if (extensionsToUninstall.length) { + await Promise.all(extensionsToUninstall.map(e => this.extensionManagementService.uninstall(e))); + } + } + + private async getLocalExtensions(): Promise<IProfileExtension[]> { + const result: IProfileExtension[] = []; + const installedExtensions = await this.extensionManagementService.getInstalled(undefined); + for (const extension of installedExtensions) { + const { identifier, preRelease } = extension; + const enablementState = this.extensionEnablementService.getEnablementState(extension); + const disabled = !this.extensionEnablementService.isEnabledEnablementState(enablementState); + if (!disabled && extension.type === ExtensionType.System) { + // skip enabled system extensions + continue; + } + if (disabled && enablementState !== EnablementState.DisabledGlobally && enablementState !== EnablementState.DisabledWorkspace) { + //skip extensions that are not disabled by user + continue; + } + const profileExtension: IProfileExtension = { identifier }; + if (disabled) { + profileExtension.disabled = true; + } + if (preRelease) { + profileExtension.preRelease = true; + } + result.push(profileExtension); + } + return result; + } +} diff --git a/src/vs/workbench/services/profiles/common/globalStateProfile.ts b/src/vs/workbench/services/profiles/common/globalStateProfile.ts new file mode 100644 index 00000000000..7016a4c6136 --- /dev/null +++ b/src/vs/workbench/services/profiles/common/globalStateProfile.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 { 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'; + +interface IGlobalState { + storage: IStringDictionary<string>; +} + +export class GlobalStateProfile implements IResourceProfile { + + constructor( + @IStorageService private readonly storageService: IStorageService, + @ILogService private readonly logService: ILogService, + ) { + } + + async getProfileContent(): Promise<string> { + const globalState = await this.getLocalGlobalState(); + return JSON.stringify(globalState); + } + + async applyProfile(content: string): Promise<void> { + const globalState: IGlobalState = JSON.parse(content); + await this.writeLocalGlobalState(globalState); + } + + 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); + if (value) { + storage[key] = value; + } + } + return { storage }; + } + + private async writeLocalGlobalState(globalState: IGlobalState): Promise<void> { + const profileKeys: string[] = Object.keys(globalState.storage); + const updatedStorage: IStringDictionary<any> = globalState.storage; + for (const { key } of Registry.as<IProfileStorageRegistry>(Extensions.ProfileStorageRegistry).all) { + if (!profileKeys.includes(key)) { + // Remove the key if it does not exist in the profile + updatedStorage[key] = undefined; + } + } + const updatedStorageKeys: string[] = Object.keys(updatedStorage); + 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.logService.info(`Profile: Updated global state`, updatedStorageKeys); + } + } +} diff --git a/src/vs/workbench/services/profiles/common/profile.ts b/src/vs/workbench/services/profiles/common/profile.ts new file mode 100644 index 00000000000..a57331a9c56 --- /dev/null +++ b/src/vs/workbench/services/profiles/common/profile.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 { 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 new file mode 100644 index 00000000000..053b9e59bcf --- /dev/null +++ b/src/vs/workbench/services/profiles/common/profileService.ts @@ -0,0 +1,65 @@ +/*--------------------------------------------------------------------------------------------- + * 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/profiles/common/profileStorageRegistry.ts b/src/vs/workbench/services/profiles/common/profileStorageRegistry.ts new file mode 100644 index 00000000000..807b20e259b --- /dev/null +++ b/src/vs/workbench/services/profiles/common/profileStorageRegistry.ts @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event, Emitter } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { Registry } from 'vs/platform/registry/common/platform'; + +export namespace Extensions { + export const ProfileStorageRegistry = 'workbench.registry.profile.storage'; +} + +export interface IProfileStorageKey { + readonly key: string; + readonly description?: string; +} + +/** + * A registry for storage keys used for profiles + */ +export interface IProfileStorageRegistry { + /** + * An event that is triggered when storage keys are registered. + */ + readonly onDidRegister: Event<readonly IProfileStorageKey[]>; + + /** + * All registered storage keys + */ + readonly all: IProfileStorageKey[]; + + /** + * Register profile storage keys + * + * @param keys keys to register + */ + registerKeys(keys: IProfileStorageKey[]): void; +} + +class ProfileStorageRegistryImpl extends Disposable implements IProfileStorageRegistry { + + private readonly _onDidRegister = this._register(new Emitter<readonly IProfileStorageKey[]>()); + readonly onDidRegister = this._onDidRegister.event; + + private readonly storageKeys = new Map<string, IProfileStorageKey>(); + + get all(): IProfileStorageKey[] { + return [...this.storageKeys.values()].flat(); + } + + registerKeys(keys: IProfileStorageKey[]): void { + for (const key of keys) { + this.storageKeys.set(key.key, key); + } + this._onDidRegister.fire(keys); + } + +} + +Registry.add(Extensions.ProfileStorageRegistry, new ProfileStorageRegistryImpl()); + diff --git a/src/vs/workbench/services/profiles/common/settingsProfile.ts b/src/vs/workbench/services/profiles/common/settingsProfile.ts new file mode 100644 index 00000000000..906e0891449 --- /dev/null +++ b/src/vs/workbench/services/profiles/common/settingsProfile.ts @@ -0,0 +1,69 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { 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 { 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; +} + +export class SettingsProfile implements IResourceProfile { + + constructor( + @IFileService private readonly fileService: IFileService, + @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IUserDataSyncUtilService private readonly userDataSyncUtilService: IUserDataSyncUtilService, + @ILogService private readonly logService: ILogService, + ) { + } + + async getProfileContent(options?: ProfileCreationOptions): Promise<string> { + const ignoredSettings = this.getIgnoredSettings(); + const formattingOptions = await this.userDataSyncUtilService.resolveFormattingOptions(this.environmentService.settingsResource); + const localContent = await this.getLocalFileContent(); + let settingsProfileContent = updateIgnoredSettings(localContent || '{}', '{}', ignoredSettings, formattingOptions); + if (options?.skipComments) { + settingsProfileContent = removeComments(settingsProfileContent, formattingOptions); + } + const settingsContent: ISettingsContent = { + settings: settingsProfileContent + }; + return JSON.stringify(settingsContent); + } + + async applyProfile(content: string): Promise<void> { + 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 contentToUpdate = updateIgnoredSettings(settingsContent.settings, localSettingsContent || '{}', this.getIgnoredSettings(), formattingOptions); + await this.fileService.writeFile(this.environmentService.settingsResource, VSBuffer.fromString(contentToUpdate)); + this.logService.info(`Profile: Applied settings`); + } + + private getIgnoredSettings(): string[] { + const allSettings = Registry.as<IConfigurationRegistry>(Extensions.Configuration).getConfigurationProperties(); + const ignoredSettings = Object.keys(allSettings).filter(key => allSettings[key]?.scope === ConfigurationScope.MACHINE || allSettings[key]?.scope === ConfigurationScope.MACHINE_OVERRIDABLE); + return ignoredSettings; + } + + private async getLocalFileContent(): Promise<string | null> { + try { + const content = await this.fileService.readFile(this.environmentService.settingsResource); + return content.value.toString(); + } catch (error) { + return null; + } + } + +} diff --git a/src/vs/workbench/services/remote/test/common/testServices.ts b/src/vs/workbench/services/remote/test/common/testServices.ts deleted file mode 100644 index f4323c7bc65..00000000000 --- a/src/vs/workbench/services/remote/test/common/testServices.ts +++ /dev/null @@ -1,53 +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 { URI } from 'vs/base/common/uri'; -import { IDiagnosticInfoOptions, IDiagnosticInfo } from 'vs/platform/diagnostics/common/diagnostics'; -import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { ISocketFactory } from 'vs/platform/remote/common/remoteAgentConnection'; -import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment'; -import { ITelemetryData, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; -import { IExtensionHostExitInfo, IRemoteAgentConnection, IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; - -export class TestRemoteAgentService implements IRemoteAgentService { - _serviceBrand: undefined; - socketFactory: ISocketFactory = { - connect() { } - }; - getConnection(): IRemoteAgentConnection | null { - throw new Error('Method not implemented.'); - } - getEnvironment(): Promise<IRemoteAgentEnvironment | null> { - throw new Error('Method not implemented.'); - } - getRawEnvironment(): Promise<IRemoteAgentEnvironment | null> { - throw new Error('Method not implemented.'); - } - getExtensionHostExitInfo(reconnectionToken: string): Promise<IExtensionHostExitInfo | null> { - throw new Error('Method not implemented.'); - } - whenExtensionsReady(): Promise<void> { - throw new Error('Method not implemented.'); - } - scanExtensions(skipExtensions?: ExtensionIdentifier[]): Promise<IExtensionDescription[]> { - throw new Error('Method not implemented.'); - } - scanSingleExtension(extensionLocation: URI, isBuiltin: boolean): Promise<IExtensionDescription | null> { - throw new Error('Method not implemented.'); - } - getDiagnosticInfo(options: IDiagnosticInfoOptions): Promise<IDiagnosticInfo | undefined> { - throw new Error('Method not implemented.'); - } - updateTelemetryLevel(telemetryLevel: TelemetryLevel): Promise<void> { - throw new Error('Method not implemented.'); - } - logTelemetry(eventName: string, data?: ITelemetryData): Promise<void> { - throw new Error('Method not implemented.'); - } - flushTelemetry(): Promise<void> { - throw new Error('Method not implemented.'); - } - -} diff --git a/src/vs/workbench/services/search/common/fileSearchManager.ts b/src/vs/workbench/services/search/common/fileSearchManager.ts index 27453fd8138..c5c8b2d10b4 100644 --- a/src/vs/workbench/services/search/common/fileSearchManager.ts +++ b/src/vs/workbench/services/search/common/fileSearchManager.ts @@ -224,8 +224,8 @@ class FileSearchEngine { // Check exclude pattern // If the user searches for the exact file name, we adjust the glob matching - // to ignore filtering by siblings because the user seems to know what she - // is searching for and we want to include the result in that case anyway + // to ignore filtering by siblings because the user seems to know what they + // are searching for and we want to include the result in that case anyway if (queryTester.matchesExcludesSync(relativePath, basename, filePattern !== basename ? hasSibling : undefined)) { continue; } diff --git a/src/vs/workbench/services/search/common/searchExtTypes.ts b/src/vs/workbench/services/search/common/searchExtTypes.ts index 27f646183f4..5b8449ffd83 100644 --- a/src/vs/workbench/services/search/common/searchExtTypes.ts +++ b/src/vs/workbench/services/search/common/searchExtTypes.ts @@ -74,7 +74,7 @@ export interface RelativePattern { * (like `** /*.{ts,js}` without space before / or `*.{ts,js}`) or a [relative pattern](#RelativePattern). * * Glob patterns can have the following syntax: - * * `*` to match one or more characters in a path segment + * * `*` to match zero or more characters in a path segment * * `?` to match on one character in a path segment * * `**` to match any number of path segments, including none * * `{}` to group conditions (e.g. `** /*.{ts,js}` without space before / matches all TypeScript and JavaScript files) diff --git a/src/vs/workbench/services/search/node/fileSearch.ts b/src/vs/workbench/services/search/node/fileSearch.ts index 60719151216..e6b9bb1633c 100644 --- a/src/vs/workbench/services/search/node/fileSearch.ts +++ b/src/vs/workbench/services/search/node/fileSearch.ts @@ -429,8 +429,8 @@ export class FileWalker { // Check exclude pattern // If the user searches for the exact file name, we adjust the glob matching - // to ignore filtering by siblings because the user seems to know what she - // is searching for and we want to include the result in that case anyway + // to ignore filtering by siblings because the user seems to know what they + // are searching for and we want to include the result in that case anyway if (excludePattern.test(relativePath, basename, filePattern !== basename ? hasSibling : undefined)) { continue; } @@ -479,8 +479,8 @@ export class FileWalker { // Check exclude pattern // If the user searches for the exact file name, we adjust the glob matching - // to ignore filtering by siblings because the user seems to know what she - // is searching for and we want to include the result in that case anyway + // to ignore filtering by siblings because the user seems to know what they + // are searching for and we want to include the result in that case anyway const currentRelativePath = relativeParentPath ? [relativeParentPath, file].join(path.sep) : file; if (this.folderExcludePatterns.get(folderQuery.folder.fsPath)!.test(currentRelativePath, file, this.config.filePattern !== file ? hasSibling : undefined)) { return clb(null); diff --git a/src/vs/workbench/services/search/worker/localFileSearch.ts b/src/vs/workbench/services/search/worker/localFileSearch.ts index e083611508a..108ffef411e 100644 --- a/src/vs/workbench/services/search/worker/localFileSearch.ts +++ b/src/vs/workbench/services/search/worker/localFileSearch.ts @@ -190,7 +190,7 @@ export class LocalFileSearchSimpleWorker implements ILocalFileSearchSimpleWorker const isFileIncluded = (path: string, basename: string, hasSibling: (query: string) => boolean) => { path = path.slice(1); if (folderExcludes(path, basename, hasSibling)) { return false; } - if (!pathIncludedInQuery(queryProps, URI.file(path), extUri)) { return false; } + if (!pathIncludedInQuery(queryProps, path, extUri)) { return false; } return true; }; @@ -325,13 +325,13 @@ function pathExcludedInQuery(queryProps: ICommonQueryProps<URI>, fsPath: string) return false; } -function pathIncludedInQuery(queryProps: ICommonQueryProps<URI>, path: URI, extUri: ExtUri): boolean { - if (queryProps.excludePattern && glob.match(queryProps.excludePattern, path.fsPath)) { +function pathIncludedInQuery(queryProps: ICommonQueryProps<URI>, path: string, extUri: ExtUri): boolean { + if (queryProps.excludePattern && glob.match(queryProps.excludePattern, path)) { return false; } if (queryProps.includePattern || queryProps.usingSearchPaths) { - if (queryProps.includePattern && glob.match(queryProps.includePattern, path.fsPath)) { + if (queryProps.includePattern && glob.match(queryProps.includePattern, path)) { return true; } @@ -340,8 +340,9 @@ function pathIncludedInQuery(queryProps: ICommonQueryProps<URI>, path: URI, extU return !!queryProps.folderQueries && queryProps.folderQueries.some(fq => { const searchPath = fq.folder; - if (extUri.isEqualOrParent(path, searchPath)) { - const relPath = paths.relative(searchPath.path, path.path); + const uri = URI.file(path); + if (extUri.isEqualOrParent(uri, searchPath)) { + const relPath = paths.relative(searchPath.path, uri.path); return !fq.includePattern || !!glob.match(fq.includePattern, relPath); } else { return false; diff --git a/src/vs/workbench/services/sharedProcess/electron-sandbox/sharedProcessService.ts b/src/vs/workbench/services/sharedProcess/electron-sandbox/sharedProcessService.ts index 2806378bc82..6590bc91938 100644 --- a/src/vs/workbench/services/sharedProcess/electron-sandbox/sharedProcessService.ts +++ b/src/vs/workbench/services/sharedProcess/electron-sandbox/sharedProcessService.ts @@ -42,6 +42,7 @@ export class SharedProcessService extends Disposable implements ISharedProcessSe // Acquire a message port connected to the shared process mark('code/willConnectSharedProcess'); + this.logService.trace('Renderer->SharedProcess#connect: before acquirePort'); const port = await acquirePort('vscode:createSharedProcessMessageChannel', 'vscode:createSharedProcessMessageChannelResult'); mark('code/didConnectSharedProcess'); this.logService.trace('Renderer->SharedProcess#connect: connection established'); diff --git a/src/vs/workbench/services/telemetry/browser/telemetryService.ts b/src/vs/workbench/services/telemetry/browser/telemetryService.ts index c65ee021e76..4b4d5ead47c 100644 --- a/src/vs/workbench/services/telemetry/browser/telemetryService.ts +++ b/src/vs/workbench/services/telemetry/browser/telemetryService.ts @@ -5,6 +5,7 @@ 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'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ILoggerService } from 'vs/platform/log/common/log'; @@ -25,7 +26,7 @@ class WebAppInsightsAppender implements ITelemetryAppender { private _telemetryCache: { eventName: string; data: any }[] = []; constructor(private _eventPrefix: string, aiKey: string) { - const endpointUrl = 'https://vortex.data.microsoft.com/collect/v1'; + const endpointUrl = 'https://mobile.events.data.microsoft.com/collect/v1'; import('@microsoft/applicationinsights-web').then(aiLibrary => { this._aiClient = new aiLibrary.ApplicationInsights({ config: { @@ -138,7 +139,7 @@ export class TelemetryService extends Disposable implements ITelemetryService { return this.impl.setExperimentProperty(name, value); } - get telemetryLevel(): TelemetryLevel { + get telemetryLevel(): IObservableValue<TelemetryLevel> { return this.impl.telemetryLevel; } diff --git a/src/vs/workbench/services/telemetry/electron-sandbox/telemetryService.ts b/src/vs/workbench/services/telemetry/electron-sandbox/telemetryService.ts index ca23c70a78f..a91c4a23be9 100644 --- a/src/vs/workbench/services/telemetry/electron-sandbox/telemetryService.ts +++ b/src/vs/workbench/services/telemetry/electron-sandbox/telemetryService.ts @@ -17,6 +17,7 @@ import { TelemetryService as BaseTelemetryService, ITelemetryServiceConfig } fro import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ClassifiedEvent, StrictPropertyCheck, GDPRClassification } from 'vs/platform/telemetry/common/gdprTypings'; import { IFileService } from 'vs/platform/files/common/files'; +import { IObservableValue } from 'vs/base/common/observableValue'; export class TelemetryService extends Disposable implements ITelemetryService { @@ -56,7 +57,7 @@ export class TelemetryService extends Disposable implements ITelemetryService { return this.impl.setExperimentProperty(name, value); } - get telemetryLevel(): TelemetryLevel { + get telemetryLevel(): IObservableValue<TelemetryLevel> { return this.impl.telemetryLevel; } diff --git a/src/vs/workbench/services/textMate/browser/abstractTextMateService.ts b/src/vs/workbench/services/textMate/browser/abstractTextMateService.ts index 57aa1cd2c21..0df6157e1bd 100644 --- a/src/vs/workbench/services/textMate/browser/abstractTextMateService.ts +++ b/src/vs/workbench/services/textMate/browser/abstractTextMateService.ts @@ -133,6 +133,16 @@ export abstract class AbstractTextMateService extends Disposable implements ITex validLanguageId = grammar.language; } + function asStringArray(array: unknown, defaultValue: string[]): string[] { + if (!Array.isArray(array)) { + return defaultValue; + } + if (!array.every(e => typeof e === 'string')) { + return defaultValue; + } + return array; + } + this._grammarDefinitions.push({ location: grammarLocation, language: validLanguageId ? validLanguageId : undefined, @@ -140,6 +150,8 @@ export abstract class AbstractTextMateService extends Disposable implements ITex embeddedLanguages: embeddedLanguages, tokenTypes: tokenTypes, injectTo: grammar.injectTo, + balancedBracketSelectors: asStringArray(grammar.balancedBracketScopes, ['*']), + unbalancedBracketSelectors: asStringArray(grammar.unbalancedBracketScopes, []), }); if (validLanguageId) { diff --git a/src/vs/workbench/services/textMate/browser/nativeTextMateService.ts b/src/vs/workbench/services/textMate/browser/nativeTextMateService.ts index de5b056af74..bbf4fddc3e0 100644 --- a/src/vs/workbench/services/textMate/browser/nativeTextMateService.ts +++ b/src/vs/workbench/services/textMate/browser/nativeTextMateService.ts @@ -120,7 +120,7 @@ class ModelWorkerTextMateTokenizer extends Disposable { } } - this._model.setTokens(tokens); + this._model.tokenization.setTokens(tokens); } } diff --git a/src/vs/workbench/services/textMate/browser/textMateWorker.ts b/src/vs/workbench/services/textMate/browser/textMateWorker.ts index d5169697d97..6b848d33c24 100644 --- a/src/vs/workbench/services/textMate/browser/textMateWorker.ts +++ b/src/vs/workbench/services/textMate/browser/textMateWorker.ts @@ -25,6 +25,8 @@ export interface IValidGrammarDefinitionDTO { embeddedLanguages: IValidEmbeddedLanguagesMap; tokenTypes: IValidTokenTypeMap; injectTo?: string[]; + balancedBracketSelectors: string[]; + unbalancedBracketSelectors: string[]; } export interface ICreateData { @@ -143,6 +145,8 @@ export class TextMateWorker { embeddedLanguages: def.embeddedLanguages, tokenTypes: def.tokenTypes, injectTo: def.injectTo, + balancedBracketSelectors: def.balancedBracketSelectors, + unbalancedBracketSelectors: def.unbalancedBracketSelectors, }; }); this._grammarFactory = this._loadTMGrammarFactory(grammarDefinitions); diff --git a/src/vs/workbench/services/textMate/common/TMGrammarFactory.ts b/src/vs/workbench/services/textMate/common/TMGrammarFactory.ts index 2bb9be17ad0..fc43c1b81da 100644 --- a/src/vs/workbench/services/textMate/common/TMGrammarFactory.ts +++ b/src/vs/workbench/services/textMate/common/TMGrammarFactory.ts @@ -138,7 +138,16 @@ export class TMGrammarFactory extends Disposable { let grammar: IGrammar | null; try { - grammar = await this._grammarRegistry.loadGrammarWithConfiguration(scopeName, encodedLanguageId, { embeddedLanguages, tokenTypes: <any>grammarDefinition.tokenTypes }); + grammar = await this._grammarRegistry.loadGrammarWithConfiguration( + scopeName, + encodedLanguageId, + { + embeddedLanguages, + tokenTypes: <any>grammarDefinition.tokenTypes, + balancedBracketSelectors: grammarDefinition.balancedBracketSelectors, + unbalancedBracketSelectors: grammarDefinition.unbalancedBracketSelectors, + } + ); } catch (err) { if (err.message && err.message.startsWith('No grammar provided for')) { // No TM grammar defined diff --git a/src/vs/workbench/services/textMate/common/TMGrammars.ts b/src/vs/workbench/services/textMate/common/TMGrammars.ts index e67da586b66..b460653c97b 100644 --- a/src/vs/workbench/services/textMate/common/TMGrammars.ts +++ b/src/vs/workbench/services/textMate/common/TMGrammars.ts @@ -22,6 +22,8 @@ export interface ITMSyntaxExtensionPoint { embeddedLanguages: IEmbeddedLanguagesMap; tokenTypes: TokenTypesContribution; injectTo: string[]; + balancedBracketScopes: string[]; + unbalancedBracketScopes: string[]; } export const grammarsExtPoint: IExtensionPoint<ITMSyntaxExtensionPoint[]> = ExtensionsRegistry.registerExtensionPoint<ITMSyntaxExtensionPoint[]>({ @@ -64,7 +66,23 @@ export const grammarsExtPoint: IExtensionPoint<ITMSyntaxExtensionPoint[]> = Exte items: { type: 'string' } - } + }, + balancedBracketScopes: { + description: nls.localize('vscode.extension.contributes.grammars.balancedBracketScopes', 'Defines which scope names contain balanced brackets.'), + type: 'array', + items: { + type: 'string' + }, + default: ['*'], + }, + unbalancedBracketScopes: { + description: nls.localize('vscode.extension.contributes.grammars.unbalancedBracketScopes', 'Defines which scope names do not contain balanced brackets.'), + type: 'array', + items: { + type: 'string' + }, + default: [], + }, }, required: ['scopeName', 'path'] } diff --git a/src/vs/workbench/services/textMate/common/TMScopeRegistry.ts b/src/vs/workbench/services/textMate/common/TMScopeRegistry.ts index ab0be4a2f46..83b041e7823 100644 --- a/src/vs/workbench/services/textMate/common/TMScopeRegistry.ts +++ b/src/vs/workbench/services/textMate/common/TMScopeRegistry.ts @@ -15,6 +15,8 @@ export interface IValidGrammarDefinition { embeddedLanguages: IValidEmbeddedLanguagesMap; tokenTypes: IValidTokenTypeMap; injectTo?: string[]; + balancedBracketSelectors: string[]; + unbalancedBracketSelectors: string[]; } export interface IValidTokenTypeMap { diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index 02493a12664..4b5f8ac1838 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -23,7 +23,7 @@ import { IWorkingCopyBackup, WorkingCopyCapabilities, NO_TYPE_ID, IWorkingCopyBa import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { ILabelService } from 'vs/platform/label/common/label'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; -import { UTF8 } from 'vs/workbench/services/textfile/common/encoding'; +import { UTF16be, UTF16le, UTF8, UTF8_with_bom } from 'vs/workbench/services/textfile/common/encoding'; import { createTextBufferFactoryFromStream } from 'vs/editor/common/model/textModel'; import { ILanguageDetectionService } from 'vs/workbench/services/languageDetection/common/languageDetectionWorkerService'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; @@ -272,11 +272,11 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil //#region Resolve override async resolve(options?: ITextFileResolveOptions): Promise<void> { - this.trace('[text file model] resolve() - enter'); + this.trace('resolve() - enter'); // Return early if we are disposed if (this.isDisposed()) { - this.trace('[text file model] resolve() - exit - without resolving because model is disposed'); + this.trace('resolve() - exit - without resolving because model is disposed'); return; } @@ -285,7 +285,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // resolve a model that is dirty or is in the process of saving to prevent data // loss. if (!options?.contents && (this.dirty || this.saveSequentializer.hasPending())) { - this.trace('[text file model] resolve() - exit - without resolving because model is dirty or being saved'); + this.trace('resolve() - exit - without resolving because model is dirty or being saved'); return; } @@ -314,7 +314,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } private async resolveFromBuffer(buffer: ITextBufferFactory, options?: ITextFileResolveOptions): Promise<void> { - this.trace('[text file model] resolveFromBuffer()'); + this.trace('resolveFromBuffer()'); // Try to resolve metdata from disk let mtime: number; @@ -372,7 +372,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Abort if someone else managed to resolve the model by now let isNewModel = !this.isResolved(); if (!isNewModel) { - this.trace('[text file model] resolveFromBackup() - exit - without resolving because previously new model got created meanwhile'); + this.trace('resolveFromBackup() - exit - without resolving because previously new model got created meanwhile'); return true; // imply that resolving has happened in another operation } @@ -389,7 +389,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } private async doResolveFromBackup(backup: IResolvedWorkingCopyBackup<IBackupMetaData>, encoding: string, options?: ITextFileResolveOptions): Promise<void> { - this.trace('[text file model] doResolveFromBackup()'); + this.trace('doResolveFromBackup()'); // Resolve with backup this.resolveFromContent({ @@ -411,7 +411,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } private async resolveFromFile(options?: ITextFileResolveOptions): Promise<void> { - this.trace('[text file model] resolveFromFile()'); + this.trace('resolveFromFile()'); const forceReadFromFile = options?.forceReadFromFile; const allowBinary = this.isResolved() /* always allow if we resolved previously */ || options?.allowBinary; @@ -438,7 +438,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Return early if the model content has changed // meanwhile to prevent loosing any changes if (currentVersionId !== this.versionId) { - this.trace('[text file model] resolveFromFile() - exit - without resolving because model content changed'); + this.trace('resolveFromFile() - exit - without resolving because model content changed'); return; } @@ -475,11 +475,11 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } private resolveFromContent(content: ITextFileStreamContent, dirty: boolean, options?: ITextFileResolveOptions): void { - this.trace('[text file model] resolveFromContent() - enter'); + this.trace('resolveFromContent() - enter'); // Return early if we are disposed if (this.isDisposed()) { - this.trace('[text file model] resolveFromContent() - exit - because model is disposed'); + this.trace('resolveFromContent() - exit - because model is disposed'); return; } @@ -532,7 +532,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } private doCreateTextModel(resource: URI, value: ITextBufferFactory): void { - this.trace('[text file model] doCreateTextModel()'); + this.trace('doCreateTextModel()'); // Create model const textModel = this.createTextEditorModel(value, resource, this.preferredLanguageId); @@ -545,7 +545,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } private doUpdateTextModel(value: ITextBufferFactory): void { - this.trace('[text file model] doUpdateTextModel()'); + this.trace('doUpdateTextModel()'); // Update model value in a block that ignores content change events for dirty tracking this.ignoreDirtyOnModelContentChange = true; @@ -564,15 +564,15 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Listen to text model events this._register(model.onDidChangeContent(e => this.onModelContentChanged(model, e.isUndoing || e.isRedoing))); - this._register(model.onDidChangeLanguage(e => this.onMaybeShouldChangeEncoding())); // detect possible encoding change via language specific settings + this._register(model.onDidChangeLanguage(() => this.onMaybeShouldChangeEncoding())); // detect possible encoding change via language specific settings } private onModelContentChanged(model: ITextModel, isUndoingOrRedoing: boolean): void { - this.trace(`[text file model] onModelContentChanged() - enter`); + this.trace(`onModelContentChanged() - enter`); // In any case increment the version id because it tracks the textual content state of the model at all times this.versionId++; - this.trace(`[text file model] onModelContentChanged() - new versionId ${this.versionId}`); + this.trace(`onModelContentChanged() - new versionId ${this.versionId}`); // Remember when the user changed the model through a undo/redo operation. // We need this information to throttle save participants to fix @@ -589,7 +589,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // The contents changed as a matter of Undo and the version reached matches the saved one // In this case we clear the dirty flag and emit a SAVED event to indicate this state. if (model.getAlternativeVersionId() === this.bufferSavedVersionId) { - this.trace('[text file model] onModelContentChanged() - model content changed back to last saved version'); + this.trace('onModelContentChanged() - model content changed back to last saved version'); // Clear flags const wasDirty = this.dirty; @@ -603,7 +603,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Otherwise the content has changed and we signal this as becoming dirty else { - this.trace('[text file model] onModelContentChanged() - model content changed and marked as dirty'); + this.trace('onModelContentChanged() - model content changed and marked as dirty'); // Mark as dirty this.setDirty(true); @@ -707,7 +707,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } if (this.isReadonly()) { - this.trace('[text file model] save() - ignoring request for readonly resource'); + this.trace('save() - ignoring request for readonly resource'); return false; // if model is readonly we do not attempt to save at all } @@ -716,15 +716,15 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil (this.hasState(TextFileEditorModelState.CONFLICT) || this.hasState(TextFileEditorModelState.ERROR)) && (options.reason === SaveReason.AUTO || options.reason === SaveReason.FOCUS_CHANGE || options.reason === SaveReason.WINDOW_CHANGE) ) { - this.trace('[text file model] save() - ignoring auto save request for model that is in conflict or error'); + this.trace('save() - ignoring auto save request for model that is in conflict or error'); return false; // if model is in save conflict or error, do not save unless save reason is explicit } // Actually do save and log - this.trace('[text file model] save() - enter'); + this.trace('save() - enter'); await this.doSave(options); - this.trace('[text file model] save() - exit'); + this.trace('save() - exit'); return this.hasState(TextFileEditorModelState.SAVED); } @@ -735,7 +735,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } let versionId = this.versionId; - this.trace(`[text file model] doSave(${versionId}) - enter with versionId ${versionId}`); + this.trace(`doSave(${versionId}) - enter with versionId ${versionId}`); // Lookup any running pending save for this versionId and return it if found // @@ -743,7 +743,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // while the save was not yet finished to disk // if (this.saveSequentializer.hasPending(versionId)) { - this.trace(`[text file model] doSave(${versionId}) - exit - found a pending save for versionId ${versionId}`); + this.trace(`doSave(${versionId}) - exit - found a pending save for versionId ${versionId}`); return this.saveSequentializer.pending; } @@ -752,7 +752,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // // Scenario: user invoked save action even though the model is not dirty if (!options.force && !this.dirty) { - this.trace(`[text file model] doSave(${versionId}) - exit - because not dirty and/or versionId is different (this.isDirty: ${this.dirty}, this.versionId: ${this.versionId})`); + this.trace(`doSave(${versionId}) - exit - because not dirty and/or versionId is different (this.isDirty: ${this.dirty}, this.versionId: ${this.versionId})`); return; } @@ -766,7 +766,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // while the first save has not returned yet. // if (this.saveSequentializer.hasPending()) { - this.trace(`[text file model] doSave(${versionId}) - exit - because busy saving`); + this.trace(`doSave(${versionId}) - exit - because busy saving`); // Indicate to the save sequentializer that we want to // cancel the pending operation so that ours can run @@ -862,7 +862,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Save to Disk. We mark the save operation as currently pending with // the latest versionId because it might have changed from a save // participant triggering - this.trace(`[text file model] doSave(${versionId}) - before write()`); + this.trace(`doSave(${versionId}) - before write()`); const lastResolvedFileStat = assertIsDefined(this.lastResolvedFileStat); const resolvedTextFileEditorModel = this; return this.saveSequentializer.setPending(versionId, (async () => { @@ -890,10 +890,10 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Update dirty state unless model has changed meanwhile if (versionId === this.versionId) { - this.trace(`[text file model] handleSaveSuccess(${versionId}) - setting dirty to false because versionId did not change`); + this.trace(`handleSaveSuccess(${versionId}) - setting dirty to false because versionId did not change`); this.setDirty(false); } else { - this.trace(`[text file model] handleSaveSuccess(${versionId}) - not setting dirty to false because versionId did change meanwhile`); + this.trace(`handleSaveSuccess(${versionId}) - not setting dirty to false because versionId did change meanwhile`); } // Update orphan state given save was successful @@ -1019,15 +1019,27 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // (see https://github.com/microsoft/vscode/issues/127936) if (this.hasEncodingSetExplicitly) { + this.trace('onMaybeShouldChangeEncoding() - ignoring because encoding was set explicitly'); + return; // never change the user's choice of encoding } + if (this.contentEncoding === UTF8_with_bom || this.contentEncoding === UTF16be || this.contentEncoding === UTF16le) { + this.trace('onMaybeShouldChangeEncoding() - ignoring because content encoding has a BOM'); + + return; // never change an encoding that we can detect 100% via BOMs + } + const { encoding } = await this.textFileService.encoding.getPreferredReadEncoding(this.resource); if (typeof encoding !== 'string' || !this.isNewEncoding(encoding)) { + this.trace(`onMaybeShouldChangeEncoding() - ignoring because preferred encoding ${encoding} is not new`); + return; // return early if encoding is invalid or did not change } if (this.isDirty()) { + this.trace('onMaybeShouldChangeEncoding() - ignoring because model is dirty'); + return; // return early to prevent accident saves in this case } @@ -1110,7 +1122,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil //#endregion private trace(msg: string): void { - this.logService.trace(msg, this.resource.toString()); + this.logService.trace(`[text file model] ${msg}`, this.resource.toString()); } override isResolved(): this is IResolvedTextFileEditorModel { @@ -1122,7 +1134,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } override dispose(): void { - this.trace('[text file model] dispose()'); + this.trace('dispose()'); this.inConflictMode = false; this.inOrphanMode = false; diff --git a/src/vs/workbench/services/textfile/electron-sandbox/nativeTextFileService.ts b/src/vs/workbench/services/textfile/electron-sandbox/nativeTextFileService.ts index d4f8852280b..94cd05ed2e3 100644 --- a/src/vs/workbench/services/textfile/electron-sandbox/nativeTextFileService.ts +++ b/src/vs/workbench/services/textfile/electron-sandbox/nativeTextFileService.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize } from 'vs/nls'; import { process } from 'vs/base/parts/sandbox/electron-sandbox/globals'; import { AbstractTextFileService } from 'vs/workbench/services/textfile/browser/textFileService'; import { ITextFileService, ITextFileStreamContent, ITextFileContent, IReadTextFileOptions, TextFileEditorModelState, ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; @@ -63,7 +64,7 @@ export class NativeTextFileService extends AbstractTextFileService { private registerListeners(): void { // Lifecycle - this.lifecycleService.onWillShutdown(event => event.join(this.onWillShutdown(), 'join.textFiles')); + this.lifecycleService.onWillShutdown(event => event.join(this.onWillShutdown(), { id: 'join.textFiles', label: localize('join.textFiles', "Saving text files") })); } private async onWillShutdown(): Promise<void> { diff --git a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts index 6f8986a8963..901f92d9f3e 100644 --- a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts @@ -129,7 +129,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { this.fileIconThemeWatcher = new ThemeFileWatcher(fileService, environmentService, this.reloadCurrentFileIconTheme.bind(this)); this.fileIconThemeRegistry = new ThemeRegistry(fileIconThemesExtPoint, FileIconThemeData.fromExtensionTheme, true, FileIconThemeData.noIconTheme); this.fileIconThemeLoader = new FileIconThemeLoader(extensionResourceLoaderService, languageService); - this.onFileIconThemeChange = new Emitter<IWorkbenchFileIconTheme>(); + this.onFileIconThemeChange = new Emitter<IWorkbenchFileIconTheme>({ leakWarningThreshold: 400 }); this.currentFileIconTheme = FileIconThemeData.createUnloadedTheme(''); this.fileIconThemeSequencer = new Sequencer(); diff --git a/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts b/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts index 29ff6f8a218..482be496d9b 100644 --- a/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts +++ b/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts @@ -20,7 +20,6 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { localize } from 'vs/nls'; -import { canceled } from 'vs/base/common/errors'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -35,6 +34,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { UserDataSyncStoreClient } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; import { UserDataSyncStoreTypeSynchronizer } from 'vs/platform/userDataSync/common/globalStateSync'; import { ICredentialsService } from 'vs/platform/credentials/common/credentials'; +import { CancellationError } from 'vs/base/common/errors'; type UserAccountClassification = { id: { classification: 'EndUserPseudonymizedInformation'; purpose: 'BusinessInsight' }; @@ -272,7 +272,7 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat const picked = await this.pick(); if (!picked) { - throw canceled(); + throw new CancellationError(); } // User did not pick an account or login failed @@ -450,7 +450,7 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat return 'manual'; } this.telemetryService.publicLog2<{ action: string }, FirstTimeSyncClassification>('sync/firstTimeSync', { action: 'cancelled' }); - throw canceled(); + throw new CancellationError(); } private async syncManually(task: IManualSyncTask): Promise<void> { @@ -769,7 +769,7 @@ class UserDataSyncPreview extends Disposable implements IUserDataSyncPreview { } await this.manualSync.task.stop(); this.updatePreview([]); - this._onDidCompleteManualSync.fire(canceled()); + this._onDidCompleteManualSync.fire(new CancellationError()); } async pull(): Promise<void> { diff --git a/src/vs/workbench/services/views/browser/viewDescriptorService.ts b/src/vs/workbench/services/views/browser/viewDescriptorService.ts index 91dce814e18..4e84c1e5796 100644 --- a/src/vs/workbench/services/views/browser/viewDescriptorService.ts +++ b/src/vs/workbench/services/views/browser/viewDescriptorService.ts @@ -19,6 +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'; interface ICachedViewContainerInfo { containerId: string; @@ -142,6 +143,17 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor this._register(this.storageService.onDidChangeValue((e) => { this.onDidStorageChange(e); })); this._register(this.extensionService.onDidRegisterExtensions(() => this.onDidRegisterExtensions())); + + // Cached View Containers Locations should be registered before Cached View Positions + // Because View Containers cache should be updated first because View Positions Cache depends on View Containers Cache if views are moved to generated view containers + Registry.as<IProfileStorageRegistry>(Extensions.ProfileStorageRegistry) + .registerKeys([{ + key: ViewDescriptorService.CACHED_VIEW_CONTAINER_LOCATIONS, + description: localize('cachedViewContainerPositions', "View Container locations customizations"), + }, { + key: ViewDescriptorService.CACHED_VIEW_POSITIONS, + description: localize('cachedViewPositions', "View locations customizations"), + }]); } private registerGroupedViews(groupedViews: Map<string, { cachedContainerInfo?: ICachedViewContainerInfo; views: IViewDescriptor[] }>): void { @@ -306,7 +318,7 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor return this.viewContainersRegistry.getDefaultViewContainer(location); } - moveViewContainerToLocation(viewContainer: ViewContainer, location: ViewContainerLocation, requestedIndex?: number): void { + private doMoveViewContainerToLocation(viewContainer: ViewContainer, location: ViewContainerLocation, requestedIndex?: number, skipDiskWrite?: boolean): void { const from = this.getViewContainerLocation(viewContainer); const to = location; if (from !== to) { @@ -321,10 +333,18 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor const views = this.getViewsByContainer(viewContainer); this._onDidChangeLocation.fire({ views, from, to }); - this.saveViewContainerLocationsToCache(); + // Need to skip when syncing multiple container movements - vscode#148363 + if (!skipDiskWrite) { + this.saveViewContainerLocationsToCache(); + } } } + moveViewContainerToLocation(viewContainer: ViewContainer, location: ViewContainerLocation, requestedIndex?: number): void { + this.doMoveViewContainerToLocation(viewContainer, location, requestedIndex); + } + + moveViewToLocation(view: IViewDescriptor, location: ViewContainerLocation): void { let container = this.registerGeneratedViewContainer(location); this.moveViewsToContainer([view], container); @@ -419,11 +439,13 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor } type ViewDescriptorServiceMoveViewsClassification = { - viewCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; - fromContainer: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; - toContainer: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; - fromLocation: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; - toLocation: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; + owner: 'sbatten'; + comment: 'Logged when views are moved from one view container to another'; + viewCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of views moved' }; + fromContainer: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The starting view container of the moved views' }; + toContainer: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The destination view container of the moved views' }; + fromLocation: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The location of the starting view container. e.g. Primary Side Bar' }; + toLocation: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The location of the destination view container. e.g. Panel' }; }; this.telemetryService.publicLog2<ViewDescriptorServiceMoveViewsEvent, ViewDescriptorServiceMoveViewsClassification>('viewDescriptorService.moveViews', { viewCount, fromContainer, toContainer, fromLocation, toLocation }); @@ -508,90 +530,98 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor private onDidStorageChange(e: IStorageValueChangeEvent): void { if (e.key === ViewDescriptorService.CACHED_VIEW_POSITIONS && e.scope === StorageScope.GLOBAL && this.cachedViewPositionsValue !== this.getStoredCachedViewPositionsValue() /* This checks if current window changed the value or not */) { - this._cachedViewPositionsValue = this.getStoredCachedViewPositionsValue(); + this.onDidCachedViewPositionsStorageChange(); + } + + if (e.key === ViewDescriptorService.CACHED_VIEW_CONTAINER_LOCATIONS && e.scope === StorageScope.GLOBAL + && this.cachedViewContainerLocationsValue !== this.getStoredCachedViewContainerLocationsValue() /* This checks if current window changed the value or not */) { + this.onDidCachedViewContainerLocationsStorageChange(); + } + } - const newCachedPositions = this.getCachedViewPositions(); - const viewsToMove: { views: IViewDescriptor[]; from: ViewContainer; to: ViewContainer }[] = []; + private onDidCachedViewPositionsStorageChange(): void { + this._cachedViewPositionsValue = this.getStoredCachedViewPositionsValue(); - for (let viewId of newCachedPositions.keys()) { - const viewDescriptor = this.getViewDescriptorById(viewId); - if (!viewDescriptor) { - continue; - } + const newCachedPositions = this.getCachedViewPositions(); + const viewsToMove: { views: IViewDescriptor[]; from: ViewContainer; to: ViewContainer }[] = []; - const prevViewContainer = this.getViewContainerByViewId(viewId); - const newViewContainerInfo = newCachedPositions.get(viewId)!; - // Verify if we need to create the destination container - if (!this.viewContainersRegistry.get(newViewContainerInfo.containerId)) { - const location = this.cachedViewContainerInfo.get(newViewContainerInfo.containerId); - if (location !== undefined) { - this.registerGeneratedViewContainer(location, newViewContainerInfo.containerId); - } + for (let viewId of newCachedPositions.keys()) { + const viewDescriptor = this.getViewDescriptorById(viewId); + if (!viewDescriptor) { + continue; + } + + const prevViewContainer = this.getViewContainerByViewId(viewId); + const newViewContainerInfo = newCachedPositions.get(viewId)!; + // Verify if we need to create the destination container + if (!this.viewContainersRegistry.get(newViewContainerInfo.containerId)) { + const location = this.cachedViewContainerInfo.get(newViewContainerInfo.containerId); + if (location !== undefined) { + this.registerGeneratedViewContainer(location, newViewContainerInfo.containerId); } + } - // Try moving to the new container - const newViewContainer = this.viewContainersRegistry.get(newViewContainerInfo.containerId); - if (prevViewContainer && newViewContainer && newViewContainer !== prevViewContainer) { - const viewDescriptor = this.getViewDescriptorById(viewId); - if (viewDescriptor) { - viewsToMove.push({ views: [viewDescriptor], from: prevViewContainer, to: newViewContainer }); - } + // Try moving to the new container + const newViewContainer = this.viewContainersRegistry.get(newViewContainerInfo.containerId); + if (prevViewContainer && newViewContainer && newViewContainer !== prevViewContainer) { + const viewDescriptor = this.getViewDescriptorById(viewId); + if (viewDescriptor) { + viewsToMove.push({ views: [viewDescriptor], from: prevViewContainer, to: newViewContainer }); } } + } - // If a value is not present in the cache, it must be reset to default - this.viewContainers.forEach(viewContainer => { - const viewContainerModel = this.getViewContainerModel(viewContainer); - viewContainerModel.allViewDescriptors.forEach(viewDescriptor => { - if (!newCachedPositions.has(viewDescriptor.id)) { - const currentContainer = this.getViewContainerByViewId(viewDescriptor.id); - const defaultContainer = this.getDefaultContainerById(viewDescriptor.id); - if (currentContainer && defaultContainer && currentContainer !== defaultContainer) { - viewsToMove.push({ views: [viewDescriptor], from: currentContainer, to: defaultContainer }); - } + // If a value is not present in the cache, it must be reset to default + this.viewContainers.forEach(viewContainer => { + const viewContainerModel = this.getViewContainerModel(viewContainer); + viewContainerModel.allViewDescriptors.forEach(viewDescriptor => { + if (!newCachedPositions.has(viewDescriptor.id)) { + const currentContainer = this.getViewContainerByViewId(viewDescriptor.id); + const defaultContainer = this.getDefaultContainerById(viewDescriptor.id); + if (currentContainer && defaultContainer && currentContainer !== defaultContainer) { + viewsToMove.push({ views: [viewDescriptor], from: currentContainer, to: defaultContainer }); } - }); + } }); + }); - this.cachedViewInfo = newCachedPositions; - for (const { views, from, to } of viewsToMove) { - this.moveViews(views, from, to); - } + this.cachedViewInfo = newCachedPositions; + for (const { views, from, to } of viewsToMove) { + this.moveViews(views, from, to); } + } - if (e.key === ViewDescriptorService.CACHED_VIEW_CONTAINER_LOCATIONS && e.scope === StorageScope.GLOBAL - && this.cachedViewContainerLocationsValue !== this.getStoredCachedViewContainerLocationsValue() /* This checks if current window changed the value or not */) { - this._cachedViewContainerLocationsValue = this.getStoredCachedViewContainerLocationsValue(); - const newCachedLocations = this.getCachedViewContainerLocations(); - const viewContainersToMove: [ViewContainer, ViewContainerLocation][] = []; - - for (const [containerId, location] of newCachedLocations.entries()) { - const container = this.getViewContainerById(containerId); - if (container) { - if (location !== this.getViewContainerLocation(container)) { - viewContainersToMove.push([container, location]); - } + private onDidCachedViewContainerLocationsStorageChange(): void { + this._cachedViewContainerLocationsValue = this.getStoredCachedViewContainerLocationsValue(); + const newCachedLocations = this.getCachedViewContainerLocations(); + const viewContainersToMove: [ViewContainer, ViewContainerLocation][] = []; + + for (const [containerId, location] of newCachedLocations.entries()) { + const container = this.getViewContainerById(containerId); + if (container) { + if (location !== this.getViewContainerLocation(container)) { + viewContainersToMove.push([container, location]); } } + } - this.viewContainers.forEach(viewContainer => { - if (!newCachedLocations.has(viewContainer.id)) { - const currentLocation = this.getViewContainerLocation(viewContainer); - const defaultLocation = this.getDefaultViewContainerLocation(viewContainer); + this.viewContainers.forEach(viewContainer => { + if (!newCachedLocations.has(viewContainer.id)) { + const currentLocation = this.getViewContainerLocation(viewContainer); + const defaultLocation = this.getDefaultViewContainerLocation(viewContainer); - if (currentLocation !== defaultLocation) { - viewContainersToMove.push([viewContainer, defaultLocation]); - } + if (currentLocation !== defaultLocation) { + viewContainersToMove.push([viewContainer, defaultLocation]); } - }); - - // Execute View Container Movement - for (const [container, location] of viewContainersToMove) { - this.moveViewContainerToLocation(container, location); } + }); - this.cachedViewContainerInfo = this.getCachedViewContainerLocations(); + // Execute View Container Movement + for (const [container, location] of viewContainersToMove) { + this.doMoveViewContainerToLocation(container, location, undefined, true); } + + this.cachedViewContainerInfo = this.getCachedViewContainerLocations(); } // Generated Container Id Format diff --git a/src/vs/workbench/services/views/common/viewContainerModel.ts b/src/vs/workbench/services/views/common/viewContainerModel.ts index 00965871db3..5cb12926dc9 100644 --- a/src/vs/workbench/services/views/common/viewContainerModel.ts +++ b/src/vs/workbench/services/views/common/viewContainerModel.ts @@ -16,6 +16,8 @@ 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 { localize } from 'vs/nls'; export function getViewsStateStorageId(viewContainerStorageId: string): string { return `${viewContainerStorageId}.hidden`; } @@ -84,6 +86,7 @@ class ViewDescriptorsState extends Disposable { constructor( viewContainerStorageId: string, + viewContainerName: string, @IStorageService private readonly storageService: IStorageService, ) { super(); @@ -93,6 +96,12 @@ class ViewDescriptorsState extends Disposable { this._register(this.storageService.onDidChangeValue(e => this.onDidStorageChange(e))); this.state = this.initialize(); + + Registry.as<IProfileStorageRegistry>(Extensions.ProfileStorageRegistry) + .registerKeys([{ + key: this.globalViewsStateStorageId, + description: localize('globalViewsStateStorageId', "Views visibility customizations in {0} view container", viewContainerName), + }]); } set(id: string, state: IViewDescriptorState): void { @@ -343,7 +352,7 @@ export class ViewContainerModel extends Disposable implements IViewContainerMode super(); this._register(Event.filter(contextKeyService.onDidChangeContext, e => e.affectsSome(this.contextKeys))(() => this.onDidChangeContext())); - this.viewDescriptorsState = this._register(instantiationService.createInstance(ViewDescriptorsState, viewContainer.storageId || `${viewContainer.id}.state`)); + this.viewDescriptorsState = this._register(instantiationService.createInstance(ViewDescriptorsState, viewContainer.storageId || `${viewContainer.id}.state`, viewContainer.title)); this._register(this.viewDescriptorsState.onDidChangeStoredState(items => this.updateVisibility(items))); this._register(Event.any( diff --git a/src/vs/workbench/services/workingCopy/browser/workingCopyHistoryService.ts b/src/vs/workbench/services/workingCopy/browser/workingCopyHistoryService.ts index 2ada834a567..5b620f0b1e8 100644 --- a/src/vs/workbench/services/workingCopy/browser/workingCopyHistoryService.ts +++ b/src/vs/workbench/services/workingCopy/browser/workingCopyHistoryService.ts @@ -3,54 +3,23 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { URI } from 'vs/base/common/uri'; -import { SaveSource } from 'vs/workbench/common/editor'; -import { CancellationToken } from 'vs/base/common/cancellation'; import { IFileService } from 'vs/platform/files/common/files'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { ILabelService } from 'vs/platform/label/common/label'; import { ILogService } from 'vs/platform/log/common/log'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { WorkingCopyHistoryModel, WorkingCopyHistoryService } from 'vs/workbench/services/workingCopy/common/workingCopyHistoryService'; +import { IWorkingCopyHistoryModelOptions, WorkingCopyHistoryService } from 'vs/workbench/services/workingCopy/common/workingCopyHistoryService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { IWorkingCopyHistoryEntry, IWorkingCopyHistoryService } from 'vs/workbench/services/workingCopy/common/workingCopyHistory'; - -class BrowserWorkingCopyHistoryModel extends WorkingCopyHistoryModel { - - override async addEntry(source: SaveSource, timestamp: number, token: CancellationToken): Promise<IWorkingCopyHistoryEntry> { - const entry = await super.addEntry(source, timestamp, token); - if (!token.isCancellationRequested) { - await this.store(token); // need to store on each add because we do not have long running shutdown support in web - } - - return entry; - } - - override async updateEntry(entry: IWorkingCopyHistoryEntry, properties: { source: SaveSource }, token: CancellationToken): Promise<void> { - await super.updateEntry(entry, properties, token); - if (!token.isCancellationRequested) { - await this.store(token); // need to store on each remove because we do not have long running shutdown support in web - } - } - - override async removeEntry(entry: IWorkingCopyHistoryEntry, token: CancellationToken): Promise<boolean> { - const removed = await super.removeEntry(entry, token); - if (removed && !token.isCancellationRequested) { - await this.store(token); // need to store on each remove because we do not have long running shutdown support in web - } - - return removed; - } -} +import { IWorkingCopyHistoryService } from 'vs/workbench/services/workingCopy/common/workingCopyHistory'; export class BrowserWorkingCopyHistoryService extends WorkingCopyHistoryService { constructor( @IFileService fileService: IFileService, @IRemoteAgentService remoteAgentService: IRemoteAgentService, - @IEnvironmentService environmentService: IEnvironmentService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, @IUriIdentityService uriIdentityService: IUriIdentityService, @ILabelService labelService: ILabelService, @ILogService logService: ILogService, @@ -59,8 +28,8 @@ export class BrowserWorkingCopyHistoryService extends WorkingCopyHistoryService super(fileService, remoteAgentService, environmentService, uriIdentityService, labelService, logService, configurationService); } - protected override createModel(resource: URI, historyHome: URI): WorkingCopyHistoryModel { - return new BrowserWorkingCopyHistoryModel(resource, historyHome, this._onDidAddEntry, this._onDidChangeEntry, this._onDidReplaceEntry, this._onDidRemoveEntry, this.fileService, this.labelService, this.logService, this.configurationService); + protected getModelOptions(): IWorkingCopyHistoryModelOptions { + return { flushOnChange: true /* because browsers support no long running shutdown */ }; } } diff --git a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts index 15b31899685..ff9b4a7c408 100644 --- a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts @@ -405,11 +405,11 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend } async resolve(options?: IStoredFileWorkingCopyResolveOptions): Promise<void> { - this.trace('[stored file working copy] resolve() - enter'); + this.trace('resolve() - enter'); // Return early if we are disposed if (this.isDisposed()) { - this.trace('[stored file working copy] resolve() - exit - without resolving because file working copy is disposed'); + this.trace('resolve() - exit - without resolving because file working copy is disposed'); return; } @@ -418,7 +418,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend // resolve a working copy that is dirty or is in the process of saving to prevent // data loss. if (!options?.contents && (this.dirty || this.saveSequentializer.hasPending())) { - this.trace('[stored file working copy] resolve() - exit - without resolving because file working copy is dirty or being saved'); + this.trace('resolve() - exit - without resolving because file working copy is dirty or being saved'); return; } @@ -447,7 +447,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend } private async resolveFromBuffer(buffer: VSBufferReadableStream): Promise<void> { - this.trace('[stored file working copy] resolveFromBuffer()'); + this.trace('resolveFromBuffer()'); // Try to resolve metdata from disk let mtime: number; @@ -496,7 +496,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend // Abort if someone else managed to resolve the working copy by now let isNew = !this.isResolved(); if (!isNew) { - this.trace('[stored file working copy] resolveFromBackup() - exit - withoutresolving because previously new file working copy got created meanwhile'); + this.trace('resolveFromBackup() - exit - withoutresolving because previously new file working copy got created meanwhile'); return true; // imply that resolving has happened in another operation } @@ -513,7 +513,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend } private async doResolveFromBackup(backup: IResolvedWorkingCopyBackup<IStoredFileWorkingCopyBackupMetaData>): Promise<void> { - this.trace('[stored file working copy] doResolveFromBackup()'); + this.trace('doResolveFromBackup()'); // Resolve with backup await this.resolveFromContent({ @@ -534,7 +534,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend } private async resolveFromFile(options?: IStoredFileWorkingCopyResolveOptions): Promise<void> { - this.trace('[stored file working copy] resolveFromFile()'); + this.trace('resolveFromFile()'); const forceReadFromFile = options?.forceReadFromFile; @@ -561,7 +561,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend // Return early if the working copy content has changed // meanwhile to prevent loosing any changes if (currentVersionId !== this.versionId) { - this.trace('[stored file working copy] resolveFromFile() - exit - without resolving because file working copy content changed'); + this.trace('resolveFromFile() - exit - without resolving because file working copy content changed'); return; } @@ -598,11 +598,11 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend } private async resolveFromContent(content: IFileStreamContent, dirty: boolean): Promise<void> { - this.trace('[stored file working copy] resolveFromContent() - enter'); + this.trace('resolveFromContent() - enter'); // Return early if we are disposed if (this.isDisposed()) { - this.trace('[stored file working copy] resolveFromContent() - exit - because working copy is disposed'); + this.trace('resolveFromContent() - exit - because working copy is disposed'); return; } @@ -644,7 +644,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend } private async doCreateModel(contents: VSBufferReadableStream): Promise<void> { - this.trace('[stored file working copy] doCreateModel()'); + this.trace('doCreateModel()'); // Create model and dispose it when we get disposed this._model = this._register(await this.modelFactory.createModel(this.resource, contents, CancellationToken.None)); @@ -656,7 +656,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend private ignoreDirtyOnModelContentChange = false; private async doUpdateModel(contents: VSBufferReadableStream): Promise<void> { - this.trace('[stored file working copy] doUpdateModel()'); + this.trace('doUpdateModel()'); // Update model value in a block that ignores content change events for dirty tracking this.ignoreDirtyOnModelContentChange = true; @@ -681,11 +681,11 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend } private onModelContentChanged(model: M, isUndoingOrRedoing: boolean): void { - this.trace(`[stored file working copy] onModelContentChanged() - enter`); + this.trace(`onModelContentChanged() - enter`); // In any case increment the version id because it tracks the content state of the model at all times this.versionId++; - this.trace(`[stored file working copy] onModelContentChanged() - new versionId ${this.versionId}`); + this.trace(`onModelContentChanged() - new versionId ${this.versionId}`); // Remember when the user changed the model through a undo/redo operation. // We need this information to throttle save participants to fix @@ -702,7 +702,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend // The contents changed as a matter of Undo and the version reached matches the saved one // In this case we clear the dirty flag and emit a SAVED event to indicate this state. if (model.versionId === this.savedVersionId) { - this.trace('[stored file working copy] onModelContentChanged() - model content changed back to last saved version'); + this.trace('onModelContentChanged() - model content changed back to last saved version'); // Clear flags const wasDirty = this.dirty; @@ -716,7 +716,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend // Otherwise the content has changed and we signal this as becoming dirty else { - this.trace('[stored file working copy] onModelContentChanged() - model content changed and marked as dirty'); + this.trace('onModelContentChanged() - model content changed and marked as dirty'); // Mark as dirty this.setDirty(true); @@ -787,7 +787,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend } if (this.isReadonly()) { - this.trace('[stored file working copy] save() - ignoring request for readonly resource'); + this.trace('save() - ignoring request for readonly resource'); return false; // if working copy is readonly we do not attempt to save at all } @@ -796,15 +796,15 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend (this.hasState(StoredFileWorkingCopyState.CONFLICT) || this.hasState(StoredFileWorkingCopyState.ERROR)) && (options.reason === SaveReason.AUTO || options.reason === SaveReason.FOCUS_CHANGE || options.reason === SaveReason.WINDOW_CHANGE) ) { - this.trace('[stored file working copy] save() - ignoring auto save request for file working copy that is in conflict or error'); + this.trace('save() - ignoring auto save request for file working copy that is in conflict or error'); return false; // if working copy is in save conflict or error, do not save unless save reason is explicit } // Actually do save - this.trace('[stored file working copy] save() - enter'); + this.trace('save() - enter'); await this.doSave(options); - this.trace('[stored file working copy] save() - exit'); + this.trace('save() - exit'); return this.hasState(StoredFileWorkingCopyState.SAVED); } @@ -815,7 +815,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend } let versionId = this.versionId; - this.trace(`[stored file working copy] doSave(${versionId}) - enter with versionId ${versionId}`); + this.trace(`doSave(${versionId}) - enter with versionId ${versionId}`); // Lookup any running pending save for this versionId and return it if found // @@ -823,7 +823,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend // while the save was not yet finished to disk // if (this.saveSequentializer.hasPending(versionId)) { - this.trace(`[stored file working copy] doSave(${versionId}) - exit - found a pending save for versionId ${versionId}`); + this.trace(`doSave(${versionId}) - exit - found a pending save for versionId ${versionId}`); return this.saveSequentializer.pending; } @@ -832,7 +832,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend // // Scenario: user invoked save action even though the working copy is not dirty if (!options.force && !this.dirty) { - this.trace(`[stored file working copy] doSave(${versionId}) - exit - because not dirty and/or versionId is different (this.isDirty: ${this.dirty}, this.versionId: ${this.versionId})`); + this.trace(`doSave(${versionId}) - exit - because not dirty and/or versionId is different (this.isDirty: ${this.dirty}, this.versionId: ${this.versionId})`); return; } @@ -846,7 +846,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend // while the first save has not returned yet. // if (this.saveSequentializer.hasPending()) { - this.trace(`[stored file working copy] doSave(${versionId}) - exit - because busy saving`); + this.trace(`doSave(${versionId}) - exit - because busy saving`); // Indicate to the save sequentializer that we want to // cancel the pending operation so that ours can run @@ -939,7 +939,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend // Save to Disk. We mark the save operation as currently pending with // the latest versionId because it might have changed from a save // participant triggering - this.trace(`[stored file working copy] doSave(${versionId}) - before write()`); + this.trace(`doSave(${versionId}) - before write()`); const lastResolvedFileStat = assertIsDefined(this.lastResolvedFileStat); const resolvedFileWorkingCopy = this; return this.saveSequentializer.setPending(versionId, (async () => { @@ -989,10 +989,10 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend // Update dirty state unless working copy has changed meanwhile if (versionId === this.versionId) { - this.trace(`[stored file working copy] handleSaveSuccess(${versionId}) - setting dirty to false because versionId did not change`); + this.trace(`handleSaveSuccess(${versionId}) - setting dirty to false because versionId did not change`); this.setDirty(false); } else { - this.trace(`[stored file working copy] handleSaveSuccess(${versionId}) - not setting dirty to false because versionId did change meanwhile`); + this.trace(`handleSaveSuccess(${versionId}) - not setting dirty to false because versionId did change meanwhile`); } // Update orphan state given save was successful @@ -1003,7 +1003,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend } private handleSaveError(error: Error, versionId: number, options: IStoredFileWorkingCopySaveOptions): void { - (options.ignoreErrorHandler ? this.logService.trace : this.logService.error)(`[stored file working copy] handleSaveError(${versionId}) - exit - resulted in a save error: ${error.toString()}`, this.resource.toString(), this.typeId); + (options.ignoreErrorHandler ? this.logService.trace : this.logService.error).apply(this.logService, [`[stored file working copy] handleSaveError(${versionId}) - exit - resulted in a save error: ${error.toString()}`, this.resource.toString(), this.typeId]); // Return early if the save() call was made asking to // handle the save error itself. @@ -1149,7 +1149,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend return; // ignore if not resolved or not dirty and not enforced } - this.trace('[stored file working copy] revert()'); + this.trace('revert()'); // Unset flags const wasDirty = this.dirty; @@ -1219,7 +1219,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend } private trace(msg: string): void { - this.logService.trace(msg, this.resource.toString(), this.typeId); + this.logService.trace(`[stored file working copy] ${msg}`, this.resource.toString(), this.typeId); } //#endregion @@ -1227,7 +1227,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend //#region Dispose override dispose(): void { - this.trace('[stored file working copy] dispose()'); + this.trace('dispose()'); // State this.inConflictMode = false; diff --git a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts index b92776c6c5a..d7c4f83dcc0 100644 --- a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize } from 'vs/nls'; import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; import { StoredFileWorkingCopy, StoredFileWorkingCopyState, IStoredFileWorkingCopy, IStoredFileWorkingCopyModel, IStoredFileWorkingCopyModelFactory, IStoredFileWorkingCopyResolveOptions, IStoredFileWorkingCopySaveEvent as IBaseStoredFileWorkingCopySaveEvent } from 'vs/workbench/services/workingCopy/common/storedFileWorkingCopy'; @@ -207,7 +208,7 @@ export class StoredFileWorkingCopyManager<M extends IStoredFileWorkingCopyModel> // Lifecycle this.lifecycleService.onBeforeShutdown(event => event.veto(this.onBeforeShutdown(), 'veto.fileWorkingCopyManager')); - this.lifecycleService.onWillShutdown(event => event.join(this.onWillShutdown(), 'join.fileWorkingCopyManager')); + this.lifecycleService.onWillShutdown(event => event.join(this.onWillShutdown(), { id: 'join.fileWorkingCopyManager', label: localize('join.fileWorkingCopyManager', "Saving working copies") })); } private onBeforeShutdown(): boolean { diff --git a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts index baa8d0587d4..59d79abeefd 100644 --- a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts @@ -157,10 +157,10 @@ export class UntitledFileWorkingCopy<M extends IUntitledFileWorkingCopyModel> ex //#region Resolve async resolve(): Promise<void> { - this.trace('[untitled file working copy] resolve()'); + this.trace('resolve()'); if (this.isResolved()) { - this.trace('[untitled file working copy] resolve() - exit (already resolved)'); + this.trace('resolve() - exit (already resolved)'); // return early if the untitled file working copy is already // resolved assuming that the contents have meanwhile changed @@ -173,15 +173,15 @@ export class UntitledFileWorkingCopy<M extends IUntitledFileWorkingCopyModel> ex // Check for backups or use initial value or empty const backup = await this.workingCopyBackupService.resolve(this); if (backup) { - this.trace('[untitled file working copy] resolve() - with backup'); + this.trace('resolve() - with backup'); untitledContents = backup.value; } else if (this.initialContents?.value) { - this.trace('[untitled file working copy] resolve() - with initial contents'); + this.trace('resolve() - with initial contents'); untitledContents = this.initialContents.value; } else { - this.trace('[untitled file working copy] resolve() - empty'); + this.trace('resolve() - empty'); untitledContents = emptyStream(); } @@ -200,7 +200,7 @@ export class UntitledFileWorkingCopy<M extends IUntitledFileWorkingCopyModel> ex } private async doCreateModel(contents: VSBufferReadableStream): Promise<void> { - this.trace('[untitled file working copy] doCreateModel()'); + this.trace('doCreateModel()'); // Create model and dispose it when we get disposed this._model = this._register(await this.modelFactory.createModel(this.resource, contents, CancellationToken.None)); @@ -267,7 +267,7 @@ export class UntitledFileWorkingCopy<M extends IUntitledFileWorkingCopyModel> ex //#region Save async save(options?: ISaveOptions): Promise<boolean> { - this.trace('[untitled file working copy] save()'); + this.trace('save()'); const result = await this.saveDelegate(this, options); @@ -285,7 +285,7 @@ export class UntitledFileWorkingCopy<M extends IUntitledFileWorkingCopyModel> ex //#region Revert async revert(): Promise<void> { - this.trace('[untitled file working copy] revert()'); + this.trace('revert()'); // No longer dirty this.setDirty(false); @@ -302,7 +302,7 @@ export class UntitledFileWorkingCopy<M extends IUntitledFileWorkingCopyModel> ex //#endregion override dispose(): void { - this.trace('[untitled file working copy] dispose()'); + this.trace('dispose()'); this._onWillDispose.fire(); @@ -310,6 +310,6 @@ export class UntitledFileWorkingCopy<M extends IUntitledFileWorkingCopyModel> ex } private trace(msg: string): void { - this.logService.trace(msg, this.resource.toString(), this.typeId); + this.logService.trace(`[untitled file working copy] ${msg}`, this.resource.toString(), this.typeId); } } diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyHistoryService.ts b/src/vs/workbench/services/workingCopy/common/workingCopyHistoryService.ts index 946c595596d..667440f7070 100644 --- a/src/vs/workbench/services/workingCopy/common/workingCopyHistoryService.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopyHistoryService.ts @@ -17,7 +17,7 @@ import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteA import { URI } from 'vs/base/common/uri'; import { DeferredPromise, Limiter } from 'vs/base/common/async'; import { dirname, extname, isEqual, joinPath } from 'vs/base/common/resources'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { hash } from 'vs/base/common/hash'; import { indexOfPath, randomPath } from 'vs/base/common/extpath'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -42,6 +42,17 @@ interface ISerializedWorkingCopyHistoryModelEntry { readonly source?: SaveSource; } + +export interface IWorkingCopyHistoryModelOptions { + + /** + * Whether to flush when the model changes. If not + * configured, `model.store()` has to be called + * explicitly. + */ + flushOnChange: boolean; +} + export class WorkingCopyHistoryModel { static readonly ENTRIES_FILE = 'entries.json'; @@ -65,7 +76,10 @@ export class WorkingCopyHistoryModel { private historyEntriesNameMatcher: RegExp | undefined = undefined; - private shouldStore: boolean = false; + private versionId = 0; + private storedVersionId = this.versionId; + + private readonly storeLimiter = new Limiter(1); constructor( workingCopyResource: URI, @@ -74,6 +88,7 @@ export class WorkingCopyHistoryModel { private readonly entryChangedEmitter: Emitter<IWorkingCopyHistoryEvent>, private readonly entryReplacedEmitter: Emitter<IWorkingCopyHistoryEvent>, private readonly entryRemovedEmitter: Emitter<IWorkingCopyHistoryEvent>, + private readonly options: IWorkingCopyHistoryModelOptions, private readonly fileService: IFileService, private readonly labelService: ILabelService, private readonly logService: ILogService, @@ -118,15 +133,24 @@ export class WorkingCopyHistoryModel { } } + let entry: IWorkingCopyHistoryEntry; + // Replace lastest entry in history if (entryToReplace) { - return this.doReplaceEntry(entryToReplace, timestamp, token); + entry = await this.doReplaceEntry(entryToReplace, timestamp, token); } // Add entry to history else { - return this.doAddEntry(source, timestamp, token); + entry = await this.doAddEntry(source, timestamp, token); + } + + // Flush now if configured + if (this.options.flushOnChange && !token.isCancellationRequested) { + await this.store(token); } + + return entry; } private async doAddEntry(source: SaveSource, timestamp: number, token: CancellationToken): Promise<IWorkingCopyHistoryEntry> { @@ -149,8 +173,8 @@ export class WorkingCopyHistoryModel { }; this.entries.push(entry); - // Mark as in need to be stored to disk - this.shouldStore = true; + // Update version ID of model to use for storing later + this.versionId++; // Events this.entryAddedEmitter.fire({ entry }); @@ -167,8 +191,8 @@ export class WorkingCopyHistoryModel { // Update entry entry.timestamp = timestamp; - // Mark as in need to be stored to disk - this.shouldStore = true; + // Update version ID of model to use for storing later + this.versionId++; // Events this.entryReplacedEmitter.fire({ entry }); @@ -196,12 +220,17 @@ export class WorkingCopyHistoryModel { // Remove from model this.entries.splice(index, 1); - // Mark as in need to be stored to disk - this.shouldStore = true; + // Update version ID of model to use for storing later + this.versionId++; // Events this.entryRemovedEmitter.fire({ entry }); + // Flush now if configured + if (this.options.flushOnChange && !token.isCancellationRequested) { + await this.store(token); + } + return true; } @@ -222,11 +251,16 @@ export class WorkingCopyHistoryModel { // Update entry entry.source = properties.source; - // Mark as in need to be stored to disk - this.shouldStore = true; + // Update version ID of model to use for storing later + this.versionId++; // Events this.entryChangedEmitter.fire({ entry }); + + // Flush now if configured + if (this.options.flushOnChange && !token.isCancellationRequested) { + await this.store(token); + } } async getEntries(): Promise<readonly IWorkingCopyHistoryEntry[]> { @@ -353,12 +387,29 @@ export class WorkingCopyHistoryModel { } async store(token: CancellationToken): Promise<void> { - const historyEntriesFolder = assertIsDefined(this.historyEntriesFolder); - - if (!this.shouldStore) { - return; // fast return to avoid disk access when nothing changed + if (!this.shouldStore()) { + return; } + // Use a `Limiter` to prevent multiple `store` operations + // potentially running at the same time + + await this.storeLimiter.queue(async () => { + if (token.isCancellationRequested || !this.shouldStore()) { + return; + } + + return this.doStore(token); + }); + } + + private shouldStore(): boolean { + return this.storedVersionId !== this.versionId; + } + + private async doStore(token: CancellationToken): Promise<void> { + const historyEntriesFolder = assertIsDefined(this.historyEntriesFolder); + // Make sure to await resolving when persisting await this.resolveEntriesOnce(); @@ -370,6 +421,7 @@ export class WorkingCopyHistoryModel { await this.cleanUpEntries(); // Without entries, remove the history folder + const storedVersion = this.versionId; if (this.entries.length === 0) { try { await this.fileService.del(historyEntriesFolder, { recursive: true }); @@ -383,8 +435,8 @@ export class WorkingCopyHistoryModel { await this.writeEntriesFile(); } - // Mark as being up to date on disk - this.shouldStore = false; + // Mark as stored version + this.storedVersionId = storedVersion; } private async cleanUpEntries(): Promise<void> { @@ -516,7 +568,7 @@ export abstract class WorkingCopyHistoryService extends Disposable implements IW constructor( @IFileService protected readonly fileService: IFileService, @IRemoteAgentService protected readonly remoteAgentService: IRemoteAgentService, - @IEnvironmentService protected readonly environmentService: IEnvironmentService, + @IWorkbenchEnvironmentService protected readonly environmentService: IWorkbenchEnvironmentService, @IUriIdentityService protected readonly uriIdentityService: IUriIdentityService, @ILabelService protected readonly labelService: ILabelService, @ILogService protected readonly logService: ILogService, @@ -722,16 +774,15 @@ export abstract class WorkingCopyHistoryService extends Disposable implements IW let model = this.models.get(resource); if (!model) { - model = this.createModel(resource, historyHome); + model = new WorkingCopyHistoryModel(resource, historyHome, this._onDidAddEntry, this._onDidChangeEntry, this._onDidReplaceEntry, this._onDidRemoveEntry, this.getModelOptions(), this.fileService, this.labelService, this.logService, this.configurationService); this.models.set(resource, model); } return model; } - protected createModel(resource: URI, historyHome: URI): WorkingCopyHistoryModel { - return new WorkingCopyHistoryModel(resource, historyHome, this._onDidAddEntry, this._onDidChangeEntry, this._onDidReplaceEntry, this._onDidRemoveEntry, this.fileService, this.labelService, this.logService, this.configurationService); - } + protected abstract getModelOptions(): IWorkingCopyHistoryModelOptions; + } // Register History Tracker diff --git a/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupService.ts b/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupService.ts index d4c07c0f67a..d9fd62ac9b4 100644 --- a/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupService.ts +++ b/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupService.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize } from 'vs/nls'; import { WorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackupService'; import { URI } from 'vs/base/common/uri'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; @@ -33,7 +34,7 @@ export class NativeWorkingCopyBackupService extends WorkingCopyBackupService { // Lifecycle: ensure to prolong the shutdown for as long // as pending backup operations have not finished yet. // Otherwise, we risk writing partial backups to disk. - this.lifecycleService.onWillShutdown(event => event.join(this.joinBackups(), 'join.workingCopyBackups')); + this.lifecycleService.onWillShutdown(event => event.join(this.joinBackups(), { id: 'join.workingCopyBackups', label: localize('join.workingCopyBackups', "Backup working copies") })); } } diff --git a/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyHistoryService.ts b/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyHistoryService.ts index 36aae715453..55167103df8 100644 --- a/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyHistoryService.ts +++ b/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyHistoryService.ts @@ -3,25 +3,35 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Limiter } from 'vs/base/common/async'; +import { localize } from 'vs/nls'; +import { Event } from 'vs/base/common/event'; +import { Limiter, RunOnceScheduler } from 'vs/base/common/async'; import { ILifecycleService, WillShutdownEvent } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IFileService } from 'vs/platform/files/common/files'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { ILabelService } from 'vs/platform/label/common/label'; import { ILogService } from 'vs/platform/log/common/log'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { WorkingCopyHistoryService } from 'vs/workbench/services/workingCopy/common/workingCopyHistoryService'; +import { IWorkingCopyHistoryModelOptions, WorkingCopyHistoryService } from 'vs/workbench/services/workingCopy/common/workingCopyHistoryService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IWorkingCopyHistoryService, MAX_PARALLEL_HISTORY_IO_OPS } from 'vs/workbench/services/workingCopy/common/workingCopyHistory'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; export class NativeWorkingCopyHistoryService extends WorkingCopyHistoryService { + private static readonly STORE_ALL_INTERVAL = 5 * 60 * 1000; // 5min + + private readonly isRemotelyStored = typeof this.environmentService.remoteAuthority === 'string'; + + private readonly storeAllCts = this._register(new CancellationTokenSource()); + private readonly storeAllScheduler = this._register(new RunOnceScheduler(() => this.storeAll(this.storeAllCts.token), NativeWorkingCopyHistoryService.STORE_ALL_INTERVAL)); + constructor( @IFileService fileService: IFileService, @IRemoteAgentService remoteAgentService: IRemoteAgentService, - @IEnvironmentService environmentService: IEnvironmentService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, @IUriIdentityService uriIdentityService: IUriIdentityService, @ILabelService labelService: ILabelService, @ILifecycleService private readonly lifecycleService: ILifecycleService, @@ -30,33 +40,60 @@ export class NativeWorkingCopyHistoryService extends WorkingCopyHistoryService { ) { super(fileService, remoteAgentService, environmentService, uriIdentityService, labelService, logService, configurationService); - this.lifecycleService.onWillShutdown(e => this.onWillShutdown(e)); + this.registerListeners(); + } + + private registerListeners(): void { + if (!this.isRemotelyStored) { + + // Local: persist all on shutdown + this.lifecycleService.onWillShutdown(e => this.onWillShutdown(e)); + + // Local: schedule persist on change + this._register(Event.any(this.onDidAddEntry, this.onDidChangeEntry, this.onDidReplaceEntry, this.onDidRemoveEntry)(() => this.onDidChangeModels())); + } + } + + protected getModelOptions(): IWorkingCopyHistoryModelOptions { + return { flushOnChange: this.isRemotelyStored /* because the connection might drop anytime */ }; } private onWillShutdown(e: WillShutdownEvent): void { - // Prolong shutdown for orderly model shutdown - e.join((async () => { - const limiter = new Limiter(MAX_PARALLEL_HISTORY_IO_OPS); - const promises = []; - - const models = Array.from(this.models.values()); - for (const model of models) { - promises.push(limiter.queue(async () => { - if (e.token.isCancellationRequested) { - return; - } - - try { - await model.store(e.token); - } catch (error) { - this.logService.trace(error); - } - })); - } - - await Promise.all(promises); - })(), 'join.workingCopyHistory'); + // Dispose the scheduler... + this.storeAllScheduler.dispose(); + this.storeAllCts.dispose(true); + + // ...because we now explicitly store all models + e.join(this.storeAll(e.token), { id: 'join.workingCopyHistory', label: localize('join.workingCopyHistory', "Saving local history") }); + } + + private onDidChangeModels(): void { + if (!this.storeAllScheduler.isScheduled()) { + this.storeAllScheduler.schedule(); + } + } + + private async storeAll(token: CancellationToken): Promise<void> { + const limiter = new Limiter(MAX_PARALLEL_HISTORY_IO_OPS); + const promises = []; + + const models = Array.from(this.models.values()); + for (const model of models) { + promises.push(limiter.queue(async () => { + if (token.isCancellationRequested) { + return; + } + + try { + await model.store(token); + } catch (error) { + this.logService.trace(error); + } + })); + } + + await Promise.all(promises); } } diff --git a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts index 157a36e5cdd..307ed8eb379 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts @@ -247,7 +247,7 @@ suite('FileWorkingCopyManager', () => { const workingCopy = await manager.resolve({ associatedResource: { path: '/some/associated.txt' } }); workingCopy.model?.updateContents('Simple Save As with associated resource'); - const target = URI.from({ scheme: Schemas.vscodeRemote, path: '/some/associated.txt' }); + const target = URI.from({ scheme: Schemas.file, path: '/some/associated.txt' }); accessor.fileService.notExistsSet.set(target, true); diff --git a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts index e37181dafe7..69f8c133efc 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts @@ -226,7 +226,7 @@ suite('UntitledFileWorkingCopyManager', () => { const workingCopy = await manager.untitled.resolve({ associatedResource: { path: '/some/associated.txt' } }); workingCopy.model?.updateContents('Simple Save with associated resource'); - accessor.fileService.notExistsSet.set(URI.from({ scheme: Schemas.vscodeRemote, path: '/some/associated.txt' }), true); + accessor.fileService.notExistsSet.set(URI.from({ scheme: Schemas.file, path: '/some/associated.txt' }), true); const result = await workingCopy.save(); assert.ok(result); 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 55fbd774430..596255d0466 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 @@ -17,13 +17,12 @@ import { dirname, join } from 'vs/base/common/path'; import { Promises } from 'vs/base/node/pfs'; import { URI } from 'vs/base/common/uri'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; -import { TestRemoteAgentService } from 'vs/workbench/services/remote/test/common/testServices'; import { existsSync, readFileSync, unlinkSync } from 'fs'; import { IWorkingCopyHistoryEntry, IWorkingCopyHistoryEntryDescriptor, IWorkingCopyHistoryEvent } from 'vs/workbench/services/workingCopy/common/workingCopyHistory'; import { IFileService } from 'vs/platform/files/common/files'; import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; import { LabelService } from 'vs/workbench/services/label/common/labelService'; -import { TestLifecycleService, TestWillShutdownEvent } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestLifecycleService, TestRemoteAgentService, TestWillShutdownEvent } from 'vs/workbench/test/browser/workbenchTestServices'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { NativeWorkingCopyHistoryService } from 'vs/workbench/services/workingCopy/electron-sandbox/workingCopyHistoryService'; import { joinPath, dirname as resourcesDirname, basename } from 'vs/base/common/resources'; @@ -58,7 +57,7 @@ export class TestWorkingCopyHistoryService extends NativeWorkingCopyHistoryServi const uriIdentityService = new UriIdentityService(fileService); - const labelService = new LabelService(environmentService, new TestContextService(), new TestNativePathService()); + const labelService = new LabelService(environmentService, new TestContextService(), new TestNativePathService(), new TestRemoteAgentService()); const lifecycleService = new TestLifecycleService(); diff --git a/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts b/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts index 72f3a0b82ed..ebe492e36ed 100644 --- a/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts +++ b/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts @@ -14,7 +14,7 @@ import { ConfigurationScope, IConfigurationRegistry, Extensions as Configuration import { Registry } from 'vs/platform/registry/common/platform'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { distinct, firstOrDefault } from 'vs/base/common/arrays'; -import { basename, isEqual, isEqualAuthority, removeTrailingPathSeparator } from 'vs/base/common/resources'; +import { basename, isEqual, isEqualAuthority, joinPath, removeTrailingPathSeparator } from 'vs/base/common/resources'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { IFileService } from 'vs/platform/files/common/files'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -58,7 +58,7 @@ export abstract class AbstractWorkspaceEditingService implements IWorkspaceEditi saveLabel: mnemonicButtonLabel(localize('save', "Save")), title: localize('saveWorkspace', "Save Workspace"), filters: WORKSPACE_FILTER, - defaultUri: await this.fileDialogService.defaultWorkspacePath(undefined, this.getNewWorkspaceName()), + defaultUri: joinPath(await this.fileDialogService.defaultWorkspacePath(), this.getNewWorkspaceName()), availableFileSystems }); diff --git a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts index 38877625d85..e5039e9bc04 100644 --- a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts +++ b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts @@ -21,8 +21,7 @@ import { Memento, MementoObject } from 'vs/workbench/common/memento'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { isEqualAuthority } from 'vs/base/common/resources'; -import { ILogService } from 'vs/platform/log/common/log'; -import { isCI } from 'vs/base/common/platform'; +import { isWeb } from 'vs/base/common/platform'; export const WORKSPACE_TRUST_ENABLED = 'security.workspace.trust.enabled'; export const WORKSPACE_TRUST_STARTUP_PROMPT = 'security.workspace.trust.startupPrompt'; @@ -119,8 +118,7 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService, - @IWorkspaceTrustEnablementService private readonly workspaceTrustEnablementService: IWorkspaceTrustEnablementService, - @ILogService protected readonly _logService: ILogService, + @IWorkspaceTrustEnablementService private readonly workspaceTrustEnablementService: IWorkspaceTrustEnablementService ) { super(); @@ -134,7 +132,7 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork this._workspaceTrustInitializedPromiseResolve = resolve; }); - this._storedTrustState = new WorkspaceTrustMemento(this.storageService); + this._storedTrustState = new WorkspaceTrustMemento(isWeb && this.workspaceService.getWorkbenchState() === WorkbenchState.EMPTY ? undefined : this.storageService); this._trustTransitionManager = this._register(new WorkspaceTrustTransitionManager()); this._trustStateInfo = this.loadTrustInfo(); @@ -147,10 +145,6 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork //#region initialize private initializeWorkspaceTrust(): void { - if (isCI) { - this._logService.info(`[WT] Enter initializeWorkspaceTrust()...`); - } - // Resolve canonical Uris this.resolveCanonicalUris() .then(async () => { @@ -158,15 +152,9 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork await this.updateWorkspaceTrust(); }) .finally(() => { - if (isCI) { - this._logService.info(`[WT] Open workspaceResolved gate...`); - } this._workspaceResolvedPromiseResolve(); if (!this.environmentService.remoteAuthority) { - if (isCI) { - this._logService.info(`[WT] Open workspaceTrustInitialized gate...`); - } this._workspaceTrustInitializedPromiseResolve(); } }); @@ -211,33 +199,21 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork } private async getCanonicalUri(uri: URI): Promise<URI> { - if (isCI) { - this._logService.info('[WT] Enter getCanonicalUri()...'); - } - + let canonicalUri = uri; if (this.environmentService.remoteAuthority && uri.scheme === Schemas.vscodeRemote) { - if (isCI) { - this._logService.info('[WT] Return this.remoteAuthorityResolverService.getCanonicalURI(uri)...'); - } - - return this.remoteAuthorityResolverService.getCanonicalURI(uri); - } - - if (uri.scheme === 'vscode-vfs') { + canonicalUri = await this.remoteAuthorityResolverService.getCanonicalURI(uri); + } else if (uri.scheme === 'vscode-vfs') { const index = uri.authority.indexOf('+'); if (index !== -1) { - return uri.with({ authority: uri.authority.substr(0, index) }); + canonicalUri = uri.with({ authority: uri.authority.substr(0, index) }); } } - return uri; + // ignore query and fragent section of uris always + return canonicalUri.with({ query: null, fragment: null }); } private async resolveCanonicalUris(): Promise<void> { - if (isCI) { - this._logService.info('[WT] Enter resolveCanonicalUris()...'); - } - // Open editors const filesToOpen: IPath[] = []; if (this.environmentService.filesToOpenOrCreate) { @@ -249,38 +225,22 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork } if (filesToOpen.length) { - const filesToOpenOrCreateUris = filesToOpen.filter(f => f.fileUri && f.fileUri.scheme === Schemas.file).map(f => f.fileUri!); + const filesToOpenOrCreateUris = filesToOpen.filter(f => !!f.fileUri).map(f => f.fileUri!); const canonicalFilesToOpen = await Promise.all(filesToOpenOrCreateUris.map(uri => this.getCanonicalUri(uri))); this._canonicalStartupFiles.push(...canonicalFilesToOpen.filter(uri => this._canonicalStartupFiles.every(u => !this.uriIdentityService.extUri.isEqual(uri, u)))); } - if (isCI) { - this._logService.info('[WT] Done processing open editors...'); - } - // Workspace const workspaceUris = this.workspaceService.getWorkspace().folders.map(f => f.uri); const canonicalWorkspaceFolders = await Promise.all(workspaceUris.map(uri => this.getCanonicalUri(uri))); - if (isCI) { - this._logService.info('[WT] Done processing workspace folders...'); - } - let canonicalWorkspaceConfiguration = this.workspaceService.getWorkspace().configuration; if (canonicalWorkspaceConfiguration && isSavedWorkspace(canonicalWorkspaceConfiguration, this.environmentService)) { canonicalWorkspaceConfiguration = await this.getCanonicalUri(canonicalWorkspaceConfiguration); } - if (isCI) { - this._logService.info('[WT] Done processing workspace configuration...'); - } - this._canonicalWorkspace = new CanonicalWorkspace(this.workspaceService.getWorkspace(), canonicalWorkspaceFolders, canonicalWorkspaceConfiguration); - - if (isCI) { - this._logService.info('[WT] Exit resolveCanonicalUris()...'); - } } private loadTrustInfo(): IWorkspaceTrustInfo { @@ -362,16 +322,7 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork } private async updateWorkspaceTrust(trusted?: boolean): Promise<void> { - if (isCI) { - this._logService.info(`[WT] Enter updateWorkspaceTrust()...`); - } - if (!this.workspaceTrustEnablementService.isWorkspaceTrustEnabled()) { - if (isCI) { - this._logService.info(`[WT] Workspace trust is disabled.`); - this._logService.info(`[WT] Exit updateWorkspaceTrust()...`); - } - return; } @@ -883,15 +834,19 @@ class WorkspaceTrustTransitionManager extends Disposable { class WorkspaceTrustMemento { - private readonly _memento: Memento; + private readonly _memento?: Memento; private readonly _mementoObject: MementoObject; private readonly _acceptsOutOfWorkspaceFilesKey = 'acceptsOutOfWorkspaceFiles'; private readonly _isEmptyWorkspaceTrustedKey = 'isEmptyWorkspaceTrusted'; - constructor(storageService: IStorageService) { - this._memento = new Memento('workspaceTrust', storageService); - this._mementoObject = this._memento.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE); + constructor(storageService?: IStorageService) { + if (storageService) { + this._memento = new Memento('workspaceTrust', storageService); + this._mementoObject = this._memento.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE); + } else { + this._mementoObject = {}; + } } get acceptsOutOfWorkspaceFiles(): boolean { @@ -900,7 +855,10 @@ class WorkspaceTrustMemento { set acceptsOutOfWorkspaceFiles(value: boolean) { this._mementoObject[this._acceptsOutOfWorkspaceFilesKey] = value; - this._memento.saveMemento(); + + if (this._memento) { + this._memento.saveMemento(); + } } get isEmptyWorkspaceTrusted(): boolean | undefined { @@ -909,7 +867,10 @@ class WorkspaceTrustMemento { set isEmptyWorkspaceTrusted(value: boolean | undefined) { this._mementoObject[this._isEmptyWorkspaceTrustedKey] = value; - this._memento.saveMemento(); + + if (this._memento) { + this._memento.saveMemento(); + } } } 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 da388396897..cb8808efc50 100644 --- a/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts +++ b/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts @@ -10,7 +10,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur 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 { ILogService, NullLogService } from 'vs/platform/log/common/log'; +import { NullLogService } from 'vs/platform/log/common/log'; import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; @@ -28,7 +28,6 @@ suite('Workspace Trust', () => { let instantiationService: TestInstantiationService; let configurationService: TestConfigurationService; let environmentService: IWorkbenchEnvironmentService; - let logService: ILogService; setup(async () => { instantiationService = new TestInstantiationService(); @@ -39,10 +38,7 @@ suite('Workspace Trust', () => { environmentService = {} as IWorkbenchEnvironmentService; instantiationService.stub(IWorkbenchEnvironmentService, environmentService); - logService = new NullLogService(); - instantiationService.stub(ILogService, logService); - - instantiationService.stub(IUriIdentityService, new UriIdentityService(new FileService(logService))); + instantiationService.stub(IUriIdentityService, new UriIdentityService(new FileService(new NullLogService()))); instantiationService.stub(IRemoteAuthorityResolverService, new class extends mock<IRemoteAuthorityResolverService>() { }); }); 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 4023aef7563..3f4659894a3 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorGroupModel.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorGroupModel.test.ts @@ -2232,6 +2232,40 @@ suite('EditorGroupModel', () => { assert.strictEqual(group.indexOf(input4), 2); }); + test('Sticky/Unsticky Editors sends correct editor index', function () { + const group = createEditorGroupModel(); + + const input1 = input(); + const input2 = input(); + const input3 = input(); + + group.openEditor(input1, { pinned: true, active: true }); + group.openEditor(input2, { pinned: true, active: true }); + group.openEditor(input3, { pinned: false, active: true }); + + assert.strictEqual(group.stickyCount, 0); + + const events = groupListener(group); + + group.stick(input3); + + assert.strictEqual(events.sticky[0].editorIndex, 0); + assert.strictEqual(group.isSticky(input3), true); + assert.strictEqual(group.stickyCount, 1); + + group.stick(input2); + + assert.strictEqual(events.sticky[1].editorIndex, 1); + assert.strictEqual(group.isSticky(input2), true); + assert.strictEqual(group.stickyCount, 2); + + group.unstick(input3); + assert.strictEqual(events.unsticky[0].editorIndex, 1); + assert.strictEqual(group.isSticky(input3), false); + assert.strictEqual(group.isSticky(input2), true); + assert.strictEqual(group.stickyCount, 1); + }); + test('onDidMoveEditor Event', () => { const group1 = createEditorGroupModel(); const group2 = createEditorGroupModel(); diff --git a/src/vs/workbench/test/browser/parts/editor/editorPane.test.ts b/src/vs/workbench/test/browser/parts/editor/editorPane.test.ts index dbb629c311f..7d19418acc5 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorPane.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorPane.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { EditorPane, EditorMemento } from 'vs/workbench/browser/parts/editor/editorPane'; -import { WorkspaceTrustRequiredEditor } from 'vs/workbench/browser/parts/editor/editorPlaceholder'; +import { WorkspaceTrustRequiredPlaceholderEditor } from 'vs/workbench/browser/parts/editor/editorPlaceholder'; import { IEditorSerializer, IEditorFactoryRegistry, EditorExtensions, EditorInputCapabilities, IEditorDescriptor, IEditorPane } from 'vs/workbench/common/editor'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -505,7 +505,7 @@ suite('EditorPane', () => { const testInput = new TrustRequiredTestInput(); await group.openEditor(testInput); - assert.strictEqual(group.activeEditorPane?.getId(), WorkspaceTrustRequiredEditor.ID); + assert.strictEqual(group.activeEditorPane?.getId(), WorkspaceTrustRequiredPlaceholderEditor.ID); const getEditorPaneIdAsync = () => new Promise(resolve => { disposables.add(editorService.onDidActiveEditorChange(() => { @@ -518,7 +518,7 @@ suite('EditorPane', () => { assert.strictEqual(await getEditorPaneIdAsync(), 'trustRequiredTestEditor'); workspaceTrustService.setWorkspaceTrust(false); - assert.strictEqual(await getEditorPaneIdAsync(), WorkspaceTrustRequiredEditor.ID); + assert.strictEqual(await getEditorPaneIdAsync(), WorkspaceTrustRequiredPlaceholderEditor.ID); dispose(disposables); }); diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index fbd98039411..62e691e60bb 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -7,7 +7,7 @@ import { FileEditorInput } from 'vs/workbench/contrib/files/browser/editors/file import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { basename, isEqual } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { ITelemetryData, ITelemetryService, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { EditorInputWithOptions, IEditorIdentifier, IUntitledTextResourceEditorInput, IResourceDiffEditorInput, IEditorPane, IEditorCloseEvent, IEditorPartOptions, IRevertOptions, GroupIdentifier, EditorsOrder, IFileEditorInput, IEditorFactoryRegistry, IEditorSerializer, EditorExtensions, ISaveOptions, IMoveResult, ITextDiffEditorPane, IVisibleEditorPane, IEditorOpenContext, EditorExtensions as Extensions, EditorInputCapabilities, IUntypedEditorInput, IEditorWillMoveEvent, IEditorWillOpenEvent, IActiveEditorChangeEvent, EditorPaneSelectionChangeReason, IEditorPaneSelection } from 'vs/workbench/common/editor'; @@ -21,7 +21,7 @@ import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { IEditorOptions, IResourceEditorInput, IEditorModel, IResourceEditorInputIdentifier, ITextResourceEditorInput, ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { IUntitledTextEditorService, UntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; import { IWorkspaceContextService, IWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; -import { ILifecycleService, ShutdownReason, StartupKind, LifecyclePhase, WillShutdownEvent, BeforeShutdownErrorEvent, InternalBeforeShutdownEvent } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { ILifecycleService, ShutdownReason, StartupKind, LifecyclePhase, WillShutdownEvent, BeforeShutdownErrorEvent, InternalBeforeShutdownEvent, IWillShutdownEventJoiner } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { FileOperationEvent, IFileService, IFileStat, IFileStatResult, FileChangesEvent, IResolveFileOptions, ICreateFileOptions, IFileSystemProvider, FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileType, IFileDeleteOptions, IFileOverwriteOptions, IFileWriteOptions, IFileOpenOptions, IFileStatWithMetadata, IResolveMetadataFileOptions, IWriteFileOptions, IReadFileOptions, IFileContent, IFileStreamContent, FileOperationError, IFileSystemProviderWithFileReadStreamCapability, IFileReadStreamOptions, IReadFileStreamOptions, IFileSystemProviderCapabilitiesChangeEvent, IFileStatWithPartialMetadata } from 'vs/platform/files/common/files'; import { IModelService } from 'vs/editor/common/services/model'; @@ -153,6 +153,12 @@ import { TextEditorPaneSelection } from 'vs/workbench/browser/parts/editor/textE import { Selection } from 'vs/editor/common/core/selection'; import { IFolderBackupInfo, IWorkspaceBackupInfo } from 'vs/platform/backup/common/backup'; import { TestEditorWorkerService } from 'vs/editor/test/common/services/testEditorWorkerService'; +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 { ISocketFactory } from 'vs/platform/remote/common/remoteAgentConnection'; +import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment'; export function createFileEditorInput(instantiationService: IInstantiationService, resource: URI): FileEditorInput { return instantiationService.createInstance(FileEditorInput, resource, undefined, undefined, undefined, undefined, undefined, undefined); @@ -252,6 +258,8 @@ export function workbenchInstantiationService( instantiationService.stub(ITextResourceConfigurationService, new TestTextResourceConfigurationService(configService)); instantiationService.stub(IUntitledTextEditorService, disposables.add(instantiationService.createInstance(UntitledTextEditorService))); instantiationService.stub(IStorageService, disposables.add(new TestStorageService())); + instantiationService.stub(IRemoteAgentService, new TestRemoteAgentService()); + instantiationService.stub(ILanguageDetectionService, new TestLanguageDetectionService()); instantiationService.stub(IPathService, overrides?.pathService ? overrides.pathService(instantiationService) : new TestPathService()); const layoutService = new TestLayoutService(); instantiationService.stub(IWorkbenchLayoutService, layoutService); @@ -865,7 +873,7 @@ export class TestEditorGroupView implements IEditorGroupView { copyEditors(_editors: EditorInputWithOptions[], _target: IEditorGroup): void { } async closeEditor(_editor?: EditorInput, options?: ICloseEditorOptions): Promise<boolean> { return true; } async closeEditors(_editors: EditorInput[] | ICloseEditorsFilter, options?: ICloseEditorOptions): Promise<boolean> { return true; } - async closeAllEditors(options?: ICloseAllEditorsOptions): Promise<void> { } + async closeAllEditors(options?: ICloseAllEditorsOptions): Promise<boolean> { return true; } async replaceEditors(_editors: IEditorReplacement[]): Promise<void> { } pinEditor(_editor?: EditorInput): void { } stickEditor(editor?: EditorInput | undefined): void { } @@ -1227,6 +1235,7 @@ export class TestLifecycleService implements ILifecycleService { join: p => { this.shutdownJoiners.push(p); }, + joiners: () => [], force: () => { /* No-Op in tests */ }, token: CancellationToken.None, reason @@ -1261,10 +1270,11 @@ export class TestBeforeShutdownEvent implements InternalBeforeShutdownEvent { export class TestWillShutdownEvent implements WillShutdownEvent { value: Promise<void>[] = []; + joiners = () => []; reason = ShutdownReason.CLOSE; token = CancellationToken.None; - join(promise: Promise<void>, id: string): void { + join(promise: Promise<void>, joiner: IWillShutdownEventJoiner): void { this.value.push(promise); } @@ -1675,7 +1685,7 @@ export class TestPathService implements IPathService { declare readonly _serviceBrand: undefined; - constructor(private readonly fallbackUserHome: URI = URI.from({ scheme: Schemas.vscodeRemote, path: '/' }), public defaultUriScheme = Schemas.vscodeRemote) { } + constructor(private readonly fallbackUserHome: URI = URI.from({ scheme: Schemas.file, path: '/' }), public defaultUriScheme = Schemas.file) { } hasValidBasename(resource: URI, basename?: string): Promise<boolean>; hasValidBasename(resource: URI, os: OperatingSystem, basename?: string): boolean; @@ -1689,7 +1699,12 @@ export class TestPathService implements IPathService { get path() { return Promise.resolve(isWindows ? win32 : posix); } - async userHome() { return this.fallbackUserHome; } + userHome(options?: { preferLocal: boolean }): Promise<URI>; + userHome(options: { preferLocal: true }): URI; + userHome(options?: { preferLocal: boolean }): Promise<URI> | URI { + return options?.preferLocal ? this.fallbackUserHome : Promise.resolve(this.fallbackUserHome); + } + get resolvedUserHome() { return this.fallbackUserHome; } async fileURI(path: string): Promise<URI> { @@ -1874,3 +1889,32 @@ export class TestQuickInputService implements IQuickInputService { back(): Promise<void> { throw new Error('not implemented.'); } cancel(): Promise<void> { throw new Error('not implemented.'); } } + +class TestLanguageDetectionService implements ILanguageDetectionService { + + declare readonly _serviceBrand: undefined; + + isEnabledForLanguage(languageId: string): boolean { return false; } + async detectLanguage(resource: URI, supportedLangs?: string[] | undefined): Promise<string | undefined> { return undefined; } +} + +export class TestRemoteAgentService implements IRemoteAgentService { + + declare readonly _serviceBrand: undefined; + + socketFactory: ISocketFactory = { + connect() { } + }; + + getConnection(): IRemoteAgentConnection | null { return null; } + async getEnvironment(): Promise<IRemoteAgentEnvironment | null> { return null; } + async getRawEnvironment(): Promise<IRemoteAgentEnvironment | null> { return null; } + async getExtensionHostExitInfo(reconnectionToken: string): Promise<IExtensionHostExitInfo | null> { return null; } + async whenExtensionsReady(): Promise<void> { } + scanExtensions(skipExtensions?: ExtensionIdentifier[]): Promise<IExtensionDescription[]> { throw new Error('Method not implemented.'); } + scanSingleExtension(extensionLocation: URI, isBuiltin: boolean): Promise<IExtensionDescription | null> { throw new Error('Method not implemented.'); } + async getDiagnosticInfo(options: IDiagnosticInfoOptions): Promise<IDiagnosticInfo | undefined> { return undefined; } + async updateTelemetryLevel(telemetryLevel: TelemetryLevel): Promise<void> { } + async logTelemetry(eventName: string, data?: ITelemetryData): Promise<void> { } + async flushTelemetry(): Promise<void> { } +} diff --git a/src/vs/workbench/test/common/notifications.test.ts b/src/vs/workbench/test/common/notifications.test.ts index dbeb93bfad0..1bdef91760a 100644 --- a/src/vs/workbench/test/common/notifications.test.ts +++ b/src/vs/workbench/test/common/notifications.test.ts @@ -127,7 +127,7 @@ suite('Notifications', () => { assert.strictEqual(called, 1); // Error with Action - let item7 = NotificationViewItem.create({ severity: Severity.Error, message: createErrorWithActions('Hello Error', { actions: [new Action('id', 'label')] }) })!; + let item7 = NotificationViewItem.create({ severity: Severity.Error, message: createErrorWithActions('Hello Error', [new Action('id', 'label')]) })!; assert.strictEqual(item7.actions!.primary!.length, 1); // Filter diff --git a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts index ba5b81b1d97..9b1228a4c69 100644 --- a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts @@ -203,6 +203,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 setMinimumSize(width: number | undefined, height: number | undefined): Promise<void> { } async saveWindowSplash(value: IPartsSplash): Promise<void> { } async focusWindow(options?: { windowId?: number | undefined } | undefined): Promise<void> { } @@ -245,6 +246,7 @@ export class TestNativeHostService implements INativeHostService { async toggleDevTools(): Promise<void> { } async toggleSharedProcessWindow(): Promise<void> { } async resolveProxy(url: string): Promise<string | undefined> { return undefined; } + async findFreePort(startPort: number, giveUpAfter: number, timeout: number, stride?: number): Promise<number> { return -1; } async readClipboardText(type?: 'selection' | 'clipboard' | undefined): Promise<string> { return ''; } async writeClipboardText(text: string, type?: 'selection' | 'clipboard' | undefined): Promise<void> { } async readClipboardFindText(): Promise<string> { return ''; } diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index dfc2bcadef0..bf87db19d53 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -83,6 +83,7 @@ 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/remote/common/remoteExplorerService'; import 'vs/workbench/services/workingCopy/common/workingCopyService'; import 'vs/workbench/services/workingCopy/common/workingCopyFileService'; @@ -303,6 +304,9 @@ import 'vs/workbench/contrib/typeHierarchy/browser/typeHierarchy.contribution'; import 'vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline'; import 'vs/workbench/contrib/outline/browser/outline.contribution'; +// Language Detection +import 'vs/workbench/contrib/languageDetection/browser/languageDetection.contribution'; + // Language Status import 'vs/workbench/contrib/languageStatus/browser/languageStatus.contribution'; @@ -315,6 +319,9 @@ 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'; + // Code Actions import 'vs/workbench/contrib/codeActions/browser/codeActions.contribution'; @@ -336,4 +343,7 @@ import 'vs/workbench/contrib/list/browser/list.contribution'; // Audio Cues import 'vs/workbench/contrib/audioCues/browser/audioCues.contribution'; +// Drop into editor +import 'vs/workbench/contrib/dropIntoEditor/browser/dropIntoEditor.contibution'; + //#endregion diff --git a/src/vs/workbench/workbench.sandbox.main.ts b/src/vs/workbench/workbench.sandbox.main.ts index 87f933f5fe0..e0708193c34 100644 --- a/src/vs/workbench/workbench.sandbox.main.ts +++ b/src/vs/workbench/workbench.sandbox.main.ts @@ -61,6 +61,7 @@ import 'vs/workbench/services/encryption/electron-sandbox/encryptionService'; import 'vs/workbench/services/localizations/electron-sandbox/localizationsService'; import 'vs/workbench/services/telemetry/electron-sandbox/telemetryService'; import 'vs/workbench/services/extensions/electron-sandbox/extensionHostStarter'; +import 'vs/platform/extensionManagement/electron-sandbox/extensionsScannerService'; import 'vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementServerService'; import 'vs/workbench/services/extensionManagement/electron-sandbox/extensionTipsService'; import 'vs/workbench/services/userDataSync/electron-sandbox/userDataSyncMachinesService'; diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts index 8f3d9020fd0..925c0010a0b 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -61,6 +61,7 @@ import 'vs/workbench/services/tunnel/browser/tunnelService'; import 'vs/workbench/services/files/browser/elevatedFileService'; import 'vs/workbench/services/workingCopy/browser/workingCopyHistoryService'; import 'vs/workbench/services/userDataSync/browser/webUserDataSyncEnablementService'; +import 'vs/workbench/services/configurationResolver/browser/configurationResolverService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; @@ -85,8 +86,6 @@ import { NullEndpointTelemetryService } from 'vs/platform/telemetry/common/telem 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 { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; -import { ConfigurationResolverService } from 'vs/workbench/services/configurationResolver/browser/configurationResolverService'; import { IDiagnosticsService, NullDiagnosticsService } from 'vs/platform/diagnostics/common/diagnostics'; registerSingleton(IWorkbenchExtensionManagementService, ExtensionManagementService); @@ -102,7 +101,6 @@ registerSingleton(IUserDataAutoSyncService, UserDataAutoSyncService); registerSingleton(ITitleService, TitlebarPart); registerSingleton(IExtensionTipsService, ExtensionTipsService); registerSingleton(ITimerService, TimerService); -registerSingleton(IConfigurationResolverService, ConfigurationResolverService, true); registerSingleton(ICustomEndpointTelemetryService, NullEndpointTelemetryService, true); registerSingleton(IDiagnosticsService, NullDiagnosticsService, true); @@ -114,6 +112,9 @@ registerSingleton(IDiagnosticsService, NullDiagnosticsService, true); // Output import 'vs/workbench/contrib/output/common/outputChannelModelService'; +// Logs +import 'vs/workbench/contrib/logs/browser/logs.contribution'; + // Explorer import 'vs/workbench/contrib/files/browser/files.web.contribution'; @@ -168,7 +169,7 @@ import 'vs/workbench/contrib/offline/browser/offline.contribution'; // // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -import { create, commands, env } from 'vs/workbench/browser/web.factory'; +import { create, commands, env, window } from 'vs/workbench/browser/web.factory'; import { IWorkbench, ICommand, ICommonTelemetryPropertiesResolver, IDefaultEditor, IDefaultLayout, IDefaultView, IDevelopmentOptions, IExternalUriResolver, IExternalURLOpener, IHomeIndicator, IInitialColorTheme, IPosition, IProductQualityChangeHandler, IRange, IResourceUriProvider, ISettingsSyncOptions, IShowPortCandidate, ITunnel, ITunnelFactory, ITunnelOptions, ITunnelProvider, IWelcomeBanner, IWelcomeBannerAction, IWindowIndicator, IWorkbenchConstructionOptions, Menu } from 'vs/workbench/browser/web.api'; import { UriComponents, URI } from 'vs/base/common/uri'; import { IWebSocketFactory, IWebSocket } from 'vs/platform/remote/browser/browserSocketFactory'; @@ -183,7 +184,6 @@ import type { IUpdateProvider, IUpdate } from 'vs/workbench/services/update/brow // eslint-disable-next-line no-duplicate-imports import type { IWorkspace, IWorkspaceProvider } from 'vs/workbench/services/host/browser/browserHostService'; - export { // Factory @@ -250,6 +250,9 @@ export { commands, Menu, + // Window + window, + // Branding IHomeIndicator, IWelcomeBanner, |