From 4685d75964105e78f64ad84a2abf898c7c91e8d8 Mon Sep 17 00:00:00 2001 From: Derek Yang Date: Wed, 4 Dec 2024 23:48:25 -0500 Subject: [PATCH 01/22] Fix CSS errors when using HTML escaped quotes --- .../server/src/modes/embeddedSupport.ts | 11 +++++++++++ .../server/src/test/embedded.test.ts | 7 +++++++ 2 files changed, 18 insertions(+) diff --git a/extensions/html-language-features/server/src/modes/embeddedSupport.ts b/extensions/html-language-features/server/src/modes/embeddedSupport.ts index 26ef68439da96..a6874e043b44b 100644 --- a/extensions/html-language-features/server/src/modes/embeddedSupport.ts +++ b/extensions/html-language-features/server/src/modes/embeddedSupport.ts @@ -198,6 +198,17 @@ function updateContent(c: EmbeddedRegion, content: string): string { if (!c.attributeValue && c.languageId === 'javascript') { return content.replace(``, ` */`); } + if (c.languageId === 'css') { + const quoteEscape = /("|")/g; + return content.replace(quoteEscape, (match, _, offset) => { + const spaces = ' '.repeat(match.length - 1); + const afterChar = content[offset + match.length]; + if (!afterChar || afterChar.includes(' ')) { + return `${spaces}"`; + } + return `"${spaces}`; + }); + } return content; } diff --git a/extensions/html-language-features/server/src/test/embedded.test.ts b/extensions/html-language-features/server/src/test/embedded.test.ts index 87698f3971882..c5b7bca17ea7c 100644 --- a/extensions/html-language-features/server/src/test/embedded.test.ts +++ b/extensions/html-language-features/server/src/test/embedded.test.ts @@ -128,4 +128,11 @@ suite('HTML Embedded Support', () => { assertEmbeddedLanguageContent('
', 'javascript', ' return;\n foo(); '); }); + test('Script content - HTML escape characters', function (): any { + assertEmbeddedLanguageContent('
', 'css', ' __{font-family: " Arial "} '); + assertEmbeddedLanguageContent('
', 'css', ' __{font-family: " Arial "} '); + assertEmbeddedLanguageContent('
', 'css', ' __{font-family: " Arial "} '); + assertEmbeddedLanguageContent('
', 'css', ' __{font-family: " Arial " } '); + }); + }); From 77f0483dcd560b2f905e1bf192eef0865a731562 Mon Sep 17 00:00:00 2001 From: Derek Yang Date: Thu, 5 Dec 2024 14:22:54 -0500 Subject: [PATCH 02/22] Add additional test case containing no escaped quotes --- .../html-language-features/server/src/test/embedded.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/extensions/html-language-features/server/src/test/embedded.test.ts b/extensions/html-language-features/server/src/test/embedded.test.ts index c5b7bca17ea7c..abba5b5858e75 100644 --- a/extensions/html-language-features/server/src/test/embedded.test.ts +++ b/extensions/html-language-features/server/src/test/embedded.test.ts @@ -133,6 +133,7 @@ suite('HTML Embedded Support', () => { assertEmbeddedLanguageContent('
', 'css', ' __{font-family: " Arial "} '); assertEmbeddedLanguageContent('
', 'css', ' __{font-family: " Arial "} '); assertEmbeddedLanguageContent('
', 'css', ' __{font-family: " Arial " } '); + assertEmbeddedLanguageContent('
', 'css', ' __{font-family: Arial} '); }); }); From fc90ee9b9237ee6368fe9fde3a6e23f530e9cd26 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sun, 8 Dec 2024 20:41:43 +0100 Subject: [PATCH 03/22] chat - resolve entitlements after potential upgrade (#235580) --- .../chatContentParts/chatQuotaExceededPart.ts | 8 +- .../contrib/chat/browser/chatQuotasService.ts | 41 ++-------- .../contrib/chat/browser/chatSetup.ts | 75 ++++++++++++++++++- 3 files changed, 78 insertions(+), 46 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatQuotaExceededPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatQuotaExceededPart.ts index bd584402ca04b..e3e06c9b05a13 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatQuotaExceededPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatQuotaExceededPart.ts @@ -11,11 +11,9 @@ import { MarkdownString } from '../../../../../base/common/htmlContent.js'; import { Disposable } from '../../../../../base/common/lifecycle.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { assertType } from '../../../../../base/common/types.js'; -import { URI } from '../../../../../base/common/uri.js'; import { MarkdownRenderer } from '../../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js'; import { localize } from '../../../../../nls.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; -import { IProductService } from '../../../../../platform/product/common/productService.js'; import { defaultButtonStyles } from '../../../../../platform/theme/browser/defaultStyles.js'; import { asCssVariable, textLinkForeground } from '../../../../../platform/theme/common/colorRegistry.js'; import { IChatResponseViewModel } from '../../common/chatViewModel.js'; @@ -34,8 +32,7 @@ export class ChatQuotaExceededPart extends Disposable implements IChatContentPar element: IChatResponseViewModel, renderer: MarkdownRenderer, @IChatWidgetService chatWidgetService: IChatWidgetService, - @ICommandService commandService: ICommandService, - @IProductService productService: IProductService, + @ICommandService commandService: ICommandService ) { super(); @@ -56,8 +53,7 @@ export class ChatQuotaExceededPart extends Disposable implements IChatContentPar let didAddSecondary = false; this._register(button1.onDidClick(async () => { - const url = productService.defaultChatAgent?.upgradePlanUrl; - await commandService.executeCommand('vscode.open', url ? URI.parse(url) : undefined); + await commandService.executeCommand('workbench.action.chat.upgradePlan'); if (!didAddSecondary) { didAddSecondary = true; diff --git a/src/vs/workbench/contrib/chat/browser/chatQuotasService.ts b/src/vs/workbench/contrib/chat/browser/chatQuotasService.ts index efa9304c9742f..467affc78991b 100644 --- a/src/vs/workbench/contrib/chat/browser/chatQuotasService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatQuotasService.ts @@ -11,17 +11,16 @@ import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; import { localize, localize2 } from '../../../../nls.js'; import { Categories } from '../../../../platform/action/common/actionCommonCategories.js'; -import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'; -import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { createDecorator, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js'; -import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import product from '../../../../platform/product/common/product.js'; -import { URI } from '../../../../base/common/uri.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; import { IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from '../../../services/statusbar/browser/statusbar.js'; import { ChatContextKeys } from '../common/chatContextKeys.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; export const IChatQuotasService = createDecorator('chatQuotasService'); @@ -102,35 +101,6 @@ export class ChatQuotasService extends Disposable implements IChatQuotasService private registerActions(): void { const that = this; - class UpgradePlanAction extends Action2 { - constructor() { - super({ - id: 'workbench.action.chat.upgradePlan', - title: localize2('managePlan', "Upgrade to Copilot Pro"), - category: localize2('chat.category', 'Chat'), - f1: true, - precondition: ChatContextKeys.enabled, - menu: { - id: MenuId.ChatCommandCenter, - group: 'a_first', - order: 1, - when: ContextKeyExpr.and( - ChatContextKeys.Setup.installed, - ContextKeyExpr.or( - ChatContextKeys.chatQuotaExceeded, - ChatContextKeys.completionsQuotaExceeded - ) - ) - } - }); - } - - override async run(accessor: ServicesAccessor): Promise { - const openerService = accessor.get(IOpenerService); - openerService.open(URI.parse(product.defaultChatAgent?.upgradePlanUrl ?? '')); - } - } - class ShowLimitReachedDialogAction extends Action2 { constructor() { @@ -141,7 +111,7 @@ export class ChatQuotasService extends Disposable implements IChatQuotasService } override async run(accessor: ServicesAccessor) { - const openerService = accessor.get(IOpenerService); + const commandService = accessor.get(ICommandService); const dialogService = accessor.get(IDialogService); const dateFormatter = safeIntl.DateTimeFormat(language, { year: 'numeric', month: 'long', day: 'numeric' }); @@ -168,7 +138,7 @@ export class ChatQuotasService extends Disposable implements IChatQuotasService buttons: [ { label: localize('managePlan', "Upgrade to Copilot Pro"), - run: () => { openerService.open(URI.parse(product.defaultChatAgent?.upgradePlanUrl ?? '')); } + run: () => commandService.executeCommand('workbench.action.chat.upgradePlan') }, ], custom: { @@ -211,7 +181,6 @@ export class ChatQuotasService extends Disposable implements IChatQuotasService } } - registerAction2(UpgradePlanAction); registerAction2(ShowLimitReachedDialogAction); if (product.quality !== 'stable') { registerAction2(SimulateCopilotQuotaExceeded); diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index d31113c0d6b19..6d5104baddf58 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -7,7 +7,7 @@ import './media/chatViewSetup.css'; import { $, getActiveElement, setVisibility } from '../../../../base/browser/dom.js'; import { Button, ButtonWithDropdown } from '../../../../base/browser/ui/button/button.js'; import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; -import { IAction, toAction } from '../../../../base/common/actions.js'; +import { IAction, toAction, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../../../base/common/actions.js'; import { Barrier, timeout } from '../../../../base/common/async.js'; import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { Codicon } from '../../../../base/common/codicons.js'; @@ -15,7 +15,7 @@ import { isCancellationError } from '../../../../base/common/errors.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { Lazy } from '../../../../base/common/lazy.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; import { IRequestContext } from '../../../../base/parts/request/common/request.js'; import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js'; import { MarkdownRenderer } from '../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js'; @@ -56,6 +56,9 @@ import { CHAT_EDITING_SIDEBAR_PANEL_ID, CHAT_SIDEBAR_PANEL_ID } from './chatView import { ChatViewsWelcomeExtensions, IChatViewsWelcomeContributionRegistry } from './viewsWelcome/chatViewsWelcome.js'; import { IChatQuotasService } from './chatQuotasService.js'; import { mainWindow } from '../../../../base/browser/window.js'; +import { IOpenerService } from '../../../../platform/opener/common/opener.js'; +import { URI } from '../../../../base/common/uri.js'; +import { IHostService } from '../../../services/host/browser/host.js'; const defaultChat = { extensionId: product.defaultChatAgent?.extensionId ?? '', @@ -231,6 +234,61 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr } } + const windowFocusListener = this._register(new MutableDisposable()); + class UpgradePlanAction extends Action2 { + constructor() { + super({ + id: 'workbench.action.chat.upgradePlan', + title: localize2('managePlan', "Upgrade to Copilot Pro"), + category: localize2('chat.category', 'Chat'), + f1: true, + precondition: ChatContextKeys.enabled, + menu: { + id: MenuId.ChatCommandCenter, + group: 'a_first', + order: 1, + when: ContextKeyExpr.and( + ChatContextKeys.Setup.installed, + ContextKeyExpr.or( + ChatContextKeys.chatQuotaExceeded, + ChatContextKeys.completionsQuotaExceeded + ) + ) + } + }); + } + + override async run(accessor: ServicesAccessor): Promise { + const openerService = accessor.get(IOpenerService); + const productService = accessor.get(IProductService); + const telemetryService = accessor.get(ITelemetryService); + const hostService = accessor.get(IHostService); + const commandService = accessor.get(ICommandService); + + telemetryService.publicLog2('workbenchActionExecuted', { id: this.desc.id, from: 'chat' }); + + openerService.open(URI.parse(productService.defaultChatAgent?.upgradePlanUrl ?? '')); + + const entitlement = that.context.state.entitlement; + if (entitlement !== ChatEntitlement.Pro) { + // If the user is not yet Pro, we listen to window focus to refresh the token + // when the user has come back to the window assuming the user signed up. + windowFocusListener.value = hostService.onDidChangeFocus(focus => this.onWindowFocus(focus, commandService)); + } + } + + private async onWindowFocus(focus: boolean, commandService: ICommandService): Promise { + if (focus) { + windowFocusListener.clear(); + + const entitlement = await that.requests.forceResolveEntitlement(undefined); + if (entitlement === ChatEntitlement.Pro) { + commandService.executeCommand('github.copilot.refreshToken'); // ugly, but we need to signal to the extension that entitlements changed + } + } + } + } + async function hideSetupView(viewsDescriptorService: IViewDescriptorService, layoutService: IWorkbenchLayoutService): Promise { const location = viewsDescriptorService.getViewLocationById(ChatViewId); @@ -246,6 +304,7 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr registerAction2(ChatSetupTriggerAction); registerAction2(ChatSetupHideAction); + registerAction2(UpgradePlanAction); } } @@ -507,7 +566,15 @@ class ChatSetupRequests extends Disposable { } } - async forceResolveEntitlement(session: AuthenticationSession): Promise { + async forceResolveEntitlement(session: AuthenticationSession | undefined): Promise { + if (!session) { + session = await this.findMatchingProviderSession(CancellationToken.None); + } + + if (!session) { + return undefined; + } + return this.resolveEntitlement(session, CancellationToken.None); } @@ -798,7 +865,7 @@ class ChatSetupWelcomeContent extends Disposable { } // Limited SKU - const limitedSkuHeader = localize({ key: 'limitedSkuHeader', comment: ['{Locked="[]({0})"}'] }, "$(sparkle-filled) We now offer [Copilot for free]({0}) with 50 chat messages and 2000 code completions per month.", defaultChat.skusDocumentationUrl); + const limitedSkuHeader = localize({ key: 'limitedSkuHeader', comment: ['{Locked="[]({0})"}'] }, "$(sparkle-filled) We now offer [Copilot for free]({0}) with 2,000 code completions and 50 chat messages per month.", defaultChat.skusDocumentationUrl); const limitedSkuHeaderContainer = this.element.appendChild($('p')); limitedSkuHeaderContainer.appendChild(this._register(markdown.render(new MarkdownString(limitedSkuHeader, { isTrusted: true, supportThemeIcons: true }))).element); From c61b174d4b33904ab8520430ae7b12749d423ae5 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Mon, 9 Dec 2024 06:11:45 +0000 Subject: [PATCH 04/22] Fix missing responses when transferring messages into chat panel (#235583) * Fix missing responses when transferring messages into chat panel Fix #235531 * Add test --- src/vs/workbench/contrib/chat/common/chatModel.ts | 2 +- .../contrib/chat/test/common/chatModel.test.ts | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index ee0c9c9805379..14fa03dffa0f7 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -1152,7 +1152,7 @@ export class ChatModel extends Disposable implements IChatModel { addRequest(message: IParsedChatRequest, variableData: IChatRequestVariableData, attempt: number, chatAgent?: IChatAgentData, slashCommand?: IChatAgentCommand, confirmation?: string, locationData?: IChatLocationData, attachments?: IChatRequestVariableEntry[], workingSet?: URI[], isCompleteAddedRequest?: boolean): ChatRequestModel { const request = new ChatRequestModel(this, message, variableData, Date.now(), attempt, confirmation, locationData, attachments, workingSet, isCompleteAddedRequest); - request.response = new ChatResponseModel([], this, chatAgent, slashCommand, request.id, undefined, undefined, undefined, undefined, undefined, undefined, undefined, isCompleteAddedRequest); + request.response = new ChatResponseModel([], this, chatAgent, slashCommand, request.id, undefined, undefined, undefined, undefined, undefined, undefined, isCompleteAddedRequest); this._requests.push(request); this._lastMessageDate = Date.now(); diff --git a/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts b/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts index 1beb12e6adea5..be40ba599bd0c 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts @@ -151,6 +151,21 @@ suite('ChatModel', () => { assert.strictEqual(request1.response.response.toString(), 'Hello'); }); + + test('addCompleteRequest', async function () { + const model1 = testDisposables.add(instantiationService.createInstance(ChatModel, undefined, ChatAgentLocation.Panel)); + + model1.startInitialize(); + model1.initialize(undefined); + + const text = 'hello'; + const request1 = model1.addRequest({ text, parts: [new ChatRequestTextPart(new OffsetRange(0, text.length), new Range(1, text.length, 1, text.length), text)] }, { variables: [] }, 0, undefined, undefined, undefined, undefined, undefined, undefined, true); + + assert.strictEqual(request1.isCompleteAddedRequest, true); + assert.strictEqual(request1.response!.isCompleteAddedRequest, true); + assert.strictEqual(request1.isHidden, false); + assert.strictEqual(request1.response!.isHidden, false); + }); }); suite('Response', () => { From f10bb2140999946ed845db28da29f65a75373ae7 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 9 Dec 2024 08:00:27 +0100 Subject: [PATCH 05/22] SCM - switch to using ReferenceCollection in the IdirtyDiffModelService (#235579) Initial implementation --- .../api/browser/mainThreadEditors.ts | 46 ++++--- .../mainThreadDocumentsAndEditors.test.ts | 4 +- .../contrib/scm/browser/dirtyDiffModel.ts | 120 ++++++------------ .../contrib/scm/browser/dirtydiffDecorator.ts | 102 ++++++++------- 4 files changed, 129 insertions(+), 143 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadEditors.ts b/src/vs/workbench/api/browser/mainThreadEditors.ts index deeecec96f268..024a09f283a7a 100644 --- a/src/vs/workbench/api/browser/mainThreadEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadEditors.ts @@ -96,7 +96,7 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { this._proxy.$acceptEditorPropertiesChanged(id, data); })); - const diffInformationObs = this._getTextEditorDiffInformation(textEditor); + const diffInformationObs = this._getTextEditorDiffInformation(textEditor, toDispose); toDispose.push(autorun(reader => { const diffInformation = diffInformationObs.read(reader); this._proxy.$acceptEditorDiffInformation(id, diffInformation); @@ -131,17 +131,17 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { return result; } - private _getTextEditorDiffInformation(textEditor: MainThreadTextEditor): IObservable { + private _getTextEditorDiffInformation(textEditor: MainThreadTextEditor, toDispose: IDisposable[]): IObservable { const codeEditor = textEditor.getCodeEditor(); if (!codeEditor) { return constObservable(undefined); } // Check if the TextModel belongs to a DiffEditor - const diffEditors = this._codeEditorService.listDiffEditors(); - const [diffEditor] = diffEditors.filter(d => - d.getOriginalEditor().getId() === codeEditor.getId() || - d.getModifiedEditor().getId() === codeEditor.getId()); + const [diffEditor] = this._codeEditorService.listDiffEditors() + .filter(d => + d.getOriginalEditor().getId() === codeEditor.getId() || + d.getModifiedEditor().getId() === codeEditor.getId()); const editorModelObs = diffEditor ? observableFromEvent(this, diffEditor.onDidChangeModel, () => diffEditor.getModel()) @@ -159,13 +159,14 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { // TextEditor if (isITextModel(editorModel)) { - const dirtyDiffModel = this._dirtyDiffModelService.getDirtyDiffModel(editorModelUri); - if (!dirtyDiffModel) { + const dirtyDiffModelRef = this._dirtyDiffModelService.createDirtyDiffModelReference(editorModelUri); + if (!dirtyDiffModelRef) { return constObservable(undefined); } - return observableFromEvent(this, dirtyDiffModel.onDidChange, () => { - return dirtyDiffModel.getQuickDiffResults() + toDispose.push(dirtyDiffModelRef); + return observableFromEvent(this, dirtyDiffModelRef.object.onDidChange, () => { + return dirtyDiffModelRef.object.getQuickDiffResults() .map(result => ({ original: result.original, modified: result.modified, @@ -178,13 +179,14 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { // we can provide multiple "original resources" to diff with the modified // resource. const diffAlgorithm = this._configurationService.getValue('diffEditor.diffAlgorithm'); - const dirtyDiffModel = this._dirtyDiffModelService.getDiffModel(editorModelUri, diffAlgorithm); - if (!dirtyDiffModel) { + const dirtyDiffModelRef = this._dirtyDiffModelService.createDiffModelReference(editorModelUri, diffAlgorithm); + if (!dirtyDiffModelRef) { return constObservable(undefined); } - return observableFromEvent(Event.any(dirtyDiffModel.onDidChange, diffEditor.onDidUpdateDiff), () => { - const dirtyDiffInformation = dirtyDiffModel.getQuickDiffResults() + toDispose.push(dirtyDiffModelRef); + return observableFromEvent(Event.any(dirtyDiffModelRef.object.onDidChange, diffEditor.onDidUpdateDiff), () => { + const dirtyDiffInformation = dirtyDiffModelRef.object.getQuickDiffResults() .map(result => ({ original: result.original, modified: result.modified, @@ -389,11 +391,19 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { return Promise.resolve([]); } - const dirtyDiffModel = this._dirtyDiffModelService.getDirtyDiffModel(codeEditor.getModel().uri); - const scmQuickDiff = dirtyDiffModel?.quickDiffs.find(quickDiff => quickDiff.isSCM); - const scmQuickDiffChanges = dirtyDiffModel?.changes.filter(change => change.label === scmQuickDiff?.label); + const dirtyDiffModelRef = this._dirtyDiffModelService.createDirtyDiffModelReference(codeEditor.getModel().uri); + if (!dirtyDiffModelRef) { + return Promise.resolve([]); + } - return Promise.resolve(scmQuickDiffChanges?.map(change => change.change) ?? []); + try { + const scmQuickDiff = dirtyDiffModelRef.object.quickDiffs.find(quickDiff => quickDiff.isSCM); + const scmQuickDiffChanges = dirtyDiffModelRef.object.changes.filter(change => change.label === scmQuickDiff?.label); + + return Promise.resolve(scmQuickDiffChanges.map(change => change.change) ?? []); + } finally { + dirtyDiffModelRef.dispose(); + } } } diff --git a/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts b/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts index 230fc9d2126cc..015cad1161cdb 100644 --- a/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts @@ -125,10 +125,10 @@ suite('MainThreadDocumentsAndEditors', () => { new TestPathService(), new TestConfigurationService(), new class extends mock() { - override getDirtyDiffModel() { + override createDiffModelReference() { return undefined; } - override getDiffModel() { + override createDirtyDiffModelReference() { return undefined; } } diff --git a/src/vs/workbench/contrib/scm/browser/dirtyDiffModel.ts b/src/vs/workbench/contrib/scm/browser/dirtyDiffModel.ts index 3b49650a294a8..9d9c1d395aa04 100644 --- a/src/vs/workbench/contrib/scm/browser/dirtyDiffModel.ts +++ b/src/vs/workbench/contrib/scm/browser/dirtyDiffModel.ts @@ -6,10 +6,7 @@ import { ResourceMap } from '../../../../base/common/map.js'; import { createDecorator, IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { EncodingMode, IResolvedTextFileEditorModel, isTextFileEditorModel, ITextFileEditorModel, ITextFileService } from '../../../services/textfile/common/textfiles.js'; -import { Disposable, DisposableMap, DisposableStore } from '../../../../base/common/lifecycle.js'; -import { IEditorService } from '../../../services/editor/common/editorService.js'; -import { autorun, observableFromEvent } from '../../../../base/common/observable.js'; -import { isCodeEditor, isDiffEditor } from '../../../../editor/browser/editorBrowser.js'; +import { Disposable, DisposableMap, DisposableStore, IReference, ReferenceCollection } from '../../../../base/common/lifecycle.js'; import { DiffAlgorithmName, IEditorWorkerService } from '../../../../editor/common/services/editorWorker.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { URI } from '../../../../base/common/uri.js'; @@ -38,108 +35,73 @@ export interface IDirtyDiffModelService { _serviceBrand: undefined; /** - * Returns `undefined` if the editor model is not resolved - * @param uri + * Returns `undefined` if the editor model is not resolved. + * Model refrence has to be disposed once not needed anymore. + * @param resource + * @param algorithm */ - getDirtyDiffModel(uri: URI): DirtyDiffModel | undefined; + createDiffModelReference(resource: URI, algorithm: DiffAlgorithmName): IReference | undefined; /** - * Returns `undefined` if the editor model is not resolved - * @param uri - * @param algorithm + * Returns `undefined` if the editor model is not resolved. + * Model refrence has to be disposed once not needed anymore. + * @param resource */ - getDiffModel(uri: URI, algorithm: DiffAlgorithmName): DirtyDiffModel | undefined; + createDirtyDiffModelReference(resource: URI): IReference | undefined; } -export class DirtyDiffModelService extends Disposable implements IDirtyDiffModelService { - _serviceBrand: undefined; +class DirtyDiffModelReferenceCollection extends ReferenceCollection { + constructor(@IInstantiationService private readonly _instantiationService: IInstantiationService) { + super(); + } - private readonly _dirtyDiffModels = new ResourceMap(); - private readonly _diffModels = new ResourceMap>(); + protected override createReferencedObject(_key: string, textFileModel: IResolvedTextFileEditorModel, algorithm: DiffAlgorithmName | undefined): DirtyDiffModel { + return this._instantiationService.createInstance(DirtyDiffModel, textFileModel, algorithm); + } - private _visibleTextEditorControls = observableFromEvent( - this.editorService.onDidVisibleEditorsChange, - () => this.editorService.visibleTextEditorControls); + protected override destroyReferencedObject(_key: string, object: DirtyDiffModel): void { + object.dispose(); + } +} + +export class DirtyDiffModelService implements IDirtyDiffModelService { + _serviceBrand: undefined; + + private readonly _references: DirtyDiffModelReferenceCollection; constructor( - @IEditorService private readonly editorService: IEditorService, @IInstantiationService private readonly instantiationService: IInstantiationService, @ITextFileService private readonly textFileService: ITextFileService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService ) { - super(); - - this._register(autorun(reader => { - const visibleTextEditorControls = this._visibleTextEditorControls.read(reader); - - // Dispose dirty diff models for text editors that are not visible - for (const [uri, dirtyDiffModel] of this._dirtyDiffModels) { - const textEditorControl = visibleTextEditorControls - .find(editor => isCodeEditor(editor) && - this.uriIdentityService.extUri.isEqual(editor.getModel()?.uri, uri)); - - if (textEditorControl) { - continue; - } - - dirtyDiffModel.dispose(); - this._dirtyDiffModels.delete(uri); - } - - // Dispose diff models for diff editors that are not visible - for (const [uri, dirtyDiffModel] of this._diffModels) { - const diffEditorControl = visibleTextEditorControls - .find(editor => isDiffEditor(editor) && - this.uriIdentityService.extUri.isEqual(editor.getModel()?.modified.uri, uri)); - - if (diffEditorControl) { - continue; - } - - for (const algorithm of dirtyDiffModel.keys()) { - dirtyDiffModel.get(algorithm)?.dispose(); - dirtyDiffModel.delete(algorithm); - } - this._diffModels.delete(uri); - } - })); + this._references = this.instantiationService.createInstance(DirtyDiffModelReferenceCollection); } - getDirtyDiffModel(uri: URI): DirtyDiffModel | undefined { - let model = this._dirtyDiffModels.get(uri); - if (model) { - return model; - } - - const textFileModel = this.textFileService.files.get(uri); + createDiffModelReference(resource: URI, algorithm: DiffAlgorithmName): IReference | undefined { + const textFileModel = this.textFileService.files.get(resource); if (!textFileModel?.isResolved()) { return undefined; } - model = this.instantiationService.createInstance(DirtyDiffModel, textFileModel, undefined); - this._dirtyDiffModels.set(uri, model); - - return model; + return this._createModelReference(resource, textFileModel, algorithm); } - getDiffModel(uri: URI, algorithm: DiffAlgorithmName): DirtyDiffModel | undefined { - let model = this._diffModels.get(uri)?.get(algorithm); - if (model) { - return model; - } - - const textFileModel = this.textFileService.files.get(uri); + createDirtyDiffModelReference(resource: URI): IReference | undefined { + const textFileModel = this.textFileService.files.get(resource); if (!textFileModel?.isResolved()) { return undefined; } - model = this.instantiationService.createInstance(DirtyDiffModel, textFileModel, algorithm); - if (!this._diffModels.has(uri)) { - this._diffModels.set(uri, new Map()); - } - this._diffModels.get(uri)!.set(algorithm, model); + return this._createModelReference(resource, textFileModel, undefined); + } + + private _createModelReference(resource: URI, textFileModel: IResolvedTextFileEditorModel, algorithm: DiffAlgorithmName | undefined): IReference { + resource = algorithm === undefined + ? this.uriIdentityService.asCanonicalUri(resource) + : this.uriIdentityService.asCanonicalUri(resource) + .with({ query: `algorithm=${algorithm}` }); - return model; + return this._references.acquire(resource.toString(), textFileModel, algorithm); } } diff --git a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts index 473c2f5d41814..f14fd38555f5a 100644 --- a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts +++ b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts @@ -6,7 +6,7 @@ import * as nls from '../../../../nls.js'; import './media/dirtydiffDecorator.css'; -import { IDisposable, toDisposable, Disposable, DisposableStore, DisposableMap } from '../../../../base/common/lifecycle.js'; +import { IDisposable, toDisposable, Disposable, DisposableStore, DisposableMap, IReference } from '../../../../base/common/lifecycle.js'; import { Event } from '../../../../base/common/event.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; @@ -533,16 +533,20 @@ export class GotoPreviousChangeAction extends EditorAction { return; } - const model = dirtyDiffModelService.getDirtyDiffModel(outerEditor.getModel().uri); - if (!model || model.changes.length === 0) { - return; - } + const modelRef = dirtyDiffModelService.createDirtyDiffModelReference(outerEditor.getModel().uri); + try { + if (!modelRef || modelRef.object.changes.length === 0) { + return; + } - const lineNumber = outerEditor.getPosition().lineNumber; - const index = model.findPreviousClosestChange(lineNumber, false); - const change = model.changes[index]; - await playAccessibilitySymbolForChange(change.change, accessibilitySignalService); - setPositionAndSelection(change.change, outerEditor, accessibilityService, codeEditorService); + const lineNumber = outerEditor.getPosition().lineNumber; + const index = modelRef.object.findPreviousClosestChange(lineNumber, false); + const change = modelRef.object.changes[index]; + await playAccessibilitySymbolForChange(change.change, accessibilitySignalService); + setPositionAndSelection(change.change, outerEditor, accessibilityService, codeEditorService); + } finally { + modelRef?.dispose(); + } } } registerEditorAction(GotoPreviousChangeAction); @@ -569,17 +573,20 @@ export class GotoNextChangeAction extends EditorAction { return; } - const model = dirtyDiffModelService.getDirtyDiffModel(outerEditor.getModel().uri); + const modelRef = dirtyDiffModelService.createDirtyDiffModelReference(outerEditor.getModel().uri); + try { + if (!modelRef || modelRef.object.changes.length === 0) { + return; + } - if (!model || model.changes.length === 0) { - return; + const lineNumber = outerEditor.getPosition().lineNumber; + const index = modelRef.object.findNextClosestChange(lineNumber, false); + const change = modelRef.object.changes[index].change; + await playAccessibilitySymbolForChange(change, accessibilitySignalService); + setPositionAndSelection(change, outerEditor, accessibilityService, codeEditorService); + } finally { + modelRef?.dispose(); } - - const lineNumber = outerEditor.getPosition().lineNumber; - const index = model.findNextClosestChange(lineNumber, false); - const change = model.changes[index].change; - await playAccessibilitySymbolForChange(change, accessibilitySignalService); - setPositionAndSelection(change, outerEditor, accessibilityService, codeEditorService); } } @@ -774,29 +781,31 @@ export class DirtyDiffController extends Disposable implements IEditorContributi return false; } - const model = this.dirtyDiffModelService.getDirtyDiffModel(editorModel.uri); + const modelRef = this.dirtyDiffModelService.createDirtyDiffModelReference(editorModel.uri); - if (!model) { + if (!modelRef) { return false; } - if (model.changes.length === 0) { + if (modelRef.object.changes.length === 0) { + modelRef.dispose(); return false; } - this.model = model; - this.widget = this.instantiationService.createInstance(DirtyDiffWidget, this.editor, model); + this.model = modelRef.object; + this.widget = this.instantiationService.createInstance(DirtyDiffWidget, this.editor, this.model); this.isDirtyDiffVisible.set(true); const disposables = new DisposableStore(); disposables.add(Event.once(this.widget.onDidClose)(this.close, this)); - const onDidModelChange = Event.chain(model.onDidChange, $ => + const onDidModelChange = Event.chain(this.model.onDidChange, $ => $.filter(e => e.diff.length > 0) .map(e => e.diff) ); onDidModelChange(this.onDidModelChange, this, disposables); + disposables.add(modelRef); disposables.add(this.widget); disposables.add(toDisposable(() => { this.model = null; @@ -883,22 +892,27 @@ export class DirtyDiffController extends Disposable implements IEditorContributi return; } - const model = this.dirtyDiffModelService.getDirtyDiffModel(editorModel.uri); + const modelRef = this.dirtyDiffModelService.createDirtyDiffModelReference(editorModel.uri); - if (!model) { + if (!modelRef) { return; } - const index = model.changes.findIndex(change => lineIntersectsChange(lineNumber, change.change)); + try { + const index = modelRef.object.changes + .findIndex(change => lineIntersectsChange(lineNumber, change.change)); - if (index < 0) { - return; - } + if (index < 0) { + return; + } - if (index === this.widget?.index) { - this.close(); - } else { - this.next(lineNumber); + if (index === this.widget?.index) { + this.close(); + } else { + this.next(lineNumber); + } + } finally { + modelRef.dispose(); } } @@ -973,7 +987,7 @@ class DirtyDiffDecorator extends Disposable { constructor( private readonly codeEditor: ICodeEditor, - private readonly dirtyDiffModel: DirtyDiffModel, + private readonly dirtyDiffModelRef: IReference, @IConfigurationService private readonly configurationService: IConfigurationService ) { super(); @@ -1022,7 +1036,7 @@ class DirtyDiffDecorator extends Disposable { } })); - this._register(Event.runAndSubscribe(dirtyDiffModel.onDidChange, () => this.onDidChange())); + this._register(Event.runAndSubscribe(this.dirtyDiffModelRef.object.onDidChange, () => this.onDidChange())); } private onDidChange(): void { @@ -1030,10 +1044,10 @@ class DirtyDiffDecorator extends Disposable { return; } - const visibleQuickDiffs = this.dirtyDiffModel.quickDiffs.filter(quickDiff => quickDiff.visible); + const visibleQuickDiffs = this.dirtyDiffModelRef.object.quickDiffs.filter(quickDiff => quickDiff.visible); const pattern = this.configurationService.getValue<{ added: boolean; modified: boolean }>('scm.diffDecorationsGutterPattern'); - const decorations = this.dirtyDiffModel.changes + const decorations = this.dirtyDiffModelRef.object.changes .filter(labeledChange => visibleQuickDiffs.some(quickDiff => quickDiff.label === labeledChange.label)) .map((labeledChange) => { const change = labeledChange.change; @@ -1080,8 +1094,8 @@ class DirtyDiffDecorator extends Disposable { if (this.decorationsCollection) { this.decorationsCollection?.clear(); } - this.decorationsCollection = undefined; + this.dirtyDiffModelRef.dispose(); super.dispose(); } } @@ -1217,8 +1231,8 @@ export class DirtyDiffWorkbenchController extends Disposable implements IWorkben continue; } - const dirtyDiffModel = this.dirtyDiffModelService.getDirtyDiffModel(textModel.uri); - if (!dirtyDiffModel) { + const dirtyDiffModelRef = this.dirtyDiffModelService.createDirtyDiffModelReference(textModel.uri); + if (!dirtyDiffModelRef) { continue; } @@ -1226,10 +1240,10 @@ export class DirtyDiffWorkbenchController extends Disposable implements IWorkben this.decorators.set(textModel.uri, new DisposableMap()); } - this.decorators.get(textModel.uri)!.set(editorId, new DirtyDiffDecorator(editor, dirtyDiffModel, this.configurationService)); + this.decorators.get(textModel.uri)!.set(editorId, new DirtyDiffDecorator(editor, dirtyDiffModelRef, this.configurationService)); } - // Dispose decorators for editors that are no longer visible + // Dispose decorators for editors that are no longer visible. for (const [uri, decoratorMap] of this.decorators.entries()) { for (const editorId of decoratorMap.keys()) { const codeEditor = this.editorService.visibleTextEditorControls From 291a57a4c7e00fa33a39257b24bf1002a6ea70f6 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Mon, 9 Dec 2024 10:16:36 +0100 Subject: [PATCH 06/22] add docs/desc for atEnd context key (#235595) https://github.com/microsoft/vscode/issues/205415 --- src/vs/editor/contrib/suggest/browser/wordContextKey.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/contrib/suggest/browser/wordContextKey.ts b/src/vs/editor/contrib/suggest/browser/wordContextKey.ts index 901b440d8a5f5..65820c8dda480 100644 --- a/src/vs/editor/contrib/suggest/browser/wordContextKey.ts +++ b/src/vs/editor/contrib/suggest/browser/wordContextKey.ts @@ -7,10 +7,11 @@ import { IDisposable } from '../../../../base/common/lifecycle.js'; import { ICodeEditor } from '../../../browser/editorBrowser.js'; import { EditorOption } from '../../../common/config/editorOptions.js'; import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; +import { localize } from '../../../../nls.js'; export class WordContextKey { - static readonly AtEnd = new RawContextKey('atEndOfWord', false); + static readonly AtEnd = new RawContextKey('atEndOfWord', false, { type: 'boolean', description: localize('desc', "A context key that is true when at the end of a word. Note that this is only defined when tab-completions are enabled") }); private readonly _ckAtEnd: IContextKey; private readonly _configListener: IDisposable; From 18bd4c58c7566a8bd68c3891ec1870cb653e867c Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Mon, 9 Dec 2024 10:26:42 +0100 Subject: [PATCH 07/22] Add more detail to expandable-hover setting description (#235596) * adding more detail to hover * polish --- extensions/typescript-language-features/package.nls.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/typescript-language-features/package.nls.json b/extensions/typescript-language-features/package.nls.json index 1e5ca174b55ab..6a7dc26a24d63 100644 --- a/extensions/typescript-language-features/package.nls.json +++ b/extensions/typescript-language-features/package.nls.json @@ -225,7 +225,7 @@ "configuration.tsserver.web.typeAcquisition.enabled": "Enable/disable package acquisition on the web. This enables IntelliSense for imported packages. Requires `#typescript.tsserver.web.projectWideIntellisense.enabled#`. Currently not supported for Safari.", "configuration.tsserver.nodePath": "Run TS Server on a custom Node installation. This can be a path to a Node executable, or 'node' if you want VS Code to detect a Node installation.", "configuration.updateImportsOnPaste": "Enable updating imports when pasting code. Requires TypeScript 5.7+.\n\nBy default this shows a option to update imports after pasting. You can use the `#editor.pasteAs.preferences#` setting to update imports automatically when pasting: `\"editor.pasteAs.preferences\": [ \"text.updateImports.jsts\" ]`.", - "configuration.expandableHover": "Enable/disable expanding on hover.", + "configuration.expandableHover": "Enable expanding/contracting the hover to reveal more/less information from the TS server.", "walkthroughs.nodejsWelcome.title": "Get started with JavaScript and Node.js", "walkthroughs.nodejsWelcome.description": "Make the most of Visual Studio Code's first-class JavaScript experience.", "walkthroughs.nodejsWelcome.downloadNode.forMacOrWindows.title": "Install Node.js", From 36cd80a3a3f9a2b5af47d50d8ac0edc4fd8cb502 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Mon, 9 Dec 2024 10:49:52 +0100 Subject: [PATCH 08/22] editor sticky scroll: only collapse outermost region with folding icon (#235597) only collapse the given region --- .../contrib/stickyScroll/browser/stickyScrollController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts index 3387930fcd27b..4f94c67055219 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts @@ -394,7 +394,7 @@ export class StickyScrollController extends Disposable implements IEditorContrib if (!foldingIcon) { return; } - toggleCollapseState(this._foldingModel, Number.MAX_VALUE, [line]); + toggleCollapseState(this._foldingModel, 1, [line]); foldingIcon.isCollapsed = !foldingIcon.isCollapsed; const scrollTop = (foldingIcon.isCollapsed ? this._editor.getTopForLineNumber(foldingIcon.foldingEndLine) From 5410686643f746d444c3379690113a84351ec9e5 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Mon, 9 Dec 2024 11:09:09 +0100 Subject: [PATCH 09/22] know and honour `TextEditorSelectionSource` (#235599) fixes https://github.com/microsoft/vscode/issues/230928 --- src/vs/workbench/api/common/extHostTypes.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 6cdb305f07c44..90f82a69ce06e 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -22,6 +22,7 @@ import { FileSystemProviderErrorCode, markAsFileSystemProviderError } from '../. import { RemoteAuthorityResolverErrorCode } from '../../../platform/remote/common/remoteAuthorityResolver.js'; import { CellEditType, ICellMetadataEdit, IDocumentMetadataEdit, isTextStreamMime } from '../../contrib/notebook/common/notebookCommon.js'; import { IRelativePatternDto } from './extHost.protocol.js'; +import { TextEditorSelectionSource } from '../../../platform/editor/common/editor.js'; /** * @deprecated @@ -1943,11 +1944,15 @@ export enum DecorationRangeBehavior { } export namespace TextEditorSelectionChangeKind { - export function fromValue(s: string | undefined) { + export function fromValue(s: TextEditorSelectionSource | string | undefined) { switch (s) { case 'keyboard': return TextEditorSelectionChangeKind.Keyboard; case 'mouse': return TextEditorSelectionChangeKind.Mouse; - case 'api': return TextEditorSelectionChangeKind.Command; + case 'api': + case TextEditorSelectionSource.PROGRAMMATIC: + case TextEditorSelectionSource.JUMP: + case TextEditorSelectionSource.NAVIGATION: + return TextEditorSelectionChangeKind.Command; } return undefined; } From 5ae456f7f1149a933742968ec7c00904f0e5a069 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 9 Dec 2024 11:31:45 +0100 Subject: [PATCH 10/22] chat - setup tweaks (#235600) --- .../contrib/chat/browser/chatSetup.ts | 39 +++++++------------ .../chat/browser/media/chatViewSetup.css | 8 ++-- 2 files changed, 18 insertions(+), 29 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index 6d5104baddf58..485d1489dfd0d 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -23,7 +23,7 @@ import { localize, localize2 } from '../../../../nls.js'; import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { ContextKeyExpr, IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { IExtensionManagementService } from '../../../../platform/extensionManagement/common/extensionManagement.js'; @@ -67,6 +67,7 @@ const defaultChat = { termsStatementUrl: product.defaultChatAgent?.termsStatementUrl ?? '', privacyStatementUrl: product.defaultChatAgent?.privacyStatementUrl ?? '', skusDocumentationUrl: product.defaultChatAgent?.skusDocumentationUrl ?? '', + upgradePlanUrl: product.defaultChatAgent?.upgradePlanUrl ?? '', providerId: product.defaultChatAgent?.providerId ?? '', providerName: product.defaultChatAgent?.providerName ?? '', providerScopes: product.defaultChatAgent?.providerScopes ?? [[]], @@ -260,14 +261,13 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr override async run(accessor: ServicesAccessor): Promise { const openerService = accessor.get(IOpenerService); - const productService = accessor.get(IProductService); const telemetryService = accessor.get(ITelemetryService); const hostService = accessor.get(IHostService); const commandService = accessor.get(ICommandService); telemetryService.publicLog2('workbenchActionExecuted', { id: this.desc.id, from: 'chat' }); - openerService.open(URI.parse(productService.defaultChatAgent?.upgradePlanUrl ?? '')); + openerService.open(URI.parse(defaultChat.upgradePlanUrl)); const entitlement = that.context.state.entitlement; if (entitlement !== ChatEntitlement.Pro) { @@ -869,12 +869,6 @@ class ChatSetupWelcomeContent extends Disposable { const limitedSkuHeaderContainer = this.element.appendChild($('p')); limitedSkuHeaderContainer.appendChild(this._register(markdown.render(new MarkdownString(limitedSkuHeader, { isTrusted: true, supportThemeIcons: true }))).element); - // Terms - const terms = localize({ key: 'termsLabel', comment: ['{Locked="["}', '{Locked="]({0})"}', '{Locked="]({1})"}'] }, "By continuing, you agree to our [Terms]({0}) and [Privacy Policy]({1}).", defaultChat.termsStatementUrl, defaultChat.privacyStatementUrl); - const termsContainer = this.element.appendChild($('p')); - termsContainer.classList.add('terms-container'); - termsContainer.appendChild(this._register(markdown.render(new MarkdownString(terms, { isTrusted: true }))).element); - // Setup Button const actions: IAction[] = []; if (this.context.state.installed) { @@ -882,6 +876,7 @@ class ChatSetupWelcomeContent extends Disposable { actions.push(toAction({ id: 'chatSetup.signInGhe', label: localize('signInGhe', "Sign in with a GHE.com Account"), run: () => this.commandService.executeCommand('github.copilotChat.signInGHE') })); } const buttonContainer = this.element.appendChild($('p')); + buttonContainer.classList.add('button-container'); const button = this._register(actions.length === 0 ? new Button(buttonContainer, { supportIcons: true, ...defaultButtonStyles @@ -894,6 +889,10 @@ class ChatSetupWelcomeContent extends Disposable { })); this._register(button.onDidClick(() => this.controller.setup())); + // Terms + const terms = localize({ key: 'termsLabel', comment: ['{Locked="["}', '{Locked="]({0})"}', '{Locked="]({1})"}'] }, "By continuing, you agree to the [Terms]({0}) and [Privacy Policy]({1}).", defaultChat.termsStatementUrl, defaultChat.privacyStatementUrl); + this.element.appendChild($('p')).appendChild(this._register(markdown.render(new MarkdownString(terms, { isTrusted: true }))).element); + // Update based on model state this._register(Event.runAndSubscribe(this.controller.onDidChange, () => this.update(limitedSkuHeaderContainer, button))); } @@ -1056,23 +1055,13 @@ class ChatSetupContext extends Disposable { this.storageService.remove('interactive.sessions', this.workspaceContextService.getWorkspace().folders.length ? StorageScope.WORKSPACE : StorageScope.APPLICATION); } - let changed = false; - changed = this.updateContextKey(this.signedOutContextKey, this._state.entitlement === ChatEntitlement.Unknown) || changed; - changed = this.updateContextKey(this.canSignUpContextKey, this._state.entitlement === ChatEntitlement.Available) || changed; - changed = this.updateContextKey(this.limitedContextKey, this._state.entitlement === ChatEntitlement.Limited) || changed; - changed = this.updateContextKey(this.triggeredContext, !!this._state.triggered) || changed; - changed = this.updateContextKey(this.installedContext, !!this._state.installed) || changed; + this.signedOutContextKey.set(this._state.entitlement === ChatEntitlement.Unknown); + this.canSignUpContextKey.set(this._state.entitlement === ChatEntitlement.Available); + this.limitedContextKey.set(this._state.entitlement === ChatEntitlement.Limited); + this.triggeredContext.set(!!this._state.triggered); + this.installedContext.set(!!this._state.installed); - if (changed) { - this._onDidChange.fire(); - } - } - - private updateContextKey(contextKey: IContextKey, value: boolean): boolean { - const current = contextKey.get(); - contextKey.set(value); - - return current !== value; + this._onDidChange.fire(); } suspend(): void { diff --git a/src/vs/workbench/contrib/chat/browser/media/chatViewSetup.css b/src/vs/workbench/contrib/chat/browser/media/chatViewSetup.css index 97a06eac5be10..8b3a81f428e78 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatViewSetup.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatViewSetup.css @@ -17,10 +17,6 @@ background-color: var(--vscode-chat-requestBackground); } - .terms-container { - padding-top: 5px; - } - .chat-feature-container { display: flex; align-items: center; @@ -38,6 +34,10 @@ vertical-align: bottom; } + .button-container { + padding-top: 20px; + } + /** Dropdown Button */ .monaco-button-dropdown { width: 100%; From 0e9de87e69958bd3fd59a68374fb8b65bb04038c Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 9 Dec 2024 11:35:20 +0100 Subject: [PATCH 11/22] SCM - rename dirtyDiff to quickDiff for consistency (#235601) * Rename dirtyDiff -> quickDiff for consistency * More renames * Extract quick diff widget into separate file --- .../browser/mainThreadDocumentsAndEditors.ts | 6 +- .../api/browser/mainThreadEditors.ts | 36 +- .../mainThreadDocumentsAndEditors.test.ts | 9 +- .../chat/browser/chatEditorController.ts | 2 +- .../contrib/format/browser/formatModified.ts | 3 +- .../chatEdit/notebookCellDecorators.ts | 2 +- .../contrib/scm/browser/dirtyDiffSwitcher.ts | 72 -- .../contrib/scm/browser/quickDiffDecorator.ts | 338 +++++++ .../{dirtyDiffModel.ts => quickDiffModel.ts} | 131 +-- ...rtydiffDecorator.ts => quickDiffWidget.ts} | 888 ++++++------------ .../contrib/scm/browser/scm.contribution.ts | 13 +- .../workbench/contrib/scm/common/quickDiff.ts | 108 +++ .../contrib/scm/common/quickDiffService.ts | 5 + 13 files changed, 801 insertions(+), 812 deletions(-) delete mode 100644 src/vs/workbench/contrib/scm/browser/dirtyDiffSwitcher.ts create mode 100644 src/vs/workbench/contrib/scm/browser/quickDiffDecorator.ts rename src/vs/workbench/contrib/scm/browser/{dirtyDiffModel.ts => quickDiffModel.ts} (79%) rename src/vs/workbench/contrib/scm/browser/{dirtydiffDecorator.ts => quickDiffWidget.ts} (58%) diff --git a/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts b/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts index 992229b91b760..66ae21fb2f8ee 100644 --- a/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts @@ -32,7 +32,7 @@ import { diffSets, diffMaps } from '../../../base/common/collections.js'; import { IPaneCompositePartService } from '../../services/panecomposite/browser/panecomposite.js'; import { ViewContainerLocation } from '../../common/views.js'; import { IConfigurationService } from '../../../platform/configuration/common/configuration.js'; -import { IDirtyDiffModelService } from '../../contrib/scm/browser/dirtyDiffModel.js'; +import { IQuickDiffModelService } from '../../contrib/scm/browser/quickDiffModel.js'; class TextEditorSnapshot { @@ -298,14 +298,14 @@ export class MainThreadDocumentsAndEditors { @IClipboardService private readonly _clipboardService: IClipboardService, @IPathService pathService: IPathService, @IConfigurationService configurationService: IConfigurationService, - @IDirtyDiffModelService dirtyDiffModelService: IDirtyDiffModelService + @IQuickDiffModelService quickDiffModelService: IQuickDiffModelService ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostDocumentsAndEditors); this._mainThreadDocuments = this._toDispose.add(new MainThreadDocuments(extHostContext, this._modelService, this._textFileService, fileService, textModelResolverService, environmentService, uriIdentityService, workingCopyFileService, pathService)); extHostContext.set(MainContext.MainThreadDocuments, this._mainThreadDocuments); - this._mainThreadEditors = this._toDispose.add(new MainThreadTextEditors(this, extHostContext, codeEditorService, this._editorService, this._editorGroupService, configurationService, dirtyDiffModelService, uriIdentityService)); + this._mainThreadEditors = this._toDispose.add(new MainThreadTextEditors(this, extHostContext, codeEditorService, this._editorService, this._editorGroupService, configurationService, quickDiffModelService, uriIdentityService)); extHostContext.set(MainContext.MainThreadTextEditors, this._mainThreadEditors); // It is expected that the ctor of the state computer calls our `_onDelta`. diff --git a/src/vs/workbench/api/browser/mainThreadEditors.ts b/src/vs/workbench/api/browser/mainThreadEditors.ts index 024a09f283a7a..a61ba3c780107 100644 --- a/src/vs/workbench/api/browser/mainThreadEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadEditors.ts @@ -28,7 +28,7 @@ import { IExtHostContext } from '../../services/extensions/common/extHostCustome import { IEditorControl } from '../../common/editor.js'; import { getCodeEditor, ICodeEditor } from '../../../editor/browser/editorBrowser.js'; import { IConfigurationService } from '../../../platform/configuration/common/configuration.js'; -import { IDirtyDiffModelService } from '../../contrib/scm/browser/dirtyDiffModel.js'; +import { IQuickDiffModelService } from '../../contrib/scm/browser/quickDiffModel.js'; import { autorun, constObservable, derived, derivedOpts, IObservable, observableFromEvent } from '../../../base/common/observable.js'; import { IUriIdentityService } from '../../../platform/uriIdentity/common/uriIdentity.js'; import { isITextModel } from '../../../editor/common/model.js'; @@ -61,7 +61,7 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { @IEditorService private readonly _editorService: IEditorService, @IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService, @IConfigurationService private readonly _configurationService: IConfigurationService, - @IDirtyDiffModelService private readonly _dirtyDiffModelService: IDirtyDiffModelService, + @IQuickDiffModelService private readonly _quickDiffModelService: IQuickDiffModelService, @IUriIdentityService private readonly _uriIdentityService: IUriIdentityService ) { this._instanceId = String(++MainThreadTextEditors.INSTANCE_COUNT); @@ -159,14 +159,14 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { // TextEditor if (isITextModel(editorModel)) { - const dirtyDiffModelRef = this._dirtyDiffModelService.createDirtyDiffModelReference(editorModelUri); - if (!dirtyDiffModelRef) { + const quickDiffModelRef = this._quickDiffModelService.createQuickDiffModelReference(editorModelUri); + if (!quickDiffModelRef) { return constObservable(undefined); } - toDispose.push(dirtyDiffModelRef); - return observableFromEvent(this, dirtyDiffModelRef.object.onDidChange, () => { - return dirtyDiffModelRef.object.getQuickDiffResults() + toDispose.push(quickDiffModelRef); + return observableFromEvent(this, quickDiffModelRef.object.onDidChange, () => { + return quickDiffModelRef.object.getQuickDiffResults() .map(result => ({ original: result.original, modified: result.modified, @@ -179,14 +179,14 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { // we can provide multiple "original resources" to diff with the modified // resource. const diffAlgorithm = this._configurationService.getValue('diffEditor.diffAlgorithm'); - const dirtyDiffModelRef = this._dirtyDiffModelService.createDiffModelReference(editorModelUri, diffAlgorithm); - if (!dirtyDiffModelRef) { + const quickDiffModelRef = this._quickDiffModelService.createQuickDiffModelReference(editorModelUri, { algorithm: diffAlgorithm }); + if (!quickDiffModelRef) { return constObservable(undefined); } - toDispose.push(dirtyDiffModelRef); - return observableFromEvent(Event.any(dirtyDiffModelRef.object.onDidChange, diffEditor.onDidUpdateDiff), () => { - const dirtyDiffInformation = dirtyDiffModelRef.object.getQuickDiffResults() + toDispose.push(quickDiffModelRef); + return observableFromEvent(Event.any(quickDiffModelRef.object.onDidChange, diffEditor.onDidUpdateDiff), () => { + const quickDiffInformation = quickDiffModelRef.object.getQuickDiffResults() .map(result => ({ original: result.original, modified: result.modified, @@ -200,7 +200,7 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { changes: diffChanges.map(change => change as LineRangeMapping) }]; - return [...dirtyDiffInformation, ...diffInformation]; + return [...quickDiffInformation, ...diffInformation]; }); }); @@ -391,18 +391,18 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { return Promise.resolve([]); } - const dirtyDiffModelRef = this._dirtyDiffModelService.createDirtyDiffModelReference(codeEditor.getModel().uri); - if (!dirtyDiffModelRef) { + const quickDiffModelRef = this._quickDiffModelService.createQuickDiffModelReference(codeEditor.getModel().uri); + if (!quickDiffModelRef) { return Promise.resolve([]); } try { - const scmQuickDiff = dirtyDiffModelRef.object.quickDiffs.find(quickDiff => quickDiff.isSCM); - const scmQuickDiffChanges = dirtyDiffModelRef.object.changes.filter(change => change.label === scmQuickDiff?.label); + const scmQuickDiff = quickDiffModelRef.object.quickDiffs.find(quickDiff => quickDiff.isSCM); + const scmQuickDiffChanges = quickDiffModelRef.object.changes.filter(change => change.label === scmQuickDiff?.label); return Promise.resolve(scmQuickDiffChanges.map(change => change.change) ?? []); } finally { - dirtyDiffModelRef.dispose(); + quickDiffModelRef.dispose(); } } } diff --git a/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts b/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts index 015cad1161cdb..e077dfc7ac7e9 100644 --- a/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts @@ -36,7 +36,7 @@ import { LanguageService } from '../../../../editor/common/services/languageServ import { ILanguageConfigurationService } from '../../../../editor/common/languages/languageConfigurationRegistry.js'; import { TestLanguageConfigurationService } from '../../../../editor/test/common/modes/testLanguageConfigurationService.js'; import { IUndoRedoService } from '../../../../platform/undoRedo/common/undoRedo.js'; -import { IDirtyDiffModelService } from '../../../contrib/scm/browser/dirtyDiffModel.js'; +import { IQuickDiffModelService } from '../../../contrib/scm/browser/quickDiffModel.js'; import { ITextEditorDiffInformation } from '../../../../platform/editor/common/editor.js'; suite('MainThreadDocumentsAndEditors', () => { @@ -124,11 +124,8 @@ suite('MainThreadDocumentsAndEditors', () => { }, new TestPathService(), new TestConfigurationService(), - new class extends mock() { - override createDiffModelReference() { - return undefined; - } - override createDirtyDiffModelReference() { + new class extends mock() { + override createQuickDiffModelReference() { return undefined; } } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorController.ts b/src/vs/workbench/contrib/chat/browser/chatEditorController.ts index c4a60c82cc888..0cca055ece25c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorController.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorController.ts @@ -21,7 +21,6 @@ import { ModelDecorationOptions } from '../../../../editor/common/model/textMode import { InlineDecoration, InlineDecorationType } from '../../../../editor/common/viewModel.js'; import { localize } from '../../../../nls.js'; import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; -import { minimapGutterAddedBackground, minimapGutterDeletedBackground, minimapGutterModifiedBackground, overviewRulerAddedForeground, overviewRulerDeletedForeground, overviewRulerModifiedForeground } from '../../scm/browser/dirtydiffDecorator.js'; import { ChatEditingSessionState, IChatEditingService, IModifiedFileEntry, WorkingSetEntryState } from '../common/chatEditingService.js'; import { Event } from '../../../../base/common/event.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; @@ -31,6 +30,7 @@ import { Position } from '../../../../editor/common/core/position.js'; import { Selection } from '../../../../editor/common/core/selection.js'; import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; import { observableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js'; +import { minimapGutterAddedBackground, minimapGutterDeletedBackground, minimapGutterModifiedBackground, overviewRulerAddedForeground, overviewRulerDeletedForeground, overviewRulerModifiedForeground } from '../../scm/common/quickDiff.js'; export const ctxHasEditorModification = new RawContextKey('chat.hasEditorModifications', undefined, localize('chat.hasEditorModifications', "The current editor contains chat modifications")); export const ctxHasRequestInProgress = new RawContextKey('chat.ctxHasRequestInProgress', false, localize('chat.ctxHasRequestInProgress', "The current editor shows a file from an edit session which is still in progress")); diff --git a/src/vs/workbench/contrib/format/browser/formatModified.ts b/src/vs/workbench/contrib/format/browser/formatModified.ts index bddd514e821e1..d0fce02f1c4c2 100644 --- a/src/vs/workbench/contrib/format/browser/formatModified.ts +++ b/src/vs/workbench/contrib/format/browser/formatModified.ts @@ -17,8 +17,8 @@ import * as nls from '../../../../nls.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { Progress } from '../../../../platform/progress/common/progress.js'; -import { getOriginalResource } from '../../scm/browser/dirtydiffDecorator.js'; import { IQuickDiffService } from '../../scm/common/quickDiff.js'; +import { getOriginalResource } from '../../scm/common/quickDiffService.js'; registerEditorAction(class FormatModifiedAction extends EditorAction { @@ -48,7 +48,6 @@ registerEditorAction(class FormatModifiedAction extends EditorAction { } }); - export async function getModifiedRanges(accessor: ServicesAccessor, modified: ITextModel): Promise { const quickDiffService = accessor.get(IQuickDiffService); const workerService = accessor.get(IEditorWorkerService); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookCellDecorators.ts b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookCellDecorators.ts index 17517c36bff56..81a5b3776ac46 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookCellDecorators.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookCellDecorators.ts @@ -21,7 +21,6 @@ import { IDocumentDiff } from '../../../../../../editor/common/diff/documentDiff import { ITextModel, TrackedRangeStickiness, MinimapPosition, IModelDeltaDecoration, OverviewRulerLane } from '../../../../../../editor/common/model.js'; import { ModelDecorationOptions } from '../../../../../../editor/common/model/textModel.js'; import { InlineDecoration, InlineDecorationType } from '../../../../../../editor/common/viewModel.js'; -import { overviewRulerModifiedForeground, minimapGutterModifiedBackground, overviewRulerAddedForeground, minimapGutterAddedBackground, overviewRulerDeletedForeground, minimapGutterDeletedBackground } from '../../../../scm/browser/dirtydiffDecorator.js'; import { Range } from '../../../../../../editor/common/core/range.js'; import { NotebookCellTextModel } from '../../../common/model/notebookCellTextModel.js'; import { tokenizeToString } from '../../../../../../editor/common/languages/textToHtmlTokenizer.js'; @@ -32,6 +31,7 @@ import { DefaultLineHeight } from '../../diff/diffElementViewModel.js'; import { INotebookOriginalCellModelFactory } from './notebookOriginalCellModelFactory.js'; import { DetailedLineRangeMapping } from '../../../../../../editor/common/diff/rangeMapping.js'; import { isEqual } from '../../../../../../base/common/resources.js'; +import { minimapGutterAddedBackground, minimapGutterDeletedBackground, minimapGutterModifiedBackground, overviewRulerAddedForeground, overviewRulerDeletedForeground, overviewRulerModifiedForeground } from '../../../../scm/common/quickDiff.js'; export class NotebookCellDiffDecorator extends DisposableStore { diff --git a/src/vs/workbench/contrib/scm/browser/dirtyDiffSwitcher.ts b/src/vs/workbench/contrib/scm/browser/dirtyDiffSwitcher.ts deleted file mode 100644 index cf2d4469049bc..0000000000000 --- a/src/vs/workbench/contrib/scm/browser/dirtyDiffSwitcher.ts +++ /dev/null @@ -1,72 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as nls from '../../../../nls.js'; -import { Action, IAction } from '../../../../base/common/actions.js'; -import { IContextViewService } from '../../../../platform/contextview/browser/contextView.js'; -import { ISelectOptionItem } from '../../../../base/browser/ui/selectBox/selectBox.js'; -import { SelectActionViewItem } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; -import { defaultSelectBoxStyles } from '../../../../platform/theme/browser/defaultStyles.js'; -import { IThemeService } from '../../../../platform/theme/common/themeService.js'; -import { peekViewTitleBackground } from '../../../../editor/contrib/peekView/browser/peekView.js'; -import { editorBackground } from '../../../../platform/theme/common/colorRegistry.js'; - -export interface IQuickDiffSelectItem extends ISelectOptionItem { - provider: string; -} - -export class SwitchQuickDiffViewItem extends SelectActionViewItem { - private readonly optionsItems: IQuickDiffSelectItem[]; - - constructor( - action: IAction, - providers: string[], - selected: string, - @IContextViewService contextViewService: IContextViewService, - @IThemeService themeService: IThemeService - ) { - const items = providers.map(provider => ({ provider, text: provider })); - let startingSelection = providers.indexOf(selected); - if (startingSelection === -1) { - startingSelection = 0; - } - const styles = { ...defaultSelectBoxStyles }; - const theme = themeService.getColorTheme(); - const editorBackgroundColor = theme.getColor(editorBackground); - const peekTitleColor = theme.getColor(peekViewTitleBackground); - const opaqueTitleColor = peekTitleColor?.makeOpaque(editorBackgroundColor!) ?? editorBackgroundColor!; - styles.selectBackground = opaqueTitleColor.lighten(.6).toString(); - super(null, action, items, startingSelection, contextViewService, styles, { ariaLabel: nls.localize('remotes', 'Switch quick diff base') }); - this.optionsItems = items; - } - - public setSelection(provider: string) { - const index = this.optionsItems.findIndex(item => item.provider === provider); - this.select(index); - } - - protected override getActionContext(_: string, index: number): IQuickDiffSelectItem { - return this.optionsItems[index]; - } - - override render(container: HTMLElement): void { - super.render(container); - this.setFocusable(true); - } -} - -export class SwitchQuickDiffBaseAction extends Action { - - public static readonly ID = 'quickDiff.base.switch'; - public static readonly LABEL = nls.localize('quickDiff.base.switch', "Switch Quick Diff Base"); - - constructor(private readonly callback: (event?: IQuickDiffSelectItem) => void) { - super(SwitchQuickDiffBaseAction.ID, SwitchQuickDiffBaseAction.LABEL, undefined, undefined); - } - - override async run(event?: IQuickDiffSelectItem): Promise { - return this.callback(event); - } -} diff --git a/src/vs/workbench/contrib/scm/browser/quickDiffDecorator.ts b/src/vs/workbench/contrib/scm/browser/quickDiffDecorator.ts new file mode 100644 index 0000000000000..8e701d3d92c36 --- /dev/null +++ b/src/vs/workbench/contrib/scm/browser/quickDiffDecorator.ts @@ -0,0 +1,338 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from '../../../../nls.js'; + +import './media/dirtydiffDecorator.css'; +import { Disposable, DisposableStore, DisposableMap, IReference } from '../../../../base/common/lifecycle.js'; +import { Event } from '../../../../base/common/event.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { ModelDecorationOptions } from '../../../../editor/common/model/textModel.js'; +import { themeColorFromId } from '../../../../platform/theme/common/themeService.js'; +import { ICodeEditor, isCodeEditor } from '../../../../editor/browser/editorBrowser.js'; +import { IEditorDecorationsCollection } from '../../../../editor/common/editorCommon.js'; +import { OverviewRulerLane, IModelDecorationOptions, MinimapPosition } from '../../../../editor/common/model.js'; +import * as domStylesheetsJs from '../../../../base/browser/domStylesheets.js'; +import { IEditorService } from '../../../services/editor/common/editorService.js'; +import { ChangeType, getChangeType, minimapGutterAddedBackground, minimapGutterDeletedBackground, minimapGutterModifiedBackground, overviewRulerAddedForeground, overviewRulerDeletedForeground, overviewRulerModifiedForeground } from '../common/quickDiff.js'; +import { QuickDiffModel, IQuickDiffModelService } from './quickDiffModel.js'; +import { IWorkbenchContribution } from '../../../common/contributions.js'; +import { ResourceMap } from '../../../../base/common/map.js'; +import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; + +class QuickDiffDecorator extends Disposable { + + static createDecoration(className: string, tooltip: string | null, options: { gutter: boolean; overview: { active: boolean; color: string }; minimap: { active: boolean; color: string }; isWholeLine: boolean }): ModelDecorationOptions { + const decorationOptions: IModelDecorationOptions = { + description: 'dirty-diff-decoration', + isWholeLine: options.isWholeLine, + }; + + if (options.gutter) { + decorationOptions.linesDecorationsClassName = `dirty-diff-glyph ${className}`; + decorationOptions.linesDecorationsTooltip = tooltip; + } + + if (options.overview.active) { + decorationOptions.overviewRuler = { + color: themeColorFromId(options.overview.color), + position: OverviewRulerLane.Left + }; + } + + if (options.minimap.active) { + decorationOptions.minimap = { + color: themeColorFromId(options.minimap.color), + position: MinimapPosition.Gutter + }; + } + + return ModelDecorationOptions.createDynamic(decorationOptions); + } + + private addedOptions: ModelDecorationOptions; + private addedPatternOptions: ModelDecorationOptions; + private modifiedOptions: ModelDecorationOptions; + private modifiedPatternOptions: ModelDecorationOptions; + private deletedOptions: ModelDecorationOptions; + private decorationsCollection: IEditorDecorationsCollection | undefined; + + constructor( + private readonly codeEditor: ICodeEditor, + private readonly quickDiffModelRef: IReference, + @IConfigurationService private readonly configurationService: IConfigurationService + ) { + super(); + + const decorations = configurationService.getValue('scm.diffDecorations'); + const gutter = decorations === 'all' || decorations === 'gutter'; + const overview = decorations === 'all' || decorations === 'overview'; + const minimap = decorations === 'all' || decorations === 'minimap'; + + const diffAdded = nls.localize('diffAdded', 'Added lines'); + this.addedOptions = QuickDiffDecorator.createDecoration('dirty-diff-added', diffAdded, { + gutter, + overview: { active: overview, color: overviewRulerAddedForeground }, + minimap: { active: minimap, color: minimapGutterAddedBackground }, + isWholeLine: true + }); + this.addedPatternOptions = QuickDiffDecorator.createDecoration('dirty-diff-added-pattern', diffAdded, { + gutter, + overview: { active: overview, color: overviewRulerAddedForeground }, + minimap: { active: minimap, color: minimapGutterAddedBackground }, + isWholeLine: true + }); + const diffModified = nls.localize('diffModified', 'Changed lines'); + this.modifiedOptions = QuickDiffDecorator.createDecoration('dirty-diff-modified', diffModified, { + gutter, + overview: { active: overview, color: overviewRulerModifiedForeground }, + minimap: { active: minimap, color: minimapGutterModifiedBackground }, + isWholeLine: true + }); + this.modifiedPatternOptions = QuickDiffDecorator.createDecoration('dirty-diff-modified-pattern', diffModified, { + gutter, + overview: { active: overview, color: overviewRulerModifiedForeground }, + minimap: { active: minimap, color: minimapGutterModifiedBackground }, + isWholeLine: true + }); + this.deletedOptions = QuickDiffDecorator.createDecoration('dirty-diff-deleted', nls.localize('diffDeleted', 'Removed lines'), { + gutter, + overview: { active: overview, color: overviewRulerDeletedForeground }, + minimap: { active: minimap, color: minimapGutterDeletedBackground }, + isWholeLine: false + }); + + this._register(configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('scm.diffDecorationsGutterPattern')) { + this.onDidChange(); + } + })); + + this._register(Event.runAndSubscribe(this.quickDiffModelRef.object.onDidChange, () => this.onDidChange())); + } + + private onDidChange(): void { + if (!this.codeEditor.hasModel()) { + return; + } + + const visibleQuickDiffs = this.quickDiffModelRef.object.quickDiffs.filter(quickDiff => quickDiff.visible); + const pattern = this.configurationService.getValue<{ added: boolean; modified: boolean }>('scm.diffDecorationsGutterPattern'); + + const decorations = this.quickDiffModelRef.object.changes + .filter(labeledChange => visibleQuickDiffs.some(quickDiff => quickDiff.label === labeledChange.label)) + .map((labeledChange) => { + const change = labeledChange.change; + const changeType = getChangeType(change); + const startLineNumber = change.modifiedStartLineNumber; + const endLineNumber = change.modifiedEndLineNumber || startLineNumber; + + switch (changeType) { + case ChangeType.Add: + return { + range: { + startLineNumber: startLineNumber, startColumn: 1, + endLineNumber: endLineNumber, endColumn: 1 + }, + options: pattern.added ? this.addedPatternOptions : this.addedOptions + }; + case ChangeType.Delete: + return { + range: { + startLineNumber: startLineNumber, startColumn: Number.MAX_VALUE, + endLineNumber: startLineNumber, endColumn: Number.MAX_VALUE + }, + options: this.deletedOptions + }; + case ChangeType.Modify: + return { + range: { + startLineNumber: startLineNumber, startColumn: 1, + endLineNumber: endLineNumber, endColumn: 1 + }, + options: pattern.modified ? this.modifiedPatternOptions : this.modifiedOptions + }; + } + }); + + if (!this.decorationsCollection) { + this.decorationsCollection = this.codeEditor.createDecorationsCollection(decorations); + } else { + this.decorationsCollection.set(decorations); + } + } + + override dispose(): void { + if (this.decorationsCollection) { + this.decorationsCollection?.clear(); + } + this.decorationsCollection = undefined; + this.quickDiffModelRef.dispose(); + super.dispose(); + } +} + +interface QuickDiffWorkbenchControllerViewState { + readonly width: number; + readonly visibility: 'always' | 'hover'; +} + +export class QuickDiffWorkbenchController extends Disposable implements IWorkbenchContribution { + + private enabled = false; + + // Resource URI -> Code Editor Id -> Decoration (Disposable) + private readonly decorators = new ResourceMap>(); + private viewState: QuickDiffWorkbenchControllerViewState = { width: 3, visibility: 'always' }; + private readonly transientDisposables = this._register(new DisposableStore()); + private readonly stylesheet: HTMLStyleElement; + + constructor( + @IEditorService private readonly editorService: IEditorService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IQuickDiffModelService private readonly quickDiffModelService: IQuickDiffModelService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + ) { + super(); + this.stylesheet = domStylesheetsJs.createStyleSheet(undefined, undefined, this._store); + + const onDidChangeConfiguration = Event.filter(configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.diffDecorations')); + this._register(onDidChangeConfiguration(this.onDidChangeConfiguration, this)); + this.onDidChangeConfiguration(); + + const onDidChangeDiffWidthConfiguration = Event.filter(configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.diffDecorationsGutterWidth')); + this._register(onDidChangeDiffWidthConfiguration(this.onDidChangeDiffWidthConfiguration, this)); + this.onDidChangeDiffWidthConfiguration(); + + const onDidChangeDiffVisibilityConfiguration = Event.filter(configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.diffDecorationsGutterVisibility')); + this._register(onDidChangeDiffVisibilityConfiguration(this.onDidChangeDiffVisibilityConfiguration, this)); + this.onDidChangeDiffVisibilityConfiguration(); + } + + private onDidChangeConfiguration(): void { + const enabled = this.configurationService.getValue('scm.diffDecorations') !== 'none'; + + if (enabled) { + this.enable(); + } else { + this.disable(); + } + } + + private onDidChangeDiffWidthConfiguration(): void { + let width = this.configurationService.getValue('scm.diffDecorationsGutterWidth'); + + if (isNaN(width) || width <= 0 || width > 5) { + width = 3; + } + + this.setViewState({ ...this.viewState, width }); + } + + private onDidChangeDiffVisibilityConfiguration(): void { + const visibility = this.configurationService.getValue<'always' | 'hover'>('scm.diffDecorationsGutterVisibility'); + this.setViewState({ ...this.viewState, visibility }); + } + + private setViewState(state: QuickDiffWorkbenchControllerViewState): void { + this.viewState = state; + this.stylesheet.textContent = ` + .monaco-editor .dirty-diff-added, + .monaco-editor .dirty-diff-modified { + border-left-width:${state.width}px; + } + .monaco-editor .dirty-diff-added-pattern, + .monaco-editor .dirty-diff-added-pattern:before, + .monaco-editor .dirty-diff-modified-pattern, + .monaco-editor .dirty-diff-modified-pattern:before { + background-size: ${state.width}px ${state.width}px; + } + .monaco-editor .dirty-diff-added, + .monaco-editor .dirty-diff-added-pattern, + .monaco-editor .dirty-diff-modified, + .monaco-editor .dirty-diff-modified-pattern, + .monaco-editor .dirty-diff-deleted { + opacity: ${state.visibility === 'always' ? 1 : 0}; + } + `; + } + + private enable(): void { + if (this.enabled) { + this.disable(); + } + + this.transientDisposables.add(Event.any(this.editorService.onDidCloseEditor, this.editorService.onDidVisibleEditorsChange)(() => this.onEditorsChanged())); + this.onEditorsChanged(); + this.enabled = true; + } + + private disable(): void { + if (!this.enabled) { + return; + } + + this.transientDisposables.clear(); + + for (const [uri, decoratorMap] of this.decorators.entries()) { + decoratorMap.dispose(); + this.decorators.delete(uri); + } + + this.enabled = false; + } + + private onEditorsChanged(): void { + for (const editor of this.editorService.visibleTextEditorControls) { + if (!isCodeEditor(editor)) { + continue; + } + + const textModel = editor.getModel(); + if (!textModel) { + continue; + } + + const editorId = editor.getId(); + if (this.decorators.get(textModel.uri)?.has(editorId)) { + continue; + } + + const quickDiffModelRef = this.quickDiffModelService.createQuickDiffModelReference(textModel.uri); + if (!quickDiffModelRef) { + continue; + } + + if (!this.decorators.has(textModel.uri)) { + this.decorators.set(textModel.uri, new DisposableMap()); + } + + this.decorators.get(textModel.uri)!.set(editorId, new QuickDiffDecorator(editor, quickDiffModelRef, this.configurationService)); + } + + // Dispose decorators for editors that are no longer visible. + for (const [uri, decoratorMap] of this.decorators.entries()) { + for (const editorId of decoratorMap.keys()) { + const codeEditor = this.editorService.visibleTextEditorControls + .find(editor => isCodeEditor(editor) && editor.getId() === editorId && + this.uriIdentityService.extUri.isEqual(editor.getModel()?.uri, uri)); + + if (!codeEditor) { + decoratorMap.deleteAndDispose(editorId); + } + } + + if (decoratorMap.size === 0) { + decoratorMap.dispose(); + this.decorators.delete(uri); + } + } + } + + override dispose(): void { + this.disable(); + super.dispose(); + } +} diff --git a/src/vs/workbench/contrib/scm/browser/dirtyDiffModel.ts b/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts similarity index 79% rename from src/vs/workbench/contrib/scm/browser/dirtyDiffModel.ts rename to src/vs/workbench/contrib/scm/browser/quickDiffModel.ts index 9d9c1d395aa04..e7f97e04082a0 100644 --- a/src/vs/workbench/contrib/scm/browser/dirtyDiffModel.ts +++ b/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts @@ -13,7 +13,7 @@ import { URI } from '../../../../base/common/uri.js'; import { IChange } from '../../../../editor/common/diff/legacyLinesDiffComputer.js'; import { IResolvedTextEditorModel, ITextModelService } from '../../../../editor/common/services/resolverService.js'; import { ITextModel, shouldSynchronizeModel } from '../../../../editor/common/model.js'; -import { IQuickDiffService, QuickDiff, QuickDiffChange, QuickDiffResult } from '../common/quickDiff.js'; +import { compareChanges, getModifiedEndLineNumber, IQuickDiffService, QuickDiff, QuickDiffChange, QuickDiffResult } from '../common/quickDiff.js'; import { ThrottledDelayer } from '../../../../base/common/async.js'; import { ISCMRepository, ISCMService } from '../common/scm.js'; import { sortedDiff, equals } from '../../../../base/common/arrays.js'; @@ -29,83 +29,66 @@ import { IProgressService, ProgressLocation } from '../../../../platform/progres import { IChatEditingService, WorkingSetEntryState } from '../../chat/common/chatEditingService.js'; import { Emitter, Event } from '../../../../base/common/event.js'; -export const IDirtyDiffModelService = createDecorator('IDirtyDiffModelService'); +export const IQuickDiffModelService = createDecorator('IQuickDiffModelService'); -export interface IDirtyDiffModelService { - _serviceBrand: undefined; +export interface QuickDiffModelOptions { + readonly algorithm: DiffAlgorithmName; +} - /** - * Returns `undefined` if the editor model is not resolved. - * Model refrence has to be disposed once not needed anymore. - * @param resource - * @param algorithm - */ - createDiffModelReference(resource: URI, algorithm: DiffAlgorithmName): IReference | undefined; +export interface IQuickDiffModelService { + _serviceBrand: undefined; /** * Returns `undefined` if the editor model is not resolved. * Model refrence has to be disposed once not needed anymore. * @param resource + * @param options */ - createDirtyDiffModelReference(resource: URI): IReference | undefined; + createQuickDiffModelReference(resource: URI, options?: QuickDiffModelOptions): IReference | undefined; } -class DirtyDiffModelReferenceCollection extends ReferenceCollection { +class QuickDiffModelReferenceCollection extends ReferenceCollection { constructor(@IInstantiationService private readonly _instantiationService: IInstantiationService) { super(); } - protected override createReferencedObject(_key: string, textFileModel: IResolvedTextFileEditorModel, algorithm: DiffAlgorithmName | undefined): DirtyDiffModel { - return this._instantiationService.createInstance(DirtyDiffModel, textFileModel, algorithm); + protected override createReferencedObject(_key: string, textFileModel: IResolvedTextFileEditorModel, options?: QuickDiffModelOptions): QuickDiffModel { + return this._instantiationService.createInstance(QuickDiffModel, textFileModel, options); } - protected override destroyReferencedObject(_key: string, object: DirtyDiffModel): void { + protected override destroyReferencedObject(_key: string, object: QuickDiffModel): void { object.dispose(); } } -export class DirtyDiffModelService implements IDirtyDiffModelService { +export class QuickDiffModelService implements IQuickDiffModelService { _serviceBrand: undefined; - private readonly _references: DirtyDiffModelReferenceCollection; + private readonly _references: QuickDiffModelReferenceCollection; constructor( @IInstantiationService private readonly instantiationService: IInstantiationService, @ITextFileService private readonly textFileService: ITextFileService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService ) { - this._references = this.instantiationService.createInstance(DirtyDiffModelReferenceCollection); - } - - createDiffModelReference(resource: URI, algorithm: DiffAlgorithmName): IReference | undefined { - const textFileModel = this.textFileService.files.get(resource); - if (!textFileModel?.isResolved()) { - return undefined; - } - - return this._createModelReference(resource, textFileModel, algorithm); + this._references = this.instantiationService.createInstance(QuickDiffModelReferenceCollection); } - createDirtyDiffModelReference(resource: URI): IReference | undefined { + createQuickDiffModelReference(resource: URI, options?: QuickDiffModelOptions): IReference | undefined { const textFileModel = this.textFileService.files.get(resource); if (!textFileModel?.isResolved()) { return undefined; } - return this._createModelReference(resource, textFileModel, undefined); - } - - private _createModelReference(resource: URI, textFileModel: IResolvedTextFileEditorModel, algorithm: DiffAlgorithmName | undefined): IReference { - resource = algorithm === undefined + resource = options === undefined ? this.uriIdentityService.asCanonicalUri(resource) - : this.uriIdentityService.asCanonicalUri(resource) - .with({ query: `algorithm=${algorithm}` }); + : this.uriIdentityService.asCanonicalUri(resource).with({ query: JSON.stringify(options) }); - return this._references.acquire(resource.toString(), textFileModel, algorithm); + return this._references.acquire(resource.toString(), textFileModel, options); } } -export class DirtyDiffModel extends Disposable { +export class QuickDiffModel extends Disposable { private _model: ITextFileEditorModel; @@ -136,7 +119,7 @@ export class DirtyDiffModel extends Disposable { constructor( textFileModel: IResolvedTextFileEditorModel, - private readonly algorithm: DiffAlgorithmName | undefined, + private readonly options: QuickDiffModelOptions | undefined, @ISCMService private readonly scmService: ISCMService, @IQuickDiffService private readonly quickDiffService: IQuickDiffService, @IEditorWorkerService private readonly editorWorkerService: IEditorWorkerService, @@ -263,15 +246,15 @@ export class DirtyDiffModel extends Disposable { const allDiffs: QuickDiffChange[] = []; for (const quickDiff of filteredToDiffable) { - const dirtyDiff = await this._diff(quickDiff.originalResource, this._model.resource, ignoreTrimWhitespace); - if (dirtyDiff.changes && dirtyDiff.changes2 && dirtyDiff.changes.length === dirtyDiff.changes2.length) { - for (let index = 0; index < dirtyDiff.changes.length; index++) { + const diff = await this._diff(quickDiff.originalResource, this._model.resource, ignoreTrimWhitespace); + if (diff.changes && diff.changes2 && diff.changes.length === diff.changes2.length) { + for (let index = 0; index < diff.changes.length; index++) { allDiffs.push({ label: quickDiff.label, original: quickDiff.originalResource, modified: this._model.resource, - change: dirtyDiff.changes[index], - change2: dirtyDiff.changes2[index] + change: diff.changes[index], + change2: diff.changes2[index] }); } } @@ -290,7 +273,7 @@ export class DirtyDiffModel extends Disposable { } private async _diff(original: URI, modified: URI, ignoreTrimWhitespace: boolean): Promise<{ changes: readonly IChange[] | null; changes2: readonly LineRangeMapping[] | null }> { - if (this.algorithm === undefined) { + if (this.options?.algorithm === undefined) { const changes = await this.editorWorkerService.computeDirtyDiff(original, modified, ignoreTrimWhitespace); return { changes, changes2: changes?.map(change => lineRangeMappingFromChange(change)) ?? null }; } @@ -299,7 +282,7 @@ export class DirtyDiffModel extends Disposable { computeMoves: false, ignoreTrimWhitespace, maxComputationTimeMs: Number.MAX_SAFE_INTEGER - }, this.algorithm); + }, this.options.algorithm); return { changes: result ? toLineChanges(DiffState.fromDiffResult(result)) : null, changes2: result?.changes ?? null }; } @@ -377,10 +360,10 @@ export class DirtyDiffModel extends Disposable { const quickDiffs = await this.quickDiffService.getQuickDiffs(uri, this._model.getLanguageId(), isSynchronized); // TODO@lszomoru - find a long term solution for this - // When the DirtyDiffModel is created for a diff editor, there is no + // When the QuickDiffModel is created for a diff editor, there is no // need to compute the diff information for the `isSCM` quick diff // provider as that information will be provided by the diff editor - return this.algorithm !== undefined + return this.options?.algorithm !== undefined ? quickDiffs.filter(quickDiff => !quickDiff.isSCM) : quickDiffs; } @@ -464,55 +447,3 @@ export class DirtyDiffModel extends Disposable { super.dispose(); } } - -function compareChanges(a: IChange, b: IChange): number { - let result = a.modifiedStartLineNumber - b.modifiedStartLineNumber; - - if (result !== 0) { - return result; - } - - result = a.modifiedEndLineNumber - b.modifiedEndLineNumber; - - if (result !== 0) { - return result; - } - - result = a.originalStartLineNumber - b.originalStartLineNumber; - - if (result !== 0) { - return result; - } - - return a.originalEndLineNumber - b.originalEndLineNumber; -} - -export function getChangeHeight(change: IChange): number { - const modified = change.modifiedEndLineNumber - change.modifiedStartLineNumber + 1; - const original = change.originalEndLineNumber - change.originalStartLineNumber + 1; - - if (change.originalEndLineNumber === 0) { - return modified; - } else if (change.modifiedEndLineNumber === 0) { - return original; - } else { - return modified + original; - } -} - -export function getModifiedEndLineNumber(change: IChange): number { - if (change.modifiedEndLineNumber === 0) { - return change.modifiedStartLineNumber === 0 ? 1 : change.modifiedStartLineNumber; - } else { - return change.modifiedEndLineNumber; - } -} - -export function lineIntersectsChange(lineNumber: number, change: IChange): boolean { - // deletion at the beginning of the file - if (lineNumber === 1 && change.modifiedStartLineNumber === 0 && change.modifiedEndLineNumber === 0) { - return true; - } - - return lineNumber >= change.modifiedStartLineNumber && lineNumber <= (change.modifiedEndLineNumber || change.modifiedStartLineNumber); -} diff --git a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts b/src/vs/workbench/contrib/scm/browser/quickDiffWidget.ts similarity index 58% rename from src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts rename to src/vs/workbench/contrib/scm/browser/quickDiffWidget.ts index f14fd38555f5a..35771008fe6b7 100644 --- a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts +++ b/src/vs/workbench/contrib/scm/browser/quickDiffWidget.ts @@ -4,58 +4,112 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from '../../../../nls.js'; - -import './media/dirtydiffDecorator.css'; -import { IDisposable, toDisposable, Disposable, DisposableStore, DisposableMap, IReference } from '../../../../base/common/lifecycle.js'; +import * as dom from '../../../../base/browser/dom.js'; +import * as domStylesheetsJs from '../../../../base/browser/domStylesheets.js'; +import { Action, ActionRunner, IAction } from '../../../../base/common/actions.js'; import { Event } from '../../../../base/common/event.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { IContextViewService } from '../../../../platform/contextview/browser/contextView.js'; +import { ISelectOptionItem } from '../../../../base/browser/ui/selectBox/selectBox.js'; +import { SelectActionViewItem } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; +import { defaultSelectBoxStyles } from '../../../../platform/theme/browser/defaultStyles.js'; +import { IColorTheme, IThemeService } from '../../../../platform/theme/common/themeService.js'; +import { getOuterEditor, peekViewBorder, peekViewTitleBackground, peekViewTitleForeground, peekViewTitleInfoForeground, PeekViewWidget } from '../../../../editor/contrib/peekView/browser/peekView.js'; +import { editorBackground } from '../../../../platform/theme/common/colorRegistry.js'; +import { IMenu, IMenuService, MenuId, MenuItemAction, MenuRegistry } from '../../../../platform/actions/common/actions.js'; +import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from '../../../../editor/browser/editorBrowser.js'; +import { EditorAction, registerEditorAction } from '../../../../editor/browser/editorExtensions.js'; +import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; +import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; +import { EmbeddedDiffEditorWidget } from '../../../../editor/browser/widget/diffEditor/embeddedDiffEditorWidget.js'; +import { IEditorContribution, ScrollType } from '../../../../editor/common/editorCommon.js'; +import { IQuickDiffModelService, QuickDiffModel } from './quickDiffModel.js'; +import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { URI } from '../../../../base/common/uri.js'; -import { ModelDecorationOptions } from '../../../../editor/common/model/textModel.js'; -import { IColorTheme, themeColorFromId, IThemeService } from '../../../../platform/theme/common/themeService.js'; -import { editorErrorForeground, registerColor, transparent } from '../../../../platform/theme/common/colorRegistry.js'; -import { ICodeEditor, IEditorMouseEvent, isCodeEditor, MouseTargetType } from '../../../../editor/browser/editorBrowser.js'; -import { registerEditorAction, registerEditorContribution, ServicesAccessor, EditorAction, EditorContributionInstantiation } from '../../../../editor/browser/editorExtensions.js'; -import { PeekViewWidget, getOuterEditor, peekViewBorder, peekViewTitleBackground, peekViewTitleForeground, peekViewTitleInfoForeground } from '../../../../editor/contrib/peekView/browser/peekView.js'; -import { IContextKeyService, IContextKey, ContextKeyExpr, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; -import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; -import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; -import { Position } from '../../../../editor/common/core/position.js'; -import { Range } from '../../../../editor/common/core/range.js'; import { rot } from '../../../../base/common/numbers.js'; -import { KeybindingsRegistry, KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; -import { EmbeddedDiffEditorWidget } from '../../../../editor/browser/widget/diffEditor/embeddedDiffEditorWidget.js'; -import { IDiffEditorOptions, EditorOption } from '../../../../editor/common/config/editorOptions.js'; -import { Action, IAction, ActionRunner } from '../../../../base/common/actions.js'; -import { IActionBarOptions } from '../../../../base/browser/ui/actionbar/actionbar.js'; -import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; -import { basename } from '../../../../base/common/resources.js'; -import { MenuId, IMenuService, IMenu, MenuItemAction, MenuRegistry } from '../../../../platform/actions/common/actions.js'; -import { getFlatActionBarActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; -import { ScrollType, IEditorContribution, IEditorDecorationsCollection } from '../../../../editor/common/editorCommon.js'; -import { OverviewRulerLane, IModelDecorationOptions, MinimapPosition } from '../../../../editor/common/model.js'; -import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; import { ISplice } from '../../../../base/common/sequence.js'; -import * as dom from '../../../../base/browser/dom.js'; -import * as domStylesheetsJs from '../../../../base/browser/domStylesheets.js'; -import { gotoNextLocation, gotoPreviousLocation } from '../../../../platform/theme/common/iconRegistry.js'; -import { Codicon } from '../../../../base/common/codicons.js'; -import { ThemeIcon } from '../../../../base/common/themables.js'; +import { ChangeType, getChangeHeight, getChangeType, getChangeTypeColor, getModifiedEndLineNumber, lineIntersectsChange, QuickDiffChange } from '../common/quickDiff.js'; +import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; import { TextCompareEditorActiveContext } from '../../../common/contextkeys.js'; +import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; +import { KeybindingsRegistry, KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; import { IChange } from '../../../../editor/common/diff/legacyLinesDiffComputer.js'; -import { Color } from '../../../../base/common/color.js'; -import { IEditorService } from '../../../services/editor/common/editorService.js'; import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; -import { IQuickDiffService, QuickDiffChange } from '../common/quickDiff.js'; -import { IQuickDiffSelectItem, SwitchQuickDiffBaseAction, SwitchQuickDiffViewItem } from './dirtyDiffSwitcher.js'; import { Iterable } from '../../../../base/common/iterator.js'; -import { DirtyDiffModel, getChangeHeight, getModifiedEndLineNumber, IDirtyDiffModelService, lineIntersectsChange } from './dirtyDiffModel.js'; -import { IWorkbenchContribution } from '../../../common/contributions.js'; -import { ResourceMap } from '../../../../base/common/map.js'; -import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; +import { basename } from '../../../../base/common/resources.js'; +import { EditorOption, IDiffEditorOptions } from '../../../../editor/common/config/editorOptions.js'; +import { Position } from '../../../../editor/common/core/position.js'; +import { Range } from '../../../../editor/common/core/range.js'; +import { getFlatActionBarActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; +import { IActionBarOptions } from '../../../../base/browser/ui/actionbar/actionbar.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; +import { gotoNextLocation, gotoPreviousLocation } from '../../../../platform/theme/common/iconRegistry.js'; +import { Codicon } from '../../../../base/common/codicons.js'; +import { Color } from '../../../../base/common/color.js'; +import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; + +export const isQuickDiffVisible = new RawContextKey('dirtyDiffVisible', false); + +export interface IQuickDiffSelectItem extends ISelectOptionItem { + provider: string; +} + +export class QuickDiffPickerViewItem extends SelectActionViewItem { + private readonly optionsItems: IQuickDiffSelectItem[]; + + constructor( + action: IAction, + providers: string[], + selected: string, + @IContextViewService contextViewService: IContextViewService, + @IThemeService themeService: IThemeService + ) { + const items = providers.map(provider => ({ provider, text: provider })); + let startingSelection = providers.indexOf(selected); + if (startingSelection === -1) { + startingSelection = 0; + } + const styles = { ...defaultSelectBoxStyles }; + const theme = themeService.getColorTheme(); + const editorBackgroundColor = theme.getColor(editorBackground); + const peekTitleColor = theme.getColor(peekViewTitleBackground); + const opaqueTitleColor = peekTitleColor?.makeOpaque(editorBackgroundColor!) ?? editorBackgroundColor!; + styles.selectBackground = opaqueTitleColor.lighten(.6).toString(); + super(null, action, items, startingSelection, contextViewService, styles, { ariaLabel: nls.localize('remotes', 'Switch quick diff base') }); + this.optionsItems = items; + } -class DiffActionRunner extends ActionRunner { + public setSelection(provider: string) { + const index = this.optionsItems.findIndex(item => item.provider === provider); + this.select(index); + } + + protected override getActionContext(_: string, index: number): IQuickDiffSelectItem { + return this.optionsItems[index]; + } + + override render(container: HTMLElement): void { + super.render(container); + this.setFocusable(true); + } +} + +export class QuickDiffPickerBaseAction extends Action { + + public static readonly ID = 'quickDiff.base.switch'; + public static readonly LABEL = nls.localize('quickDiff.base.switch', "Switch Quick Diff Base"); + + constructor(private readonly callback: (event?: IQuickDiffSelectItem) => void) { + super(QuickDiffPickerBaseAction.ID, QuickDiffPickerBaseAction.LABEL, undefined, undefined); + } + + override async run(event?: IQuickDiffSelectItem): Promise { + return this.callback(event); + } +} + +class QuickDiffWidgetActionRunner extends ActionRunner { protected override runAction(action: IAction, context: any): Promise { if (action instanceof MenuItemAction) { @@ -66,9 +120,7 @@ class DiffActionRunner extends ActionRunner { } } -export const isDirtyDiffVisible = new RawContextKey('dirtyDiffVisible', false); - -class UIEditorAction extends Action { +class QuickDiffWidgetEditorAction extends Action { private editor: ICodeEditor; private action: EditorAction; @@ -96,43 +148,7 @@ class UIEditorAction extends Action { } } -enum ChangeType { - Modify, - Add, - Delete -} - -function getChangeType(change: IChange): ChangeType { - if (change.originalEndLineNumber === 0) { - return ChangeType.Add; - } else if (change.modifiedEndLineNumber === 0) { - return ChangeType.Delete; - } else { - return ChangeType.Modify; - } -} - -function getChangeTypeColor(theme: IColorTheme, changeType: ChangeType): Color | undefined { - switch (changeType) { - case ChangeType.Modify: return theme.getColor(editorGutterModifiedBackground); - case ChangeType.Add: return theme.getColor(editorGutterAddedBackground); - case ChangeType.Delete: return theme.getColor(editorGutterDeletedBackground); - } -} - -function getOuterEditorFromDiffEditor(accessor: ServicesAccessor): ICodeEditor | null { - const diffEditors = accessor.get(ICodeEditorService).listDiffEditors(); - - for (const diffEditor of diffEditors) { - if (diffEditor.hasTextFocus() && diffEditor instanceof EmbeddedDiffEditorWidget) { - return diffEditor.getParentEditor(); - } - } - - return getOuterEditor(accessor); -} - -class DirtyDiffWidget extends PeekViewWidget { +class QuickDiffWidget extends PeekViewWidget { private diffEditor!: EmbeddedDiffEditorWidget; private title: string; @@ -141,12 +157,12 @@ class DirtyDiffWidget extends PeekViewWidget { private _provider: string = ''; private change: IChange | undefined; private height: number | undefined = undefined; - private dropdown: SwitchQuickDiffViewItem | undefined; + private dropdown: QuickDiffPickerViewItem | undefined; private dropdownContainer: HTMLElement | undefined; constructor( editor: ICodeEditor, - private model: DirtyDiffModel, + private model: QuickDiffModel, @IThemeService private readonly themeService: IThemeService, @IInstantiationService instantiationService: IInstantiationService, @IMenuService private readonly menuService: IMenuService, @@ -297,8 +313,8 @@ class DirtyDiffWidget extends PeekViewWidget { if (!this._actionbarWidget) { return; } - const previous = this.instantiationService.createInstance(UIEditorAction, this.editor, new ShowPreviousChangeAction(this.editor), ThemeIcon.asClassName(gotoPreviousLocation)); - const next = this.instantiationService.createInstance(UIEditorAction, this.editor, new ShowNextChangeAction(this.editor), ThemeIcon.asClassName(gotoNextLocation)); + const previous = this.instantiationService.createInstance(QuickDiffWidgetEditorAction, this.editor, new ShowPreviousChangeAction(this.editor), ThemeIcon.asClassName(gotoPreviousLocation)); + const next = this.instantiationService.createInstance(QuickDiffWidgetEditorAction, this.editor, new ShowNextChangeAction(this.editor), ThemeIcon.asClassName(gotoNextLocation)); this._disposables.add(previous); this._disposables.add(next); @@ -318,18 +334,18 @@ class DirtyDiffWidget extends PeekViewWidget { super._fillHead(container, true); this.dropdownContainer = dom.prepend(this._titleElement!, dom.$('.dropdown')); - this.dropdown = this.instantiationService.createInstance(SwitchQuickDiffViewItem, new SwitchQuickDiffBaseAction((event?: IQuickDiffSelectItem) => this.switchQuickDiff(event)), + this.dropdown = this.instantiationService.createInstance(QuickDiffPickerViewItem, new QuickDiffPickerBaseAction((event?: IQuickDiffSelectItem) => this.switchQuickDiff(event)), this.model.quickDiffs.map(quickDiffer => quickDiffer.label), this.model.changes[this._index].label); this.dropdown.render(this.dropdownContainer); this.updateActions(); } protected override _getActionBarOptions(): IActionBarOptions { - const actionRunner = new DiffActionRunner(); + const actionRunner = new QuickDiffWidgetActionRunner(); // close widget on successful action actionRunner.onDidRun(e => { - if (!(e.action instanceof UIEditorAction) && !e.error) { + if (!(e.action instanceof QuickDiffWidgetEditorAction) && !e.error) { this.dispose(); } }); @@ -426,231 +442,17 @@ class DirtyDiffWidget extends PeekViewWidget { } } -export class ShowPreviousChangeAction extends EditorAction { - - constructor(private readonly outerEditor?: ICodeEditor) { - super({ - id: 'editor.action.dirtydiff.previous', - label: nls.localize2('show previous change', "Show Previous Change"), - precondition: TextCompareEditorActiveContext.toNegated(), - kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, primary: KeyMod.Shift | KeyMod.Alt | KeyCode.F3, weight: KeybindingWeight.EditorContrib } - }); - } - - run(accessor: ServicesAccessor): void { - const outerEditor = this.outerEditor ?? getOuterEditorFromDiffEditor(accessor); - - if (!outerEditor) { - return; - } - - const controller = DirtyDiffController.get(outerEditor); - - if (!controller) { - return; - } +export class QuickDiffEditorController extends Disposable implements IEditorContribution { - if (!controller.canNavigate()) { - return; - } + public static readonly ID = 'editor.contrib.quickdiff'; - controller.previous(); + static get(editor: ICodeEditor): QuickDiffEditorController | null { + return editor.getContribution(QuickDiffEditorController.ID); } -} -registerEditorAction(ShowPreviousChangeAction); - -export class ShowNextChangeAction extends EditorAction { - constructor(private readonly outerEditor?: ICodeEditor) { - super({ - id: 'editor.action.dirtydiff.next', - label: nls.localize2('show next change', "Show Next Change"), - precondition: TextCompareEditorActiveContext.toNegated(), - kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, primary: KeyMod.Alt | KeyCode.F3, weight: KeybindingWeight.EditorContrib } - }); - } - - run(accessor: ServicesAccessor): void { - const outerEditor = this.outerEditor ?? getOuterEditorFromDiffEditor(accessor); - - if (!outerEditor) { - return; - } - - const controller = DirtyDiffController.get(outerEditor); - - if (!controller) { - return; - } - - if (!controller.canNavigate()) { - return; - } - - controller.next(); - } -} -registerEditorAction(ShowNextChangeAction); - -// Go to menu -MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { - group: '7_change_nav', - command: { - id: 'editor.action.dirtydiff.next', - title: nls.localize({ key: 'miGotoNextChange', comment: ['&& denotes a mnemonic'] }, "Next &&Change") - }, - order: 1 -}); - -MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { - group: '7_change_nav', - command: { - id: 'editor.action.dirtydiff.previous', - title: nls.localize({ key: 'miGotoPreviousChange', comment: ['&& denotes a mnemonic'] }, "Previous &&Change") - }, - order: 2 -}); - -export class GotoPreviousChangeAction extends EditorAction { - - constructor() { - super({ - id: 'workbench.action.editor.previousChange', - label: nls.localize2('move to previous change', "Go to Previous Change"), - precondition: TextCompareEditorActiveContext.toNegated(), - kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, primary: KeyMod.Shift | KeyMod.Alt | KeyCode.F5, weight: KeybindingWeight.EditorContrib } - }); - } - - async run(accessor: ServicesAccessor): Promise { - const outerEditor = getOuterEditorFromDiffEditor(accessor); - const accessibilitySignalService = accessor.get(IAccessibilitySignalService); - const accessibilityService = accessor.get(IAccessibilityService); - const codeEditorService = accessor.get(ICodeEditorService); - const dirtyDiffModelService = accessor.get(IDirtyDiffModelService); - - if (!outerEditor || !outerEditor.hasModel()) { - return; - } - - const modelRef = dirtyDiffModelService.createDirtyDiffModelReference(outerEditor.getModel().uri); - try { - if (!modelRef || modelRef.object.changes.length === 0) { - return; - } - - const lineNumber = outerEditor.getPosition().lineNumber; - const index = modelRef.object.findPreviousClosestChange(lineNumber, false); - const change = modelRef.object.changes[index]; - await playAccessibilitySymbolForChange(change.change, accessibilitySignalService); - setPositionAndSelection(change.change, outerEditor, accessibilityService, codeEditorService); - } finally { - modelRef?.dispose(); - } - } -} -registerEditorAction(GotoPreviousChangeAction); - -export class GotoNextChangeAction extends EditorAction { - - constructor() { - super({ - id: 'workbench.action.editor.nextChange', - label: nls.localize2('move to next change', "Go to Next Change"), - precondition: TextCompareEditorActiveContext.toNegated(), - kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, primary: KeyMod.Alt | KeyCode.F5, weight: KeybindingWeight.EditorContrib } - }); - } - - async run(accessor: ServicesAccessor): Promise { - const accessibilitySignalService = accessor.get(IAccessibilitySignalService); - const outerEditor = getOuterEditorFromDiffEditor(accessor); - const accessibilityService = accessor.get(IAccessibilityService); - const codeEditorService = accessor.get(ICodeEditorService); - const dirtyDiffModelService = accessor.get(IDirtyDiffModelService); - - if (!outerEditor || !outerEditor.hasModel()) { - return; - } - - const modelRef = dirtyDiffModelService.createDirtyDiffModelReference(outerEditor.getModel().uri); - try { - if (!modelRef || modelRef.object.changes.length === 0) { - return; - } - - const lineNumber = outerEditor.getPosition().lineNumber; - const index = modelRef.object.findNextClosestChange(lineNumber, false); - const change = modelRef.object.changes[index].change; - await playAccessibilitySymbolForChange(change, accessibilitySignalService); - setPositionAndSelection(change, outerEditor, accessibilityService, codeEditorService); - } finally { - modelRef?.dispose(); - } - } -} - -function setPositionAndSelection(change: IChange, editor: ICodeEditor, accessibilityService: IAccessibilityService, codeEditorService: ICodeEditorService) { - const position = new Position(change.modifiedStartLineNumber, 1); - editor.setPosition(position); - editor.revealPositionInCenter(position); - if (accessibilityService.isScreenReaderOptimized()) { - editor.setSelection({ startLineNumber: change.modifiedStartLineNumber, startColumn: 0, endLineNumber: change.modifiedStartLineNumber, endColumn: Number.MAX_VALUE }); - codeEditorService.getActiveCodeEditor()?.writeScreenReaderContent('diff-navigation'); - } -} - -async function playAccessibilitySymbolForChange(change: IChange, accessibilitySignalService: IAccessibilitySignalService) { - const changeType = getChangeType(change); - switch (changeType) { - case ChangeType.Add: - accessibilitySignalService.playSignal(AccessibilitySignal.diffLineInserted, { allowManyInParallel: true, source: 'dirtyDiffDecoration' }); - break; - case ChangeType.Delete: - accessibilitySignalService.playSignal(AccessibilitySignal.diffLineDeleted, { allowManyInParallel: true, source: 'dirtyDiffDecoration' }); - break; - case ChangeType.Modify: - accessibilitySignalService.playSignal(AccessibilitySignal.diffLineModified, { allowManyInParallel: true, source: 'dirtyDiffDecoration' }); - break; - } -} - -registerEditorAction(GotoNextChangeAction); - -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: 'closeDirtyDiff', - weight: KeybindingWeight.EditorContrib + 50, - primary: KeyCode.Escape, - secondary: [KeyMod.Shift | KeyCode.Escape], - when: ContextKeyExpr.and(isDirtyDiffVisible), - handler: (accessor: ServicesAccessor) => { - const outerEditor = getOuterEditorFromDiffEditor(accessor); - - if (!outerEditor) { - return; - } - - const controller = DirtyDiffController.get(outerEditor); - - if (!controller) { - return; - } - - controller.close(); - } -}); - -export class DirtyDiffController extends Disposable implements IEditorContribution { - - public static readonly ID = 'editor.contrib.dirtydiff'; - - static get(editor: ICodeEditor): DirtyDiffController | null { - return editor.getContribution(DirtyDiffController.ID); - } - - private model: DirtyDiffModel | null = null; - private widget: DirtyDiffWidget | null = null; - private readonly isDirtyDiffVisible!: IContextKey; + private model: QuickDiffModel | null = null; + private widget: QuickDiffWidget | null = null; + private readonly isQuickDiffVisible!: IContextKey; private session: IDisposable = Disposable.None; private mouseDownInfo: { lineNumber: number } | null = null; private enabled = false; @@ -661,7 +463,7 @@ export class DirtyDiffController extends Disposable implements IEditorContributi private editor: ICodeEditor, @IContextKeyService contextKeyService: IContextKeyService, @IConfigurationService private readonly configurationService: IConfigurationService, - @IDirtyDiffModelService private readonly dirtyDiffModelService: IDirtyDiffModelService, + @IQuickDiffModelService private readonly quickDiffModelService: IQuickDiffModelService, @IInstantiationService private readonly instantiationService: IInstantiationService ) { super(); @@ -669,7 +471,7 @@ export class DirtyDiffController extends Disposable implements IEditorContributi this.stylesheet = domStylesheetsJs.createStyleSheet(undefined, undefined, this._store); if (this.enabled) { - this.isDirtyDiffVisible = isDirtyDiffVisible.bindTo(contextKeyService); + this.isQuickDiffVisible = isQuickDiffVisible.bindTo(contextKeyService); this._register(editor.onDidChangeModel(() => this.close())); const onDidChangeGutterAction = Event.filter(configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.diffDecorationsGutterAction')); @@ -781,7 +583,7 @@ export class DirtyDiffController extends Disposable implements IEditorContributi return false; } - const modelRef = this.dirtyDiffModelService.createDirtyDiffModelReference(editorModel.uri); + const modelRef = this.quickDiffModelService.createQuickDiffModelReference(editorModel.uri); if (!modelRef) { return false; @@ -793,8 +595,8 @@ export class DirtyDiffController extends Disposable implements IEditorContributi } this.model = modelRef.object; - this.widget = this.instantiationService.createInstance(DirtyDiffWidget, this.editor, this.model); - this.isDirtyDiffVisible.set(true); + this.widget = this.instantiationService.createInstance(QuickDiffWidget, this.editor, this.model); + this.isQuickDiffVisible.set(true); const disposables = new DisposableStore(); disposables.add(Event.once(this.widget.onDidClose)(this.close, this)); @@ -810,7 +612,7 @@ export class DirtyDiffController extends Disposable implements IEditorContributi disposables.add(toDisposable(() => { this.model = null; this.widget = null; - this.isDirtyDiffVisible.set(false); + this.isQuickDiffVisible.set(false); this.editor.focus(); })); @@ -892,7 +694,7 @@ export class DirtyDiffController extends Disposable implements IEditorContributi return; } - const modelRef = this.dirtyDiffModelService.createDirtyDiffModelReference(editorModel.uri); + const modelRef = this.quickDiffModelService.createQuickDiffModelReference(editorModel.uri); if (!modelRef) { return; @@ -922,350 +724,226 @@ export class DirtyDiffController extends Disposable implements IEditorContributi } } -const editorGutterModifiedBackground = registerColor('editorGutter.modifiedBackground', { - dark: '#1B81A8', - light: '#2090D3', - hcDark: '#1B81A8', - hcLight: '#2090D3' -}, nls.localize('editorGutterModifiedBackground', "Editor gutter background color for lines that are modified.")); - -const editorGutterAddedBackground = registerColor('editorGutter.addedBackground', { - dark: '#487E02', - light: '#48985D', - hcDark: '#487E02', - hcLight: '#48985D' -}, nls.localize('editorGutterAddedBackground', "Editor gutter background color for lines that are added.")); - -const editorGutterDeletedBackground = registerColor('editorGutter.deletedBackground', editorErrorForeground, nls.localize('editorGutterDeletedBackground', "Editor gutter background color for lines that are deleted.")); - -export const minimapGutterModifiedBackground = registerColor('minimapGutter.modifiedBackground', editorGutterModifiedBackground, nls.localize('minimapGutterModifiedBackground', "Minimap gutter background color for lines that are modified.")); - -export const minimapGutterAddedBackground = registerColor('minimapGutter.addedBackground', editorGutterAddedBackground, nls.localize('minimapGutterAddedBackground', "Minimap gutter background color for lines that are added.")); - -export const minimapGutterDeletedBackground = registerColor('minimapGutter.deletedBackground', editorGutterDeletedBackground, nls.localize('minimapGutterDeletedBackground', "Minimap gutter background color for lines that are deleted.")); - -export const overviewRulerModifiedForeground = registerColor('editorOverviewRuler.modifiedForeground', transparent(editorGutterModifiedBackground, 0.6), nls.localize('overviewRulerModifiedForeground', 'Overview ruler marker color for modified content.')); -export const overviewRulerAddedForeground = registerColor('editorOverviewRuler.addedForeground', transparent(editorGutterAddedBackground, 0.6), nls.localize('overviewRulerAddedForeground', 'Overview ruler marker color for added content.')); -export const overviewRulerDeletedForeground = registerColor('editorOverviewRuler.deletedForeground', transparent(editorGutterDeletedBackground, 0.6), nls.localize('overviewRulerDeletedForeground', 'Overview ruler marker color for deleted content.')); +export class ShowPreviousChangeAction extends EditorAction { -class DirtyDiffDecorator extends Disposable { + constructor(private readonly outerEditor?: ICodeEditor) { + super({ + id: 'editor.action.dirtydiff.previous', + label: nls.localize2('show previous change', "Show Previous Change"), + precondition: TextCompareEditorActiveContext.toNegated(), + kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, primary: KeyMod.Shift | KeyMod.Alt | KeyCode.F3, weight: KeybindingWeight.EditorContrib } + }); + } - static createDecoration(className: string, tooltip: string | null, options: { gutter: boolean; overview: { active: boolean; color: string }; minimap: { active: boolean; color: string }; isWholeLine: boolean }): ModelDecorationOptions { - const decorationOptions: IModelDecorationOptions = { - description: 'dirty-diff-decoration', - isWholeLine: options.isWholeLine, - }; + run(accessor: ServicesAccessor): void { + const outerEditor = this.outerEditor ?? getOuterEditorFromDiffEditor(accessor); - if (options.gutter) { - decorationOptions.linesDecorationsClassName = `dirty-diff-glyph ${className}`; - decorationOptions.linesDecorationsTooltip = tooltip; + if (!outerEditor) { + return; } - if (options.overview.active) { - decorationOptions.overviewRuler = { - color: themeColorFromId(options.overview.color), - position: OverviewRulerLane.Left - }; + const controller = QuickDiffEditorController.get(outerEditor); + + if (!controller) { + return; } - if (options.minimap.active) { - decorationOptions.minimap = { - color: themeColorFromId(options.minimap.color), - position: MinimapPosition.Gutter - }; + if (!controller.canNavigate()) { + return; } - return ModelDecorationOptions.createDynamic(decorationOptions); + controller.previous(); } +} +registerEditorAction(ShowPreviousChangeAction); - private addedOptions: ModelDecorationOptions; - private addedPatternOptions: ModelDecorationOptions; - private modifiedOptions: ModelDecorationOptions; - private modifiedPatternOptions: ModelDecorationOptions; - private deletedOptions: ModelDecorationOptions; - private decorationsCollection: IEditorDecorationsCollection | undefined; - - constructor( - private readonly codeEditor: ICodeEditor, - private readonly dirtyDiffModelRef: IReference, - @IConfigurationService private readonly configurationService: IConfigurationService - ) { - super(); +export class ShowNextChangeAction extends EditorAction { - const decorations = configurationService.getValue('scm.diffDecorations'); - const gutter = decorations === 'all' || decorations === 'gutter'; - const overview = decorations === 'all' || decorations === 'overview'; - const minimap = decorations === 'all' || decorations === 'minimap'; - - const diffAdded = nls.localize('diffAdded', 'Added lines'); - this.addedOptions = DirtyDiffDecorator.createDecoration('dirty-diff-added', diffAdded, { - gutter, - overview: { active: overview, color: overviewRulerAddedForeground }, - minimap: { active: minimap, color: minimapGutterAddedBackground }, - isWholeLine: true - }); - this.addedPatternOptions = DirtyDiffDecorator.createDecoration('dirty-diff-added-pattern', diffAdded, { - gutter, - overview: { active: overview, color: overviewRulerAddedForeground }, - minimap: { active: minimap, color: minimapGutterAddedBackground }, - isWholeLine: true - }); - const diffModified = nls.localize('diffModified', 'Changed lines'); - this.modifiedOptions = DirtyDiffDecorator.createDecoration('dirty-diff-modified', diffModified, { - gutter, - overview: { active: overview, color: overviewRulerModifiedForeground }, - minimap: { active: minimap, color: minimapGutterModifiedBackground }, - isWholeLine: true - }); - this.modifiedPatternOptions = DirtyDiffDecorator.createDecoration('dirty-diff-modified-pattern', diffModified, { - gutter, - overview: { active: overview, color: overviewRulerModifiedForeground }, - minimap: { active: minimap, color: minimapGutterModifiedBackground }, - isWholeLine: true - }); - this.deletedOptions = DirtyDiffDecorator.createDecoration('dirty-diff-deleted', nls.localize('diffDeleted', 'Removed lines'), { - gutter, - overview: { active: overview, color: overviewRulerDeletedForeground }, - minimap: { active: minimap, color: minimapGutterDeletedBackground }, - isWholeLine: false + constructor(private readonly outerEditor?: ICodeEditor) { + super({ + id: 'editor.action.dirtydiff.next', + label: nls.localize2('show next change', "Show Next Change"), + precondition: TextCompareEditorActiveContext.toNegated(), + kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, primary: KeyMod.Alt | KeyCode.F3, weight: KeybindingWeight.EditorContrib } }); - - this._register(configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('scm.diffDecorationsGutterPattern')) { - this.onDidChange(); - } - })); - - this._register(Event.runAndSubscribe(this.dirtyDiffModelRef.object.onDidChange, () => this.onDidChange())); } - private onDidChange(): void { - if (!this.codeEditor.hasModel()) { + run(accessor: ServicesAccessor): void { + const outerEditor = this.outerEditor ?? getOuterEditorFromDiffEditor(accessor); + + if (!outerEditor) { return; } - const visibleQuickDiffs = this.dirtyDiffModelRef.object.quickDiffs.filter(quickDiff => quickDiff.visible); - const pattern = this.configurationService.getValue<{ added: boolean; modified: boolean }>('scm.diffDecorationsGutterPattern'); - - const decorations = this.dirtyDiffModelRef.object.changes - .filter(labeledChange => visibleQuickDiffs.some(quickDiff => quickDiff.label === labeledChange.label)) - .map((labeledChange) => { - const change = labeledChange.change; - const changeType = getChangeType(change); - const startLineNumber = change.modifiedStartLineNumber; - const endLineNumber = change.modifiedEndLineNumber || startLineNumber; - - switch (changeType) { - case ChangeType.Add: - return { - range: { - startLineNumber: startLineNumber, startColumn: 1, - endLineNumber: endLineNumber, endColumn: 1 - }, - options: pattern.added ? this.addedPatternOptions : this.addedOptions - }; - case ChangeType.Delete: - return { - range: { - startLineNumber: startLineNumber, startColumn: Number.MAX_VALUE, - endLineNumber: startLineNumber, endColumn: Number.MAX_VALUE - }, - options: this.deletedOptions - }; - case ChangeType.Modify: - return { - range: { - startLineNumber: startLineNumber, startColumn: 1, - endLineNumber: endLineNumber, endColumn: 1 - }, - options: pattern.modified ? this.modifiedPatternOptions : this.modifiedOptions - }; - } - }); + const controller = QuickDiffEditorController.get(outerEditor); - if (!this.decorationsCollection) { - this.decorationsCollection = this.codeEditor.createDecorationsCollection(decorations); - } else { - this.decorationsCollection.set(decorations); + if (!controller) { + return; } - } - override dispose(): void { - if (this.decorationsCollection) { - this.decorationsCollection?.clear(); + if (!controller.canNavigate()) { + return; } - this.decorationsCollection = undefined; - this.dirtyDiffModelRef.dispose(); - super.dispose(); - } -} - -export async function getOriginalResource(quickDiffService: IQuickDiffService, uri: URI, language: string | undefined, isSynchronized: boolean | undefined): Promise { - const quickDiffs = await quickDiffService.getQuickDiffs(uri, language, isSynchronized); - return quickDiffs.length > 0 ? quickDiffs[0].originalResource : null; -} -interface DirtyDiffWorkbenchControllerViewState { - readonly width: number; - readonly visibility: 'always' | 'hover'; + controller.next(); + } } +registerEditorAction(ShowNextChangeAction); -export class DirtyDiffWorkbenchController extends Disposable implements IWorkbenchContribution { - - private enabled = false; - - // Resource URI -> Code Editor Id -> Decoration (Disposable) - private readonly decorators = new ResourceMap>(); - private viewState: DirtyDiffWorkbenchControllerViewState = { width: 3, visibility: 'always' }; - private readonly transientDisposables = this._register(new DisposableStore()); - private readonly stylesheet: HTMLStyleElement; - - constructor( - @IEditorService private readonly editorService: IEditorService, - @IConfigurationService private readonly configurationService: IConfigurationService, - @IDirtyDiffModelService private readonly dirtyDiffModelService: IDirtyDiffModelService, - @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, - ) { - super(); - this.stylesheet = domStylesheetsJs.createStyleSheet(undefined, undefined, this._store); - - const onDidChangeConfiguration = Event.filter(configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.diffDecorations')); - this._register(onDidChangeConfiguration(this.onDidChangeConfiguration, this)); - this.onDidChangeConfiguration(); - - const onDidChangeDiffWidthConfiguration = Event.filter(configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.diffDecorationsGutterWidth')); - this._register(onDidChangeDiffWidthConfiguration(this.onDidChangeDiffWidthConfiguration, this)); - this.onDidChangeDiffWidthConfiguration(); +export class GotoPreviousChangeAction extends EditorAction { - const onDidChangeDiffVisibilityConfiguration = Event.filter(configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.diffDecorationsGutterVisibility')); - this._register(onDidChangeDiffVisibilityConfiguration(this.onDidChangeDiffVisibilityConfiguration, this)); - this.onDidChangeDiffVisibilityConfiguration(); + constructor() { + super({ + id: 'workbench.action.editor.previousChange', + label: nls.localize2('move to previous change', "Go to Previous Change"), + precondition: TextCompareEditorActiveContext.toNegated(), + kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, primary: KeyMod.Shift | KeyMod.Alt | KeyCode.F5, weight: KeybindingWeight.EditorContrib } + }); } - private onDidChangeConfiguration(): void { - const enabled = this.configurationService.getValue('scm.diffDecorations') !== 'none'; + async run(accessor: ServicesAccessor): Promise { + const outerEditor = getOuterEditorFromDiffEditor(accessor); + const accessibilitySignalService = accessor.get(IAccessibilitySignalService); + const accessibilityService = accessor.get(IAccessibilityService); + const codeEditorService = accessor.get(ICodeEditorService); + const quickDiffModelService = accessor.get(IQuickDiffModelService); - if (enabled) { - this.enable(); - } else { - this.disable(); + if (!outerEditor || !outerEditor.hasModel()) { + return; } - } - private onDidChangeDiffWidthConfiguration(): void { - let width = this.configurationService.getValue('scm.diffDecorationsGutterWidth'); + const modelRef = quickDiffModelService.createQuickDiffModelReference(outerEditor.getModel().uri); + try { + if (!modelRef || modelRef.object.changes.length === 0) { + return; + } - if (isNaN(width) || width <= 0 || width > 5) { - width = 3; + const lineNumber = outerEditor.getPosition().lineNumber; + const index = modelRef.object.findPreviousClosestChange(lineNumber, false); + const change = modelRef.object.changes[index]; + await playAccessibilitySymbolForChange(change.change, accessibilitySignalService); + setPositionAndSelection(change.change, outerEditor, accessibilityService, codeEditorService); + } finally { + modelRef?.dispose(); } - - this.setViewState({ ...this.viewState, width }); } +} +registerEditorAction(GotoPreviousChangeAction); - private onDidChangeDiffVisibilityConfiguration(): void { - const visibility = this.configurationService.getValue<'always' | 'hover'>('scm.diffDecorationsGutterVisibility'); - this.setViewState({ ...this.viewState, visibility }); - } +export class GotoNextChangeAction extends EditorAction { - private setViewState(state: DirtyDiffWorkbenchControllerViewState): void { - this.viewState = state; - this.stylesheet.textContent = ` - .monaco-editor .dirty-diff-added, - .monaco-editor .dirty-diff-modified { - border-left-width:${state.width}px; - } - .monaco-editor .dirty-diff-added-pattern, - .monaco-editor .dirty-diff-added-pattern:before, - .monaco-editor .dirty-diff-modified-pattern, - .monaco-editor .dirty-diff-modified-pattern:before { - background-size: ${state.width}px ${state.width}px; - } - .monaco-editor .dirty-diff-added, - .monaco-editor .dirty-diff-added-pattern, - .monaco-editor .dirty-diff-modified, - .monaco-editor .dirty-diff-modified-pattern, - .monaco-editor .dirty-diff-deleted { - opacity: ${state.visibility === 'always' ? 1 : 0}; - } - `; + constructor() { + super({ + id: 'workbench.action.editor.nextChange', + label: nls.localize2('move to next change', "Go to Next Change"), + precondition: TextCompareEditorActiveContext.toNegated(), + kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, primary: KeyMod.Alt | KeyCode.F5, weight: KeybindingWeight.EditorContrib } + }); } - private enable(): void { - if (this.enabled) { - this.disable(); - } - - this.transientDisposables.add(Event.any(this.editorService.onDidCloseEditor, this.editorService.onDidVisibleEditorsChange)(() => this.onEditorsChanged())); - this.onEditorsChanged(); - this.enabled = true; - } + async run(accessor: ServicesAccessor): Promise { + const accessibilitySignalService = accessor.get(IAccessibilitySignalService); + const outerEditor = getOuterEditorFromDiffEditor(accessor); + const accessibilityService = accessor.get(IAccessibilityService); + const codeEditorService = accessor.get(ICodeEditorService); + const quickDiffModelService = accessor.get(IQuickDiffModelService); - private disable(): void { - if (!this.enabled) { + if (!outerEditor || !outerEditor.hasModel()) { return; } - this.transientDisposables.clear(); + const modelRef = quickDiffModelService.createQuickDiffModelReference(outerEditor.getModel().uri); + try { + if (!modelRef || modelRef.object.changes.length === 0) { + return; + } - for (const [uri, decoratorMap] of this.decorators.entries()) { - decoratorMap.dispose(); - this.decorators.delete(uri); + const lineNumber = outerEditor.getPosition().lineNumber; + const index = modelRef.object.findNextClosestChange(lineNumber, false); + const change = modelRef.object.changes[index].change; + await playAccessibilitySymbolForChange(change, accessibilitySignalService); + setPositionAndSelection(change, outerEditor, accessibilityService, codeEditorService); + } finally { + modelRef?.dispose(); } - - this.enabled = false; } +} +registerEditorAction(GotoNextChangeAction); - private onEditorsChanged(): void { - for (const editor of this.editorService.visibleTextEditorControls) { - if (!isCodeEditor(editor)) { - continue; - } +MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { + group: '7_change_nav', + command: { + id: 'editor.action.dirtydiff.next', + title: nls.localize({ key: 'miGotoNextChange', comment: ['&& denotes a mnemonic'] }, "Next &&Change") + }, + order: 1 +}); - const textModel = editor.getModel(); - if (!textModel) { - continue; - } +MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { + group: '7_change_nav', + command: { + id: 'editor.action.dirtydiff.previous', + title: nls.localize({ key: 'miGotoPreviousChange', comment: ['&& denotes a mnemonic'] }, "Previous &&Change") + }, + order: 2 +}); - const editorId = editor.getId(); - if (this.decorators.get(textModel.uri)?.has(editorId)) { - continue; - } +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'closeQuickDiff', + weight: KeybindingWeight.EditorContrib + 50, + primary: KeyCode.Escape, + secondary: [KeyMod.Shift | KeyCode.Escape], + when: ContextKeyExpr.and(isQuickDiffVisible), + handler: (accessor: ServicesAccessor) => { + const outerEditor = getOuterEditorFromDiffEditor(accessor); - const dirtyDiffModelRef = this.dirtyDiffModelService.createDirtyDiffModelReference(textModel.uri); - if (!dirtyDiffModelRef) { - continue; - } + if (!outerEditor) { + return; + } - if (!this.decorators.has(textModel.uri)) { - this.decorators.set(textModel.uri, new DisposableMap()); - } + const controller = QuickDiffEditorController.get(outerEditor); - this.decorators.get(textModel.uri)!.set(editorId, new DirtyDiffDecorator(editor, dirtyDiffModelRef, this.configurationService)); + if (!controller) { + return; } - // Dispose decorators for editors that are no longer visible. - for (const [uri, decoratorMap] of this.decorators.entries()) { - for (const editorId of decoratorMap.keys()) { - const codeEditor = this.editorService.visibleTextEditorControls - .find(editor => isCodeEditor(editor) && editor.getId() === editorId && - this.uriIdentityService.extUri.isEqual(editor.getModel()?.uri, uri)); - - if (!codeEditor) { - decoratorMap.deleteAndDispose(editorId); - } - } + controller.close(); + } +}); - if (decoratorMap.size === 0) { - decoratorMap.dispose(); - this.decorators.delete(uri); - } - } +function setPositionAndSelection(change: IChange, editor: ICodeEditor, accessibilityService: IAccessibilityService, codeEditorService: ICodeEditorService) { + const position = new Position(change.modifiedStartLineNumber, 1); + editor.setPosition(position); + editor.revealPositionInCenter(position); + if (accessibilityService.isScreenReaderOptimized()) { + editor.setSelection({ startLineNumber: change.modifiedStartLineNumber, startColumn: 0, endLineNumber: change.modifiedStartLineNumber, endColumn: Number.MAX_VALUE }); + codeEditorService.getActiveCodeEditor()?.writeScreenReaderContent('diff-navigation'); } +} - override dispose(): void { - this.disable(); - super.dispose(); +async function playAccessibilitySymbolForChange(change: IChange, accessibilitySignalService: IAccessibilitySignalService) { + const changeType = getChangeType(change); + switch (changeType) { + case ChangeType.Add: + accessibilitySignalService.playSignal(AccessibilitySignal.diffLineInserted, { allowManyInParallel: true, source: 'quickDiffDecoration' }); + break; + case ChangeType.Delete: + accessibilitySignalService.playSignal(AccessibilitySignal.diffLineDeleted, { allowManyInParallel: true, source: 'quickDiffDecoration' }); + break; + case ChangeType.Modify: + accessibilitySignalService.playSignal(AccessibilitySignal.diffLineModified, { allowManyInParallel: true, source: 'quickDiffDecoration' }); + break; } } -registerEditorContribution(DirtyDiffController.ID, DirtyDiffController, EditorContributionInstantiation.AfterFirstRender); +function getOuterEditorFromDiffEditor(accessor: ServicesAccessor): ICodeEditor | null { + const diffEditors = accessor.get(ICodeEditorService).listDiffEditors(); + + for (const diffEditor of diffEditors) { + if (diffEditor.hasTextFocus() && diffEditor instanceof EmbeddedDiffEditorWidget) { + return diffEditor.getParentEditor(); + } + } + + return getOuterEditor(accessor); +} diff --git a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts index d7e997d29ff5c..0c2d7c1af5f1d 100644 --- a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts +++ b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts @@ -6,7 +6,7 @@ import { localize, localize2 } from '../../../../nls.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { IWorkbenchContributionsRegistry, registerWorkbenchContribution2, Extensions as WorkbenchExtensions, WorkbenchPhase } from '../../../common/contributions.js'; -import { DirtyDiffWorkbenchController } from './dirtydiffDecorator.js'; +import { QuickDiffWorkbenchController } from './quickDiffDecorator.js'; import { VIEWLET_ID, ISCMService, VIEW_PANE_ID, ISCMProvider, ISCMViewService, REPOSITORIES_VIEW_PANE_ID, HISTORY_VIEW_PANE_ID } from '../common/scm.js'; import { KeyMod, KeyCode } from '../../../../base/common/keyCodes.js'; import { MenuRegistry, MenuId } from '../../../../platform/actions/common/actions.js'; @@ -40,7 +40,9 @@ import { isSCMRepository } from './util.js'; import { SCMHistoryViewPane } from './scmHistoryViewPane.js'; import { IsWebContext } from '../../../../platform/contextkey/common/contextkeys.js'; import { RemoteNameContext } from '../../../common/contextkeys.js'; -import { DirtyDiffModelService, IDirtyDiffModelService } from './dirtyDiffModel.js'; +import { QuickDiffModelService, IQuickDiffModelService } from './quickDiffModel.js'; +import { QuickDiffEditorController } from './quickDiffWidget.js'; +import { EditorContributionInstantiation, registerEditorContribution } from '../../../../editor/browser/editorExtensions.js'; ModesRegistry.registerLanguage({ id: 'scminput', @@ -50,7 +52,10 @@ ModesRegistry.registerLanguage({ }); Registry.as(WorkbenchExtensions.Workbench) - .registerWorkbenchContribution(DirtyDiffWorkbenchController, LifecyclePhase.Restored); + .registerWorkbenchContribution(QuickDiffWorkbenchController, LifecyclePhase.Restored); + +registerEditorContribution(QuickDiffEditorController.ID, + QuickDiffEditorController, EditorContributionInstantiation.AfterFirstRender); const sourceControlViewIcon = registerIcon('source-control-view-icon', Codicon.sourceControl, localize('sourceControlViewIcon', 'View icon of the Source Control view.')); @@ -597,4 +602,4 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ registerSingleton(ISCMService, SCMService, InstantiationType.Delayed); registerSingleton(ISCMViewService, SCMViewService, InstantiationType.Delayed); registerSingleton(IQuickDiffService, QuickDiffService, InstantiationType.Delayed); -registerSingleton(IDirtyDiffModelService, DirtyDiffModelService, InstantiationType.Delayed); +registerSingleton(IQuickDiffModelService, QuickDiffModelService, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/scm/common/quickDiff.ts b/src/vs/workbench/contrib/scm/common/quickDiff.ts index 3770f071c7219..55cd69561f08a 100644 --- a/src/vs/workbench/contrib/scm/common/quickDiff.ts +++ b/src/vs/workbench/contrib/scm/common/quickDiff.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as nls from '../../../../nls.js'; + import { URI } from '../../../../base/common/uri.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { IDisposable } from '../../../../base/common/lifecycle.js'; @@ -10,9 +12,39 @@ import { LanguageSelector } from '../../../../editor/common/languageSelector.js' import { Event } from '../../../../base/common/event.js'; import { LineRangeMapping } from '../../../../editor/common/diff/rangeMapping.js'; import { IChange } from '../../../../editor/common/diff/legacyLinesDiffComputer.js'; +import { IColorTheme } from '../../../../platform/theme/common/themeService.js'; +import { Color } from '../../../../base/common/color.js'; +import { editorErrorForeground, registerColor, transparent } from '../../../../platform/theme/common/colorRegistry.js'; export const IQuickDiffService = createDecorator('quickDiff'); +const editorGutterModifiedBackground = registerColor('editorGutter.modifiedBackground', { + dark: '#1B81A8', light: '#2090D3', hcDark: '#1B81A8', hcLight: '#2090D3' +}, nls.localize('editorGutterModifiedBackground', "Editor gutter background color for lines that are modified.")); + +const editorGutterAddedBackground = registerColor('editorGutter.addedBackground', { + dark: '#487E02', light: '#48985D', hcDark: '#487E02', hcLight: '#48985D' +}, nls.localize('editorGutterAddedBackground', "Editor gutter background color for lines that are added.")); + +const editorGutterDeletedBackground = registerColor('editorGutter.deletedBackground', + editorErrorForeground, nls.localize('editorGutterDeletedBackground', "Editor gutter background color for lines that are deleted.")); + +export const minimapGutterModifiedBackground = registerColor('minimapGutter.modifiedBackground', + editorGutterModifiedBackground, nls.localize('minimapGutterModifiedBackground', "Minimap gutter background color for lines that are modified.")); + +export const minimapGutterAddedBackground = registerColor('minimapGutter.addedBackground', + editorGutterAddedBackground, nls.localize('minimapGutterAddedBackground', "Minimap gutter background color for lines that are added.")); + +export const minimapGutterDeletedBackground = registerColor('minimapGutter.deletedBackground', + editorGutterDeletedBackground, nls.localize('minimapGutterDeletedBackground', "Minimap gutter background color for lines that are deleted.")); + +export const overviewRulerModifiedForeground = registerColor('editorOverviewRuler.modifiedForeground', + transparent(editorGutterModifiedBackground, 0.6), nls.localize('overviewRulerModifiedForeground', 'Overview ruler marker color for modified content.')); +export const overviewRulerAddedForeground = registerColor('editorOverviewRuler.addedForeground', + transparent(editorGutterAddedBackground, 0.6), nls.localize('overviewRulerAddedForeground', 'Overview ruler marker color for added content.')); +export const overviewRulerDeletedForeground = registerColor('editorOverviewRuler.deletedForeground', + transparent(editorGutterDeletedBackground, 0.6), nls.localize('overviewRulerDeletedForeground', 'Overview ruler marker color for deleted content.')); + export interface QuickDiffProvider { label: string; rootUri: URI | undefined; @@ -52,3 +84,79 @@ export interface IQuickDiffService { addQuickDiffProvider(quickDiff: QuickDiffProvider): IDisposable; getQuickDiffs(uri: URI, language?: string, isSynchronized?: boolean): Promise; } + +export enum ChangeType { + Modify, + Add, + Delete +} + +export function getChangeType(change: IChange): ChangeType { + if (change.originalEndLineNumber === 0) { + return ChangeType.Add; + } else if (change.modifiedEndLineNumber === 0) { + return ChangeType.Delete; + } else { + return ChangeType.Modify; + } +} + +export function getChangeTypeColor(theme: IColorTheme, changeType: ChangeType): Color | undefined { + switch (changeType) { + case ChangeType.Modify: return theme.getColor(editorGutterModifiedBackground); + case ChangeType.Add: return theme.getColor(editorGutterAddedBackground); + case ChangeType.Delete: return theme.getColor(editorGutterDeletedBackground); + } +} + +export function compareChanges(a: IChange, b: IChange): number { + let result = a.modifiedStartLineNumber - b.modifiedStartLineNumber; + + if (result !== 0) { + return result; + } + + result = a.modifiedEndLineNumber - b.modifiedEndLineNumber; + + if (result !== 0) { + return result; + } + + result = a.originalStartLineNumber - b.originalStartLineNumber; + + if (result !== 0) { + return result; + } + + return a.originalEndLineNumber - b.originalEndLineNumber; +} + +export function getChangeHeight(change: IChange): number { + const modified = change.modifiedEndLineNumber - change.modifiedStartLineNumber + 1; + const original = change.originalEndLineNumber - change.originalStartLineNumber + 1; + + if (change.originalEndLineNumber === 0) { + return modified; + } else if (change.modifiedEndLineNumber === 0) { + return original; + } else { + return modified + original; + } +} + +export function getModifiedEndLineNumber(change: IChange): number { + if (change.modifiedEndLineNumber === 0) { + return change.modifiedStartLineNumber === 0 ? 1 : change.modifiedStartLineNumber; + } else { + return change.modifiedEndLineNumber; + } +} + +export function lineIntersectsChange(lineNumber: number, change: IChange): boolean { + // deletion at the beginning of the file + if (lineNumber === 1 && change.modifiedStartLineNumber === 0 && change.modifiedEndLineNumber === 0) { + return true; + } + + return lineNumber >= change.modifiedStartLineNumber && lineNumber <= (change.modifiedEndLineNumber || change.modifiedStartLineNumber); +} diff --git a/src/vs/workbench/contrib/scm/common/quickDiffService.ts b/src/vs/workbench/contrib/scm/common/quickDiffService.ts index 38a9d67a55508..5b872ae57227e 100644 --- a/src/vs/workbench/contrib/scm/common/quickDiffService.ts +++ b/src/vs/workbench/contrib/scm/common/quickDiffService.ts @@ -80,3 +80,8 @@ export class QuickDiffService extends Disposable implements IQuickDiffService { return diffs.filter(this.isQuickDiff); } } + +export async function getOriginalResource(quickDiffService: IQuickDiffService, uri: URI, language: string | undefined, isSynchronized: boolean | undefined): Promise { + const quickDiffs = await quickDiffService.getQuickDiffs(uri, language, isSynchronized); + return quickDiffs.length > 0 ? quickDiffs[0].originalResource : null; +} From d66eb6b82f4fd73db17a4b7cc0799f90a09207bc Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Mon, 9 Dec 2024 11:37:50 +0100 Subject: [PATCH 12/22] fixes https://github.com/microsoft/vscode/issues/210254 (#235603) * fixes https://github.com/microsoft/vscode/issues/213148 * fixes https://github.com/microsoft/vscode/issues/210254 --- src/vs/base/common/filters.ts | 2 +- src/vs/editor/contrib/suggest/browser/suggestWidget.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/base/common/filters.ts b/src/vs/base/common/filters.ts index 2ab6c40126d99..aa0b036ac76bf 100644 --- a/src/vs/base/common/filters.ts +++ b/src/vs/base/common/filters.ts @@ -675,7 +675,7 @@ export function fuzzyScore(pattern: string, patternLow: string, patternStart: nu } let diagScore = 0; - if (score !== Number.MAX_SAFE_INTEGER) { + if (score !== Number.MIN_SAFE_INTEGER) { canComeDiag = true; diagScore = score + _table[row - 1][column - 1]; } diff --git a/src/vs/editor/contrib/suggest/browser/suggestWidget.ts b/src/vs/editor/contrib/suggest/browser/suggestWidget.ts index 7b728a6e17176..048572143b1e6 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestWidget.ts @@ -928,7 +928,7 @@ export class SuggestWidget implements IDisposable { typicalHalfwidthCharacterWidth: fontInfo.typicalHalfwidthCharacterWidth, verticalPadding: 22, horizontalPadding: 14, - defaultSize: new dom.Dimension(430, statusBarHeight + 12 * itemHeight + borderHeight) + defaultSize: new dom.Dimension(430, statusBarHeight + 12 * itemHeight) }; } From 2299470205bc79ed6a35526384056ed6a90fee8c Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Mon, 9 Dec 2024 12:19:51 +0100 Subject: [PATCH 13/22] Fix separator overlay issue in settings editor (#235608) fixes #203353 --- .../contrib/preferences/browser/media/settingsEditor2.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css index 18b04188fc62f..aa1fb1c053675 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css +++ b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css @@ -151,6 +151,10 @@ margin-top: 14px; } +.settings-editor > .settings-body > .monaco-split-view2.separator-border .split-view-view:not(:first-child):before { + z-index: 16; /* Above sticky scroll */ +} + .settings-editor > .settings-body .settings-toc-container, .settings-editor > .settings-body .settings-tree-container { height: 100%; From abe1f677d0c149a6789c6464ed78c4b362f6b3c8 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Mon, 9 Dec 2024 13:04:01 +0100 Subject: [PATCH 14/22] Fix focus handling during outline control changes (#234905) * fix setting focus when outline is changing control * :lipstik: --- .../workbench/contrib/outline/browser/outlinePane.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/outline/browser/outlinePane.ts b/src/vs/workbench/contrib/outline/browser/outlinePane.ts index 6149cd6de1735..aa34cef4e641b 100644 --- a/src/vs/workbench/contrib/outline/browser/outlinePane.ts +++ b/src/vs/workbench/contrib/outline/browser/outlinePane.ts @@ -126,8 +126,10 @@ export class OutlinePane extends ViewPane implements IOutlinePane { } override focus(): void { - super.focus(); - this._tree?.domFocus(); + this._editorControlChangePromise.then(() => { + super.focus(); + this._tree?.domFocus(); + }); } protected override renderBody(container: HTMLElement): void { @@ -197,17 +199,18 @@ export class OutlinePane extends ViewPane implements IOutlinePane { return false; } + private _editorControlChangePromise: Promise = Promise.resolve(); private _handleEditorChanged(pane: IEditorPane | undefined): void { this._editorPaneDisposables.clear(); if (pane) { // react to control changes from within pane (https://github.com/microsoft/vscode/issues/134008) this._editorPaneDisposables.add(pane.onDidChangeControl(() => { - this._handleEditorControlChanged(pane); + this._editorControlChangePromise = this._handleEditorControlChanged(pane); })); } - this._handleEditorControlChanged(pane); + this._editorControlChangePromise = this._handleEditorControlChanged(pane); } private async _handleEditorControlChanged(pane: IEditorPane | undefined): Promise { From d5746c5593f6afe15e44a9f7877a064df6605d11 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Mon, 9 Dec 2024 14:12:56 +0100 Subject: [PATCH 15/22] Fix inconsistent focus behavior when toggling views/panels (#235622) fix #232790 --- src/vs/workbench/browser/layout.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index eb8657fba825a..782d0f48f385f 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -1754,9 +1754,9 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi else if (!hidden && !this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.Sidebar)) { const viewletToOpen = this.paneCompositeService.getLastActivePaneCompositeId(ViewContainerLocation.Sidebar); if (viewletToOpen) { - const viewlet = this.paneCompositeService.openPaneComposite(viewletToOpen, ViewContainerLocation.Sidebar, true); + const viewlet = this.paneCompositeService.openPaneComposite(viewletToOpen, ViewContainerLocation.Sidebar); if (!viewlet) { - this.paneCompositeService.openPaneComposite(this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.Sidebar)?.id, ViewContainerLocation.Sidebar, true); + this.paneCompositeService.openPaneComposite(this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.Sidebar)?.id, ViewContainerLocation.Sidebar); } } } @@ -1895,8 +1895,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } if (panelToOpen) { - const focus = !skipLayout; - this.paneCompositeService.openPaneComposite(panelToOpen, ViewContainerLocation.Panel, focus); + this.paneCompositeService.openPaneComposite(panelToOpen, ViewContainerLocation.Panel); } } @@ -1995,8 +1994,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } if (panelToOpen) { - const focus = !skipLayout; - this.paneCompositeService.openPaneComposite(panelToOpen, ViewContainerLocation.AuxiliaryBar, focus); + this.paneCompositeService.openPaneComposite(panelToOpen, ViewContainerLocation.AuxiliaryBar); } } From 07a9dc6328ea308487036d555553bffe914735f2 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Mon, 9 Dec 2024 15:01:44 +0100 Subject: [PATCH 16/22] IPC transform error cause (#235632) --- src/vs/base/common/errors.ts | 9 ++++- src/vs/base/test/common/errors.test.ts | 51 ++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/vs/base/common/errors.ts b/src/vs/base/common/errors.ts index fb18cb9b2e526..a6930ef51cf5b 100644 --- a/src/vs/base/common/errors.ts +++ b/src/vs/base/common/errors.ts @@ -126,20 +126,22 @@ export interface SerializedError { readonly message: string; readonly stack: string; readonly noTelemetry: boolean; + readonly cause?: SerializedError; } export function transformErrorForSerialization(error: Error): SerializedError; export function transformErrorForSerialization(error: any): any; export function transformErrorForSerialization(error: any): any { if (error instanceof Error) { - const { name, message } = error; + const { name, message, cause } = error; const stack: string = (error).stacktrace || (error).stack; return { $isError: true, name, message, stack, - noTelemetry: ErrorNoTelemetry.isErrorNoTelemetry(error) + noTelemetry: ErrorNoTelemetry.isErrorNoTelemetry(error), + cause: cause ? transformErrorForSerialization(cause) : undefined }; } @@ -157,6 +159,9 @@ export function transformErrorFromSerialization(data: SerializedError): Error { } error.message = data.message; error.stack = data.stack; + if (data.cause) { + error.cause = transformErrorFromSerialization(data.cause); + } return error; } diff --git a/src/vs/base/test/common/errors.test.ts b/src/vs/base/test/common/errors.test.ts index 9e33ed81868e5..9dec9b4d26ad7 100644 --- a/src/vs/base/test/common/errors.test.ts +++ b/src/vs/base/test/common/errors.test.ts @@ -6,6 +6,8 @@ import assert from 'assert'; import { toErrorMessage } from '../../common/errorMessage.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from './utils.js'; +import { transformErrorForSerialization, transformErrorFromSerialization } from '../../common/errors.js'; +import { assertType } from '../../common/types.js'; suite('Errors', () => { ensureNoDisposablesAreLeakedInTestSuite(); @@ -33,4 +35,53 @@ suite('Errors', () => { assert.ok(toErrorMessage(error, true).length > 'An unknown error occurred. Please consult the log for more details.'.length); } }); + + test('Transform Error for Serialization', function () { + const error = new Error('Test error'); + const serializedError = transformErrorForSerialization(error); + assert.strictEqual(serializedError.name, 'Error'); + assert.strictEqual(serializedError.message, 'Test error'); + assert.strictEqual(serializedError.stack, error.stack); + assert.strictEqual(serializedError.noTelemetry, false); + assert.strictEqual(serializedError.cause, undefined); + }); + + test('Transform Error with Cause for Serialization', function () { + const cause = new Error('Cause error'); + const error = new Error('Test error', { cause }); + const serializedError = transformErrorForSerialization(error); + assert.strictEqual(serializedError.name, 'Error'); + assert.strictEqual(serializedError.message, 'Test error'); + assert.strictEqual(serializedError.stack, error.stack); + assert.strictEqual(serializedError.noTelemetry, false); + assert.ok(serializedError.cause); + assert.strictEqual(serializedError.cause?.name, 'Error'); + assert.strictEqual(serializedError.cause?.message, 'Cause error'); + assert.strictEqual(serializedError.cause?.stack, cause.stack); + }); + + test('Transform Error from Serialization', function () { + const serializedError = transformErrorForSerialization(new Error('Test error')); + const error = transformErrorFromSerialization(serializedError); + assert.strictEqual(error.name, 'Error'); + assert.strictEqual(error.message, 'Test error'); + assert.strictEqual(error.stack, serializedError.stack); + assert.strictEqual(error.cause, undefined); + }); + + test('Transform Error with Cause from Serialization', function () { + const cause = new Error('Cause error'); + const serializedCause = transformErrorForSerialization(cause); + const error = new Error('Test error', { cause }); + const serializedError = transformErrorForSerialization(error); + const deserializedError = transformErrorFromSerialization(serializedError); + assert.strictEqual(deserializedError.name, 'Error'); + assert.strictEqual(deserializedError.message, 'Test error'); + assert.strictEqual(deserializedError.stack, serializedError.stack); + assert.ok(deserializedError.cause); + assertType(deserializedError.cause instanceof Error); + assert.strictEqual(deserializedError.cause?.name, 'Error'); + assert.strictEqual(deserializedError.cause?.message, 'Cause error'); + assert.strictEqual(deserializedError.cause?.stack, serializedCause.stack); + }); }); From 4cbf59e5dbeff7a177aae4e2a038ee9ee825e81e Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 9 Dec 2024 15:28:48 +0100 Subject: [PATCH 17/22] fix #235462 (#235636) --- src/vs/platform/userDataSync/common/extensionsSync.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/platform/userDataSync/common/extensionsSync.ts b/src/vs/platform/userDataSync/common/extensionsSync.ts index 1715f8cbbe958..4054babf6f051 100644 --- a/src/vs/platform/userDataSync/common/extensionsSync.ts +++ b/src/vs/platform/userDataSync/common/extensionsSync.ts @@ -485,6 +485,7 @@ export class LocalExtensionsProvider { installGivenVersion: e.pinned && !!e.version, pinned: e.pinned, installPreReleaseVersion: e.preRelease, + preRelease: e.preRelease, profileLocation: profile.extensionsResource, isApplicationScoped: e.isApplicationScoped, context: { [EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT]: true, [EXTENSION_INSTALL_SOURCE_CONTEXT]: ExtensionInstallSource.SETTINGS_SYNC } From f3a02f3d14117b7db8824237f71da3c61908e25a Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 9 Dec 2024 15:30:25 +0100 Subject: [PATCH 18/22] chat - indicate quota hit in `prominent` status (#235637) --- .../parts/statusbar/media/statusbarpart.css | 12 +++- .../browser/parts/statusbar/statusbarPart.ts | 70 +++++++++++++++++-- .../contrib/chat/browser/chatQuotasService.ts | 25 ++++--- .../services/statusbar/browser/statusbar.ts | 12 ++++ 4 files changed, 101 insertions(+), 18 deletions(-) diff --git a/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css b/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css index b30d2bee088aa..b7b1bd7dd7c88 100644 --- a/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css +++ b/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css @@ -205,7 +205,17 @@ background-color: var(--vscode-statusBarItem-prominentBackground); } -.monaco-workbench .part.statusbar > .items-container > .statusbar-item.prominent-kind a:hover:not(.disabled) { +/** + * Using :not(.compact-right):not(.compact-left) here to improve the visual appearance + * when a prominent item uses `compact: true` with other items. The presence of the + * !important directive for `background-color` otherwise blocks our special hover handling + * code here: + * https://github.com/microsoft/vscode/blob/c2037f152b2bb3119ce1d87f52987776540dd57f/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts#L483-L505 + * + * Note: this is currently only done for the prominent kind, but needs to be expanded if + * other kinds use compact feature. + */ +.monaco-workbench .part.statusbar > .items-container > .statusbar-item.prominent-kind:not(.compact-right):not(.compact-left) a:hover:not(.disabled) { color: var(--vscode-statusBarItem-prominentHoverForeground); background-color: var(--vscode-statusBarItem-prominentHoverBackground) !important; } diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index fddbd06e30762..7454d382d0c4e 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -5,11 +5,11 @@ import './media/statusbarpart.css'; import { localize } from '../../../../nls.js'; -import { Disposable, DisposableStore, dispose, disposeIfDisposable, IDisposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, disposeIfDisposable, IDisposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { MultiWindowParts, Part } from '../../part.js'; import { EventType as TouchEventType, Gesture, GestureEvent } from '../../../../base/browser/touch.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { StatusbarAlignment, IStatusbarService, IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarStyleOverride, isStatusbarEntryLocation, IStatusbarEntryLocation, isStatusbarEntryPriority, IStatusbarEntryPriority } from '../../../services/statusbar/browser/statusbar.js'; +import { StatusbarAlignment, IStatusbarService, IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarStyleOverride, isStatusbarEntryLocation, IStatusbarEntryLocation, isStatusbarEntryPriority, IStatusbarEntryPriority, IStatusbarEntryOverride } from '../../../services/statusbar/browser/statusbar.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; import { IAction, Separator, toAction } from '../../../../base/common/actions.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; @@ -75,6 +75,12 @@ export interface IStatusbarEntryContainer extends IDisposable { */ updateEntryVisibility(id: string, visible: boolean): void; + /** + * Allows to override the appearance of an entry with the provided ID. Only a subset + * of properties is allowed to be overridden. + */ + overrideEntry(id: string, override: IStatusbarEntryOverride): IDisposable; + /** * Focused the status bar. If one of the status bar entries was focused, focuses it directly. */ @@ -134,6 +140,9 @@ class StatusbarPart extends Part implements IStatusbarEntryContainer { private readonly _onWillDispose = this._register(new Emitter()); readonly onWillDispose = this._onWillDispose.event; + private readonly onDidOverrideEntry = this._register(new Emitter()); + private readonly entryOverrides = new Map(); + private leftItemsContainer: HTMLElement | undefined; private rightItemsContainer: HTMLElement | undefined; @@ -173,6 +182,28 @@ class StatusbarPart extends Part implements IStatusbarEntryContainer { this._register(this.contextService.onDidChangeWorkbenchState(() => this.updateStyles())); } + overrideEntry(id: string, override: IStatusbarEntryOverride): IDisposable { + this.entryOverrides.set(id, override); + this.onDidOverrideEntry.fire(id); + + return toDisposable(() => { + const currentOverride = this.entryOverrides.get(id); + if (currentOverride === override) { + this.entryOverrides.delete(id); + this.onDidOverrideEntry.fire(id); + } + }); + } + + private withEntryOverride(entry: IStatusbarEntry, id: string): IStatusbarEntry { + const override = this.entryOverrides.get(id); + if (override) { + entry = { ...entry, ...override }; + } + + return entry; + } + addEntry(entry: IStatusbarEntry, id: string, alignment: StatusbarAlignment, priorityOrLocation: number | IStatusbarEntryLocation | IStatusbarEntryPriority = 0): IStatusbarEntryAccessor { let priority: IStatusbarEntryPriority; if (isStatusbarEntryPriority(priorityOrLocation)) { @@ -220,10 +251,11 @@ class StatusbarPart extends Part implements IStatusbarEntryContainer { } private doAddEntry(entry: IStatusbarEntry, id: string, alignment: StatusbarAlignment, priority: IStatusbarEntryPriority): IStatusbarEntryAccessor { + const disposables = new DisposableStore(); // View model item const itemContainer = this.doCreateStatusItem(id, alignment); - const item = this.instantiationService.createInstance(StatusbarEntryItem, itemContainer, entry, this.hoverDelegate); + const item = disposables.add(this.instantiationService.createInstance(StatusbarEntryItem, itemContainer, this.withEntryOverride(entry, id), this.hoverDelegate)); // View model entry const viewModelEntry: IStatusbarViewModelEntry = new class implements IStatusbarViewModelEntry { @@ -245,9 +277,11 @@ class StatusbarPart extends Part implements IStatusbarEntryContainer { this.appendStatusbarEntry(viewModelEntry); } - return { + let lastEntry = entry; + const accessor: IStatusbarEntryAccessor = { update: entry => { - item.update(entry); + lastEntry = entry; + item.update(this.withEntryOverride(entry, id)); }, dispose: () => { const { needsFullRefresh } = this.doAddOrRemoveModelEntry(viewModelEntry, false); @@ -255,10 +289,20 @@ class StatusbarPart extends Part implements IStatusbarEntryContainer { this.appendStatusbarEntries(); } else { itemContainer.remove(); + this.updateCompactEntries(); } - dispose(item); + disposables.dispose(); } }; + + // React to overrides + disposables.add(this.onDidOverrideEntry.event(overrideEntryId => { + if (overrideEntryId === id) { + accessor.update(lastEntry); + } + })); + + return accessor; } private doCreateStatusItem(id: string, alignment: StatusbarAlignment, ...extraClasses: string[]): HTMLElement { @@ -790,6 +834,16 @@ export class StatusbarService extends MultiWindowParts implements } } + overrideEntry(id: string, override: IStatusbarEntryOverride): IDisposable { + const disposables = new DisposableStore(); + + for (const part of this.parts) { + disposables.add(part.overrideEntry(id, override)); + } + + return disposables; + } + focus(preserveEntryFocus?: boolean): void { this.activePart.focus(preserveEntryFocus); } @@ -856,6 +910,10 @@ export class ScopedStatusbarService extends Disposable implements IStatusbarServ this.statusbarEntryContainer.updateEntryVisibility(id, visible); } + overrideEntry(id: string, override: IStatusbarEntryOverride): IDisposable { + return this.statusbarEntryContainer.overrideEntry(id, override); + } + focus(preserveEntryFocus?: boolean): void { this.statusbarEntryContainer.focus(preserveEntryFocus); } diff --git a/src/vs/workbench/contrib/chat/browser/chatQuotasService.ts b/src/vs/workbench/contrib/chat/browser/chatQuotasService.ts index 467affc78991b..7b82ddbb330c3 100644 --- a/src/vs/workbench/contrib/chat/browser/chatQuotasService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatQuotasService.ts @@ -8,7 +8,7 @@ import { safeIntl } from '../../../../base/common/date.js'; import { language } from '../../../../base/common/platform.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { MarkdownString } from '../../../../base/common/htmlContent.js'; -import { Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; import { localize, localize2 } from '../../../../nls.js'; import { Categories } from '../../../../platform/action/common/actionCommonCategories.js'; import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js'; @@ -18,7 +18,7 @@ import { createDecorator, ServicesAccessor } from '../../../../platform/instanti import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js'; import product from '../../../../platform/product/common/product.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; -import { IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from '../../../services/statusbar/browser/statusbar.js'; +import { IStatusbarService, StatusbarAlignment } from '../../../services/statusbar/browser/statusbar.js'; import { ChatContextKeys } from '../common/chatContextKeys.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; @@ -212,7 +212,7 @@ export class ChatQuotasStatusBarEntry extends Disposable implements IWorkbenchCo private static readonly COPILOT_STATUS_ID = 'GitHub.copilot.status'; // TODO@bpasero unify into 1 core indicator - private readonly _entry = this._register(new MutableDisposable()); + private readonly entry = this._register(new DisposableStore()); constructor( @IStatusbarService private readonly statusbarService: IStatusbarService, @@ -221,12 +221,17 @@ export class ChatQuotasStatusBarEntry extends Disposable implements IWorkbenchCo super(); this._register(Event.runAndSubscribe(this.chatQuotasService.onDidChangeQuotas, () => this.updateStatusbarEntry())); + this._register(this.statusbarService.onDidChangeEntryVisibility(e => { + if (e.id === ChatQuotasStatusBarEntry.COPILOT_STATUS_ID) { + this.updateStatusbarEntry(); + } + })); } private updateStatusbarEntry(): void { - const { chatQuotaExceeded, completionsQuotaExceeded } = this.chatQuotasService.quotas; + this.entry.clear(); - // Some quota exceeded, show indicator + const { chatQuotaExceeded, completionsQuotaExceeded } = this.chatQuotasService.quotas; if (chatQuotaExceeded || completionsQuotaExceeded) { let text: string; if (chatQuotaExceeded && !completionsQuotaExceeded) { @@ -242,23 +247,21 @@ export class ChatQuotasStatusBarEntry extends Disposable implements IWorkbenchCo text = `$(copilot-warning) ${text}`; } - this._entry.value = this.statusbarService.addEntry({ + this.entry.add(this.statusbarService.addEntry({ name: localize('indicator', "Copilot Limit Indicator"), text, ariaLabel: text, command: OPEN_CHAT_QUOTA_EXCEEDED_DIALOG, showInAllWindows: true, + kind: 'prominent', tooltip: quotaToButtonMessage({ chatQuotaExceeded, completionsQuotaExceeded }) }, ChatQuotasStatusBarEntry.ID, StatusbarAlignment.RIGHT, { id: ChatQuotasStatusBarEntry.COPILOT_STATUS_ID, alignment: StatusbarAlignment.RIGHT, compact: isCopilotStatusVisible - }); - } + })); - // No quota exceeded, remove indicator - else { - this._entry.clear(); + this.entry.add(this.statusbarService.overrideEntry(ChatQuotasStatusBarEntry.COPILOT_STATUS_ID, { kind: 'prominent' })); } } } diff --git a/src/vs/workbench/services/statusbar/browser/statusbar.ts b/src/vs/workbench/services/statusbar/browser/statusbar.ts index 3b3f1f1fe1739..1c589dc4f6621 100644 --- a/src/vs/workbench/services/statusbar/browser/statusbar.ts +++ b/src/vs/workbench/services/statusbar/browser/statusbar.ts @@ -197,3 +197,15 @@ export interface IStatusbarEntryAccessor extends IDisposable { */ update(properties: IStatusbarEntry): void; } + +/** + * A way to override a status bar entry appearance. Only a subset of + * properties are currently allowed to override. + */ +export interface IStatusbarEntryOverride { + + /** + * The kind of status bar entry. This applies different colors to the entry. + */ + readonly kind?: StatusbarEntryKind; +} From 5fff871b240a64708aa5083b36907fc859049df1 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 9 Dec 2024 15:47:47 +0100 Subject: [PATCH 19/22] fix #233160 (#235638) --- src/vs/platform/configuration/common/configuration.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/vs/platform/configuration/common/configuration.ts b/src/vs/platform/configuration/common/configuration.ts index a93872f4cd77e..e9a9143fa22a4 100644 --- a/src/vs/platform/configuration/common/configuration.ts +++ b/src/vs/platform/configuration/common/configuration.ts @@ -258,6 +258,10 @@ export function removeFromValueTree(valueTree: any, key: string): void { } function doRemoveFromValueTree(valueTree: any, segments: string[]): void { + if (!valueTree) { + return; + } + const first = segments.shift()!; if (segments.length === 0) { // Reached last segment From 869a6422160a8716f09ae54f3ba6a767dc30bb05 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Mon, 9 Dec 2024 17:23:26 +0100 Subject: [PATCH 20/22] fix falsy error (#235651) --- src/vs/editor/common/languages/defaultDocumentColorsComputer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/editor/common/languages/defaultDocumentColorsComputer.ts b/src/vs/editor/common/languages/defaultDocumentColorsComputer.ts index 3c8acf2c7cd61..74bb07579052d 100644 --- a/src/vs/editor/common/languages/defaultDocumentColorsComputer.ts +++ b/src/vs/editor/common/languages/defaultDocumentColorsComputer.ts @@ -36,7 +36,7 @@ function _toIColor(r: number, g: number, b: number, a: number): IColor { function _findRange(model: IDocumentColorComputerTarget, match: RegExpMatchArray): IRange | undefined { const index = match.index; const length = match[0].length; - if (!index) { + if (index === undefined) { return; } const startPosition = model.positionAt(index); From 686c08b1529ba857b3dcd3cb8de298efca43454e Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Mon, 9 Dec 2024 18:01:01 +0100 Subject: [PATCH 21/22] Fix focus outline cropping issue in tabs (#235655) fix #227552 --- .../browser/parts/editor/media/multieditortabscontrol.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/media/multieditortabscontrol.css b/src/vs/workbench/browser/parts/editor/media/multieditortabscontrol.css index 93559402aa575..d0bb1d3d1860b 100644 --- a/src/vs/workbench/browser/parts/editor/media/multieditortabscontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/multieditortabscontrol.css @@ -50,7 +50,7 @@ display: none; } -.monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container.tabs-border-bottom::after { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tab:not([tabIndex="0"]) .tabs-and-actions-container.tabs-border-bottom::after { content: ''; position: absolute; bottom: 0; @@ -293,7 +293,7 @@ background-color: var(--tab-border-top-color); } -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active.tab-border-bottom > .tab-border-bottom-container { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active.tab-border-bottom:not([tabIndex="0"]) > .tab-border-bottom-container { z-index: 10; bottom: 0; height: 1px; From d0507ca130176a0e89f56858bbee058bf7f2daf0 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Mon, 9 Dec 2024 18:20:38 +0100 Subject: [PATCH 22/22] add logging for non-existing chat session restoration (#235661) --- .../browser/chatEditing/chatEditingService.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts index 458687df09a9c..912b37c662477 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts @@ -22,6 +22,7 @@ import { localize, localize2 } from '../../../../../nls.js'; import { IContextKey, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { ILogService } from '../../../../../platform/log/common/log.js'; import { bindContextKey } from '../../../../../platform/observable/common/platformObservableUtils.js'; import { IProgressService, ProgressLocation } from '../../../../../platform/progress/common/progress.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; @@ -89,6 +90,7 @@ export class ChatEditingService extends Disposable implements IChatEditingServic @ILifecycleService private readonly lifecycleService: ILifecycleService, @IWorkbenchAssignmentService private readonly _workbenchAssignmentService: IWorkbenchAssignmentService, @IStorageService storageService: IStorageService, + @ILogService logService: ILogService, ) { super(); this._applyingChatEditsFailedContextKey = applyingChatEditsFailedContextKey.bindTo(contextKeyService); @@ -157,11 +159,16 @@ export class ChatEditingService extends Disposable implements IChatEditingServic void this._editingSessionFileLimitPromise; const sessionIdToRestore = storageService.get(STORAGE_KEY_EDITING_SESSION, StorageScope.WORKSPACE); - if (isString(sessionIdToRestore) && this._chatService.getOrRestoreSession(sessionIdToRestore)) { - this._restoringEditingSession = this.startOrContinueEditingSession(sessionIdToRestore); - this._restoringEditingSession.finally(() => { - this._restoringEditingSession = undefined; - }); + if (isString(sessionIdToRestore)) { + if (this._chatService.getOrRestoreSession(sessionIdToRestore)) { + this._restoringEditingSession = this.startOrContinueEditingSession(sessionIdToRestore); + this._restoringEditingSession.finally(() => { + this._restoringEditingSession = undefined; + }); + } else { + logService.error(`Edit session session to restore is a non-existing chat session: ${sessionIdToRestore}`); + } + storageService.remove(STORAGE_KEY_EDITING_SESSION, StorageScope.WORKSPACE); } }