Skip to content
Open
Show file tree
Hide file tree
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Button } from '../../../../base/browser/ui/button/button.js';
import { Codicon } from '../../../../base/common/codicons.js';
import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';
import { autorun, derived, globalTransaction, observableValue } from '../../../../base/common/observable.js';
import { localize } from '../../../../nls.js';
import { createActionViewItem } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';
import { MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js';
import { MenuId } from '../../../../platform/actions/common/actions.js';
Expand Down Expand Up @@ -244,21 +245,43 @@ export class DiffEditorItemTemplate extends Disposable implements IPooledObject<
let isDeleted = false;
let isAdded = false;
let flag = '';
let status = 'Modified';
if (data.viewModel.modifiedUri && data.viewModel.originalUri && data.viewModel.modifiedUri.path !== data.viewModel.originalUri.path) {
flag = 'R';
isRenamed = true;
status = 'Renamed';
} else if (!data.viewModel.modifiedUri) {
flag = 'D';
isDeleted = true;
status = 'Deleted';
} else if (!data.viewModel.originalUri) {
flag = 'A';
isAdded = true;
status = 'Added';
}
this._elements.status.classList.toggle('renamed', isRenamed);
this._elements.status.classList.toggle('deleted', isDeleted);
this._elements.status.classList.toggle('added', isAdded);
this._elements.status.innerText = flag;

// Set aria-label for the individual file container
const originalFileName = data.viewModel.originalUri?.path.split('/').pop();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can use basename

const modifiedFileName = data.viewModel.modifiedUri?.path.split('/').pop();

// Set aria-labels for the headers
let fileAriaLabel = '';
if (isRenamed) {
fileAriaLabel = localize('renamedFileHeader', 'Renamed file from {0} to {1}', originalFileName, modifiedFileName);
} else if (isAdded) {
fileAriaLabel = localize('modifiedFileHeader', '{0} file {1}', status, modifiedFileName);
} else {
fileAriaLabel = localize('modifiedFileHeader', '{0} file {1}', status, originalFileName);
Comment on lines +276 to +278
Copy link

Copilot AI Nov 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The localization key 'modifiedFileHeader' is used for both 'Added' and 'Modified'/'Deleted' status messages (lines 276 and 278). Consider using a more generic key name like 'fileHeader' since it's not exclusively for modified files, or use distinct keys for each status type to allow for more context-specific translations.

Suggested change
fileAriaLabel = localize('modifiedFileHeader', '{0} file {1}', status, modifiedFileName);
} else {
fileAriaLabel = localize('modifiedFileHeader', '{0} file {1}', status, originalFileName);
fileAriaLabel = localize('addedFileHeader', 'Added file {0}', modifiedFileName);
} else if (isDeleted) {
fileAriaLabel = localize('deletedFileHeader', 'Deleted file {0}', originalFileName);
} else {
fileAriaLabel = localize('modifiedFileHeader', 'Modified file {0}', originalFileName);

Copilot uses AI. Check for mistakes.
}

this._elements.root.setAttribute('aria-label', fileAriaLabel);
this._elements.root.setAttribute('role', 'region');
this._elements.root.setAttribute('tabindex', '0');

this._resourceLabel2?.setUri(isRenamed ? data.viewModel.originalUri : undefined, { strikethrough: true });

this._dataStore.clear();
Expand Down Expand Up @@ -323,4 +346,8 @@ export class DiffEditorItemTemplate extends Disposable implements IPooledObject<
this._elements.root.style.top = `-100000px`;
this._elements.root.style.visibility = 'hidden'; // Some editor parts are still visible
}

