diff --git a/.gitignore b/.gitignore index d20fe46..989efb9 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ yarn-error.log .env .precommit_stash_exists /local/ +/.debug diff --git a/.husky/.gitignore b/.husky/.gitignore index c9cdc63..31354ec 100644 --- a/.husky/.gitignore +++ b/.husky/.gitignore @@ -1 +1 @@ -_ \ No newline at end of file +_ diff --git a/.vscode/launch.json b/.vscode/launch.json index 098b0e5..d096cde 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,36 +6,53 @@ "version": "0.2.0", "configurations": [ { - "name": "run extension", + "name": "debug extension", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": ["--extensionDevelopmentPath=${workspaceFolder}"], + "outFiles": [ + "${workspaceFolder}/out/dist/extension.js", + "${workspaceFolder}/out/src/**/*.js" + ], + "preLaunchTask": "debug:pre" + }, + { + "name": "debug extension, start paused", "type": "extensionHost", "request": "launch", "runtimeExecutable": "${execPath}", "args": [ - "--extensionDevelopmentPath=${workspaceFolder}" + "--extensionDevelopmentPath=${workspaceFolder}", + "--inspect-brk-extensions=3714" ], "outFiles": [ "${workspaceFolder}/out/dist/extension.js", "${workspaceFolder}/out/src/**/*.js" ], - "preLaunchTask": "build:dev" + "preLaunchTask": "debug:pre" }, { - "name": "run extension, start paused", + "name": "debug extension in selected path", "type": "extensionHost", "request": "launch", "runtimeExecutable": "${execPath}", "args": [ "--extensionDevelopmentPath=${workspaceFolder}", - "--inspect-brk-extensions=3714" + "--inspect-brk-extensions=3714", + "--user-data-dir=${input:data-dir}/user-data-dir" ], "outFiles": [ "${workspaceFolder}/out/dist/extension.js", "${workspaceFolder}/out/src/**/*.js" ], - "preLaunchTask": "build:dev" + "preLaunchTask": "debug:pre", + "env": { + "HOME": "${input:data-dir}" + } }, { - "name": "debug tests", + "name": "debug opened test", "port": 3714, "request": "attach", "skipFiles": ["/**"], @@ -49,6 +66,22 @@ "${workspaceFolder}/out/src/**/*.js", "${workspaceFolder}/out/test/**/*.js" ] + }, + { + "name": "debug test runner", + "program": "${workspaceFolder}/out/test/runTests.js", + "request": "launch", + "skipFiles": ["/**"], + "type": "pwa-node", + "preLaunchTask": "test:dev:pre" + } + ], + "inputs": [ + { + "id": "data-dir", + "type": "promptString", + "default": "${workspaceFolder}/.debug", + "description": "Select the environment data directory" } ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 31325cf..d80b355 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -35,6 +35,7 @@ "postversion", "precommit", "preversion", + "protocolled", "quickstart", "s", "screencast", diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 24af4ab..afc66d7 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -21,9 +21,9 @@ } }, { - "label": "build:dev", + "label": "debug:pre", "type": "npm", - "script": "build:dev", + "script": "debug:pre", "group": "build", "runOptions": { "instanceLimit": 1 @@ -74,6 +74,23 @@ "endsPattern": "^Debugging test .+\\. Waiting on port" } } + }, + { + "label": "test:dev:pre", + "type": "npm", + "script": "test:dev:pre", + "group": "build", + "runOptions": { + "instanceLimit": 1 + }, + "presentation": { + "echo": true, + "reveal": "silent", + "focus": false, + "panel": "shared", + "showReuseMessage": true, + "clear": true + } } ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bf0498..aa4f414 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,27 @@ and this project adheres to It should be slightly more stable now. - Switched to Webpack as bundler +## [0.14.0] - 2021-04-07 + +### Added + +- Command to run the initial settings assistant on demand. + +### Changed + +- Initial settings assistant + - Fixes. It now aggregates the decisions and allows to apply them at the end. + - The settings assistant startup is now rather configured per repository. + - Malfunctioning creation of backup setting entries + was replaced by creating a log file of changes made. + - The messages were improved. +- Switched to Webpack as bundler + +### Fixed + +- Wrong file paths were opened when `git mergetool` was launched + by the extension. + ## [0.13.3] - 2021-02-21 ## Fixed @@ -328,7 +349,8 @@ This will be the first version published on the - Disables line numbers and sets diff layout to “inline” while a diff layout is active -[Unreleased]: https://github.com/zawys/vscode-as-git-mergetool/compare/v0.13.3...HEAD +[Unreleased]: https://github.com/zawys/vscode-as-git-mergetool/compare/v0.14.0...HEAD +[0.14.0]: https://github.com/zawys/vscode-as-git-mergetool/releases/tag/v0.14.0 [0.13.3]: https://github.com/zawys/vscode-as-git-mergetool/releases/tag/v0.13.3 [0.13.2]: https://github.com/zawys/vscode-as-git-mergetool/releases/tag/v0.13.2 [0.13.1]: https://github.com/zawys/vscode-as-git-mergetool/releases/tag/v0.13.1 diff --git a/package.json b/package.json index a727890..ed13b89 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "vscode-as-git-mergetool", "displayName": "VS Code as Git Mergetool", "description": "Diff editor layouts for common-base merging", - "version": "0.13.3", + "version": "0.14.0", "engines": { "vscode": "^1.48.0" }, @@ -149,6 +149,11 @@ "command": "vscode-as-git-mergetool.zoomBottom", "title": "zoom: bottom", "category": "Git Mergetool" + }, + { + "command": "vscode-as-git-mergetool.runSettingsAssistant", + "title": "Launch settings assistant", + "category": "Git Mergetool" } ], "configuration": { @@ -158,7 +163,7 @@ "type": "boolean", "default": true, "description": "If the setup assistant for the 'VS Code as Git Mergetool' extension shall be run on next VS Code startup.", - "scope": "application" + "scope": "window" }, "vscode-as-git-mergetool.editCommitMessageAfterMergetool": { "type": "boolean", @@ -334,7 +339,8 @@ "bundle:dev": "webpack --mode development", "bundle": "webpack --mode production", "package": "mkdir packages && vsce package --out=packages/${npm_package_name}-${npm_package_version}.vsix --githubBranch=master --yarn", - "build:dev": "yarn run copy_files", + "debug:pre": "yarn run copy_files", + "build:dev": "yarn install && yarn run clean && yarn run bundle:dev && yarn run package", "build": "yarn install && yarn run clean && yarn run bundle && yarn run package", "working_dir_is_clean": "git diff --quiet && ! git ls-files -o --exclude-standard | grep '.'", "index_is_clean": "git diff --staged --quiet && git diff --quiet && ! git ls-files -o --exclude-standard | grep '.'", @@ -354,38 +360,39 @@ "devDependencies": { "@types/diff": "^5.0.0", "@types/glob": "^7.1.3", - "@types/mocha": "^8.0.3", - "@types/node": "^14.6.2", + "@types/mocha": "^8.2.2", + "@types/node": "^14.14.37", "@types/tmp": "^0.2.0", "@types/vscode": "^1.48.0", "@types/which": "^2.0.0", - "@typescript-eslint/eslint-plugin": "^4.0.1", - "@typescript-eslint/parser": "^4.0.1", + "@typescript-eslint/eslint-plugin": "^4.21.0", + "@typescript-eslint/parser": "^4.21.0", "cpy-cli": "^3.1.1", "dotenv": "^8.2.0", - "eslint": "^7.8.0", - "eslint-config-prettier": "^8.0.0", + "eslint": "^7.23.0", + "eslint-config-prettier": "^8.1.0", "eslint-plugin-eslint-comments": "^3.2.0", "eslint-plugin-header": "^3.1.0", - "eslint-plugin-prettier": "^3.1.4", - "eslint-plugin-promise": "^4.2.1", - "eslint-plugin-unicorn": "^28.0.0", + "eslint-plugin-prettier": "^3.3.1", + "eslint-plugin-promise": "^4.3.1", + "eslint-plugin-unicorn": "^29.0.0", "glob": "^7.1.6", - "husky": "^5.0.0", - "mocha": "^8.1.3", - "pinst": "^2.1.4", - "prettier": "^2.1.1", - "ts-loader": "^8.0.18", - "ts-node": "^9.0.0", - "typescript": "^4.0.2", - "vsce": "^1.79.5", - "vscode-test": "^1.4.0", - "webpack": "^5.27.1", - "webpack-cli": "^4.5.0" + "husky": "^6.0.0", + "mocha": "^8.3.2", + "pinst": "^2.1.6", + "prettier": "^2.2.1", + "ts-loader": "^8.1.0", + "ts-node": "^9.1.1", + "typescript": "^4.2.3", + "vsce": "^1.87.1", + "vscode-test": "^1.5.2", + "webpack": "^5.30.0", + "webpack-cli": "^4.6.0" }, "license": "AGPL-3.0-or-later", "dependencies": { "diff": "^5.0.0", + "p-defer": "^3.0.0", "regenerator-runtime": "^0.13.7", "tmp-promise": "^3.0.2", "which": "^2.0.2" diff --git a/src/arbitraryFilesMerger.ts b/src/arbitraryFilesMerger.ts index 47c108a..de2ebff 100644 --- a/src/arbitraryFilesMerger.ts +++ b/src/arbitraryFilesMerger.ts @@ -1,7 +1,7 @@ // Copyright (C) 2020 zawys. Licensed under AGPL-3.0-or-later. // See LICENSE file in repository root directory. -import { commands, Disposable, Uri, window } from "vscode"; +import { commands, Disposable, Uri, Memento, window } from "vscode"; import { DiffedURIs } from "./diffedURIs"; import { DiffFileSelector } from "./diffFileSelector"; import { DiffLayouterManager } from "./diffLayouterManager"; @@ -69,7 +69,10 @@ export class ArbitraryFilesMerger implements RegisterableService { public constructor( private readonly diffLayouterManager: DiffLayouterManager, private readonly readonlyDocumentProvider: ReadonlyDocumentProvider, - private diffFileSelectorLazy = new Lazy(() => new DiffFileSelector()) + private readonly workspaceState: Memento, + private diffFileSelectorLazy = new Lazy( + () => new DiffFileSelector(workspaceState) + ) ) {} private disposables: Disposable[] = []; diff --git a/src/backgroundGitTerminal.ts b/src/backgroundGitTerminal.ts index f79c3a1..c405cd3 100644 --- a/src/backgroundGitTerminal.ts +++ b/src/backgroundGitTerminal.ts @@ -4,7 +4,7 @@ import { Terminal, TerminalOptions, window } from "vscode"; import { getVSCGitPathInteractively, - getWorkingDirectoryUriInteractively, + getWorkspaceDirectoryUriInteractively, } from "./getPathsWithinVSCode"; export async function createBackgroundGitTerminal( @@ -14,7 +14,7 @@ export async function createBackgroundGitTerminal( if (gitPath === undefined) { return; } - const workingDirectory = getWorkingDirectoryUriInteractively(); + const workingDirectory = getWorkspaceDirectoryUriInteractively(); if (workingDirectory === undefined) { return; } diff --git a/src/diffFileSelector.ts b/src/diffFileSelector.ts index 105aaf2..c1ca3f0 100644 --- a/src/diffFileSelector.ts +++ b/src/diffFileSelector.ts @@ -3,10 +3,9 @@ import { R_OK, W_OK } from "constants"; import path from "path"; -import { QuickPickItem, Uri, window } from "vscode"; -import { defaultExtensionContextManager } from "./extensionContextManager"; +import { Memento, QuickPickItem, Uri, window } from "vscode"; import { FileType, getFileType, getRealPath, testFile } from "./fsHandy"; -import { getWorkingDirectoryUri } from "./getPathsWithinVSCode"; +import { getWorkspaceDirectoryUri } from "./getPathsWithinVSCode"; import { extensionID, firstLetterUppercase } from "./ids"; export class DiffFileSelector { @@ -33,11 +32,12 @@ export class DiffFileSelector { } public constructor( + private readonly workspaceState: Memento, public readonly id: string = `${extensionID}.mergeFileSelector` ) { this.selector = new MultiFileSelector( this.selectableFiles, - new FileSelectionStateStore(id) + new FileSelectionStateStore(id, workspaceState) ); } @@ -224,7 +224,7 @@ export class MultiFileSelector { return undefined; } if (result.startsWith("./") || result.startsWith(".\\")) { - const workingDirectory = getWorkingDirectoryUri()?.fsPath; + const workingDirectory = getWorkspaceDirectoryUri()?.fsPath; if (workingDirectory !== undefined) { return path.join(workingDirectory, result); } @@ -376,9 +376,7 @@ export type FileSelectionState = { export class FileSelectionStateStore { public async getSelection(key: string): Promise { const keyID = this.getKeyID(key); - const value = defaultExtensionContextManager.value.workspaceState.get( - keyID - ); + const value = this.workspaceState.get(keyID); if (value === undefined) { return undefined; } else if (typeof value === "string") { @@ -397,8 +395,7 @@ export class FileSelectionStateStore { public constructor( public readonly id: string, - public readonly workspaceState = defaultExtensionContextManager.value - .workspaceState + public readonly workspaceState: Memento ) {} private getKeyID(key: string): string { diff --git a/src/extension.ts b/src/extension.ts index bded17d..c11e1a8 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -11,14 +11,16 @@ import { createRegisteredDocumentProviderManager, RegisteredDocumentContentProvider, } from "./registeredDocumentContentProvider"; -import { defaultExtensionContextManager } from "./extensionContextManager"; import { GitMergetoolReplacement } from "./gitMergetoolReplacement"; import { createReadonlyDocumentProviderManager, ReadonlyDocumentProvider, } from "./readonlyDocumentProvider"; import { RegisterableService } from "./registerableService"; -import { SettingsAssistantCreator } from "./settingsAssistant"; +import { + OptionChangeProtocolExporter, + SettingsAssistantLauncher, +} from "./settingsAssistant"; import { GitMergetoolTemporaryFileOpenHandler } from "./gitMergetoolTemporaryFileOpenHandler"; import { TemporarySettingsManager } from "./temporarySettingsManager"; import { VSCodeConfigurator } from "./vSCodeConfigurator"; @@ -33,9 +35,8 @@ let extensionAPI: ExtensionAPI | undefined; export async function activate( context: ExtensionContext ): Promise { - defaultExtensionContextManager.value = context; extensionAPI = new ExtensionAPI( - ...new ExtensionServicesCreator().createServices() + ...new ExtensionServicesCreator().createServices(context) ); await extensionAPI.register(); return extensionAPI; @@ -73,37 +74,38 @@ export class ExtensionAPI implements RegisterableService { class ExtensionServicesCreator { public createServices( + context: ExtensionContext, services: Readonly> = {} ): [ExtensionServices, RegisterableService[]] { const registrationOrder: RegisterableService[] = []; const vSCodeConfigurator = - services?.vSCodeConfigurator || new VSCodeConfigurator(); + services?.vSCodeConfigurator ?? new VSCodeConfigurator(); const readonlyDocumentProviderManager = - services.readonlyDocumentProviderManager || + services.readonlyDocumentProviderManager ?? createReadonlyDocumentProviderManager(); registrationOrder.push(readonlyDocumentProviderManager); const readonlyDocumentProvider = readonlyDocumentProviderManager.documentProvider; const registeredDocumentProviderManager = - services.registeredDocumentProviderManager || + services.registeredDocumentProviderManager ?? createRegisteredDocumentProviderManager(); registrationOrder.push(registeredDocumentProviderManager); const registeredDocumentProvider = registeredDocumentProviderManager.documentProvider; - const zoomManager = services.zoomManager || new ZoomManager(); + const zoomManager = services.zoomManager ?? new ZoomManager(); registrationOrder.push(zoomManager); const temporarySettingsManager = - services.temporarySettingsManager || - new TemporarySettingsManager(vSCodeConfigurator); + services.temporarySettingsManager ?? + new TemporarySettingsManager(vSCodeConfigurator, context.globalState); registrationOrder.push(temporarySettingsManager); const diffLayouterManager = - services.diffLayouterManager || + services.diffLayouterManager ?? new DiffLayouterManager( vSCodeConfigurator, zoomManager, @@ -117,7 +119,7 @@ class ExtensionServicesCreator { const manualMergeProcess = new ManualMergeProcess(diffLayouterManager); const gitMergetoolReplacement = - services.gitMergetoolReplacement || + services.gitMergetoolReplacement ?? new GitMergetoolReplacement( registeredDocumentProvider, readonlyDocumentProvider, @@ -128,14 +130,14 @@ class ExtensionServicesCreator { registrationOrder.push(gitMergetoolReplacement); const temporaryFileOpenManager = - services.temporaryFileOpenManager || + services.temporaryFileOpenManager ?? new GitMergetoolTemporaryFileOpenHandler( diffLayouterManager, readonlyDocumentProvider ); const editorOpenManager = - services.editorOpenManager || + services.editorOpenManager ?? new EditorOpenManager([ { handler: gitMergetoolReplacement, @@ -149,16 +151,27 @@ class ExtensionServicesCreator { registrationOrder.push(editorOpenManager); const arbitraryFilesMerger = - services.arbitraryFilesMerger || - new ArbitraryFilesMerger(diffLayouterManager, readonlyDocumentProvider); + services.arbitraryFilesMerger ?? + new ArbitraryFilesMerger( + diffLayouterManager, + readonlyDocumentProvider, + context.workspaceState + ); registrationOrder.push(arbitraryFilesMerger); - const settingsAssistantCreator = - services.settingsAssistantCreator || - new SettingsAssistantCreator(vSCodeConfigurator); - registrationOrder.push(settingsAssistantCreator); + const optionChangeProtocolExporter = + services.optionChangeProtocolExporter ?? + new OptionChangeProtocolExporter(); + + const settingsAssistantLauncher = + services.settingsAssistantLauncher ?? + new SettingsAssistantLauncher( + vSCodeConfigurator, + optionChangeProtocolExporter + ); + registrationOrder.push(settingsAssistantLauncher); - const mergeAborter = services.mergeAborter || new MergeAborter(); + const mergeAborter = services.mergeAborter ?? new MergeAborter(); registrationOrder.push(mergeAborter); return [ @@ -168,7 +181,8 @@ class ExtensionServicesCreator { gitMergetoolReplacement, readonlyDocumentProviderManager, registeredDocumentProviderManager, - settingsAssistantCreator, + settingsAssistantLauncher, + optionChangeProtocolExporter, temporarySettingsManager, vSCodeConfigurator, zoomManager, @@ -192,7 +206,8 @@ export interface ExtensionServices { gitMergetoolReplacement: GitMergetoolReplacement; diffLayouterManager: DiffLayouterManager; arbitraryFilesMerger: ArbitraryFilesMerger; - settingsAssistantCreator: SettingsAssistantCreator; + settingsAssistantLauncher: SettingsAssistantLauncher; + optionChangeProtocolExporter: OptionChangeProtocolExporter; temporaryFileOpenManager: GitMergetoolTemporaryFileOpenHandler; editorOpenManager: EditorOpenManager; commonMergeCommandsManager: CommonMergeCommandsManager; diff --git a/src/extensionContextManager.ts b/src/extensionContextManager.ts deleted file mode 100644 index f06db83..0000000 --- a/src/extensionContextManager.ts +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (C) 2020 zawys. Licensed under AGPL-3.0-or-later. -// See LICENSE file in repository root directory. - -import { ExtensionContext } from "vscode"; -import { SingletonStore } from "./singletonStore"; - -export const defaultExtensionContextManager = new SingletonStore(); diff --git a/src/getPathsWithinVSCode.ts b/src/getPathsWithinVSCode.ts index 9e9367d..b50a6d1 100644 --- a/src/getPathsWithinVSCode.ts +++ b/src/getPathsWithinVSCode.ts @@ -21,7 +21,7 @@ function uriStartsWith(parent: Uri, child: Uri): boolean { return true; } -export function getWorkingDirectoryUri(): Uri | undefined { +export function getWorkspaceDirectoryUri(): Uri | undefined { if (window.activeTextEditor !== undefined) { const textEditorUri = window.activeTextEditor.document.uri; for (const folder of workspace.workspaceFolders || []) { @@ -39,8 +39,8 @@ export function getWorkingDirectoryUri(): Uri | undefined { return workspace.workspaceFolders[0].uri; } -export function getWorkingDirectoryUriInteractively(): Uri | undefined { - const result = getWorkingDirectoryUri(); +export function getWorkspaceDirectoryUriInteractively(): Uri | undefined { + const result = getWorkspaceDirectoryUri(); if (result === undefined) { void window.showErrorMessage( "You need need to have exactly one workspace opened." diff --git a/src/gitMergetoolReplacement.ts b/src/gitMergetoolReplacement.ts index 35fd4c8..4acd6a9 100644 --- a/src/gitMergetoolReplacement.ts +++ b/src/gitMergetoolReplacement.ts @@ -158,7 +158,7 @@ export class GitMergetoolReplacement return true; } this.manualMergeProcess; - // TODO [2021-03-31] + // TODO [2021-04-30] throw new Error("Not implemented"); } finally { await this.monitor.leave(); @@ -287,7 +287,7 @@ export class GitMergetoolReplacement if (absoluteConflictPath !== absoluteMergedPath) { const renameResult = await mkdir(nodePath.dirname(absoluteMergedPath)); if (isUIError(renameResult)) return renameResult; - // TODO [2021-03-31]: Does that work with submodules? + // TODO [2021-04-30]: Does that work with submodules? const moveResult = await rename( absoluteConflictPath, absoluteMergedPath diff --git a/src/mergeAborter.ts b/src/mergeAborter.ts index 814d10c..7a4f993 100644 --- a/src/mergeAborter.ts +++ b/src/mergeAborter.ts @@ -5,7 +5,7 @@ import path from "path"; import { commands, Disposable, QuickPickItem, window } from "vscode"; import { createBackgroundGitTerminal } from "./backgroundGitTerminal"; import { getStats } from "./fsHandy"; -import { getWorkingDirectoryUri } from "./getPathsWithinVSCode"; +import { getWorkspaceDirectoryUri } from "./getPathsWithinVSCode"; import { RegisterableService } from "./registerableService"; export class MergeAborter implements RegisterableService { @@ -22,7 +22,7 @@ export class MergeAborter implements RegisterableService { // by https://stackoverflow.com/a/30783114/1717752 public async isMergeInProgress(): Promise { - const workingDirectoryUri = getWorkingDirectoryUri(); + const workingDirectoryUri = getWorkspaceDirectoryUri(); if (workingDirectoryUri === undefined) { return undefined; } diff --git a/src/settingsAssistant.ts b/src/settingsAssistant.ts index da18c77..9764be4 100644 --- a/src/settingsAssistant.ts +++ b/src/settingsAssistant.ts @@ -2,149 +2,321 @@ // See LICENSE file in repository root directory. import { execFile } from "child_process"; -import { MessageItem, Uri, window } from "vscode"; +import { createWriteStream } from "fs"; +import { EOL, homedir } from "os"; +import pDefer from "p-defer"; +import path from "path"; +import { Writable } from "stream"; +import { commands } from "vscode"; +import { Disposable, MessageItem, Uri, window } from "vscode"; import { formatExecFileError } from "./childProcessHandy"; import { getVSCGitPathInteractively, - getWorkingDirectoryUri, + getWorkspaceDirectoryUri, } from "./getPathsWithinVSCode"; import { extensionID } from "./ids"; +import { Monitor } from "./monitor"; import { RegisterableService } from "./registerableService"; import { VSCodeConfigurator } from "./vSCodeConfigurator"; -export const settingsAssistantOnStartupID = `${extensionID}.settingsAssistantOnStartup`; +export const settingsAssistantOnStartupSettingID = `${extensionID}.settingsAssistantOnStartup`; +export const runSettingsAssistantCommandID = `${extensionID}.runSettingsAssistant`; export class SettingsAssistant { public async launch(): Promise { - let someNeedsChange = false; - for (const assistant of this.optionsAssistants) { - if (await assistant.needsChange()) { - someNeedsChange = true; - break; - } - } - if (!someNeedsChange) { + const { error, apply, pickedOptions } = await this.gatherDecisions(); + if (error !== undefined) { + void window.showErrorMessage( + `Error while running the settings assistant. ` + + `No changes were made. ${JSON.stringify(error)}` + ); return; } - // eslint-disable-next-line no-constant-condition - while (true) { - const nowItem = { title: "Now" }; - const newerItem = { title: "Never" }; - const postponeItem = { title: "Postpone to next startup" }; - const result = await window.showInformationMessage( - "Some current settings will not work well with VS Code as merge tool. When do want to change them using dialogs?", - nowItem, - newerItem, - postponeItem + if (!apply) return; + await this.applyDecisions(pickedOptions); + } + + public constructor( + private readonly gitConfigurator: GitConfigurator, + private readonly vSCodeConfigurator: VSCodeConfigurator, + private readonly optionChangeProtocolExporter: OptionChangeProtocolExporter + ) { + const createGitOptionAssistant = ( + key: string, + targetValue: string, + description: string + ) => + new GitOptionAssistant(gitConfigurator, key, targetValue, description); + const createVSCodeOptionAssistant = ( + section: string, + targetValue: T, + description: string + ) => + new VSCodeOptionAssistant( + vSCodeConfigurator, + section, + targetValue, + description ); - if (result === newerItem) { - await this.vSCodeConfigurator.set(settingsAssistantOnStartupID, false); - return; - } - if (result !== nowItem) { - return; - } - let restart = false; - let abort = false; + this.optionsAssistants = [ + createGitOptionAssistant( + "mergetool.keepTemporaries", + "false", + "Remove temporary files after the merge." + ), + createGitOptionAssistant( + "mergetool.keepBackup", + "false", + "Remove the automatically created backup files after a merge." + ), + createGitOptionAssistant( + "mergetool.code.cmd", + `"${process.execPath}" "$BASE"`, + "Make VS Code available as merge tool." + ), + createGitOptionAssistant( + "merge.tool", + "code", + "Set VS Code as merge tool." + ), + createGitOptionAssistant( + "merge.conflictstyle", + "merge", + "Do not output base hunk in merge conflict files." + ), + createVSCodeOptionAssistant( + "workbench.editor.closeEmptyGroups", + true, + "Do not keep open empty editor groups when stopping a diff layout." + ), + createVSCodeOptionAssistant( + "merge-conflict.codeLens.enabled", + true, + "Show action links for selecting changes directly above merge conflict sections." + ), + createVSCodeOptionAssistant( + "diffEditor.codeLens", + true, + "Show the merge conflict code lens in diff editors." + ), + ]; + } + + private readonly optionsAssistants: OptionAssistant[]; + + private async gatherDecisions(): Promise<{ + error: unknown; + apply: boolean; + pickedOptions: [OptionAssistant, Option][]; + }> { + let apply = false; + let pickedOptions: [OptionAssistant, Option][] = []; + let error: unknown = undefined; + try { + let someNeedsChange = false; for (const assistant of this.optionsAssistants) { - if (!(await assistant.needsChange())) { - continue; + if (await assistant.getNeedsChange()) { + someNeedsChange = true; + break; } - const question = await assistant.provideQuestionData(); - question.options.push( - this.skipOptionItem, - this.abortItem, - this.restartItem + } + if (!someNeedsChange) { + return { apply, error, pickedOptions }; + } + const nowItem = new Option("Now"); + const newerItem = new Option("Never"); + const newerWorkspaceItem = new Option("Never in this workspace"); + const postponeItem = new Option("Postpone"); + const skipOptionItem = new Option("Skip"); + const abortItem = new Option("Abort"); + const discardItem = new Option("Discard"); + const completeItem = new Option("Complete"); + const applyItem = new Option("Apply"); + const restartItem = new Option("Restart"); + + // eslint-disable-next-line no-constant-condition + while (true) { + const result = await window.showInformationMessage( + "Some current settings will not work well with VS Code as merge tool. " + + "When do want to change them using dialogs?", + nowItem, + newerItem, + newerWorkspaceItem, + postponeItem ); - const pickedOption = await this.ask(question); - if (pickedOption === undefined || pickedOption === this.abortItem) { - abort = true; - break; - } else if (pickedOption === this.skipOptionItem) { + if (result !== nowItem) { + if (result === newerItem || result === newerWorkspaceItem) { + await this.setRunOnStartup(false, result === newerItem); + } + return { apply, error, pickedOptions }; + } + + let restart = false; + let abort = false; + pickedOptions = []; + for (const assistant of this.optionsAssistants) { + if (!(await assistant.getNeedsChange())) continue; + const question = await assistant.provideQuestionData(); + question.options.push(skipOptionItem, abortItem, restartItem); + const pickedOption = await this.ask(question); + if (pickedOption === undefined || pickedOption === abortItem) { + abort = true; + break; + } else if (pickedOption === skipOptionItem) { + continue; + } else if (pickedOption === restartItem) { + restart = true; + break; + } else { + pickedOptions.push([assistant, pickedOption]); + } + } + if (abort) break; + if (restart) continue; + + const pickedItem = await (pickedOptions.length === 0 + ? this.ask({ + question: + "Settings assistant finished but no changes have been selected.", + options: [completeItem, restartItem], + }) + : this.ask({ + question: "Decisions have been gathered.", + options: [applyItem, restartItem, discardItem], + })); + if (pickedItem === completeItem) { + await this.setRunOnStartup(false, false); + } else if (pickedItem === applyItem) { + apply = true; + } else if (pickedItem === restartItem) { continue; - } else if (pickedOption === this.restartItem) { - restart = true; - break; - } else { - await assistant.handlePickedOption(pickedOption); } - } - if (abort) { break; } - if (restart) { - continue; - } - const pickedItem = await this.ask({ - question: "Settings assistant finished.", - options: [this.completeItem, this.restartItem], - }); - if (pickedItem !== this.restartItem) { - await this.vSCodeConfigurator.set(settingsAssistantOnStartupID, false); - break; + } catch (caughtError) { + error = caughtError || "unknown error"; + } + return { apply, error, pickedOptions }; + } + + private async applyDecisions(pickedOptions: [OptionAssistant, Option][]) { + const optionChangeProtocol = new OptionChangeProtocol(); + const { error } = await this.applySelection( + pickedOptions, + optionChangeProtocol + ); + if (error === undefined) { + await this.setRunOnStartup(false, false); + } + if (optionChangeProtocol.entries.length > 0) { + const saveProtocolOption = new Option("Save change protocol"); + const discardProtocolOption = new Option("Discard change protocol"); + const changesMadeStatement = "Changes were made."; + const selectedOption = await (error !== undefined + ? window.showErrorMessage( + `Error on running the settings assistant. ` + + `${changesMadeStatement} \n${JSON.stringify(error)}`, + saveProtocolOption, + discardProtocolOption + ) + : window.showInformationMessage( + `Successfully changed the settings.`, + saveProtocolOption, + discardProtocolOption + )); + if (selectedOption === saveProtocolOption) { + try { + await this.writeOptionChangeProtocol(optionChangeProtocol); + } catch (error) { + void window.showErrorMessage( + `Error on writing the change protocol: ${JSON.stringify(error)}` + ); + } } + } else { + const changesMadeStatement = "but no changes were protocolled."; + await (error !== undefined + ? window.showErrorMessage( + `Error on running the settings assistant ` + + `${changesMadeStatement} \n${JSON.stringify(error)}` + ) + : window.showInformationMessage( + `Settings assistant completed ${changesMadeStatement}` + )); } } - constructor( - private readonly gitConfigurator: GitConfigurator, - private readonly vSCodeConfigurator: VSCodeConfigurator - ) {} + private async setRunOnStartup( + value: boolean, + global: boolean + ): Promise { + await this.vSCodeConfigurator.set( + settingsAssistantOnStartupSettingID, + value, + global + ); + } - private readonly optionsAssistants = [ - new GitOptionAssistant( - this.gitConfigurator, - "mergetool.keepTemporaries", - "false", - "Remove temporary files after the merge." - ), - new GitOptionAssistant( - this.gitConfigurator, - "mergetool.keepBackup", - "false", - "Remove the automatically created backup files after a merge." - ), - new GitOptionAssistant( - this.gitConfigurator, - "mergetool.code.cmd", - `"${process.execPath}" "$BASE"`, - "Make VS Code available as merge tool." - ), - new GitOptionAssistant( - this.gitConfigurator, - "merge.tool", - "code", - "Set VS Code as merge tool" - ), - new GitOptionAssistant( - this.gitConfigurator, - "merge.conflictstyle", - "merge", - "Do not output base hunk in merge conflict files." - ), - new VSCodeOptionAssistant( - this.vSCodeConfigurator, - "workbench.editor.closeEmptyGroups", - true, - "Do not keep open empty editor groups when stopping a diff layout." - ), - new VSCodeOptionAssistant( - this.vSCodeConfigurator, - "merge-conflict.codeLens.enabled", - true, - "Show action links for selecting changes directly above merge conflict sections." - ), - new VSCodeOptionAssistant( - this.vSCodeConfigurator, - "diffEditor.codeLens", - true, - "Show the merge conflict code lens in diff editors." - ), - ]; - private readonly skipOptionItem = new Option("Skip"); - private readonly abortItem = new Option("Abort"); - private readonly completeItem = new Option("Complete"); - private readonly restartItem = new Option("Restart"); + private async applySelection( + pickedOptions: [OptionAssistant, Option][], + optionChangeProtocol: OptionChangeProtocol + ): Promise<{ error: unknown }> { + try { + for (const [assistant, pickedOption] of pickedOptions) { + await assistant.handlePickedOption(pickedOption, optionChangeProtocol); + } + } catch (error) { + return { error: error || "unknown error" }; + } + return { error: undefined }; + } + + private async writeOptionChangeProtocol( + optionChangeProtocol: OptionChangeProtocol + ): Promise { + // eslint-disable-next-line no-constant-condition + while (true) { + try { + const destinationUri = await window.showSaveDialog({ + defaultUri: Uri.file( + path.join( + homedir(), + "vscode-as-git-mergetool_option_change_protocol.yml" + ) + ), + // eslint-disable-next-line @typescript-eslint/naming-convention + filters: { YAML: ["yml", "yaml"] }, + title: "Save option change protocol (last chance)", + }); + if (destinationUri === undefined) { + return; + } + const destinationPath = destinationUri.fsPath; + let writeStream: Writable | undefined = undefined; + try { + writeStream = createWriteStream(destinationPath, { flags: "a" }); + await this.optionChangeProtocolExporter.export( + writeStream, + optionChangeProtocol + ); + } finally { + await new Promise((resolve) => writeStream?.end(resolve)); + } + break; + } catch (error: unknown) { + const retryItem: MessageItem = { title: "Retry" }; + const cancelItem: MessageItem = { title: "Cancel" }; + const selectedItem = await window.showErrorMessage( + `Saving option change protocol failed: \n${String(error)}`, + retryItem, + cancelItem + ); + if (selectedItem !== retryItem) { + break; + } + } + } + } private ask(question: QuestionData): Thenable