diff --git a/package.json b/package.json index 24482f3d05f60..0e516e7fd5e38 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.110.0", - "distro": "7ae76fff0c4611dd384784edb63189ef932327d6", + "distro": "6c461e78a091f00670d7513c1eeb1fc0f4d3e8b7", "author": { "name": "Microsoft Corporation" }, diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatMarkdownContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatMarkdownContentPart.ts index 4951f32d5f93a..d4b4cec56b25f 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatMarkdownContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatMarkdownContentPart.ts @@ -214,7 +214,13 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP try { const parsedBody = parseLocalFileData(text); range = parsedBody.range && Range.lift(parsedBody.range); - textModel = this.textModelService.createModelReference(parsedBody.uri).then(ref => ref.object.textEditorModel); + const modelRefPromise = this.textModelService.createModelReference(parsedBody.uri); + textModel = modelRefPromise.then(ref => { + if (!this._store.isDisposed) { + this._register(ref); + } + return ref.object.textEditorModel; + }); } catch (e) { return $('div'); } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentContentPart.ts index fc8c59c8d8699..d706fedd7b479 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentContentPart.ts @@ -8,6 +8,7 @@ import { $, AnimationFrameScheduler, DisposableResizeObserver } from '../../../. import { Codicon } from '../../../../../../base/common/codicons.js'; import { MarkdownString } from '../../../../../../base/common/htmlContent.js'; import { Lazy } from '../../../../../../base/common/lazy.js'; +import { IRenderedMarkdown } from '../../../../../../base/browser/markdownRenderer.js'; import { IDisposable, MutableDisposable } from '../../../../../../base/common/lifecycle.js'; import { autorun } from '../../../../../../base/common/observable.js'; import { rcut } from '../../../../../../base/common/strings.js'; @@ -100,6 +101,7 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen // Persistent title elements for shimmer private titleShimmerSpan: HTMLElement | undefined; private titleDetailContainer: HTMLElement | undefined; + private titleDetailRendered: IRenderedMarkdown | undefined; /** * Check if a tool invocation is the parent subagent tool (the tool that spawns a subagent). @@ -366,11 +368,8 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen private updateTitle(): void { const prefix = this.agentName || localize('chat.subagent.prefix', 'Subagent'); - const shimmerText = `${prefix}: `; - let detailText = this.description; - if (this.currentRunningToolMessage && this.isActive) { - detailText += ` \u2014 ${this.currentRunningToolMessage}`; - } + const shimmerText = `${prefix}: ${this.description}`; + const toolCallText = this.currentRunningToolMessage && this.isActive ? ` \u2014 ${this.currentRunningToolMessage}` : ``; if (!this._collapseButton) { return; @@ -386,18 +385,32 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen } this.titleShimmerSpan.textContent = shimmerText; - const result = this.chatContentMarkdownRenderer.render(new MarkdownString(detailText)); - result.element.classList.add('collapsible-title-content', 'chat-thinking-title-detail'); - renderFileWidgets(result.element, this.instantiationService, this.chatMarkdownAnchorService, this._store); + // Dispose previous detail rendering + if (this.titleDetailRendered) { + this.titleDetailRendered.dispose(); + this.titleDetailRendered = undefined; + } - if (this.titleDetailContainer) { - this.titleDetailContainer.replaceWith(result.element); + if (!toolCallText) { + if (this.titleDetailContainer) { + this.titleDetailContainer.remove(); + this.titleDetailContainer = undefined; + } } else { - labelElement.appendChild(result.element); + const result = this.chatContentMarkdownRenderer.render(new MarkdownString(toolCallText)); + result.element.classList.add('collapsible-title-content', 'chat-thinking-title-detail'); + renderFileWidgets(result.element, this.instantiationService, this.chatMarkdownAnchorService, this._store); + this.titleDetailRendered = result; + + if (this.titleDetailContainer) { + this.titleDetailContainer.replaceWith(result.element); + } else { + labelElement.appendChild(result.element); + } + this.titleDetailContainer = result.element; } - this.titleDetailContainer = result.element; - const fullLabel = `${shimmerText}${detailText}`; + const fullLabel = `${shimmerText}${toolCallText}`; this._collapseButton.element.ariaLabel = fullLabel; this._collapseButton.element.ariaExpanded = String(this.isExpanded()); } @@ -650,6 +663,17 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen factory: () => { domNode: HTMLElement; disposable?: IDisposable }, hookPart: IChatHookPart ): void { + // update title with hook message + const hookMessage = hookPart.stopReason + ? (hookPart.toolDisplayName + ? localize('hook.subagent.blocked', 'Blocked {0}', hookPart.toolDisplayName) + : localize('hook.subagent.blockedGeneric', 'Blocked by hook')) + : (hookPart.toolDisplayName + ? localize('hook.subagent.warning', 'Warning for {0}', hookPart.toolDisplayName) + : localize('hook.subagent.warningGeneric', 'Hook warning')); + this.currentRunningToolMessage = hookMessage; + this.updateTitle(); + if (this.isExpanded() || this.hasExpandedOnce) { const result = factory(); this.appendHookItemToDOM(result.domNode, hookPart); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts index 673b0b5d0a002..e59763850e62e 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts @@ -98,6 +98,7 @@ interface ILazyToolItem { toolInvocationId?: string; toolInvocationOrMarkdown?: IChatToolInvocation | IChatToolInvocationSerialized | IChatMarkdownContent; originalParent?: HTMLElement; + isHook?: boolean; } interface ILazyThinkingItem { @@ -160,6 +161,7 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen private appendedItemCount: number = 0; private isActive: boolean = true; private toolInvocations: (IChatToolInvocation | IChatToolInvocationSerialized)[] = []; + private hookCount: number = 0; private singleItemInfo: { element: HTMLElement; originalParent: HTMLElement; originalNextSibling: Node | null } | undefined; private lazyItems: ILazyItem[] = []; private hasExpandedOnce: boolean = false; @@ -807,13 +809,13 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen - For reasoning/thinking: "Considered", "Planned", "Analyzed", "Reviewed", "Evaluated" - Choose the synonym that best fits the context - PRIORITY RULE - BLOCKED/DENIED CONTENT: - - If any item mentions being "blocked" (e.g. "Tried to use X, but was blocked"), it MUST be reflected in the title - - Blocked content takes priority over all other tool calls - - Use natural phrasing like "Tried to , but was blocked" or "Attempted but was denied" - - If there are both blocked items AND normal tool calls, mention both: e.g. "Tried to run terminal but was blocked, edited file.ts" +${this.hookCount > 0 ? `BLOCKED/DENIED CONTENT (hooks detected): + - Only mention "blocked" if the content explicitly includes hook results that blocked or warned about a tool (e.g. "Blocked terminal" or "Warning for read_file") + - If blocked items are present alongside normal tool calls, briefly note the block but do NOT let it dominate the summary: e.g. "Updated file.ts, blocked terminal" - RULES FOR TOOL CALLS: + ` : `IMPORTANT: Do NOT use words like "blocked", "denied", or "tried" in the summary - there are no hooks or blocked items in this content. Just summarize normally. + + `}RULES FOR TOOL CALLS: 1. If the SAME file was both edited AND read: Use a combined phrase like "Reviewed and updated " 2. If exactly ONE file was edited: Start with an edit synonym + "" (include actual filename) 3. If exactly ONE file was read: Start with a read synonym + "" (include actual filename) @@ -852,13 +854,12 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen - "Edited Button.tsx, Edited Button.css, Edited index.ts" → "Modified 3 files" - "Searched codebase for error handling" → "Looked up error handling" - EXAMPLES WITH BLOCKED CONTENT: - - "Tried to use Run in Terminal, but was blocked" → "Tried to run command, but was blocked" - - "Tried to use Run in Terminal, but was blocked, Edited config.ts" → "Tried to run command but was blocked, edited config.ts" - - "Tried to use Edit File, but was blocked, Tried to use Run in Terminal, but was blocked" → "Tried to use 2 tools, but was blocked" - - "Used Read File, but received a warning, Edited utils.ts" → "Read file with a warning, edited utils.ts" +${this.hookCount > 0 ? `EXAMPLES WITH BLOCKED CONTENT (from hooks): + - "Blocked terminal, Edited config.ts" → "Edited config.ts, terminal was blocked" + - "Blocked terminal, Blocked read_file" → "Two tools were blocked by hooks" + - "Warning for read_file, Edited utils.ts" → "Edited utils.ts with a hook warning" - EXAMPLES WITH REASONING HEADERS (no tools): + ` : ''}EXAMPLES WITH REASONING HEADERS (no tools): - "Analyzing component architecture" → "Considered component architecture" - "Planning refactor strategy" → "Planned refactor strategy" - "Reviewing error handling approach, Considering edge cases" → "Analyzed error handling approach" @@ -1016,7 +1017,8 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen lazy: new Lazy(factory), toolInvocationId, toolInvocationOrMarkdown, - originalParent + originalParent, + isHook: !toolInvocationOrMarkdown && !!toolInvocationId }; this.lazyItems.push(item); } @@ -1034,9 +1036,14 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen return false; } + const removedItem = this.lazyItems[index]; this.lazyItems.splice(index, 1); this.appendedItemCount--; - this.toolInvocationCount--; + if (removedItem.kind === 'tool' && removedItem.isHook) { + this.hookCount = Math.max(0, this.hookCount - 1); + } else { + this.toolInvocationCount--; + } const toolInvocationsIndex = this.toolInvocations.findIndex(t => (t.kind === 'toolInvocation' || t.kind === 'toolInvocationSerialized') && t.toolId === toolInvocationId @@ -1103,7 +1110,14 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen return; } - this.toolInvocationCount++; + // Track hooks separately: if toolInvocationOrMarkdown is undefined, it's a hook item + const isHook = !toolInvocationOrMarkdown; + if (isHook) { + this.hookCount++; + } else { + this.toolInvocationCount++; + } + let toolCallLabel: string; const isToolInvocation = toolInvocationOrMarkdown && (toolInvocationOrMarkdown.kind === 'toolInvocation' || toolInvocationOrMarkdown.kind === 'toolInvocationSerialized'); diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index a6493ae2c62d7..8800e0f570dd6 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -2172,7 +2172,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.setCurrentLanguageModel(model); this.renderAttachedContext(); }, - getModels: () => this.getModels() + getModels: () => this.getModels(), + canManageModels: () => !this.getCurrentSessionType() }; return this.modelWidget = this.instantiationService.createInstance(ModelPickerActionItem, action, undefined, itemDelegate, pickerOptions); } else if (action.id === OpenModePickerAction.ID && action instanceof MenuItemAction) { diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts index 5a64dde9506db..525e9ee8236ef 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts @@ -29,6 +29,7 @@ export interface IModelPickerDelegate { readonly currentModel: IObservable; setModel(model: ILanguageModelChatMetadataAndIdentifier): void; getModels(): ILanguageModelChatMetadataAndIdentifier[]; + canManageModels(): boolean; } type ChatModelChangeClassification = { @@ -165,9 +166,10 @@ export class ModelPickerActionItem extends ChatInputPickerActionViewItem { run: () => { } }; + const baseActionBarActionProvider = getModelPickerActionBarActionProvider(commandService, chatEntitlementService, productService); const modelPickerActionWidgetOptions: Omit = { actionProvider: modelDelegateToWidgetActionsProvider(delegate, telemetryService, pickerOptions), - actionBarActionProvider: getModelPickerActionBarActionProvider(commandService, chatEntitlementService, productService), + actionBarActionProvider: { getActions: () => delegate.canManageModels() ? baseActionBarActionProvider.getActions() : [] }, reporter: { id: 'ChatModelPicker', name: 'ChatModelPicker', includeOptions: true }, }; diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts index 3ca82e90d52ca..c02f3dfcd536d 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts @@ -317,7 +317,7 @@ export class PromptHeaderAutocompletion implements CompletionItemProvider { const result = []; for (const model of this.languageModelsService.getLanguageModelIds()) { const metadata = this.languageModelsService.lookupLanguageModel(model); - if (metadata && metadata.isUserSelectable !== false) { + if (metadata && metadata.isUserSelectable !== false && !metadata.targetChatSessionType) { if (!agentModeOnly || ILanguageModelChatMetadata.suitableForAgentMode(metadata)) { result.push({ name: ILanguageModelChatMetadata.asQualifiedName(metadata), diff --git a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHeaderAutocompletion.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHeaderAutocompletion.test.ts index db6a9d11f99ae..fe90e7a434e45 100644 --- a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHeaderAutocompletion.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHeaderAutocompletion.test.ts @@ -56,6 +56,7 @@ suite('PromptHeaderAutocompletion', () => { { id: 'mae-4', name: 'MAE 4', vendor: 'olama', version: '1.0', family: 'mae', modelPickerCategory: undefined, extension: new ExtensionIdentifier('a.b'), isUserSelectable: true, maxInputTokens: 8192, maxOutputTokens: 1024, capabilities: { agentMode: true, toolCalling: true }, isDefaultForLocation: { [ChatAgentLocation.Chat]: true } } satisfies ILanguageModelChatMetadata, { id: 'mae-4.1', name: 'MAE 4.1', vendor: 'copilot', version: '1.0', family: 'mae', modelPickerCategory: undefined, extension: new ExtensionIdentifier('a.b'), isUserSelectable: true, maxInputTokens: 8192, maxOutputTokens: 1024, capabilities: { agentMode: true, toolCalling: true }, isDefaultForLocation: { [ChatAgentLocation.Chat]: true } } satisfies ILanguageModelChatMetadata, { id: 'gpt-4', name: 'GPT 4', vendor: 'openai', version: '1.0', family: 'gpt', modelPickerCategory: undefined, extension: new ExtensionIdentifier('a.b'), isUserSelectable: true, maxInputTokens: 8192, maxOutputTokens: 1024, capabilities: { agentMode: false, toolCalling: true }, isDefaultForLocation: { [ChatAgentLocation.Chat]: true } } satisfies ILanguageModelChatMetadata, + { id: 'bg-agent-model', name: 'BG Agent Model', vendor: 'copilot', version: '1.0', family: 'bg', modelPickerCategory: undefined, extension: new ExtensionIdentifier('a.b'), isUserSelectable: true, maxInputTokens: 8192, maxOutputTokens: 1024, capabilities: { agentMode: true, toolCalling: true }, isDefaultForLocation: { [ChatAgentLocation.Chat]: true }, targetChatSessionType: 'background' } satisfies ILanguageModelChatMetadata, ]; instaService.stub(ILanguageModelsService, { @@ -361,6 +362,33 @@ suite('PromptHeaderAutocompletion', () => { { label: 'true', result: 'disable-model-invocation: true' }, ].sort(sortByLabel)); }); + + test('exclude models with targetChatSessionType from agent model completions', async () => { + const content = [ + '---', + 'description: "Test"', + 'model: |', + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.agent); + const labels = actual.map(a => a.label); + // BG Agent Model has targetChatSessionType set, so it should be excluded + assert.ok(!labels.includes('BG Agent Model (copilot)'), 'Models with targetChatSessionType should be excluded from agent model completions'); + }); + + test('exclude models with targetChatSessionType from agent model array completions', async () => { + const content = [ + '---', + 'description: "Test"', + 'model: [|]', + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.agent); + const labels = actual.map(a => a.label); + assert.ok(!labels.includes('BG Agent Model (copilot)'), 'Models with targetChatSessionType should be excluded from agent model array completions'); + }); }); suite('claude agent header completions', () => { @@ -546,5 +574,18 @@ suite('PromptHeaderAutocompletion', () => { { label: 'GPT 4 (openai)', result: 'model: GPT 4 (openai)' }, ].sort(sortByLabel)); }); + + test('exclude models with targetChatSessionType from prompt model completions', async () => { + const content = [ + '---', + 'description: "Test"', + 'model: |', + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.prompt); + const labels = actual.map(a => a.label); + assert.ok(!labels.includes('BG Agent Model (copilot)'), 'Models with targetChatSessionType should be excluded from prompt model completions'); + }); }); }); diff --git a/src/vs/workbench/contrib/debug/browser/media/repl.css b/src/vs/workbench/contrib/debug/browser/media/repl.css index 59696ad50041a..6aad626c79f0d 100644 --- a/src/vs/workbench/contrib/debug/browser/media/repl.css +++ b/src/vs/workbench/contrib/debug/browser/media/repl.css @@ -11,9 +11,13 @@ overflow: hidden; } -.monaco-workbench .repl .repl-tree .monaco-tl-contents { +.monaco-workbench .repl .repl-tree .monaco-tl-contents, +.monaco-workbench .repl .repl-tree .monaco-tl-twistie { user-select: text; -webkit-user-select: text; +} + +.monaco-workbench .repl .repl-tree .monaco-tl-contents { white-space: pre; } @@ -44,7 +48,7 @@ } .monaco-workbench .repl .repl-tree .monaco-tl-twistie.collapsible + .monaco-tl-contents, -.monaco-workbench .repl .repl-tree .monaco-tl-twistie { +.monaco-workbench .repl .repl-tree .monaco-tl-twistie.collapsible { cursor: pointer; } @@ -69,6 +73,7 @@ .monaco-workbench .repl .repl-tree .monaco-tl-contents .arrow { position:absolute; left: 2px; + pointer-events: none; } .monaco-workbench .repl .repl-tree .output.expression.value-and-source .source, diff --git a/src/vs/workbench/contrib/scm/browser/scmInput.ts b/src/vs/workbench/contrib/scm/browser/scmInput.ts new file mode 100644 index 0000000000000..32c7bdd4f5c44 --- /dev/null +++ b/src/vs/workbench/contrib/scm/browser/scmInput.ts @@ -0,0 +1,870 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import './media/scm.css'; +import { Event, Emitter } from '../../../../base/common/event.js'; +import { Disposable, DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js'; +import { append, $, Dimension, trackFocus } from '../../../../base/browser/dom.js'; +import { InputValidationType, ISCMInput, IInputValidation, ISCMViewService, SCMInputChangeReason, ISCMInputValueProviderContext } from '../common/scm.js'; +import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; +import { IContextViewService, IContextMenuService, IOpenContextView } from '../../../../platform/contextview/browser/contextView.js'; +import { IContextKeyService, IContextKey, ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; +import { MenuItemAction, IMenuService, registerAction2, MenuId, Action2 } from '../../../../platform/actions/common/actions.js'; +import { IAction, ActionRunner, Action } from '../../../../base/common/actions.js'; +import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js'; +import { IConfigurationService, ConfigurationTarget } from '../../../../platform/configuration/common/configuration.js'; +import { ThrottledDelayer } from '../../../../base/common/async.js'; +import { localize } from '../../../../nls.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; +import { CodeEditorWidget, ICodeEditorWidgetOptions } from '../../../../editor/browser/widget/codeEditor/codeEditorWidget.js'; +import { IEditorConstructionOptions } from '../../../../editor/browser/config/editorConfiguration.js'; +import { getSimpleEditorOptions, setupSimpleEditorSelectionStyling } from '../../codeEditor/browser/simpleEditorOptions.js'; +import { IModelService } from '../../../../editor/common/services/model.js'; +import { EditorExtensionsRegistry } from '../../../../editor/browser/editorExtensions.js'; +import { MenuPreventer } from '../../codeEditor/browser/menuPreventer.js'; +import { SelectionClipboardContributionID } from '../../codeEditor/browser/selectionClipboard.js'; +import { EditorDictation } from '../../codeEditor/browser/dictation/editorDictation.js'; +import { ContextMenuController } from '../../../../editor/contrib/contextmenu/browser/contextmenu.js'; +import * as platform from '../../../../base/common/platform.js'; +import { format } from '../../../../base/common/strings.js'; +import { SuggestController } from '../../../../editor/contrib/suggest/browser/suggestController.js'; +import { SnippetController2 } from '../../../../editor/contrib/snippet/browser/snippetController2.js'; +import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js'; +import { ColorDetector } from '../../../../editor/contrib/colorPicker/browser/colorDetector.js'; +import { LinkDetector } from '../../../../editor/contrib/links/browser/links.js'; +import { IOpenerService } from '../../../../platform/opener/common/opener.js'; +import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; +import { KeyCode } from '../../../../base/common/keyCodes.js'; +import { DEFAULT_FONT_FAMILY } from '../../../../base/browser/fonts.js'; +import { Codicon } from '../../../../base/common/codicons.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; +import { AnchorAlignment } from '../../../../base/browser/ui/contextview/contextview.js'; +import { Selection } from '../../../../editor/common/core/selection.js'; +import { createActionViewItem, getFlatActionBarActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; +import { IMarkdownRendererService, openLinkFromMarkdown } from '../../../../platform/markdown/browser/markdownRenderer.js'; +import { DragAndDropController } from '../../../../editor/contrib/dnd/browser/dnd.js'; +import { CopyPasteController } from '../../../../editor/contrib/dropOrPasteInto/browser/copyPasteController.js'; +import { DropIntoEditorController } from '../../../../editor/contrib/dropOrPasteInto/browser/dropIntoEditorController.js'; +import { MessageController } from '../../../../editor/contrib/message/browser/messageController.js'; +import { InlineCompletionsController } from '../../../../editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.js'; +import { CodeActionController } from '../../../../editor/contrib/codeAction/browser/codeActionController.js'; +import { FormatOnType } from '../../../../editor/contrib/format/browser/formatActions.js'; +import { EditorOption, EditorOptions, IEditorOptions } from '../../../../editor/common/config/editorOptions.js'; +import { EditOperation } from '../../../../editor/common/core/editOperation.js'; +import { HiddenItemStrategy, IMenuWorkbenchToolBarOptions, WorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; +import { CancellationTokenSource } from '../../../../base/common/cancellation.js'; +import { DropdownWithPrimaryActionViewItem } from '../../../../platform/actions/browser/dropdownWithPrimaryActionViewItem.js'; +import { clamp } from '../../../../base/common/numbers.js'; +import { ContentHoverController } from '../../../../editor/contrib/hover/browser/contentHoverController.js'; +import { GlyphHoverController } from '../../../../editor/contrib/hover/browser/glyphHoverController.js'; +import { ITextModel } from '../../../../editor/common/model.js'; +import { autorun, runOnChange } from '../../../../base/common/observable.js'; +import { PlaceholderTextContribution } from '../../../../editor/contrib/placeholderText/browser/placeholderTextContribution.js'; +import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; +import { AccessibilityVerbositySettingId } from '../../accessibility/browser/accessibilityConfiguration.js'; +import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; +import { AccessibilityCommandId } from '../../accessibility/common/accessibilityCommands.js'; +import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js'; +import product from '../../../../platform/product/common/product.js'; +import { CHAT_SETUP_SUPPORT_ANONYMOUS_ACTION_ID } from '../../chat/browser/actions/chatActions.js'; +import { ContextKeys } from './scmViewPane.js'; + +const enum SCMInputWidgetCommandId { + CancelAction = 'scm.input.cancelAction', + SetupAction = 'scm.input.triggerSetup' +} + +const enum SCMInputWidgetStorageKey { + LastActionId = 'scm.input.lastActionId' +} + +class SCMInputWidgetActionRunner extends ActionRunner { + + private readonly _runningActions = new Set(); + public get runningActions(): Set { return this._runningActions; } + + private _cts: CancellationTokenSource | undefined; + + constructor( + private readonly input: ISCMInput, + @IStorageService private readonly storageService: IStorageService + ) { + super(); + } + + protected override async runAction(action: IAction): Promise { + try { + // Cancel previous action + if (this.runningActions.size !== 0) { + this._cts?.cancel(); + + if (action.id === SCMInputWidgetCommandId.CancelAction) { + return; + } + } + + // Create action context + const context: ISCMInputValueProviderContext[] = []; + for (const group of this.input.repository.provider.groups) { + context.push({ + resourceGroupId: group.id, + resources: [...group.resources.map(r => r.sourceUri)] + }); + } + + // Run action + this._runningActions.add(action); + this._cts = new CancellationTokenSource(); + await action.run(...[this.input.repository.provider.rootUri, context, this._cts.token]); + } finally { + this._runningActions.delete(action); + + // Save last action + if (this._runningActions.size === 0) { + const actionId = action.id === SCMInputWidgetCommandId.SetupAction + ? product.defaultChatAgent?.generateCommitMessageCommand ?? action.id + : action.id; + this.storageService.store(SCMInputWidgetStorageKey.LastActionId, actionId, StorageScope.PROFILE, StorageTarget.USER); + } + } + } + +} + +class SCMInputWidgetToolbar extends WorkbenchToolBar { + + private _dropdownActions: IAction[] = []; + get dropdownActions(): IAction[] { return this._dropdownActions; } + + private _dropdownAction: IAction; + get dropdownAction(): IAction { return this._dropdownAction; } + + private _cancelAction: IAction; + + private _onDidChange = this._register(new Emitter()); + readonly onDidChange: Event = this._onDidChange.event; + + private readonly _disposables = this._register(new MutableDisposable()); + + constructor( + container: HTMLElement, + options: IMenuWorkbenchToolBarOptions | undefined, + @IMenuService private readonly menuService: IMenuService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IContextMenuService contextMenuService: IContextMenuService, + @ICommandService commandService: ICommandService, + @IKeybindingService keybindingService: IKeybindingService, + @IStorageService private readonly storageService: IStorageService, + @ITelemetryService telemetryService: ITelemetryService, + ) { + super(container, options, menuService, contextKeyService, contextMenuService, keybindingService, commandService, telemetryService); + + this._dropdownAction = new Action( + 'scmInputMoreActions', + localize('scmInputMoreActions', "More Actions..."), + 'codicon-chevron-down'); + + this._cancelAction = new MenuItemAction({ + id: SCMInputWidgetCommandId.CancelAction, + title: localize('scmInputCancelAction', "Cancel"), + icon: Codicon.stopCircle, + }, undefined, undefined, undefined, undefined, contextKeyService, commandService); + } + + public setInput(input: ISCMInput): void { + this._disposables.value = new DisposableStore(); + + const contextKeyService = this.contextKeyService.createOverlay([ + ['scmProvider', input.repository.provider.providerId], + ['scmProviderRootUri', input.repository.provider.rootUri?.toString()], + ['scmProviderHasRootUri', !!input.repository.provider.rootUri] + ]); + + const menu = this._disposables.value.add(this.menuService.createMenu(MenuId.SCMInputBox, contextKeyService, { emitEventsForSubmenuChanges: true })); + + const isEnabled = (): boolean => { + return input.repository.provider.groups.some(g => g.resources.length > 0); + }; + + const updateToolbar = () => { + const actions = getFlatActionBarActions(menu.getActions({ shouldForwardArgs: true })); + + for (const action of actions) { + action.enabled = isEnabled(); + } + this._dropdownAction.enabled = isEnabled(); + + let primaryAction: IAction | undefined = undefined; + + if ((this.actionRunner as SCMInputWidgetActionRunner).runningActions.size !== 0) { + primaryAction = this._cancelAction; + } else if (actions.length === 1) { + primaryAction = actions[0]; + } else if (actions.length > 1) { + const lastActionId = this.storageService.get(SCMInputWidgetStorageKey.LastActionId, StorageScope.PROFILE, ''); + primaryAction = actions.find(a => a.id === lastActionId) ?? actions[0]; + } + + this._dropdownActions = actions.length === 1 ? [] : actions; + super.setActions(primaryAction ? [primaryAction] : [], []); + + this._onDidChange.fire(); + }; + + this._disposables.value.add(menu.onDidChange(() => updateToolbar())); + this._disposables.value.add(input.repository.provider.onDidChangeResources(() => updateToolbar())); + this._disposables.value.add(this.storageService.onDidChangeValue(StorageScope.PROFILE, SCMInputWidgetStorageKey.LastActionId, this._disposables.value)(() => updateToolbar())); + + this.actionRunner = this._disposables.value.add(new SCMInputWidgetActionRunner(input, this.storageService)); + this._disposables.value.add(this.actionRunner.onWillRun(e => { + if ((this.actionRunner as SCMInputWidgetActionRunner).runningActions.size === 0) { + super.setActions([this._cancelAction], []); + this._onDidChange.fire(); + } + })); + this._disposables.value.add(this.actionRunner.onDidRun(e => { + if ((this.actionRunner as SCMInputWidgetActionRunner).runningActions.size === 0) { + updateToolbar(); + } + })); + + updateToolbar(); + } +} + +class SCMInputWidgetEditorOptions { + + private readonly _onDidChange = new Emitter(); + readonly onDidChange = this._onDidChange.event; + + private readonly defaultInputFontFamily = DEFAULT_FONT_FAMILY; + + private readonly _disposables = new DisposableStore(); + + constructor( + private readonly overflowWidgetsDomNode: HTMLElement, + private readonly configurationService: IConfigurationService) { + + const onDidChangeConfiguration = Event.filter( + this.configurationService.onDidChangeConfiguration, + e => { + return e.affectsConfiguration('editor.accessibilitySupport') || + e.affectsConfiguration('editor.cursorBlinking') || + e.affectsConfiguration('editor.cursorStyle') || + e.affectsConfiguration('editor.cursorWidth') || + e.affectsConfiguration('editor.emptySelectionClipboard') || + e.affectsConfiguration('editor.fontFamily') || + e.affectsConfiguration('editor.rulers') || + e.affectsConfiguration('editor.wordWrap') || + e.affectsConfiguration('editor.wordSegmenterLocales') || + e.affectsConfiguration('scm.inputFontFamily') || + e.affectsConfiguration('scm.inputFontSize'); + }, + this._disposables + ); + + this._disposables.add(onDidChangeConfiguration(() => this._onDidChange.fire())); + } + + getEditorConstructionOptions(): IEditorConstructionOptions { + return { + ...getSimpleEditorOptions(this.configurationService), + ...this.getEditorOptions(), + dragAndDrop: true, + dropIntoEditor: { enabled: true }, + formatOnType: true, + lineDecorationsWidth: 6, + overflowWidgetsDomNode: this.overflowWidgetsDomNode, + padding: { top: 2, bottom: 2 }, + quickSuggestions: false, + renderWhitespace: 'none', + scrollbar: { + alwaysConsumeMouseWheel: false, + vertical: 'hidden' + }, + wrappingIndent: 'none', + wrappingStrategy: 'advanced', + }; + } + + getEditorOptions(): IEditorOptions { + const fontFamily = this._getEditorFontFamily(); + const fontSize = this._getEditorFontSize(); + const lineHeight = this._getEditorLineHeight(fontSize); + const wordSegmenterLocales = this.configurationService.getValue('editor.wordSegmenterLocales'); + const accessibilitySupport = this.configurationService.getValue<'auto' | 'off' | 'on'>('editor.accessibilitySupport'); + const cursorBlinking = this.configurationService.getValue<'blink' | 'smooth' | 'phase' | 'expand' | 'solid'>('editor.cursorBlinking'); + const cursorStyle = this.configurationService.getValue('editor.cursorStyle'); + const cursorWidth = this.configurationService.getValue('editor.cursorWidth') ?? 1; + const emptySelectionClipboard = this.configurationService.getValue('editor.emptySelectionClipboard') === true; + + return { ...this._getEditorLanguageConfiguration(), accessibilitySupport, cursorBlinking, cursorStyle, cursorWidth, fontFamily, fontSize, lineHeight, emptySelectionClipboard, wordSegmenterLocales }; + } + + private _getEditorFontFamily(): string { + const inputFontFamily = this.configurationService.getValue('scm.inputFontFamily').trim(); + + if (inputFontFamily.toLowerCase() === 'editor') { + return this.configurationService.getValue('editor.fontFamily').trim(); + } + + if (inputFontFamily.length !== 0 && inputFontFamily.toLowerCase() !== 'default') { + return inputFontFamily; + } + + return this.defaultInputFontFamily; + } + + private _getEditorFontSize(): number { + return this.configurationService.getValue('scm.inputFontSize'); + } + + private _getEditorLanguageConfiguration(): IEditorOptions { + // editor.rulers + const rulersConfig = this.configurationService.inspect('editor.rulers', { overrideIdentifier: 'scminput' }); + const rulers = rulersConfig.overrideIdentifiers?.includes('scminput') ? EditorOptions.rulers.validate(rulersConfig.value) : []; + + // editor.wordWrap + const wordWrapConfig = this.configurationService.inspect('editor.wordWrap', { overrideIdentifier: 'scminput' }); + const wordWrap = wordWrapConfig.overrideIdentifiers?.includes('scminput') ? EditorOptions.wordWrap.validate(wordWrapConfig.value) : 'on'; + + return { rulers, wordWrap }; + } + + private _getEditorLineHeight(fontSize: number): number { + return Math.round(fontSize * 1.5); + } + + dispose(): void { + this._disposables.dispose(); + this._onDidChange.dispose(); + } + +} + +export class SCMInputWidget { + + private static readonly ValidationTimeouts: { [severity: number]: number } = { + [InputValidationType.Information]: 5000, + [InputValidationType.Warning]: 8000, + [InputValidationType.Error]: 10000 + }; + + private readonly contextKeyService: IContextKeyService; + + private element: HTMLElement; + private editorContainer: HTMLElement; + private readonly inputEditor: CodeEditorWidget; + private readonly inputEditorOptions: SCMInputWidgetEditorOptions; + private toolbarContainer: HTMLElement; + private toolbar: SCMInputWidgetToolbar; + private readonly disposables = new DisposableStore(); + + private model: { readonly input: ISCMInput; readonly textModel: ITextModel } | undefined; + private repositoryIdContextKey: IContextKey; + private validationMessageContextKey: IContextKey; + private readonly repositoryDisposables = new DisposableStore(); + + private validation: IInputValidation | undefined; + private validationContextView: IOpenContextView | undefined; + private validationHasFocus: boolean = false; + private _validationTimer: Timeout | undefined; + + // This is due to "Setup height change listener on next tick" above + // https://github.com/microsoft/vscode/issues/108067 + private lastLayoutWasTrash = false; + private shouldFocusAfterLayout = false; + + readonly onDidChangeContentHeight: Event; + + get input(): ISCMInput | undefined { + return this.model?.input; + } + + set input(input: ISCMInput | undefined) { + if (input === this.input) { + return; + } + + this.clearValidation(); + this.element.classList.remove('synthetic-focus'); + + this.repositoryDisposables.clear(); + this.repositoryIdContextKey.set(input?.repository.id); + + if (!input) { + this.inputEditor.setModel(undefined); + this.model = undefined; + return; + } + + 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 () => { + const position = this.inputEditor.getSelection()?.getStartPosition(); + const offset = position && textModel.getOffsetAt(position); + const value = textModel.getValue(); + + this.setValidation(await input.validateInput(value, offset || 0)); + }; + + const triggerValidation = () => validationDelayer.trigger(validate); + this.repositoryDisposables.add(validationDelayer); + this.repositoryDisposables.add(this.inputEditor.onDidChangeCursorPosition(triggerValidation)); + + // Adaptive indentation rules + const opts = this.modelService.getCreationOptions(textModel.getLanguageId(), textModel.uri, textModel.isForSimpleWidget); + const onEnter = Event.filter(this.inputEditor.onKeyDown, e => e.keyCode === KeyCode.Enter, this.repositoryDisposables); + this.repositoryDisposables.add(onEnter(() => textModel.detectIndentation(opts.insertSpaces, opts.tabSize))); + + // Keep model in sync with API + textModel.setValue(input.value); + this.repositoryDisposables.add(input.onDidChange(({ value, reason }) => { + const currentValue = textModel.getValue(); + if (value === currentValue) { // circuit breaker + return; + } + + textModel.pushStackElement(); + textModel.pushEditOperations(null, [EditOperation.replaceMove(textModel.getFullModelRange(), value)], () => []); + + const position = reason === SCMInputChangeReason.HistoryPrevious + ? textModel.getFullModelRange().getStartPosition() + : textModel.getFullModelRange().getEndPosition(); + this.inputEditor.setPosition(position); + this.inputEditor.revealPositionInCenterIfOutsideViewport(position); + })); + this.repositoryDisposables.add(input.onDidChangeFocus(() => this.focus())); + this.repositoryDisposables.add(input.onDidChangeValidationMessage((e) => this.setValidation(e, { focus: true, timeout: true }))); + this.repositoryDisposables.add(input.onDidChangeValidateInput((e) => triggerValidation())); + this.repositoryDisposables.add(input.onDidClearValidation(() => this.clearValidation())); + + // Keep API in sync with model and validate + this.repositoryDisposables.add(textModel.onDidChangeContent(() => { + input.setValue(textModel.getValue(), true); + triggerValidation(); + })); + + // Aria label & placeholder text + const accessibilityVerbosityConfig = observableConfigValue( + AccessibilityVerbositySettingId.SourceControl, true, this.configurationService); + + const getAriaLabel = (placeholder: string, verbosity?: boolean) => { + verbosity = verbosity ?? accessibilityVerbosityConfig.get(); + + if (!verbosity || !this.accessibilityService.isScreenReaderOptimized()) { + return placeholder; + } + + const kbLabel = this.keybindingService.lookupKeybinding(AccessibilityCommandId.OpenAccessibilityHelp)?.getLabel(); + return kbLabel + ? localize('scmInput.accessibilityHelp', "{0}, Use {1} to open Source Control Accessibility Help.", placeholder, kbLabel) + : localize('scmInput.accessibilityHelpNoKb', "{0}, Run the Open Accessibility Help command for more information.", placeholder); + }; + + const getPlaceholderText = (): string => { + const binding = this.keybindingService.lookupKeybinding('scm.acceptInput'); + const label = binding ? binding.getLabel() : (platform.isMacintosh ? 'Cmd+Enter' : 'Ctrl+Enter'); + return format(input.placeholder, label); + }; + + const updatePlaceholderText = () => { + const placeholder = getPlaceholderText(); + const ariaLabel = getAriaLabel(placeholder); + + this.inputEditor.updateOptions({ ariaLabel, placeholder }); + }; + + this.repositoryDisposables.add(input.onDidChangePlaceholder(updatePlaceholderText)); + this.repositoryDisposables.add(this.keybindingService.onDidUpdateKeybindings(updatePlaceholderText)); + + this.repositoryDisposables.add(runOnChange(accessibilityVerbosityConfig, verbosity => { + const placeholder = getPlaceholderText(); + const ariaLabel = getAriaLabel(placeholder, verbosity); + + this.inputEditor.updateOptions({ ariaLabel }); + })); + + updatePlaceholderText(); + + // Update input template + let commitTemplate = ''; + this.repositoryDisposables.add(autorun(reader => { + if (!input.visible) { + return; + } + + const oldCommitTemplate = commitTemplate; + commitTemplate = input.repository.provider.commitTemplate.read(reader); + + const value = textModel.getValue(); + if (value && value !== oldCommitTemplate) { + return; + } + + textModel.setValue(commitTemplate); + })); + + // Update input enablement + const updateEnablement = (enabled: boolean) => { + this.inputEditor.updateOptions({ readOnly: !enabled }); + }; + this.repositoryDisposables.add(input.onDidChangeEnablement(enabled => updateEnablement(enabled))); + updateEnablement(input.enabled); + + // Toolbar + this.toolbar.setInput(input); + + // Save model + this.model = { input, textModel }; + } + + get selections(): Selection[] | null { + return this.inputEditor.getSelections(); + } + + set selections(selections: Selection[] | null) { + if (selections) { + this.inputEditor.setSelections(selections); + } + } + + private setValidation(validation: IInputValidation | undefined, options?: { focus?: boolean; timeout?: boolean }) { + if (this._validationTimer) { + clearTimeout(this._validationTimer); + this._validationTimer = undefined; + } + + this.validation = validation; + this.renderValidation(); + + if (options?.focus && !this.hasFocus()) { + this.focus(); + } + + if (validation && options?.timeout) { + this._validationTimer = setTimeout(() => this.setValidation(undefined), SCMInputWidget.ValidationTimeouts[validation.type]); + } + } + + constructor( + container: HTMLElement, + overflowWidgetsDomNode: HTMLElement, + @IContextKeyService contextKeyService: IContextKeyService, + @IInstantiationService instantiationService: IInstantiationService, + @IModelService private modelService: IModelService, + @IKeybindingService private keybindingService: IKeybindingService, + @IConfigurationService private configurationService: IConfigurationService, + @ISCMViewService private readonly scmViewService: ISCMViewService, + @IContextViewService private readonly contextViewService: IContextViewService, + @IOpenerService private readonly openerService: IOpenerService, + @IAccessibilityService private readonly accessibilityService: IAccessibilityService, + @IMarkdownRendererService private readonly markdownRendererService: IMarkdownRendererService, + ) { + this.element = append(container, $('.scm-editor')); + this.editorContainer = append(this.element, $('.scm-editor-container')); + this.toolbarContainer = append(this.element, $('.scm-editor-toolbar')); + + this.contextKeyService = this.disposables.add(contextKeyService.createScoped(this.element)); + this.repositoryIdContextKey = this.contextKeyService.createKey('scmRepository', undefined); + this.validationMessageContextKey = ContextKeys.SCMInputHasValidationMessage.bindTo(this.contextKeyService); + + this.inputEditorOptions = new SCMInputWidgetEditorOptions(overflowWidgetsDomNode, this.configurationService); + this.disposables.add(this.inputEditorOptions.onDidChange(this.onDidChangeEditorOptions, this)); + this.disposables.add(this.inputEditorOptions); + + const codeEditorWidgetOptions: ICodeEditorWidgetOptions = { + contributions: EditorExtensionsRegistry.getSomeEditorContributions([ + CodeActionController.ID, + ColorDetector.ID, + ContextMenuController.ID, + CopyPasteController.ID, + DragAndDropController.ID, + DropIntoEditorController.ID, + EditorDictation.ID, + FormatOnType.ID, + ContentHoverController.ID, + GlyphHoverController.ID, + InlineCompletionsController.ID, + LinkDetector.ID, + MenuPreventer.ID, + MessageController.ID, + PlaceholderTextContribution.ID, + SelectionClipboardContributionID, + SnippetController2.ID, + SuggestController.ID + ]), + isSimpleWidget: true + }; + + const services = new ServiceCollection([IContextKeyService, this.contextKeyService]); + const instantiationService2 = instantiationService.createChild(services, this.disposables); + const editorConstructionOptions = this.inputEditorOptions.getEditorConstructionOptions(); + this.inputEditor = instantiationService2.createInstance(CodeEditorWidget, this.editorContainer, editorConstructionOptions, codeEditorWidgetOptions); + this.disposables.add(this.inputEditor); + + this.disposables.add(this.inputEditor.onDidFocusEditorText(() => { + if (this.input?.repository) { + this.scmViewService.focus(this.input.repository); + } + + this.element.classList.add('synthetic-focus'); + this.renderValidation(); + })); + this.disposables.add(this.inputEditor.onDidBlurEditorText(() => { + this.element.classList.remove('synthetic-focus'); + + setTimeout(() => { + if (!this.validation || !this.validationHasFocus) { + this.clearValidation(); + } + }, 0); + })); + + this.disposables.add(this.inputEditor.onDidBlurEditorWidget(() => { + CopyPasteController.get(this.inputEditor)?.clearWidgets(); + DropIntoEditorController.get(this.inputEditor)?.clearWidgets(); + })); + + const firstLineKey = this.contextKeyService.createKey('scmInputIsInFirstPosition', false); + const lastLineKey = this.contextKeyService.createKey('scmInputIsInLastPosition', false); + + this.disposables.add(this.inputEditor.onDidChangeCursorPosition(({ position }) => { + const viewModel = this.inputEditor._getViewModel()!; + const lastLineNumber = viewModel.getLineCount(); + const lastLineCol = viewModel.getLineLength(lastLineNumber) + 1; + const viewPosition = viewModel.coordinatesConverter.convertModelPositionToViewPosition(position); + firstLineKey.set(viewPosition.lineNumber === 1 && viewPosition.column === 1); + lastLineKey.set(viewPosition.lineNumber === lastLineNumber && viewPosition.column === lastLineCol); + })); + this.disposables.add(this.inputEditor.onDidScrollChange(e => { + this.toolbarContainer.classList.toggle('scroll-decoration', e.scrollTop > 0); + })); + + Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.showInputActionButton'))(() => this.layout(), this, this.disposables); + + this.onDidChangeContentHeight = Event.signal(Event.filter(this.inputEditor.onDidContentSizeChange, e => e.contentHeightChanged, this.disposables)); + + // Toolbar + this.toolbar = instantiationService2.createInstance(SCMInputWidgetToolbar, this.toolbarContainer, { + actionViewItemProvider: (action, options) => { + if (action instanceof MenuItemAction && this.toolbar.dropdownActions.length > 1) { + return instantiationService.createInstance(DropdownWithPrimaryActionViewItem, action, this.toolbar.dropdownAction, this.toolbar.dropdownActions, '', { actionRunner: this.toolbar.actionRunner, hoverDelegate: options.hoverDelegate }); + } + + return createActionViewItem(instantiationService, action, options); + }, + hiddenItemStrategy: HiddenItemStrategy.NoHide, + menuOptions: { + shouldForwardArgs: true + } + }); + this.disposables.add(this.toolbar.onDidChange(() => this.layout())); + this.disposables.add(this.toolbar); + } + + getContentHeight(): number { + const lineHeight = this.inputEditor.getOption(EditorOption.lineHeight); + const { top, bottom } = this.inputEditor.getOption(EditorOption.padding); + + const inputMinLinesConfig = this.configurationService.getValue('scm.inputMinLineCount'); + const inputMinLines = typeof inputMinLinesConfig === 'number' ? clamp(inputMinLinesConfig, 1, 50) : 1; + const editorMinHeight = inputMinLines * lineHeight + top + bottom; + + const inputMaxLinesConfig = this.configurationService.getValue('scm.inputMaxLineCount'); + const inputMaxLines = typeof inputMaxLinesConfig === 'number' ? clamp(inputMaxLinesConfig, 1, 50) : 10; + const editorMaxHeight = inputMaxLines * lineHeight + top + bottom; + + return clamp(this.inputEditor.getContentHeight(), editorMinHeight, editorMaxHeight); + } + + layout(): void { + const editorHeight = this.getContentHeight(); + const toolbarWidth = this.getToolbarWidth(); + const dimension = new Dimension(this.element.clientWidth - toolbarWidth, editorHeight); + + if (dimension.width < 0) { + this.lastLayoutWasTrash = true; + return; + } + + this.lastLayoutWasTrash = false; + this.inputEditor.layout(dimension); + this.renderValidation(); + + const showInputActionButton = this.configurationService.getValue('scm.showInputActionButton') === true; + this.toolbarContainer.classList.toggle('hidden', !showInputActionButton || this.toolbar?.isEmpty() === true); + + if (this.shouldFocusAfterLayout) { + this.shouldFocusAfterLayout = false; + this.focus(); + } + } + + focus(): void { + if (this.lastLayoutWasTrash) { + this.lastLayoutWasTrash = false; + this.shouldFocusAfterLayout = true; + return; + } + + this.inputEditor.focus(); + this.element.classList.add('synthetic-focus'); + } + + hasFocus(): boolean { + return this.inputEditor.hasTextFocus(); + } + + private onDidChangeEditorOptions(): void { + this.inputEditor.updateOptions(this.inputEditorOptions.getEditorOptions()); + } + + private renderValidation(): void { + this.clearValidation(); + + this.element.classList.toggle('validation-info', this.validation?.type === InputValidationType.Information); + this.element.classList.toggle('validation-warning', this.validation?.type === InputValidationType.Warning); + this.element.classList.toggle('validation-error', this.validation?.type === InputValidationType.Error); + + if (!this.validation || !this.inputEditor.hasTextFocus()) { + return; + } + + this.validationMessageContextKey.set(true); + const disposables = new DisposableStore(); + + this.validationContextView = this.contextViewService.showContextView({ + getAnchor: () => this.element, + render: container => { + this.element.style.borderBottomLeftRadius = '0'; + this.element.style.borderBottomRightRadius = '0'; + + const validationContainer = append(container, $('.scm-editor-validation-container')); + validationContainer.classList.toggle('validation-info', this.validation!.type === InputValidationType.Information); + validationContainer.classList.toggle('validation-warning', this.validation!.type === InputValidationType.Warning); + validationContainer.classList.toggle('validation-error', this.validation!.type === InputValidationType.Error); + validationContainer.style.width = `${this.element.clientWidth + 2}px`; + const element = append(validationContainer, $('.scm-editor-validation')); + + const message = this.validation!.message; + if (typeof message === 'string') { + element.textContent = message; + } else { + const tracker = trackFocus(element); + disposables.add(tracker); + disposables.add(tracker.onDidFocus(() => (this.validationHasFocus = true))); + disposables.add(tracker.onDidBlur(() => { + this.validationHasFocus = false; + this.element.style.borderBottomLeftRadius = '2px'; + this.element.style.borderBottomRightRadius = '2px'; + this.contextViewService.hideContextView(); + })); + + const renderedMarkdown = this.markdownRendererService.render(message, { + actionHandler: (link, mdStr) => { + openLinkFromMarkdown(this.openerService, link, mdStr.isTrusted); + this.element.style.borderBottomLeftRadius = '2px'; + this.element.style.borderBottomRightRadius = '2px'; + this.contextViewService.hideContextView(); + }, + }); + disposables.add(renderedMarkdown); + element.appendChild(renderedMarkdown.element); + } + const actionsContainer = append(validationContainer, $('.scm-editor-validation-actions')); + const actionbar = new ActionBar(actionsContainer); + const action = new Action('scmInputWidget.validationMessage.close', localize('label.close', "Close"), ThemeIcon.asClassName(Codicon.close), true, () => { + this.contextViewService.hideContextView(); + this.element.style.borderBottomLeftRadius = '2px'; + this.element.style.borderBottomRightRadius = '2px'; + }); + disposables.add(actionbar); + actionbar.push(action, { icon: true, label: false }); + + return Disposable.None; + }, + onHide: () => { + this.validationHasFocus = false; + this.element.style.borderBottomLeftRadius = '2px'; + this.element.style.borderBottomRightRadius = '2px'; + disposables.dispose(); + }, + anchorAlignment: AnchorAlignment.LEFT + }); + } + + private getToolbarWidth(): number { + const showInputActionButton = this.configurationService.getValue('scm.showInputActionButton'); + if (!this.toolbar || !showInputActionButton || this.toolbar?.isEmpty() === true) { + return 0; + } + + return this.toolbar.dropdownActions.length === 0 ? + 26 /* 22px action + 4px margin */ : + 39 /* 35px action + 4px margin */; + } + + clearValidation(): void { + this.validationContextView?.close(); + this.validationContextView = undefined; + this.validationHasFocus = false; + this.validationMessageContextKey.set(false); + } + + dispose(): void { + this.input = undefined; + this.repositoryDisposables.dispose(); + this.clearValidation(); + this.disposables.dispose(); + } +} + +registerAction2(class extends Action2 { + constructor() { + super({ + id: SCMInputWidgetCommandId.SetupAction, + title: localize('scmInputGenerateCommitMessage', "Generate Commit Message"), + icon: Codicon.sparkle, + f1: false, + menu: { + id: MenuId.SCMInputBox, + when: ContextKeyExpr.and( + ChatContextKeys.Setup.hidden.negate(), + ChatContextKeys.Setup.disabled.negate(), + ChatContextKeys.Setup.installed.negate(), + ContextKeyExpr.equals('scmProvider', 'git') + ) + } + }); + } + + override async run(accessor: ServicesAccessor, ...args: unknown[]): Promise { + const commandService = accessor.get(ICommandService); + + const result = await commandService.executeCommand(CHAT_SETUP_SUPPORT_ANONYMOUS_ACTION_ID); + if (!result) { + return; + } + + const command = product.defaultChatAgent?.generateCommitMessageCommand; + if (!command) { + return; + } + + await commandService.executeCommand(command, ...args); + } +}); + +setupSimpleEditorSelectionStyling('.scm-view .scm-editor-container'); diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index dd3439f21f933..2f54b335bad75 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -8,26 +8,26 @@ import { Event, Emitter } from '../../../../base/common/event.js'; import { basename, dirname } from '../../../../base/common/resources.js'; import { IDisposable, Disposable, DisposableStore, combinedDisposable, dispose, toDisposable, MutableDisposable, DisposableMap } from '../../../../base/common/lifecycle.js'; import { ViewPane, IViewPaneOptions, ViewAction } from '../../../browser/parts/views/viewPane.js'; -import { append, $, Dimension, trackFocus, clearNode, isPointerEvent, isActiveElement } from '../../../../base/browser/dom.js'; +import { append, $, clearNode, isPointerEvent, isActiveElement } from '../../../../base/browser/dom.js'; import { asCSSUrl } from '../../../../base/browser/cssValue.js'; import { IListVirtualDelegate, IIdentityProvider } from '../../../../base/browser/ui/list/list.js'; -import { ISCMResourceGroup, ISCMResource, InputValidationType, ISCMRepository, ISCMInput, IInputValidation, ISCMViewService, ISCMViewVisibleRepositoryChangeEvent, ISCMService, SCMInputChangeReason, VIEW_PANE_ID, ISCMActionButton, ISCMActionButtonDescriptor, ISCMRepositorySortKey, ISCMInputValueProviderContext, ViewMode, ISCMRepositorySelectionMode } from '../common/scm.js'; +import { ISCMResourceGroup, ISCMResource, ISCMRepository, ISCMInput, ISCMViewService, ISCMViewVisibleRepositoryChangeEvent, ISCMService, VIEW_PANE_ID, ISCMActionButton, ISCMActionButtonDescriptor, ISCMRepositorySortKey, ViewMode, ISCMRepositorySelectionMode } from '../common/scm.js'; import { ResourceLabels, IResourceLabel, IFileLabelOptions } from '../../../browser/labels.js'; import { CountBadge } from '../../../../base/browser/ui/countBadge/countBadge.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; -import { IContextViewService, IContextMenuService, IOpenContextView } from '../../../../platform/contextview/browser/contextView.js'; +import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; import { IContextKeyService, IContextKey, ContextKeyExpr, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; import { MenuItemAction, IMenuService, registerAction2, MenuId, IAction2Options, MenuRegistry, Action2, IMenu } from '../../../../platform/actions/common/actions.js'; -import { IAction, ActionRunner, Action, Separator, IActionRunner, toAction } from '../../../../base/common/actions.js'; -import { ActionBar, IActionViewItemProvider } from '../../../../base/browser/ui/actionbar/actionbar.js'; +import { IAction, ActionRunner, Separator, IActionRunner, toAction } from '../../../../base/common/actions.js'; +import { IActionViewItemProvider } from '../../../../base/browser/ui/actionbar/actionbar.js'; import { IThemeService, IFileIconTheme } from '../../../../platform/theme/common/themeService.js'; import { isSCMResource, isSCMResourceGroup, isSCMRepository, isSCMInput, collectContextMenuActions, getActionViewItemProvider, isSCMActionButton, isSCMViewService, isSCMResourceNode, connectPrimaryMenu } from './util.js'; import { WorkbenchCompressibleAsyncDataTree, IOpenEvent } from '../../../../platform/list/browser/listService.js'; -import { IConfigurationService, ConfigurationTarget } from '../../../../platform/configuration/common/configuration.js'; -import { disposableTimeout, Sequencer, ThrottledDelayer, Throttler } from '../../../../base/common/async.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { disposableTimeout, Sequencer, Throttler } from '../../../../base/common/async.js'; import { ITreeNode, ITreeFilter, ITreeSorter, ITreeContextMenuEvent, ITreeDragAndDrop, ITreeDragOverReaction, IAsyncDataSource } from '../../../../base/browser/ui/tree/tree.js'; import { ResourceTree, IResourceNode } from '../../../../base/common/resourceTree.js'; import { ICompressibleTreeRenderer, ICompressibleKeyboardNavigationLabelProvider } from '../../../../base/browser/ui/tree/objectTree.js'; @@ -41,76 +41,40 @@ import { IViewDescriptorService } from '../../../common/views.js'; import { localize } from '../../../../nls.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { EditorResourceAccessor, SideBySideEditor } from '../../../common/editor.js'; -import { CodeEditorWidget, ICodeEditorWidgetOptions } from '../../../../editor/browser/widget/codeEditor/codeEditorWidget.js'; -import { IEditorConstructionOptions } from '../../../../editor/browser/config/editorConfiguration.js'; -import { getSimpleEditorOptions, setupSimpleEditorSelectionStyling } from '../../codeEditor/browser/simpleEditorOptions.js'; -import { IModelService } from '../../../../editor/common/services/model.js'; -import { EditorExtensionsRegistry } from '../../../../editor/browser/editorExtensions.js'; -import { MenuPreventer } from '../../codeEditor/browser/menuPreventer.js'; -import { SelectionClipboardContributionID } from '../../codeEditor/browser/selectionClipboard.js'; -import { EditorDictation } from '../../codeEditor/browser/dictation/editorDictation.js'; -import { ContextMenuController } from '../../../../editor/contrib/contextmenu/browser/contextmenu.js'; -import * as platform from '../../../../base/common/platform.js'; -import { compare, format } from '../../../../base/common/strings.js'; -import { SuggestController } from '../../../../editor/contrib/suggest/browser/suggestController.js'; -import { SnippetController2 } from '../../../../editor/contrib/snippet/browser/snippetController2.js'; -import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js'; -import { ColorDetector } from '../../../../editor/contrib/colorPicker/browser/colorDetector.js'; -import { LinkDetector } from '../../../../editor/contrib/links/browser/links.js'; +import { compare } from '../../../../base/common/strings.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { IListAccessibilityProvider } from '../../../../base/browser/ui/list/listWidget.js'; import { ILabelService } from '../../../../platform/label/common/label.js'; -import { KeyCode } from '../../../../base/common/keyCodes.js'; -import { DEFAULT_FONT_FAMILY } from '../../../../base/browser/fonts.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; -import { AnchorAlignment } from '../../../../base/browser/ui/contextview/contextview.js'; import { RepositoryActionRunner, RepositoryRenderer } from './scmRepositoryRenderer.js'; import { isDark } from '../../../../platform/theme/common/theme.js'; import { LabelFuzzyScore } from '../../../../base/browser/ui/tree/abstractTree.js'; import { Selection } from '../../../../editor/common/core/selection.js'; import { API_OPEN_DIFF_EDITOR_COMMAND_ID, API_OPEN_EDITOR_COMMAND_ID } from '../../../browser/parts/editor/editorCommands.js'; -import { createActionViewItem, getFlatActionBarActions, getFlatContextMenuActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; -import { IMarkdownRendererService, openLinkFromMarkdown } from '../../../../platform/markdown/browser/markdownRenderer.js'; +import { getFlatContextMenuActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; import { Button, ButtonWithDescription, ButtonWithDropdown } from '../../../../base/browser/ui/button/button.js'; import { INotificationService } from '../../../../platform/notification/common/notification.js'; import { RepositoryContextKeys } from './scmViewService.js'; -import { DragAndDropController } from '../../../../editor/contrib/dnd/browser/dnd.js'; -import { CopyPasteController } from '../../../../editor/contrib/dropOrPasteInto/browser/copyPasteController.js'; -import { DropIntoEditorController } from '../../../../editor/contrib/dropOrPasteInto/browser/dropIntoEditorController.js'; -import { MessageController } from '../../../../editor/contrib/message/browser/messageController.js'; import { defaultButtonStyles, defaultCountBadgeStyles } from '../../../../platform/theme/browser/defaultStyles.js'; -import { InlineCompletionsController } from '../../../../editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.js'; -import { CodeActionController } from '../../../../editor/contrib/codeAction/browser/codeActionController.js'; import { Schemas } from '../../../../base/common/network.js'; import { IDragAndDropData } from '../../../../base/browser/dnd.js'; import { fillEditorsDragData } from '../../../browser/dnd.js'; import { ElementsDragAndDropData, ListViewTargetSector } from '../../../../base/browser/ui/list/listView.js'; import { CodeDataTransfers } from '../../../../platform/dnd/browser/dnd.js'; -import { FormatOnType } from '../../../../editor/contrib/format/browser/formatActions.js'; -import { EditorOption, EditorOptions, IEditorOptions } from '../../../../editor/common/config/editorOptions.js'; import { IAsyncDataTreeViewState, ITreeCompressionDelegate } from '../../../../base/browser/ui/tree/asyncDataTree.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; -import { EditOperation } from '../../../../editor/common/core/editOperation.js'; -import { HiddenItemStrategy, IMenuWorkbenchToolBarOptions, WorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; -import { CancellationTokenSource } from '../../../../base/common/cancellation.js'; -import { DropdownWithPrimaryActionViewItem } from '../../../../platform/actions/browser/dropdownWithPrimaryActionViewItem.js'; -import { clamp, rot } from '../../../../base/common/numbers.js'; +import { WorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; +import { rot } from '../../../../base/common/numbers.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { OpenScmGroupAction } from '../../multiDiffEditor/browser/scmMultiDiffSourceResolver.js'; -import { ContentHoverController } from '../../../../editor/contrib/hover/browser/contentHoverController.js'; -import { GlyphHoverController } from '../../../../editor/contrib/hover/browser/glyphHoverController.js'; -import { ITextModel } from '../../../../editor/common/model.js'; -import { autorun, runOnChange } from '../../../../base/common/observable.js'; -import { PlaceholderTextContribution } from '../../../../editor/contrib/placeholderText/browser/placeholderTextContribution.js'; +import { autorun } from '../../../../base/common/observable.js'; import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; import { AccessibilityVerbositySettingId } from '../../accessibility/browser/accessibilityConfiguration.js'; import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; import { AccessibilityCommandId } from '../../accessibility/common/accessibilityCommands.js'; -import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js'; -import product from '../../../../platform/product/common/product.js'; -import { CHAT_SETUP_SUPPORT_ANONYMOUS_ACTION_ID } from '../../chat/browser/actions/chatActions.js'; +import { SCMInputWidget } from './scmInput.js'; type TreeElement = ISCMRepository | ISCMInput | ISCMActionButton | ISCMResourceGroup | ISCMResource | IResourceNode; @@ -1394,800 +1358,6 @@ class CollapseAllAction extends ViewAction { registerAction2(CollapseAllAction); -const enum SCMInputWidgetCommandId { - CancelAction = 'scm.input.cancelAction', - SetupAction = 'scm.input.triggerSetup' -} - -const enum SCMInputWidgetStorageKey { - LastActionId = 'scm.input.lastActionId' -} - -registerAction2(class extends Action2 { - constructor() { - super({ - id: SCMInputWidgetCommandId.SetupAction, - title: localize('scmInputGenerateCommitMessage', "Generate Commit Message"), - icon: Codicon.sparkle, - f1: false, - menu: { - id: MenuId.SCMInputBox, - when: ContextKeyExpr.and( - ChatContextKeys.Setup.hidden.negate(), - ChatContextKeys.Setup.disabled.negate(), - ChatContextKeys.Setup.installed.negate(), - ContextKeyExpr.equals('scmProvider', 'git') - ) - } - }); - } - - override async run(accessor: ServicesAccessor, ...args: unknown[]): Promise { - const commandService = accessor.get(ICommandService); - - const result = await commandService.executeCommand(CHAT_SETUP_SUPPORT_ANONYMOUS_ACTION_ID); - if (!result) { - return; - } - - const command = product.defaultChatAgent?.generateCommitMessageCommand; - if (!command) { - return; - } - - await commandService.executeCommand(command, ...args); - } -}); - -class SCMInputWidgetActionRunner extends ActionRunner { - - private readonly _runningActions = new Set(); - public get runningActions(): Set { return this._runningActions; } - - private _cts: CancellationTokenSource | undefined; - - constructor( - private readonly input: ISCMInput, - @IStorageService private readonly storageService: IStorageService - ) { - super(); - } - - protected override async runAction(action: IAction): Promise { - try { - // Cancel previous action - if (this.runningActions.size !== 0) { - this._cts?.cancel(); - - if (action.id === SCMInputWidgetCommandId.CancelAction) { - return; - } - } - - // Create action context - const context: ISCMInputValueProviderContext[] = []; - for (const group of this.input.repository.provider.groups) { - context.push({ - resourceGroupId: group.id, - resources: [...group.resources.map(r => r.sourceUri)] - }); - } - - // Run action - this._runningActions.add(action); - this._cts = new CancellationTokenSource(); - await action.run(...[this.input.repository.provider.rootUri, context, this._cts.token]); - } finally { - this._runningActions.delete(action); - - // Save last action - if (this._runningActions.size === 0) { - const actionId = action.id === SCMInputWidgetCommandId.SetupAction - ? product.defaultChatAgent?.generateCommitMessageCommand ?? action.id - : action.id; - this.storageService.store(SCMInputWidgetStorageKey.LastActionId, actionId, StorageScope.PROFILE, StorageTarget.USER); - } - } - } - -} - -class SCMInputWidgetToolbar extends WorkbenchToolBar { - - private _dropdownActions: IAction[] = []; - get dropdownActions(): IAction[] { return this._dropdownActions; } - - private _dropdownAction: IAction; - get dropdownAction(): IAction { return this._dropdownAction; } - - private _cancelAction: IAction; - - private _onDidChange = this._register(new Emitter()); - readonly onDidChange: Event = this._onDidChange.event; - - private readonly _disposables = this._register(new MutableDisposable()); - - constructor( - container: HTMLElement, - options: IMenuWorkbenchToolBarOptions | undefined, - @IMenuService private readonly menuService: IMenuService, - @IContextKeyService private readonly contextKeyService: IContextKeyService, - @IContextMenuService contextMenuService: IContextMenuService, - @ICommandService commandService: ICommandService, - @IKeybindingService keybindingService: IKeybindingService, - @IStorageService private readonly storageService: IStorageService, - @ITelemetryService telemetryService: ITelemetryService, - ) { - super(container, options, menuService, contextKeyService, contextMenuService, keybindingService, commandService, telemetryService); - - this._dropdownAction = new Action( - 'scmInputMoreActions', - localize('scmInputMoreActions', "More Actions..."), - 'codicon-chevron-down'); - - this._cancelAction = new MenuItemAction({ - id: SCMInputWidgetCommandId.CancelAction, - title: localize('scmInputCancelAction', "Cancel"), - icon: Codicon.stopCircle, - }, undefined, undefined, undefined, undefined, contextKeyService, commandService); - } - - public setInput(input: ISCMInput): void { - this._disposables.value = new DisposableStore(); - - const contextKeyService = this.contextKeyService.createOverlay([ - ['scmProvider', input.repository.provider.providerId], - ['scmProviderRootUri', input.repository.provider.rootUri?.toString()], - ['scmProviderHasRootUri', !!input.repository.provider.rootUri] - ]); - - const menu = this._disposables.value.add(this.menuService.createMenu(MenuId.SCMInputBox, contextKeyService, { emitEventsForSubmenuChanges: true })); - - const isEnabled = (): boolean => { - return input.repository.provider.groups.some(g => g.resources.length > 0); - }; - - const updateToolbar = () => { - const actions = getFlatActionBarActions(menu.getActions({ shouldForwardArgs: true })); - - for (const action of actions) { - action.enabled = isEnabled(); - } - this._dropdownAction.enabled = isEnabled(); - - let primaryAction: IAction | undefined = undefined; - - if ((this.actionRunner as SCMInputWidgetActionRunner).runningActions.size !== 0) { - primaryAction = this._cancelAction; - } else if (actions.length === 1) { - primaryAction = actions[0]; - } else if (actions.length > 1) { - const lastActionId = this.storageService.get(SCMInputWidgetStorageKey.LastActionId, StorageScope.PROFILE, ''); - primaryAction = actions.find(a => a.id === lastActionId) ?? actions[0]; - } - - this._dropdownActions = actions.length === 1 ? [] : actions; - super.setActions(primaryAction ? [primaryAction] : [], []); - - this._onDidChange.fire(); - }; - - this._disposables.value.add(menu.onDidChange(() => updateToolbar())); - this._disposables.value.add(input.repository.provider.onDidChangeResources(() => updateToolbar())); - this._disposables.value.add(this.storageService.onDidChangeValue(StorageScope.PROFILE, SCMInputWidgetStorageKey.LastActionId, this._disposables.value)(() => updateToolbar())); - - this.actionRunner = this._disposables.value.add(new SCMInputWidgetActionRunner(input, this.storageService)); - this._disposables.value.add(this.actionRunner.onWillRun(e => { - if ((this.actionRunner as SCMInputWidgetActionRunner).runningActions.size === 0) { - super.setActions([this._cancelAction], []); - this._onDidChange.fire(); - } - })); - this._disposables.value.add(this.actionRunner.onDidRun(e => { - if ((this.actionRunner as SCMInputWidgetActionRunner).runningActions.size === 0) { - updateToolbar(); - } - })); - - updateToolbar(); - } -} - -class SCMInputWidgetEditorOptions { - - private readonly _onDidChange = new Emitter(); - readonly onDidChange = this._onDidChange.event; - - private readonly defaultInputFontFamily = DEFAULT_FONT_FAMILY; - - private readonly _disposables = new DisposableStore(); - - constructor( - private readonly overflowWidgetsDomNode: HTMLElement, - private readonly configurationService: IConfigurationService) { - - const onDidChangeConfiguration = Event.filter( - this.configurationService.onDidChangeConfiguration, - e => { - return e.affectsConfiguration('editor.accessibilitySupport') || - e.affectsConfiguration('editor.cursorBlinking') || - e.affectsConfiguration('editor.cursorStyle') || - e.affectsConfiguration('editor.cursorWidth') || - e.affectsConfiguration('editor.emptySelectionClipboard') || - e.affectsConfiguration('editor.fontFamily') || - e.affectsConfiguration('editor.rulers') || - e.affectsConfiguration('editor.wordWrap') || - e.affectsConfiguration('editor.wordSegmenterLocales') || - e.affectsConfiguration('scm.inputFontFamily') || - e.affectsConfiguration('scm.inputFontSize'); - }, - this._disposables - ); - - this._disposables.add(onDidChangeConfiguration(() => this._onDidChange.fire())); - } - - getEditorConstructionOptions(): IEditorConstructionOptions { - return { - ...getSimpleEditorOptions(this.configurationService), - ...this.getEditorOptions(), - dragAndDrop: true, - dropIntoEditor: { enabled: true }, - formatOnType: true, - lineDecorationsWidth: 6, - overflowWidgetsDomNode: this.overflowWidgetsDomNode, - padding: { top: 2, bottom: 2 }, - quickSuggestions: false, - renderWhitespace: 'none', - scrollbar: { - alwaysConsumeMouseWheel: false, - vertical: 'hidden' - }, - wrappingIndent: 'none', - wrappingStrategy: 'advanced', - }; - } - - getEditorOptions(): IEditorOptions { - const fontFamily = this._getEditorFontFamily(); - const fontSize = this._getEditorFontSize(); - const lineHeight = this._getEditorLineHeight(fontSize); - const wordSegmenterLocales = this.configurationService.getValue('editor.wordSegmenterLocales'); - const accessibilitySupport = this.configurationService.getValue<'auto' | 'off' | 'on'>('editor.accessibilitySupport'); - const cursorBlinking = this.configurationService.getValue<'blink' | 'smooth' | 'phase' | 'expand' | 'solid'>('editor.cursorBlinking'); - const cursorStyle = this.configurationService.getValue('editor.cursorStyle'); - const cursorWidth = this.configurationService.getValue('editor.cursorWidth') ?? 1; - const emptySelectionClipboard = this.configurationService.getValue('editor.emptySelectionClipboard') === true; - - return { ...this._getEditorLanguageConfiguration(), accessibilitySupport, cursorBlinking, cursorStyle, cursorWidth, fontFamily, fontSize, lineHeight, emptySelectionClipboard, wordSegmenterLocales }; - } - - private _getEditorFontFamily(): string { - const inputFontFamily = this.configurationService.getValue('scm.inputFontFamily').trim(); - - if (inputFontFamily.toLowerCase() === 'editor') { - return this.configurationService.getValue('editor.fontFamily').trim(); - } - - if (inputFontFamily.length !== 0 && inputFontFamily.toLowerCase() !== 'default') { - return inputFontFamily; - } - - return this.defaultInputFontFamily; - } - - private _getEditorFontSize(): number { - return this.configurationService.getValue('scm.inputFontSize'); - } - - private _getEditorLanguageConfiguration(): IEditorOptions { - // editor.rulers - const rulersConfig = this.configurationService.inspect('editor.rulers', { overrideIdentifier: 'scminput' }); - const rulers = rulersConfig.overrideIdentifiers?.includes('scminput') ? EditorOptions.rulers.validate(rulersConfig.value) : []; - - // editor.wordWrap - const wordWrapConfig = this.configurationService.inspect('editor.wordWrap', { overrideIdentifier: 'scminput' }); - const wordWrap = wordWrapConfig.overrideIdentifiers?.includes('scminput') ? EditorOptions.wordWrap.validate(wordWrapConfig.value) : 'on'; - - return { rulers, wordWrap }; - } - - private _getEditorLineHeight(fontSize: number): number { - return Math.round(fontSize * 1.5); - } - - dispose(): void { - this._disposables.dispose(); - this._onDidChange.dispose(); - } - -} - -class SCMInputWidget { - - private static readonly ValidationTimeouts: { [severity: number]: number } = { - [InputValidationType.Information]: 5000, - [InputValidationType.Warning]: 8000, - [InputValidationType.Error]: 10000 - }; - - private readonly contextKeyService: IContextKeyService; - - private element: HTMLElement; - private editorContainer: HTMLElement; - private readonly inputEditor: CodeEditorWidget; - private readonly inputEditorOptions: SCMInputWidgetEditorOptions; - private toolbarContainer: HTMLElement; - private toolbar: SCMInputWidgetToolbar; - private readonly disposables = new DisposableStore(); - - private model: { readonly input: ISCMInput; readonly textModel: ITextModel } | undefined; - private repositoryIdContextKey: IContextKey; - private validationMessageContextKey: IContextKey; - private readonly repositoryDisposables = new DisposableStore(); - - private validation: IInputValidation | undefined; - private validationContextView: IOpenContextView | undefined; - private validationHasFocus: boolean = false; - private _validationTimer: Timeout | undefined; - - // This is due to "Setup height change listener on next tick" above - // https://github.com/microsoft/vscode/issues/108067 - private lastLayoutWasTrash = false; - private shouldFocusAfterLayout = false; - - readonly onDidChangeContentHeight: Event; - - get input(): ISCMInput | undefined { - return this.model?.input; - } - - set input(input: ISCMInput | undefined) { - if (input === this.input) { - return; - } - - this.clearValidation(); - this.element.classList.remove('synthetic-focus'); - - this.repositoryDisposables.clear(); - this.repositoryIdContextKey.set(input?.repository.id); - - if (!input) { - this.inputEditor.setModel(undefined); - this.model = undefined; - return; - } - - 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 () => { - const position = this.inputEditor.getSelection()?.getStartPosition(); - const offset = position && textModel.getOffsetAt(position); - const value = textModel.getValue(); - - this.setValidation(await input.validateInput(value, offset || 0)); - }; - - const triggerValidation = () => validationDelayer.trigger(validate); - this.repositoryDisposables.add(validationDelayer); - this.repositoryDisposables.add(this.inputEditor.onDidChangeCursorPosition(triggerValidation)); - - // Adaptive indentation rules - const opts = this.modelService.getCreationOptions(textModel.getLanguageId(), textModel.uri, textModel.isForSimpleWidget); - const onEnter = Event.filter(this.inputEditor.onKeyDown, e => e.keyCode === KeyCode.Enter, this.repositoryDisposables); - this.repositoryDisposables.add(onEnter(() => textModel.detectIndentation(opts.insertSpaces, opts.tabSize))); - - // Keep model in sync with API - textModel.setValue(input.value); - this.repositoryDisposables.add(input.onDidChange(({ value, reason }) => { - const currentValue = textModel.getValue(); - if (value === currentValue) { // circuit breaker - return; - } - - textModel.pushStackElement(); - textModel.pushEditOperations(null, [EditOperation.replaceMove(textModel.getFullModelRange(), value)], () => []); - - const position = reason === SCMInputChangeReason.HistoryPrevious - ? textModel.getFullModelRange().getStartPosition() - : textModel.getFullModelRange().getEndPosition(); - this.inputEditor.setPosition(position); - this.inputEditor.revealPositionInCenterIfOutsideViewport(position); - })); - this.repositoryDisposables.add(input.onDidChangeFocus(() => this.focus())); - this.repositoryDisposables.add(input.onDidChangeValidationMessage((e) => this.setValidation(e, { focus: true, timeout: true }))); - this.repositoryDisposables.add(input.onDidChangeValidateInput((e) => triggerValidation())); - this.repositoryDisposables.add(input.onDidClearValidation(() => this.clearValidation())); - - // Keep API in sync with model and validate - this.repositoryDisposables.add(textModel.onDidChangeContent(() => { - input.setValue(textModel.getValue(), true); - triggerValidation(); - })); - - // Aria label & placeholder text - const accessibilityVerbosityConfig = observableConfigValue( - AccessibilityVerbositySettingId.SourceControl, true, this.configurationService); - - const getAriaLabel = (placeholder: string, verbosity?: boolean) => { - verbosity = verbosity ?? accessibilityVerbosityConfig.get(); - - if (!verbosity || !this.accessibilityService.isScreenReaderOptimized()) { - return placeholder; - } - - const kbLabel = this.keybindingService.lookupKeybinding(AccessibilityCommandId.OpenAccessibilityHelp)?.getLabel(); - return kbLabel - ? localize('scmInput.accessibilityHelp', "{0}, Use {1} to open Source Control Accessibility Help.", placeholder, kbLabel) - : localize('scmInput.accessibilityHelpNoKb', "{0}, Run the Open Accessibility Help command for more information.", placeholder); - }; - - const getPlaceholderText = (): string => { - const binding = this.keybindingService.lookupKeybinding('scm.acceptInput'); - const label = binding ? binding.getLabel() : (platform.isMacintosh ? 'Cmd+Enter' : 'Ctrl+Enter'); - return format(input.placeholder, label); - }; - - const updatePlaceholderText = () => { - const placeholder = getPlaceholderText(); - const ariaLabel = getAriaLabel(placeholder); - - this.inputEditor.updateOptions({ ariaLabel, placeholder }); - }; - - this.repositoryDisposables.add(input.onDidChangePlaceholder(updatePlaceholderText)); - this.repositoryDisposables.add(this.keybindingService.onDidUpdateKeybindings(updatePlaceholderText)); - - this.repositoryDisposables.add(runOnChange(accessibilityVerbosityConfig, verbosity => { - const placeholder = getPlaceholderText(); - const ariaLabel = getAriaLabel(placeholder, verbosity); - - this.inputEditor.updateOptions({ ariaLabel }); - })); - - updatePlaceholderText(); - - // Update input template - let commitTemplate = ''; - this.repositoryDisposables.add(autorun(reader => { - if (!input.visible) { - return; - } - - const oldCommitTemplate = commitTemplate; - commitTemplate = input.repository.provider.commitTemplate.read(reader); - - const value = textModel.getValue(); - if (value && value !== oldCommitTemplate) { - return; - } - - textModel.setValue(commitTemplate); - })); - - // Update input enablement - const updateEnablement = (enabled: boolean) => { - this.inputEditor.updateOptions({ readOnly: !enabled }); - }; - this.repositoryDisposables.add(input.onDidChangeEnablement(enabled => updateEnablement(enabled))); - updateEnablement(input.enabled); - - // Toolbar - this.toolbar.setInput(input); - - // Save model - this.model = { input, textModel }; - } - - get selections(): Selection[] | null { - return this.inputEditor.getSelections(); - } - - set selections(selections: Selection[] | null) { - if (selections) { - this.inputEditor.setSelections(selections); - } - } - - private setValidation(validation: IInputValidation | undefined, options?: { focus?: boolean; timeout?: boolean }) { - if (this._validationTimer) { - clearTimeout(this._validationTimer); - this._validationTimer = undefined; - } - - this.validation = validation; - this.renderValidation(); - - if (options?.focus && !this.hasFocus()) { - this.focus(); - } - - if (validation && options?.timeout) { - this._validationTimer = setTimeout(() => this.setValidation(undefined), SCMInputWidget.ValidationTimeouts[validation.type]); - } - } - - constructor( - container: HTMLElement, - overflowWidgetsDomNode: HTMLElement, - @IContextKeyService contextKeyService: IContextKeyService, - @IInstantiationService instantiationService: IInstantiationService, - @IModelService private modelService: IModelService, - @IKeybindingService private keybindingService: IKeybindingService, - @IConfigurationService private configurationService: IConfigurationService, - @ISCMViewService private readonly scmViewService: ISCMViewService, - @IContextViewService private readonly contextViewService: IContextViewService, - @IOpenerService private readonly openerService: IOpenerService, - @IAccessibilityService private readonly accessibilityService: IAccessibilityService, - @IMarkdownRendererService private readonly markdownRendererService: IMarkdownRendererService, - ) { - this.element = append(container, $('.scm-editor')); - this.editorContainer = append(this.element, $('.scm-editor-container')); - this.toolbarContainer = append(this.element, $('.scm-editor-toolbar')); - - this.contextKeyService = this.disposables.add(contextKeyService.createScoped(this.element)); - this.repositoryIdContextKey = this.contextKeyService.createKey('scmRepository', undefined); - this.validationMessageContextKey = ContextKeys.SCMInputHasValidationMessage.bindTo(this.contextKeyService); - - this.inputEditorOptions = new SCMInputWidgetEditorOptions(overflowWidgetsDomNode, this.configurationService); - this.disposables.add(this.inputEditorOptions.onDidChange(this.onDidChangeEditorOptions, this)); - this.disposables.add(this.inputEditorOptions); - - const codeEditorWidgetOptions: ICodeEditorWidgetOptions = { - contributions: EditorExtensionsRegistry.getSomeEditorContributions([ - CodeActionController.ID, - ColorDetector.ID, - ContextMenuController.ID, - CopyPasteController.ID, - DragAndDropController.ID, - DropIntoEditorController.ID, - EditorDictation.ID, - FormatOnType.ID, - ContentHoverController.ID, - GlyphHoverController.ID, - InlineCompletionsController.ID, - LinkDetector.ID, - MenuPreventer.ID, - MessageController.ID, - PlaceholderTextContribution.ID, - SelectionClipboardContributionID, - SnippetController2.ID, - SuggestController.ID - ]), - isSimpleWidget: true - }; - - const services = new ServiceCollection([IContextKeyService, this.contextKeyService]); - const instantiationService2 = instantiationService.createChild(services, this.disposables); - const editorConstructionOptions = this.inputEditorOptions.getEditorConstructionOptions(); - this.inputEditor = instantiationService2.createInstance(CodeEditorWidget, this.editorContainer, editorConstructionOptions, codeEditorWidgetOptions); - this.disposables.add(this.inputEditor); - - this.disposables.add(this.inputEditor.onDidFocusEditorText(() => { - if (this.input?.repository) { - this.scmViewService.focus(this.input.repository); - } - - this.element.classList.add('synthetic-focus'); - this.renderValidation(); - })); - this.disposables.add(this.inputEditor.onDidBlurEditorText(() => { - this.element.classList.remove('synthetic-focus'); - - setTimeout(() => { - if (!this.validation || !this.validationHasFocus) { - this.clearValidation(); - } - }, 0); - })); - - this.disposables.add(this.inputEditor.onDidBlurEditorWidget(() => { - CopyPasteController.get(this.inputEditor)?.clearWidgets(); - DropIntoEditorController.get(this.inputEditor)?.clearWidgets(); - })); - - const firstLineKey = this.contextKeyService.createKey('scmInputIsInFirstPosition', false); - const lastLineKey = this.contextKeyService.createKey('scmInputIsInLastPosition', false); - - this.disposables.add(this.inputEditor.onDidChangeCursorPosition(({ position }) => { - const viewModel = this.inputEditor._getViewModel()!; - const lastLineNumber = viewModel.getLineCount(); - const lastLineCol = viewModel.getLineLength(lastLineNumber) + 1; - const viewPosition = viewModel.coordinatesConverter.convertModelPositionToViewPosition(position); - firstLineKey.set(viewPosition.lineNumber === 1 && viewPosition.column === 1); - lastLineKey.set(viewPosition.lineNumber === lastLineNumber && viewPosition.column === lastLineCol); - })); - this.disposables.add(this.inputEditor.onDidScrollChange(e => { - this.toolbarContainer.classList.toggle('scroll-decoration', e.scrollTop > 0); - })); - - Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.showInputActionButton'))(() => this.layout(), this, this.disposables); - - this.onDidChangeContentHeight = Event.signal(Event.filter(this.inputEditor.onDidContentSizeChange, e => e.contentHeightChanged, this.disposables)); - - // Toolbar - this.toolbar = instantiationService2.createInstance(SCMInputWidgetToolbar, this.toolbarContainer, { - actionViewItemProvider: (action, options) => { - if (action instanceof MenuItemAction && this.toolbar.dropdownActions.length > 1) { - return instantiationService.createInstance(DropdownWithPrimaryActionViewItem, action, this.toolbar.dropdownAction, this.toolbar.dropdownActions, '', { actionRunner: this.toolbar.actionRunner, hoverDelegate: options.hoverDelegate }); - } - - return createActionViewItem(instantiationService, action, options); - }, - hiddenItemStrategy: HiddenItemStrategy.NoHide, - menuOptions: { - shouldForwardArgs: true - } - }); - this.disposables.add(this.toolbar.onDidChange(() => this.layout())); - this.disposables.add(this.toolbar); - } - - getContentHeight(): number { - const lineHeight = this.inputEditor.getOption(EditorOption.lineHeight); - const { top, bottom } = this.inputEditor.getOption(EditorOption.padding); - - const inputMinLinesConfig = this.configurationService.getValue('scm.inputMinLineCount'); - const inputMinLines = typeof inputMinLinesConfig === 'number' ? clamp(inputMinLinesConfig, 1, 50) : 1; - const editorMinHeight = inputMinLines * lineHeight + top + bottom; - - const inputMaxLinesConfig = this.configurationService.getValue('scm.inputMaxLineCount'); - const inputMaxLines = typeof inputMaxLinesConfig === 'number' ? clamp(inputMaxLinesConfig, 1, 50) : 10; - const editorMaxHeight = inputMaxLines * lineHeight + top + bottom; - - return clamp(this.inputEditor.getContentHeight(), editorMinHeight, editorMaxHeight); - } - - layout(): void { - const editorHeight = this.getContentHeight(); - const toolbarWidth = this.getToolbarWidth(); - const dimension = new Dimension(this.element.clientWidth - toolbarWidth, editorHeight); - - if (dimension.width < 0) { - this.lastLayoutWasTrash = true; - return; - } - - this.lastLayoutWasTrash = false; - this.inputEditor.layout(dimension); - this.renderValidation(); - - const showInputActionButton = this.configurationService.getValue('scm.showInputActionButton') === true; - this.toolbarContainer.classList.toggle('hidden', !showInputActionButton || this.toolbar?.isEmpty() === true); - - if (this.shouldFocusAfterLayout) { - this.shouldFocusAfterLayout = false; - this.focus(); - } - } - - focus(): void { - if (this.lastLayoutWasTrash) { - this.lastLayoutWasTrash = false; - this.shouldFocusAfterLayout = true; - return; - } - - this.inputEditor.focus(); - this.element.classList.add('synthetic-focus'); - } - - hasFocus(): boolean { - return this.inputEditor.hasTextFocus(); - } - - private onDidChangeEditorOptions(): void { - this.inputEditor.updateOptions(this.inputEditorOptions.getEditorOptions()); - } - - private renderValidation(): void { - this.clearValidation(); - - this.element.classList.toggle('validation-info', this.validation?.type === InputValidationType.Information); - this.element.classList.toggle('validation-warning', this.validation?.type === InputValidationType.Warning); - this.element.classList.toggle('validation-error', this.validation?.type === InputValidationType.Error); - - if (!this.validation || !this.inputEditor.hasTextFocus()) { - return; - } - - this.validationMessageContextKey.set(true); - const disposables = new DisposableStore(); - - this.validationContextView = this.contextViewService.showContextView({ - getAnchor: () => this.element, - render: container => { - this.element.style.borderBottomLeftRadius = '0'; - this.element.style.borderBottomRightRadius = '0'; - - const validationContainer = append(container, $('.scm-editor-validation-container')); - validationContainer.classList.toggle('validation-info', this.validation!.type === InputValidationType.Information); - validationContainer.classList.toggle('validation-warning', this.validation!.type === InputValidationType.Warning); - validationContainer.classList.toggle('validation-error', this.validation!.type === InputValidationType.Error); - validationContainer.style.width = `${this.element.clientWidth + 2}px`; - const element = append(validationContainer, $('.scm-editor-validation')); - - const message = this.validation!.message; - if (typeof message === 'string') { - element.textContent = message; - } else { - const tracker = trackFocus(element); - disposables.add(tracker); - disposables.add(tracker.onDidFocus(() => (this.validationHasFocus = true))); - disposables.add(tracker.onDidBlur(() => { - this.validationHasFocus = false; - this.element.style.borderBottomLeftRadius = '2px'; - this.element.style.borderBottomRightRadius = '2px'; - this.contextViewService.hideContextView(); - })); - - const renderedMarkdown = this.markdownRendererService.render(message, { - actionHandler: (link, mdStr) => { - openLinkFromMarkdown(this.openerService, link, mdStr.isTrusted); - this.element.style.borderBottomLeftRadius = '2px'; - this.element.style.borderBottomRightRadius = '2px'; - this.contextViewService.hideContextView(); - }, - }); - disposables.add(renderedMarkdown); - element.appendChild(renderedMarkdown.element); - } - const actionsContainer = append(validationContainer, $('.scm-editor-validation-actions')); - const actionbar = new ActionBar(actionsContainer); - const action = new Action('scmInputWidget.validationMessage.close', localize('label.close', "Close"), ThemeIcon.asClassName(Codicon.close), true, () => { - this.contextViewService.hideContextView(); - this.element.style.borderBottomLeftRadius = '2px'; - this.element.style.borderBottomRightRadius = '2px'; - }); - disposables.add(actionbar); - actionbar.push(action, { icon: true, label: false }); - - return Disposable.None; - }, - onHide: () => { - this.validationHasFocus = false; - this.element.style.borderBottomLeftRadius = '2px'; - this.element.style.borderBottomRightRadius = '2px'; - disposables.dispose(); - }, - anchorAlignment: AnchorAlignment.LEFT - }); - } - - private getToolbarWidth(): number { - const showInputActionButton = this.configurationService.getValue('scm.showInputActionButton'); - if (!this.toolbar || !showInputActionButton || this.toolbar?.isEmpty() === true) { - return 0; - } - - return this.toolbar.dropdownActions.length === 0 ? - 26 /* 22px action + 4px margin */ : - 39 /* 35px action + 4px margin */; - } - - clearValidation(): void { - this.validationContextView?.close(); - this.validationContextView = undefined; - this.validationHasFocus = false; - this.validationMessageContextKey.set(false); - } - - dispose(): void { - this.input = undefined; - this.repositoryDisposables.dispose(); - this.clearValidation(); - this.disposables.dispose(); - } -} - export class SCMViewPane extends ViewPane { private readonly _onDidLayout: Emitter; @@ -3239,5 +2409,3 @@ export class SCMActionButton implements IDisposable { } } } - -setupSimpleEditorSelectionStyling('.scm-view .scm-editor-container'); diff --git a/test/sanity/src/uiTest.ts b/test/sanity/src/uiTest.ts index 06d2dc584887f..f6dda6bc1a4a2 100644 --- a/test/sanity/src/uiTest.ts +++ b/test/sanity/src/uiTest.ts @@ -102,7 +102,7 @@ export class UITest { await page.getByText(/Start typing/).focus(); this.context.log('Typing some content into the file'); - await page.keyboard.type('Hello, World!'); + await page.keyboard.type('Hello, World!', { delay: 100 }); await this.runCommand(page, 'File: Save'); }