public getContainerElement(): HTMLElement {
return this._elements.root;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,14 @@ export class MultiDiffEditorWidget extends Disposable {
public findDocumentDiffItem(resource: URI): IDocumentDiffItem | undefined {
return this._widgetImpl.get().findDocumentDiffItem(resource);
}

public goToNextFile(): void {
this._widgetImpl.get().goToNextFile();
}

public goToPreviousFile(): void {
this._widgetImpl.get().goToPreviousFile();
}
}

export interface RevealOptions {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { Dimension, getWindow, h, scheduleAtNextAnimationFrame } from '../../../../base/browser/dom.js';
import { SmoothScrollableElement } from '../../../../base/browser/ui/scrollbar/scrollableElement.js';
import { compareBy, numberComparator } from '../../../../base/common/arrays.js';
Expand Down Expand Up @@ -88,7 +87,10 @@ export class MultiDiffEditorWidgetImpl extends Disposable {
horizontal: ScrollbarVisibility.Auto,
useShadows: false,
}, this._scrollable));
this._elements = h('div.monaco-component.multiDiffEditor', {}, [
this._elements = h('div.monaco-component.multiDiffEditor', {
'aria-label': localize('multiDiffEditor.ariaLabel', 'Multi File Diff Editor'),
'role': 'region'
}, [
h('div', {}, [this._scrollableElement.getDomNode()]),
h('div.placeholder@placeholder', {}, [h('div')]),
]);
Expand Down Expand Up @@ -346,6 +348,75 @@ export class MultiDiffEditorWidgetImpl extends Disposable {

this._scrollableElements.content.style.transform = `translateY(${-(scrollTop + contentScrollOffsetToScrollOffset)}px)`;
}

public goToNextFile(): void {
const viewModel = this._viewModel.get();
if (!viewModel) {
return;
}

const items = viewModel.items.get();
const activeItem = viewModel.activeDiffItem.get();
const currentIndex = activeItem ? items.indexOf(activeItem) : -1;
const nextIndex = Math.min(currentIndex + 1, items.length - 1);

if (nextIndex !== currentIndex && nextIndex >= 0) {
Copy link

Copilot AI Nov 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The check nextIndex >= 0 on line 363 is redundant. Since currentIndex can be at minimum -1, and nextIndex = currentIndex + 1, the minimum value of nextIndex is 0. The condition nextIndex !== currentIndex is sufficient to ensure we're not already at the last item.

Suggested change
if (nextIndex !== currentIndex && nextIndex >= 0) {
if (nextIndex !== currentIndex) {

Copilot uses AI. Check for mistakes.
const nextItem = items[nextIndex];
viewModel.activeDiffItem.setCache(nextItem, undefined);
this.scrollToItem(nextIndex);
this.focusFileContainer(nextIndex);
}
}

public goToPreviousFile(): void {
const viewModel = this._viewModel.get();
if (!viewModel) {
return;
}

const items = viewModel.items.get();
const activeItem = viewModel.activeDiffItem.get();
const currentIndex = activeItem ? items.indexOf(activeItem) : -1;
const prevIndex = Math.max(currentIndex - 1, 0);

if (prevIndex !== currentIndex && prevIndex >= 0) {
Copy link

Copilot AI Nov 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The check prevIndex >= 0 on line 382 is redundant. Since prevIndex = Math.max(currentIndex - 1, 0), it's guaranteed to be at least 0. The condition prevIndex !== currentIndex is sufficient to ensure we're not already at the first item.

Suggested change
if (prevIndex !== currentIndex && prevIndex >= 0) {
if (prevIndex !== currentIndex) {

Copilot uses AI. Check for mistakes.
const prevItem = items[prevIndex];
viewModel.activeDiffItem.setCache(prevItem, undefined);
this.scrollToItem(prevIndex);
this.focusFileContainer(prevIndex);
}
}

private scrollToItem(index: number): void {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any reason we don't want to use MultiDiffEditorWidgetImpl#reveal?

const viewItems = this._viewItems.get();
if (index < 0 || index >= viewItems.length) {
return;
}

let scrollTop = 0;
for (let i = 0; i < index; i++) {
scrollTop += viewItems[i].contentHeight.get() + this._spaceBetweenPx;
}

this._scrollableElement.setScrollPosition({ scrollTop });
}

private focusFileContainer(index: number): void {
const viewItems = this._viewItems.get();
if (index < 0 || index >= viewItems.length) {
return;
}

const viewItem = viewItems[index];
const template = viewItem.template.get();
if (template) {
// Focus the main file container div to trigger screen reader announcement
const containerElement = template.getContainerElement();
if (containerElement) {
containerElement.focus();
}
}
}
}

function highlightRange(targetEditor: ICodeEditor, range: IRange) {
Expand Down
44 changes: 44 additions & 0 deletions src/vs/workbench/contrib/multiDiffEditor/browser/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,47 @@ export class ExpandAllAction extends Action2 {
}
}
}

export class GoToNextFileAction extends Action2 {
constructor() {
super({
id: 'multiDiffEditor.goToNextFile',
title: localize2('goToNextFile', 'Go to Next File'),
icon: Codicon.goToFile,
precondition: ActiveEditorContext.isEqualTo(MultiDiffEditor.ID),
f1: true
});
}

async run(accessor: ServicesAccessor, ...args: unknown[]): Promise<void> {
const editorService = accessor.get(IEditorService);
const activeEditorPane = editorService.activeEditorPane;
if (!(activeEditorPane instanceof MultiDiffEditor)) {
return;
}

activeEditorPane.goToNextFile();
}
}

export class GoToPreviousFileAction extends Action2 {
constructor() {
super({
id: 'multiDiffEditor.goToPreviousFile',
title: localize2('goToPreviousFile', 'Go to Previous File'),
icon: Codicon.goToFile,
precondition: ActiveEditorContext.isEqualTo(MultiDiffEditor.ID),
f1: true
});
}

async run(accessor: ServicesAccessor, ...args: unknown[]): Promise<void> {
const editorService = accessor.get(IEditorService);
const activeEditorPane = editorService.activeEditorPane;
if (!(activeEditorPane instanceof MultiDiffEditor)) {
return;
}

activeEditorPane.goToPreviousFile();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@ import { WorkbenchPhase, registerWorkbenchContribution2 } from '../../../common/
import { EditorExtensions, IEditorFactoryRegistry } from '../../../common/editor.js';
import { MultiDiffEditor } from './multiDiffEditor.js';
import { MultiDiffEditorInput, MultiDiffEditorResolverContribution, MultiDiffEditorSerializer } from './multiDiffEditorInput.js';
import { CollapseAllAction, ExpandAllAction, GoToFileAction } from './actions.js';
import { CollapseAllAction, ExpandAllAction, GoToFileAction, GoToPreviousFileAction, GoToNextFileAction } from './actions.js';
import { IMultiDiffSourceResolverService, MultiDiffSourceResolverService } from './multiDiffSourceResolverService.js';
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
import { OpenScmGroupAction, ScmMultiDiffSourceResolverContribution } from './scmMultiDiffSourceResolver.js';

registerAction2(GoToFileAction);
registerAction2(CollapseAllAction);
registerAction2(ExpandAllAction);
registerAction2(GoToNextFileAction);
registerAction2(GoToPreviousFileAction);

Registry.as<IConfigurationRegistry>(Extensions.Configuration)
.registerConfiguration({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,14 @@ export class MultiDiffEditor extends AbstractEditorWithViewState<IMultiDiffEdito
public async showWhile(promise: Promise<unknown>): Promise<void> {
return this.editorProgressService.showWhile(promise);
}

public goToNextFile(): void {
this._multiDiffEditorWidget?.goToNextFile();
}

public goToPreviousFile(): void {
this._multiDiffEditorWidget?.goToPreviousFile();
}
}


Expand Down
Loading