Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 124 additions & 3 deletions src/vs/workbench/browser/parts/editor/textDiffEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,17 @@ import { CancellationToken } from '../../../../base/common/cancellation.js';
import { EditorActivation, ITextEditorOptions } from '../../../../platform/editor/common/editor.js';
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
import { isEqual } from '../../../../base/common/resources.js';
import { Dimension, multibyteAwareBtoa } from '../../../../base/browser/dom.js';
import { Dimension, multibyteAwareBtoa, append, $ } from '../../../../base/browser/dom.js';
import { ByteSize, FileOperationError, FileOperationResult, IFileService, TooLargeFileOperationError } from '../../../../platform/files/common/files.js';
import { IBoundarySashes } from '../../../../base/browser/ui/sash/sash.js';
import { IPreferencesService } from '../../../services/preferences/common/preferences.js';
import { StopWatch } from '../../../../base/common/stopwatch.js';
import { DiffEditorWidget } from '../../../../editor/browser/widget/diffEditor/diffEditorWidget.js';
import { IMenuService, MenuId, IMenu } from '../../../../platform/actions/common/actions.js';
import { ISCMService } from '../../contrib/scm/common/scm.js';
import { WorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js';
import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';
import { getFlatActionBarActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';

/**
* The text editor that leverages the diff text editor for the editing experience.
Expand All @@ -43,6 +48,10 @@ export class TextDiffEditor extends AbstractTextEditor<IDiffEditorViewState> imp
static readonly ID = TEXT_DIFF_EDITOR_ID;

private diffEditorControl: IDiffEditor | undefined = undefined;
private scmToolbarContainer: HTMLElement | undefined = undefined;
private scmToolbar: WorkbenchToolBar | undefined = undefined;
private scmMenu: IMenu | undefined = undefined;
private scmToolbarDisposables = new DisposableStore();

private inputLifecycleStopWatch: StopWatch | undefined = undefined;

Expand All @@ -67,7 +76,9 @@ export class TextDiffEditor extends AbstractTextEditor<IDiffEditorViewState> imp
@IThemeService themeService: IThemeService,
@IEditorGroupsService editorGroupService: IEditorGroupsService,
@IFileService fileService: IFileService,
@IPreferencesService private readonly preferencesService: IPreferencesService
@IPreferencesService private readonly preferencesService: IPreferencesService,
@IMenuService private readonly menuService: IMenuService,
@ISCMService private readonly scmService: ISCMService
) {
super(TextDiffEditor.ID, group, telemetryService, instantiationService, storageService, configurationService, themeService, editorService, editorGroupService, fileService);
}
Expand All @@ -81,7 +92,13 @@ export class TextDiffEditor extends AbstractTextEditor<IDiffEditorViewState> imp
}

protected override createEditorControl(parent: HTMLElement, configuration: ICodeEditorOptions): void {
this.diffEditorControl = this._register(this.instantiationService.createInstance(DiffEditorWidget, parent, configuration, {}));
// Create container for the toolbar
this.scmToolbarContainer = append(parent, $('.scm-diff-toolbar'));
this.scmToolbarContainer.style.display = 'none'; // Initially hidden

// Create the diff editor
const diffContainer = append(parent, $('.diff-editor-container'));
this.diffEditorControl = this._register(this.instantiationService.createInstance(DiffEditorWidget, diffContainer, configuration, {}));
}

protected updateEditorControlOptions(options: ICodeEditorOptions): void {
Expand All @@ -94,6 +111,99 @@ export class TextDiffEditor extends AbstractTextEditor<IDiffEditorViewState> imp

private _previousViewModel: IDiffEditorViewModel | null = null;

private isScmDiff(input: DiffEditorInput): boolean {
// Check if either the original or modified resource is from an SCM provider
const originalUri = input.original.resource;
const modifiedUri = input.modified.resource;

// Look for SCM repositories that might own these resources
for (const repository of this.scmService.repositories) {
const rootUri = repository.provider.rootUri;
if (rootUri) {
// Check if the modified file is within the repository
if (this.isUriWithinRepository(modifiedUri, rootUri)) {
return true;
}
}
}

// Also check for specific SCM schemes (git, etc.)
return originalUri?.scheme === 'git' || modifiedUri?.scheme === 'git' ||
originalUri?.path.includes('.git') || modifiedUri?.path.includes('.git');
}

private isUriWithinRepository(uri: URI, rootUri: URI): boolean {
return uri.toString().startsWith(rootUri.toString());
}

private setupScmToolbar(input: DiffEditorInput): void {
this.clearScmToolbar();

if (!this.isScmDiff(input) || !this.scmToolbarContainer) {
return;
}

// Add styling to the toolbar container
this.scmToolbarContainer.style.display = 'flex';
this.scmToolbarContainer.style.alignItems = 'center';
this.scmToolbarContainer.style.padding = '4px 8px';
this.scmToolbarContainer.style.borderBottom = '1px solid var(--vscode-panel-border)';
this.scmToolbarContainer.style.backgroundColor = 'var(--vscode-editor-background)';

// Create the SCM context menu
const contextKeyService = this.scopedContextKeyService || this.group.contextKeyService;
const modifiedUri = input.modified.resource;
const originalUri = input.original.resource;

// Set up context keys for SCM (similar to QuickDiffWidget)
const contextKeys = contextKeyService.createOverlay([
['originalResource', originalUri?.toString()],
['originalResourceScheme', originalUri?.scheme],
['resource', modifiedUri?.toString()],
['resourceScheme', modifiedUri?.scheme]
]);

this.scmMenu = this.scmToolbarDisposables.add(this.menuService.createMenu(MenuId.SCMChangeContext, contextKeys));

// Create the toolbar
this.scmToolbar = this.scmToolbarDisposables.add(this.instantiationService.createInstance(
WorkbenchToolBar,
this.scmToolbarContainer,
{
orientation: 0, // Horizontal
actionViewItemProvider: undefined,
ariaLabel: 'SCM Actions',
resetMenu: MenuId.SCMChangeContext
}
));

// Populate the toolbar with actions
this.updateScmToolbar();

// Listen for menu changes
this.scmToolbarDisposables.add(this.scmMenu.onDidChange(() => this.updateScmToolbar()));
}

private updateScmToolbar(): void {
if (!this.scmMenu || !this.scmToolbar) {
return;
}

const actions = getFlatActionBarActions(this.scmMenu.getActions({ shouldForwardArgs: true }));
this.scmToolbar.setActions(actions);
}

private clearScmToolbar(): void {
this.scmToolbarDisposables.clear();

if (this.scmToolbarContainer) {
this.scmToolbarContainer.style.display = 'none';
}

this.scmMenu = undefined;
this.scmToolbar = undefined;
}

override async setInput(input: DiffEditorInput, options: ITextEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise<void> {
if (this._previousViewModel) {
this._previousViewModel.dispose();
Expand Down Expand Up @@ -129,6 +239,9 @@ export class TextDiffEditor extends AbstractTextEditor<IDiffEditorViewState> imp
await vm?.waitForDiff();
control.setModel(vm);

// Setup SCM toolbar if this is an SCM diff
this.setupScmToolbar(input);

// Restore view state (unless provided by options)
let hasPreviousViewState = false;
if (!isTextEditorViewState(options?.viewState)) {
Expand Down Expand Up @@ -317,6 +430,9 @@ export class TextDiffEditor extends AbstractTextEditor<IDiffEditorViewState> imp
this._previousViewModel = null;
}

// Clear SCM toolbar
this.clearScmToolbar();

super.clearInput();

// Log input lifecycle telemetry
Expand Down Expand Up @@ -429,4 +545,9 @@ export class TextDiffEditor extends AbstractTextEditor<IDiffEditorViewState> imp
// create a URI that is the Base64 concatenation of original + modified resource
return URI.from({ scheme: 'diff', path: `${multibyteAwareBtoa(original.toString())}${multibyteAwareBtoa(modified.toString())}` });
}

override dispose(): void {
this.clearScmToolbar();
super.dispose();
}
}