Skip to content

Commit a567b59

Browse files
authored
introduce --merge to bring up merge editor (for #5770) (#155039)
* introduce `--merge` to bring up merge editor (for #5770) * wait on proper editor when merging * sqlite slowness * disable flush on write in tests unless disk tests * more runWithFakedTimers * disable flush also in pfs * introduce `IResourceMergeEditorInput` * cleanup * align with merge editor names * stronger check * adopt `ResourceSet` * no need to coalesce * improve `matches` method
1 parent 03c16c9 commit a567b59

File tree

33 files changed

+494
-197
lines changed

33 files changed

+494
-197
lines changed

src/vs/code/electron-main/app.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1012,6 +1012,7 @@ export class CodeApplication extends Disposable {
10121012
cli: args,
10131013
forceNewWindow: args['new-window'] || (!hasCliArgs && args['unity-launch']),
10141014
diffMode: args.diff,
1015+
mergeMode: args.merge,
10151016
noRecentEntry,
10161017
waitMarkerFileURI,
10171018
gotoLineMode: args.goto,

src/vs/platform/environment/common/argv.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export interface NativeParsedArgs {
1818
wait?: boolean;
1919
waitMarkerFilePath?: string;
2020
diff?: boolean;
21+
merge?: boolean;
2122
add?: boolean;
2223
goto?: boolean;
2324
'new-window'?: boolean;

src/vs/platform/environment/node/argv.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ type OptionTypeName<T> =
4141

4242
export const OPTIONS: OptionDescriptions<Required<NativeParsedArgs>> = {
4343
'diff': { type: 'boolean', cat: 'o', alias: 'd', args: ['file', 'file'], description: localize('diff', "Compare two files with each other.") },
44+
'merge': { type: 'boolean', cat: 'o', alias: 'm', args: ['path1', 'path2', 'base', 'result'], description: localize('merge', "Perform a three-way merge by providing paths for two modified versions of a file, the common origin of both modified versions and the output file to save merge results.") },
4445
'add': { type: 'boolean', cat: 'o', alias: 'a', args: 'folder', description: localize('add', "Add folder(s) to the last active window.") },
4546
'goto': { type: 'boolean', cat: 'o', alias: 'g', args: 'file:line[:character]', description: localize('goto', "Open a file at the path on the specified line and character position.") },
4647
'new-window': { type: 'boolean', cat: 'o', alias: 'n', description: localize('newWindow', "Force to open a new window.") },

src/vs/platform/launch/electron-main/launchMainService.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ export class LaunchMainService implements ILaunchMainService {
190190
preferNewWindow: !args['reuse-window'] && !args.wait,
191191
forceReuseWindow: args['reuse-window'],
192192
diffMode: args.diff,
193+
mergeMode: args.merge,
193194
addMode: args.add,
194195
noRecentEntry: !!args['skip-add-to-recently-opened'],
195196
gotoLineMode: args.goto

src/vs/platform/native/electron-main/nativeHostMainService.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
154154
forceReuseWindow: options.forceReuseWindow,
155155
preferNewWindow: options.preferNewWindow,
156156
diffMode: options.diffMode,
157+
mergeMode: options.mergeMode,
157158
addMode: options.addMode,
158159
gotoLineMode: options.gotoLineMode,
159160
noRecentEntry: options.noRecentEntry,

src/vs/platform/window/common/window.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export interface IOpenWindowOptions extends IBaseOpenWindowsOptions {
5252
readonly addMode?: boolean;
5353

5454
readonly diffMode?: boolean;
55+
readonly mergeMode?: boolean;
5556
readonly gotoLineMode?: boolean;
5657

5758
readonly waitMarkerFileURI?: URI;
@@ -240,6 +241,7 @@ interface IPathsToWaitForData {
240241
export interface IOpenFileRequest {
241242
readonly filesToOpenOrCreate?: IPathData[];
242243
readonly filesToDiff?: IPathData[];
244+
readonly filesToMerge?: IPathData[];
243245
}
244246

245247
/**
@@ -270,6 +272,7 @@ export interface IWindowConfiguration {
270272

271273
filesToOpenOrCreate?: IPath[];
272274
filesToDiff?: IPath[];
275+
filesToMerge?: IPath[];
273276
}
274277

275278
export interface IOSConfiguration {

src/vs/platform/windows/electron-main/window.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -899,6 +899,7 @@ export class CodeWindow extends Disposable implements ICodeWindow {
899899
// Delete some properties we do not want during reload
900900
delete configuration.filesToOpenOrCreate;
901901
delete configuration.filesToDiff;
902+
delete configuration.filesToMerge;
902903
delete configuration.filesToWait;
903904

904905
// Some configuration things get inherited if the window is being reloaded and we are

src/vs/platform/windows/electron-main/windows.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ export interface IOpenConfiguration extends IBaseOpenConfiguration {
8585
readonly forceReuseWindow?: boolean;
8686
readonly forceEmpty?: boolean;
8787
readonly diffMode?: boolean;
88+
readonly mergeMode?: boolean;
8889
addMode?: boolean;
8990
readonly gotoLineMode?: boolean;
9091
readonly initialStartup?: boolean;

src/vs/platform/windows/electron-main/windowsMainService.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ interface IFilesToOpen {
118118

119119
filesToOpenOrCreate: IPath[];
120120
filesToDiff: IPath[];
121+
filesToMerge: IPath[];
122+
121123
filesToWait?: IPathsToWaitFor;
122124
}
123125

@@ -296,7 +298,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
296298
workspacesToOpen.push(path);
297299
} else if (path.fileUri) {
298300
if (!filesToOpen) {
299-
filesToOpen = { filesToOpenOrCreate: [], filesToDiff: [], remoteAuthority: path.remoteAuthority };
301+
filesToOpen = { filesToOpenOrCreate: [], filesToDiff: [], filesToMerge: [], remoteAuthority: path.remoteAuthority };
300302
}
301303
filesToOpen.filesToOpenOrCreate.push(path);
302304
} else if (path.backupPath) {
@@ -312,9 +314,16 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
312314
filesToOpen.filesToOpenOrCreate = [];
313315
}
314316

317+
// When run with --merge, take the first 4 files to open as files to merge
318+
if (openConfig.mergeMode && filesToOpen && filesToOpen.filesToOpenOrCreate.length === 4) {
319+
filesToOpen.filesToMerge = filesToOpen.filesToOpenOrCreate.slice(0, 4);
320+
filesToOpen.filesToOpenOrCreate = [];
321+
filesToOpen.filesToDiff = [];
322+
}
323+
315324
// When run with --wait, make sure we keep the paths to wait for
316325
if (filesToOpen && openConfig.waitMarkerFileURI) {
317-
filesToOpen.filesToWait = { paths: [...filesToOpen.filesToDiff, ...filesToOpen.filesToOpenOrCreate], waitMarkerFileUri: openConfig.waitMarkerFileURI };
326+
filesToOpen.filesToWait = { paths: [...filesToOpen.filesToDiff, filesToOpen.filesToMerge[3] /* [3] is the resulting merge file */, ...filesToOpen.filesToOpenOrCreate], waitMarkerFileUri: openConfig.waitMarkerFileURI };
318327
}
319328

320329
// These are windows to restore because of hot-exit or from previous session (only performed once on startup!)
@@ -384,9 +393,10 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
384393
}
385394

386395
// Remember in recent document list (unless this opens for extension development)
387-
// Also do not add paths when files are opened for diffing, only if opened individually
396+
// Also do not add paths when files are opened for diffing or merging, only if opened individually
388397
const isDiff = filesToOpen && filesToOpen.filesToDiff.length > 0;
389-
if (!usedWindows.some(window => window.isExtensionDevelopmentHost) && !isDiff && !openConfig.noRecentEntry) {
398+
const isMerge = filesToOpen && filesToOpen.filesToMerge.length > 0;
399+
if (!usedWindows.some(window => window.isExtensionDevelopmentHost) && !isDiff && !isMerge && !openConfig.noRecentEntry) {
390400
const recents: IRecent[] = [];
391401
for (const pathToOpen of pathsToOpen) {
392402
if (isWorkspacePathToOpen(pathToOpen) && !pathToOpen.transient /* never add transient workspaces to history */) {
@@ -461,13 +471,13 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
461471
}
462472
}
463473

464-
// Handle files to open/diff or to create when we dont open a folder and we do not restore any
474+
// Handle files to open/diff/merge or to create when we dont open a folder and we do not restore any
465475
// folder/untitled from hot-exit by trying to open them in the window that fits best
466476
const potentialNewWindowsCount = foldersToOpen.length + workspacesToOpen.length + emptyToRestore.length;
467477
if (filesToOpen && potentialNewWindowsCount === 0) {
468478

469479
// Find suitable window or folder path to open files in
470-
const fileToCheck = filesToOpen.filesToOpenOrCreate[0] || filesToOpen.filesToDiff[0];
480+
const fileToCheck = filesToOpen.filesToOpenOrCreate[0] || filesToOpen.filesToDiff[0] || filesToOpen.filesToMerge[3] /* [3] is the resulting merge file */;
471481

472482
// only look at the windows with correct authority
473483
const windows = this.getWindows().filter(window => filesToOpen && isEqualAuthority(window.remoteAuthority, filesToOpen.remoteAuthority));
@@ -625,6 +635,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
625635
const params: INativeOpenFileRequest = {
626636
filesToOpenOrCreate: filesToOpen?.filesToOpenOrCreate,
627637
filesToDiff: filesToOpen?.filesToDiff,
638+
filesToMerge: filesToOpen?.filesToMerge,
628639
filesToWait: filesToOpen?.filesToWait,
629640
termProgram: configuration?.userEnv?.['TERM_PROGRAM']
630641
};
@@ -792,7 +803,12 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
792803
ignoreFileNotFound: true,
793804
gotoLineMode: cli.goto,
794805
remoteAuthority: cli.remote || undefined,
795-
forceOpenWorkspaceAsFile: cli.diff && cli._.length === 2 // special case diff mode to force open workspace as file (https://github.com/microsoft/vscode/issues/149731)
806+
forceOpenWorkspaceAsFile:
807+
// special case diff / merge mode to force open
808+
// workspace as file
809+
// https://github.com/microsoft/vscode/issues/149731
810+
cli.diff && cli._.length === 2 ||
811+
cli.merge && cli._.length === 4
796812
};
797813

798814
// folder uris
@@ -1323,6 +1339,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
13231339

13241340
filesToOpenOrCreate: options.filesToOpen?.filesToOpenOrCreate,
13251341
filesToDiff: options.filesToOpen?.filesToDiff,
1342+
filesToMerge: options.filesToOpen?.filesToMerge,
13261343
filesToWait: options.filesToOpen?.filesToWait,
13271344

13281345
logLevel: this.logService.getLevel(),

src/vs/server/node/server.cli.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ const isSupportedForPipe = (optionId: keyof RemoteParsedArgs) => {
5959
case 'file-uri':
6060
case 'add':
6161
case 'diff':
62+
case 'merge':
6263
case 'wait':
6364
case 'goto':
6465
case 'reuse-window':
@@ -297,6 +298,7 @@ export function main(desc: ProductDescription, args: string[]): void {
297298
fileURIs,
298299
folderURIs,
299300
diffMode: parsedArgs.diff,
301+
mergeMode: parsedArgs.merge,
300302
addMode: parsedArgs.add,
301303
gotoLineMode: parsedArgs.goto,
302304
forceReuseWindow: parsedArgs['reuse-window'],

src/vs/workbench/api/node/extHostCLIServer.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export interface OpenCommandPipeArgs {
1818
folderURIs?: string[];
1919
forceNewWindow?: boolean;
2020
diffMode?: boolean;
21+
mergeMode?: boolean;
2122
addMode?: boolean;
2223
gotoLineMode?: boolean;
2324
forceReuseWindow?: boolean;
@@ -118,7 +119,7 @@ export class CLIServerBase {
118119
}
119120

120121
private async open(data: OpenCommandPipeArgs): Promise<string> {
121-
const { fileURIs, folderURIs, forceNewWindow, diffMode, addMode, forceReuseWindow, gotoLineMode, waitMarkerFilePath, remoteAuthority } = data;
122+
const { fileURIs, folderURIs, forceNewWindow, diffMode, mergeMode, addMode, forceReuseWindow, gotoLineMode, waitMarkerFilePath, remoteAuthority } = data;
122123
const urisToOpen: IWindowOpenable[] = [];
123124
if (Array.isArray(folderURIs)) {
124125
for (const s of folderURIs) {
@@ -144,7 +145,7 @@ export class CLIServerBase {
144145
}
145146
const waitMarkerFileURI = waitMarkerFilePath ? URI.file(waitMarkerFilePath) : undefined;
146147
const preferNewWindow = !forceReuseWindow && !waitMarkerFileURI && !addMode;
147-
const windowOpenArgs: IOpenWindowOptions = { forceNewWindow, diffMode, addMode, gotoLineMode, forceReuseWindow, preferNewWindow, waitMarkerFileURI, remoteAuthority };
148+
const windowOpenArgs: IOpenWindowOptions = { forceNewWindow, diffMode, mergeMode, addMode, gotoLineMode, forceReuseWindow, preferNewWindow, waitMarkerFileURI, remoteAuthority };
148149
this._commands.executeCommand('_remoteCLI.windowOpen', urisToOpen, windowOpenArgs);
149150

150151
return '';

src/vs/workbench/browser/layout.ts

Lines changed: 37 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { EventType, addDisposableListener, getClientArea, Dimension, position, s
99
import { onDidChangeFullscreen, isFullscreen } from 'vs/base/browser/browser';
1010
import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup';
1111
import { isWindows, isLinux, isMacintosh, isWeb, isNative, isIOS } from 'vs/base/common/platform';
12-
import { IUntypedEditorInput, pathsToEditors } from 'vs/workbench/common/editor';
12+
import { isResourceEditorInput, IUntypedEditorInput, pathsToEditors } from 'vs/workbench/common/editor';
1313
import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput';
1414
import { SidebarPart } from 'vs/workbench/browser/parts/sidebar/sidebarPart';
1515
import { PanelPart } from 'vs/workbench/browser/parts/panel/panelPart';
@@ -75,7 +75,7 @@ interface IWorkbenchLayoutWindowInitializationState {
7575
};
7676
editor: {
7777
restoreEditors: boolean;
78-
editorsToOpen: Promise<IUntypedEditorInput[]> | IUntypedEditorInput[];
78+
editorsToOpen: Promise<IUntypedEditorInput[]>;
7979
};
8080
}
8181

@@ -98,6 +98,7 @@ enum WorkbenchLayoutClasses {
9898
interface IInitialFilesToOpen {
9999
filesToOpenOrCreate?: IPath[];
100100
filesToDiff?: IPath[];
101+
filesToMerge?: IPath[];
101102
}
102103

103104
export abstract class Layout extends Disposable implements IWorkbenchLayoutService {
@@ -278,7 +279,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi
278279
this._register(this.hostService.onDidChangeFocus(e => this.onWindowFocusChanged(e)));
279280
}
280281

281-
private onMenubarToggled(visible: boolean) {
282+
private onMenubarToggled(visible: boolean): void {
282283
if (visible !== this.windowState.runtime.menuBar.toggled) {
283284
this.windowState.runtime.menuBar.toggled = visible;
284285

@@ -565,26 +566,33 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi
565566
return this.windowState.initialization.editor.restoreEditors;
566567
}
567568

568-
private resolveEditorsToOpen(fileService: IFileService, initialFilesToOpen: IInitialFilesToOpen | undefined): Promise<IUntypedEditorInput[]> | IUntypedEditorInput[] {
569-
570-
// Files to open, diff or create
569+
private async resolveEditorsToOpen(fileService: IFileService, initialFilesToOpen: IInitialFilesToOpen | undefined): Promise<IUntypedEditorInput[]> {
571570
if (initialFilesToOpen) {
572571

573-
// Files to diff is exclusive
574-
return pathsToEditors(initialFilesToOpen.filesToDiff, fileService).then(filesToDiff => {
575-
if (filesToDiff.length === 2) {
576-
const diffEditorInput: IUntypedEditorInput[] = [{
577-
original: { resource: filesToDiff[0].resource },
578-
modified: { resource: filesToDiff[1].resource },
579-
options: { pinned: true }
580-
}];
572+
// Merge editor
573+
const filesToMerge = await pathsToEditors(initialFilesToOpen.filesToMerge, fileService);
574+
if (filesToMerge.length === 4 && isResourceEditorInput(filesToMerge[0]) && isResourceEditorInput(filesToMerge[1]) && isResourceEditorInput(filesToMerge[2]) && isResourceEditorInput(filesToMerge[3])) {
575+
return [{
576+
input1: { resource: filesToMerge[0].resource },
577+
input2: { resource: filesToMerge[1].resource },
578+
base: { resource: filesToMerge[2].resource },
579+
result: { resource: filesToMerge[3].resource },
580+
options: { pinned: true, override: 'mergeEditor.Input' } // TODO@bpasero remove the override once the resolver is ready
581+
}];
582+
}
581583

582-
return diffEditorInput;
583-
}
584+
// Diff editor
585+
const filesToDiff = await pathsToEditors(initialFilesToOpen.filesToDiff, fileService);
586+
if (filesToDiff.length === 2) {
587+
return [{
588+
original: { resource: filesToDiff[0].resource },
589+
modified: { resource: filesToDiff[1].resource },
590+
options: { pinned: true }
591+
}];
592+
}
584593

585-
// Otherwise: Open/Create files
586-
return pathsToEditors(initialFilesToOpen.filesToOpenOrCreate, fileService);
587-
});
594+
// Normal editor
595+
return pathsToEditors(initialFilesToOpen.filesToOpenOrCreate, fileService);
588596
}
589597

590598
// Empty workbench configured to open untitled file if empty
@@ -593,13 +601,12 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi
593601
return []; // do not open any empty untitled file if we restored groups/editors from previous session
594602
}
595603

596-
return this.workingCopyBackupService.hasBackups().then(hasBackups => {
597-
if (hasBackups) {
598-
return []; // do not open any empty untitled file if we have backups to restore
599-
}
604+
const hasBackups = await this.workingCopyBackupService.hasBackups();
605+
if (hasBackups) {
606+
return []; // do not open any empty untitled file if we have backups to restore
607+
}
600608

601-
return [{ resource: undefined }]; // open empty untitled file
602-
});
609+
return [{ resource: undefined }]; // open empty untitled file
603610
}
604611

605612
return [];
@@ -638,10 +645,10 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi
638645
};
639646
}
640647

641-
// Then check for files to open, create or diff from main side
642-
const { filesToOpenOrCreate, filesToDiff } = this.environmentService;
643-
if (filesToOpenOrCreate || filesToDiff) {
644-
return { filesToOpenOrCreate, filesToDiff };
648+
// Then check for files to open, create or diff/merge from main side
649+
const { filesToOpenOrCreate, filesToDiff, filesToMerge } = this.environmentService;
650+
if (filesToOpenOrCreate || filesToDiff || filesToMerge) {
651+
return { filesToOpenOrCreate, filesToDiff, filesToMerge };
645652
}
646653

647654
return undefined;
@@ -681,12 +688,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi
681688
// signaling that layout is restored, but we do
682689
// not need to await the editors from having
683690
// fully loaded.
684-
let editors: IUntypedEditorInput[];
685-
if (Array.isArray(this.windowState.initialization.editor.editorsToOpen)) {
686-
editors = this.windowState.initialization.editor.editorsToOpen;
687-
} else {
688-
editors = await this.windowState.initialization.editor.editorsToOpen;
689-
}
691+
const editors = await this.windowState.initialization.editor.editorsToOpen;
690692

691693
let openEditorsPromise: Promise<unknown> | undefined = undefined;
692694
if (editors.length) {

0 commit comments

Comments
 (0)