From 0f4718cf435fea9a6ee6ebf41e338b0d0fefb1f5 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 13 May 2024 13:27:07 +0000 Subject: [PATCH] SCM - eagerly create the input box text model (#212580) * SCM - eagerly create the input box text model * Pull request feedback --- src/vs/workbench/api/browser/mainThreadSCM.ts | 43 ++++- .../workbench/api/common/extHost.protocol.ts | 2 +- .../contrib/scm/browser/scmViewPane.ts | 161 ++++++++---------- src/vs/workbench/contrib/scm/common/scm.ts | 3 +- 4 files changed, 108 insertions(+), 101 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadSCM.ts b/src/vs/workbench/api/browser/mainThreadSCM.ts index 8f2f658e810..348cd234e76 100644 --- a/src/vs/workbench/api/browser/mainThreadSCM.ts +++ b/src/vs/workbench/api/browser/mainThreadSCM.ts @@ -5,7 +5,7 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; -import { IDisposable, DisposableStore, combinedDisposable, dispose } from 'vs/base/common/lifecycle'; +import { IDisposable, DisposableStore, combinedDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; import { ISCMService, ISCMRepository, ISCMProvider, ISCMResource, ISCMResourceGroup, ISCMResourceDecorations, IInputValidation, ISCMViewService, InputValidationType, ISCMActionButtonDescriptor } from 'vs/workbench/contrib/scm/common/scm'; import { ExtHostContext, MainThreadSCMShape, ExtHostSCMShape, SCMProviderFeatures, SCMRawResourceSplices, SCMGroupFeatures, MainContext, SCMHistoryItemGroupDto } from '../common/extHost.protocol'; import { Command } from 'vs/editor/common/languages'; @@ -20,6 +20,11 @@ import { ResourceTree } from 'vs/base/common/resourceTree'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { basename } from 'vs/base/common/resources'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { IModelService } from 'vs/editor/common/services/model'; +import { ITextModelContentProvider, ITextModelService } from 'vs/editor/common/services/resolverService'; +import { Schemas } from 'vs/base/common/network'; +import { ITextModel } from 'vs/editor/common/model'; function getIconFromIconDto(iconDto?: UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon): URI | { light: URI; dark: URI } | ThemeIcon | undefined { if (iconDto === undefined) { @@ -34,6 +39,25 @@ function getIconFromIconDto(iconDto?: UriComponents | { light: UriComponents; da } } +class SCMInputBoxContentProvider extends Disposable implements ITextModelContentProvider { + constructor( + textModelService: ITextModelService, + private readonly modelService: IModelService, + private readonly languageService: ILanguageService, + ) { + super(); + this._register(textModelService.registerTextModelContentProvider(Schemas.vscodeSourceControl, this)); + } + + async provideTextContent(resource: URI): Promise { + const existing = this.modelService.getModel(resource); + if (existing) { + return existing; + } + return this.modelService.createModel('', this.languageService.createById('scminput'), resource); + } +} + class MainThreadSCMResourceGroup implements ISCMResourceGroup { readonly resources: ISCMResource[] = []; @@ -197,7 +221,7 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { get handle(): number { return this._handle; } get label(): string { return this._label; } get rootUri(): URI | undefined { return this._rootUri; } - get inputBoxDocumentUri(): URI { return this._inputBoxDocumentUri; } + get inputBoxTextModel(): ITextModel { return this._inputBoxTextModel; } get contextValue(): string { return this._providerId; } get commitTemplate(): string { return this.features.commitTemplate || ''; } @@ -233,7 +257,7 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { private readonly _providerId: string, private readonly _label: string, private readonly _rootUri: URI | undefined, - private readonly _inputBoxDocumentUri: URI, + private readonly _inputBoxTextModel: ITextModel, private readonly _quickDiffService: IQuickDiffService, private readonly _uriIdentService: IUriIdentityService, private readonly _workspaceContextService: IWorkspaceContextService @@ -425,11 +449,16 @@ export class MainThreadSCM implements MainThreadSCMShape { extHostContext: IExtHostContext, @ISCMService private readonly scmService: ISCMService, @ISCMViewService private readonly scmViewService: ISCMViewService, + @ILanguageService private readonly languageService: ILanguageService, + @IModelService private readonly modelService: IModelService, + @ITextModelService private readonly textModelService: ITextModelService, @IQuickDiffService private readonly quickDiffService: IQuickDiffService, @IUriIdentityService private readonly _uriIdentService: IUriIdentityService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostSCM); + + this._disposables.add(new SCMInputBoxContentProvider(this.textModelService, this.modelService, this.languageService)); } dispose(): void { @@ -442,12 +471,16 @@ export class MainThreadSCM implements MainThreadSCMShape { this._disposables.dispose(); } - $registerSourceControl(handle: number, id: string, label: string, rootUri: UriComponents | undefined, inputBoxDocumentUri: UriComponents): void { - const provider = new MainThreadSCMProvider(this._proxy, handle, id, label, rootUri ? URI.revive(rootUri) : undefined, URI.revive(inputBoxDocumentUri), this.quickDiffService, this._uriIdentService, this.workspaceContextService); + async $registerSourceControl(handle: number, id: string, label: string, rootUri: UriComponents | undefined, inputBoxDocumentUri: UriComponents): Promise { + // Eagerly create the text model for the input box + const inputBoxTextModelRef = await this.textModelService.createModelReference(URI.revive(inputBoxDocumentUri)); + + const provider = new MainThreadSCMProvider(this._proxy, handle, id, label, rootUri ? URI.revive(rootUri) : undefined, inputBoxTextModelRef.object.textEditorModel, this.quickDiffService, this._uriIdentService, this.workspaceContextService); const repository = this.scmService.registerSCMProvider(provider); this._repositories.set(handle, repository); const disposable = combinedDisposable( + inputBoxTextModelRef, Event.filter(this.scmViewService.onDidFocusRepository, r => r === repository)(_ => this._proxy.$setSelectedSourceControl(handle)), repository.input.onDidChange(({ value }) => this._proxy.$onInputBoxValueChange(handle, value)) ); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index d7e6a87fb35..c4b0014ae38 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1517,7 +1517,7 @@ export interface SCMHistoryItemChangeDto { } export interface MainThreadSCMShape extends IDisposable { - $registerSourceControl(handle: number, id: string, label: string, rootUri: UriComponents | undefined, inputBoxDocumentUri: UriComponents): void; + $registerSourceControl(handle: number, id: string, label: string, rootUri: UriComponents | undefined, inputBoxDocumentUri: UriComponents): Promise; $updateSourceControl(handle: number, features: SCMProviderFeatures): void; $unregisterSourceControl(handle: number): void; diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 0796247d739..a9e0b07fdb2 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/scm'; import { Event, Emitter } from 'vs/base/common/event'; import { basename, dirname } from 'vs/base/common/resources'; -import { IDisposable, Disposable, DisposableStore, combinedDisposable, dispose, toDisposable, MutableDisposable, IReference, DisposableMap } from 'vs/base/common/lifecycle'; +import { IDisposable, Disposable, DisposableStore, combinedDisposable, dispose, toDisposable, MutableDisposable, DisposableMap } from 'vs/base/common/lifecycle'; import { ViewPane, IViewPaneOptions, ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { append, $, Dimension, asCSSUrl, trackFocus, clearNode, prepend, isPointerEvent } from 'vs/base/browser/dom'; import { IListVirtualDelegate, IIdentityProvider } from 'vs/base/browser/ui/list/list'; @@ -43,7 +43,6 @@ import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storag import { EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor'; import { SIDE_BAR_BACKGROUND, PANEL_BACKGROUND } from 'vs/workbench/common/theme'; import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; -import { ITextModel } from 'vs/editor/common/model'; import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration'; import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; import { IModelService } from 'vs/editor/common/services/model'; @@ -62,7 +61,6 @@ import { LinkDetector } from 'vs/editor/contrib/links/browser/links'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; -import { ILanguageService } from 'vs/editor/common/languages/language'; import { ILabelService } from 'vs/platform/label/common/label'; import { KeyCode } from 'vs/base/common/keyCodes'; import { DEFAULT_FONT_FAMILY } from 'vs/base/browser/fonts'; @@ -86,7 +84,6 @@ import { MessageController } from 'vs/editor/contrib/message/browser/messageCont import { defaultButtonStyles, defaultCountBadgeStyles } from 'vs/platform/theme/browser/defaultStyles'; import { InlineCompletionsController } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController'; import { CodeActionController } from 'vs/editor/contrib/codeAction/browser/codeActionController'; -import { IResolvedTextEditorModel, ITextModelContentProvider, ITextModelService } from 'vs/editor/common/services/resolverService'; import { Schemas } from 'vs/base/common/network'; import { IDragAndDropData } from 'vs/base/browser/dnd'; import { fillEditorsDragData } from 'vs/workbench/browser/dnd'; @@ -111,6 +108,7 @@ import type { IUpdatableHover, IUpdatableHoverTooltipMarkdownString } from 'vs/b import { IHoverService } from 'vs/platform/hover/browser/hover'; import { OpenScmGroupAction } from 'vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver'; import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController'; +import { ITextModel } from 'vs/editor/common/model'; // type SCMResourceTreeNode = IResourceNode; // type SCMHistoryItemChangeResourceTreeNode = IResourceNode; @@ -390,7 +388,7 @@ class InputRenderer implements ICompressibleTreeRenderer, index: number, templateData: InputTemplate): void { const input = node.element; - templateData.inputWidget.setInput(input); + templateData.inputWidget.input = input; // Remember widget const renderedWidgets = this.inputWidgets.get(input) ?? []; @@ -2280,7 +2278,7 @@ class SCMInputWidget { private toolbar: SCMInputWidgetToolbar; private readonly disposables = new DisposableStore(); - private model: { readonly input: ISCMInput; textModelRef?: IReference } | undefined; + private model: { readonly input: ISCMInput; readonly textModel: ITextModel } | undefined; private repositoryIdContextKey: IContextKey; private readonly repositoryDisposables = new DisposableStore(); @@ -2296,11 +2294,11 @@ class SCMInputWidget { readonly onDidChangeContentHeight: Event; - private get input(): ISCMInput | undefined { + get input(): ISCMInput | undefined { return this.model?.input; } - public async setInput(input: ISCMInput | undefined) { + set input(input: ISCMInput | undefined) { if (input === this.input) { return; } @@ -2312,34 +2310,18 @@ class SCMInputWidget { this.repositoryIdContextKey.set(input?.repository.id); if (!input) { - this.model?.textModelRef?.dispose(); this.inputEditor.setModel(undefined); this.model = undefined; return; } - const uri = input.repository.provider.inputBoxDocumentUri; - if (this.configurationService.getValue('editor.wordBasedSuggestions', { resource: uri }) !== 'off') { - this.configurationService.updateValue('editor.wordBasedSuggestions', 'off', { resource: uri }, ConfigurationTarget.MEMORY); - } - - const modelValue: typeof this.model = { input, textModelRef: undefined }; - - // Save model - this.model = modelValue; - - const modelRef = await this.textModelService.createModelReference(uri); - // Model has been changed in the meantime - if (this.model !== modelValue) { - modelRef.dispose(); - return; - } - - modelValue.textModelRef = modelRef; - - const textModel = modelRef.object.textEditorModel; + const textModel = input.repository.provider.inputBoxTextModel; this.inputEditor.setModel(textModel); + if (this.configurationService.getValue('editor.wordBasedSuggestions', { resource: textModel.uri }) !== 'off') { + this.configurationService.updateValue('editor.wordBasedSuggestions', 'off', { resource: textModel.uri }, ConfigurationTarget.MEMORY); + } + // Validation const validationDelayer = new ThrottledDelayer(200); const validate = async () => { @@ -2432,6 +2414,9 @@ class SCMInputWidget { // Toolbar this.toolbar.setInput(input); + + // Save model + this.model = { input, textModel }; } get selections(): Selection[] | null { @@ -2467,7 +2452,6 @@ class SCMInputWidget { overflowWidgetsDomNode: HTMLElement, @IContextKeyService contextKeyService: IContextKeyService, @IModelService private modelService: IModelService, - @ITextModelService private textModelService: ITextModelService, @IKeybindingService private keybindingService: IKeybindingService, @IConfigurationService private configurationService: IConfigurationService, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -2738,7 +2722,7 @@ class SCMInputWidget { } dispose(): void { - this.setInput(undefined); + this.input = undefined; this.repositoryDisposables.dispose(); this.clearValidation(); this.disposables.dispose(); @@ -2880,7 +2864,6 @@ export class SCMViewPane extends ViewPane { this.storeTreeViewState(); }, this, this.disposables); - this.disposables.add(this.instantiationService.createInstance(ScmInputContentProvider)); Event.any(this.scmService.onDidAddRepository, this.scmService.onDidRemoveRepository)(() => this._onDidChangeViewWelcomeState.fire(), this, this.disposables); this.disposables.add(this.revealResourceThrottler); @@ -2929,45 +2912,49 @@ export class SCMViewPane extends ViewPane { this.onDidChangeBodyVisibility(async visible => { if (visible) { - await this.tree.setInput(this.scmViewService, viewState); + this.treeOperationSequencer.queue(async () => { + await this.tree.setInput(this.scmViewService, viewState); - Event.filter(this.configurationService.onDidChangeConfiguration, - e => - e.affectsConfiguration('scm.alwaysShowRepositories'), - this.visibilityDisposables) - (() => { - this.updateActions(); - this.updateChildren(); - }, this, this.visibilityDisposables); + Event.filter(this.configurationService.onDidChangeConfiguration, + e => + e.affectsConfiguration('scm.alwaysShowRepositories'), + this.visibilityDisposables) + (() => { + this.updateActions(); + this.updateChildren(); + }, this, this.visibilityDisposables); - Event.filter(this.configurationService.onDidChangeConfiguration, - e => - e.affectsConfiguration('scm.inputMinLineCount') || - e.affectsConfiguration('scm.inputMaxLineCount') || - e.affectsConfiguration('scm.showActionButton') || - e.affectsConfiguration('scm.showChangesSummary') || - e.affectsConfiguration('scm.showIncomingChanges') || - e.affectsConfiguration('scm.showOutgoingChanges'), - this.visibilityDisposables) - (() => this.updateChildren(), this, this.visibilityDisposables); + Event.filter(this.configurationService.onDidChangeConfiguration, + e => + e.affectsConfiguration('scm.inputMinLineCount') || + e.affectsConfiguration('scm.inputMaxLineCount') || + e.affectsConfiguration('scm.showActionButton') || + e.affectsConfiguration('scm.showChangesSummary') || + e.affectsConfiguration('scm.showIncomingChanges') || + e.affectsConfiguration('scm.showOutgoingChanges'), + this.visibilityDisposables) + (() => this.updateChildren(), this, this.visibilityDisposables); - // Add visible repositories - this.editorService.onDidActiveEditorChange(this.onDidActiveEditorChange, this, this.visibilityDisposables); - this.scmViewService.onDidChangeVisibleRepositories(this.onDidChangeVisibleRepositories, this, this.visibilityDisposables); - this.onDidChangeVisibleRepositories({ added: this.scmViewService.visibleRepositories, removed: Iterable.empty() }); + // Add visible repositories + this.editorService.onDidActiveEditorChange(this.onDidActiveEditorChange, this, this.visibilityDisposables); + this.scmViewService.onDidChangeVisibleRepositories(this.onDidChangeVisibleRepositories, this, this.visibilityDisposables); + this.onDidChangeVisibleRepositories({ added: this.scmViewService.visibleRepositories, removed: Iterable.empty() }); - // Restore scroll position - if (typeof this.treeScrollTop === 'number') { - this.tree.scrollTop = this.treeScrollTop; - this.treeScrollTop = undefined; - } + // Restore scroll position + if (typeof this.treeScrollTop === 'number') { + this.tree.scrollTop = this.treeScrollTop; + this.treeScrollTop = undefined; + } + + this.updateRepositoryCollapseAllContextKeys(); + }); } else { this.visibilityDisposables.clear(); this.onDidChangeVisibleRepositories({ added: Iterable.empty(), removed: [...this.items.keys()] }); this.treeScrollTop = this.tree.scrollTop; - } - this.updateRepositoryCollapseAllContextKeys(); + this.updateRepositoryCollapseAllContextKeys(); + } }, this, this.disposables); this.disposables.add(this.instantiationService.createInstance(RepositoryVisibilityActionController)); @@ -3492,22 +3479,28 @@ export class SCMViewPane extends ViewPane { override focus(): void { super.focus(); - if (this.isExpanded()) { - if (this.tree.getFocus().length === 0) { - for (const repository of this.scmViewService.visibleRepositories) { - const widgets = this.inputRenderer.getRenderedInputWidget(repository.input); + this.treeOperationSequencer.queue(() => { + return new Promise(resolve => { + if (this.isExpanded()) { + if (this.tree.getFocus().length === 0) { + for (const repository of this.scmViewService.visibleRepositories) { + const widgets = this.inputRenderer.getRenderedInputWidget(repository.input); - if (widgets) { - for (const widget of widgets) { - widget.focus(); + if (widgets) { + for (const widget of widgets) { + widget.focus(); + } + resolve(); + return; + } } - return; } - } - } - this.tree.domFocus(); - } + this.tree.domFocus(); + resolve(); + } + }); + }); } override dispose(): void { @@ -4002,23 +3995,3 @@ export class SCMActionButton implements IDisposable { } } } - -class ScmInputContentProvider extends Disposable implements ITextModelContentProvider { - - constructor( - @ITextModelService textModelService: ITextModelService, - @IModelService private readonly _modelService: IModelService, - @ILanguageService private readonly _languageService: ILanguageService, - ) { - super(); - this._register(textModelService.registerTextModelContentProvider(Schemas.vscodeSourceControl, this)); - } - - async provideTextContent(resource: URI): Promise { - const existing = this._modelService.getModel(resource); - if (existing) { - return existing; - } - return this._modelService.createModel('', this._languageService.createById('scminput'), resource); - } -} diff --git a/src/vs/workbench/contrib/scm/common/scm.ts b/src/vs/workbench/contrib/scm/common/scm.ts index 95bb756359d..3fe568b77d5 100644 --- a/src/vs/workbench/contrib/scm/common/scm.ts +++ b/src/vs/workbench/contrib/scm/common/scm.ts @@ -14,6 +14,7 @@ import { ThemeIcon } from 'vs/base/common/themables'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { ResourceTree } from 'vs/base/common/resourceTree'; import { ISCMHistoryProvider, ISCMHistoryProviderMenus } from 'vs/workbench/contrib/scm/common/history'; +import { ITextModel } from 'vs/editor/common/model'; export const VIEWLET_ID = 'workbench.view.scm'; export const VIEW_PANE_ID = 'workbench.scm'; @@ -70,7 +71,7 @@ export interface ISCMProvider extends IDisposable { readonly onDidChangeResources: Event; readonly rootUri?: URI; - readonly inputBoxDocumentUri: URI; + readonly inputBoxTextModel: ITextModel; readonly count?: number; readonly commitTemplate: string; readonly historyProvider?: ISCMHistoryProvider;