diff --git a/package.json b/package.json index 0a86f63e..4a75fff0 100644 --- a/package.json +++ b/package.json @@ -540,6 +540,21 @@ "default": false, "description": "Default state of the \"Prune Tags\" checkbox." }, + "git-graph.dialog.general.referenceInputSpaceSubstitution": { + "type": "string", + "enum": [ + "None", + "Hyphen", + "Underscore" + ], + "enumDescriptions": [ + "Don't replace spaces.", + "Replace space characters with hyphens, for example: \"new branch\" -> \"new-branch\".", + "Replace space characters with underscores, for example: \"new branch\" -> \"new_branch\"." + ], + "default": "None", + "description": "Specifies a substitution that is automatically performed when space characters are entered or pasted into reference inputs on dialogs (e.g. Create Branch, Add Tag, etc.)." + }, "git-graph.dialog.merge.noCommit": { "type": "boolean", "default": false, diff --git a/src/config.ts b/src/config.ts index cacc37e0..4e957745 100644 --- a/src/config.ts +++ b/src/config.ts @@ -173,6 +173,7 @@ class Config { get dialogDefaults(): DialogDefaults { let resetCommitMode = this.config.get('dialog.resetCurrentBranchToCommit.mode', 'Mixed'); let resetUncommittedMode = this.config.get('dialog.resetUncommittedChanges.mode', 'Mixed'); + let refInputSpaceSubstitution = this.config.get('dialog.general.referenceInputSpaceSubstitution', 'None'); return { addTag: { @@ -199,6 +200,9 @@ class Config { prune: !!this.config.get('dialog.fetchRemote.prune', false), pruneTags: !!this.config.get('dialog.fetchRemote.pruneTags', false) }, + general: { + referenceInputSpaceSubstitution: refInputSpaceSubstitution === 'Hyphen' ? '-' : refInputSpaceSubstitution === 'Underscore' ? '_' : null + }, merge: { noCommit: !!this.config.get('dialog.merge.noCommit', false), noFastForward: !!this.config.get('dialog.merge.noFastForward', true), diff --git a/src/types.ts b/src/types.ts index 7e24085a..39afdf57 100644 --- a/src/types.ts +++ b/src/types.ts @@ -454,6 +454,9 @@ export interface DialogDefaults { readonly prune: boolean, readonly pruneTags: boolean }; + readonly general: { + readonly referenceInputSpaceSubstitution: string | null + }; readonly merge: { readonly noCommit: boolean, readonly noFastForward: boolean, diff --git a/tests/config.test.ts b/tests/config.test.ts index 47f86255..92de8e20 100644 --- a/tests/config.test.ts +++ b/tests/config.test.ts @@ -838,6 +838,7 @@ describe('Config', () => { expect(workspaceConfiguration.get).toBeCalledWith('dialog.fetchIntoLocalBranch.forceFetch', false); expect(workspaceConfiguration.get).toBeCalledWith('dialog.fetchRemote.prune', false); expect(workspaceConfiguration.get).toBeCalledWith('dialog.fetchRemote.pruneTags', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.general.referenceInputSpaceSubstitution', 'None'); expect(workspaceConfiguration.get).toBeCalledWith('dialog.merge.noCommit', false); expect(workspaceConfiguration.get).toBeCalledWith('dialog.merge.noFastForward', true); expect(workspaceConfiguration.get).toBeCalledWith('dialog.merge.squashCommits', false); @@ -874,6 +875,9 @@ describe('Config', () => { prune: true, pruneTags: true }, + general: { + referenceInputSpaceSubstitution: null + }, merge: { noCommit: true, noFastForward: true, @@ -939,6 +943,7 @@ describe('Config', () => { expect(workspaceConfiguration.get).toBeCalledWith('dialog.fetchIntoLocalBranch.forceFetch', false); expect(workspaceConfiguration.get).toBeCalledWith('dialog.fetchRemote.prune', false); expect(workspaceConfiguration.get).toBeCalledWith('dialog.fetchRemote.pruneTags', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.general.referenceInputSpaceSubstitution', 'None'); expect(workspaceConfiguration.get).toBeCalledWith('dialog.merge.noCommit', false); expect(workspaceConfiguration.get).toBeCalledWith('dialog.merge.noFastForward', true); expect(workspaceConfiguration.get).toBeCalledWith('dialog.merge.squashCommits', false); @@ -975,6 +980,9 @@ describe('Config', () => { prune: false, pruneTags: false }, + general: { + referenceInputSpaceSubstitution: null + }, merge: { noCommit: false, noFastForward: false, @@ -1040,6 +1048,7 @@ describe('Config', () => { expect(workspaceConfiguration.get).toBeCalledWith('dialog.fetchIntoLocalBranch.forceFetch', false); expect(workspaceConfiguration.get).toBeCalledWith('dialog.fetchRemote.prune', false); expect(workspaceConfiguration.get).toBeCalledWith('dialog.fetchRemote.pruneTags', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.general.referenceInputSpaceSubstitution', 'None'); expect(workspaceConfiguration.get).toBeCalledWith('dialog.merge.noCommit', false); expect(workspaceConfiguration.get).toBeCalledWith('dialog.merge.noFastForward', true); expect(workspaceConfiguration.get).toBeCalledWith('dialog.merge.squashCommits', false); @@ -1076,6 +1085,9 @@ describe('Config', () => { prune: true, pruneTags: true }, + general: { + referenceInputSpaceSubstitution: null + }, merge: { noCommit: true, noFastForward: true, @@ -1141,6 +1153,7 @@ describe('Config', () => { expect(workspaceConfiguration.get).toBeCalledWith('dialog.fetchIntoLocalBranch.forceFetch', false); expect(workspaceConfiguration.get).toBeCalledWith('dialog.fetchRemote.prune', false); expect(workspaceConfiguration.get).toBeCalledWith('dialog.fetchRemote.pruneTags', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.general.referenceInputSpaceSubstitution', 'None'); expect(workspaceConfiguration.get).toBeCalledWith('dialog.merge.noCommit', false); expect(workspaceConfiguration.get).toBeCalledWith('dialog.merge.noFastForward', true); expect(workspaceConfiguration.get).toBeCalledWith('dialog.merge.squashCommits', false); @@ -1177,6 +1190,9 @@ describe('Config', () => { prune: false, pruneTags: false }, + general: { + referenceInputSpaceSubstitution: null + }, merge: { noCommit: false, noFastForward: false, @@ -1227,6 +1243,7 @@ describe('Config', () => { expect(workspaceConfiguration.get).toBeCalledWith('dialog.fetchIntoLocalBranch.forceFetch', false); expect(workspaceConfiguration.get).toBeCalledWith('dialog.fetchRemote.prune', false); expect(workspaceConfiguration.get).toBeCalledWith('dialog.fetchRemote.pruneTags', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.general.referenceInputSpaceSubstitution', 'None'); expect(workspaceConfiguration.get).toBeCalledWith('dialog.merge.noCommit', false); expect(workspaceConfiguration.get).toBeCalledWith('dialog.merge.noFastForward', true); expect(workspaceConfiguration.get).toBeCalledWith('dialog.merge.squashCommits', false); @@ -1263,6 +1280,9 @@ describe('Config', () => { prune: false, pruneTags: false }, + general: { + referenceInputSpaceSubstitution: null + }, merge: { noCommit: false, noFastForward: true, @@ -1306,6 +1326,7 @@ describe('Config', () => { expect(workspaceConfiguration.get).toBeCalledWith('dialog.fetchIntoLocalBranch.forceFetch', false); expect(workspaceConfiguration.get).toBeCalledWith('dialog.fetchRemote.prune', false); expect(workspaceConfiguration.get).toBeCalledWith('dialog.fetchRemote.pruneTags', false); + expect(workspaceConfiguration.get).toBeCalledWith('dialog.general.referenceInputSpaceSubstitution', 'None'); expect(workspaceConfiguration.get).toBeCalledWith('dialog.merge.noCommit', false); expect(workspaceConfiguration.get).toBeCalledWith('dialog.merge.noFastForward', true); expect(workspaceConfiguration.get).toBeCalledWith('dialog.merge.squashCommits', false); @@ -1342,6 +1363,9 @@ describe('Config', () => { prune: false, pruneTags: false }, + general: { + referenceInputSpaceSubstitution: null + }, merge: { noCommit: false, noFastForward: true, @@ -1371,7 +1395,7 @@ describe('Config', () => { }); describe('dialogDefaults.addTag.type', () => { - it('Should return "annotated" the configuration value is "Annotated"', () => { + it('Should return TagType.Annotated when the configuration value is "Annotated"', () => { // Setup vscode.mockExtensionSettingReturnValue('dialog.addTag.type', 'Annotated'); @@ -1382,7 +1406,7 @@ describe('Config', () => { expect(value.addTag.type).toBe(TagType.Annotated); }); - it('Should return "lightweight" the configuration value is "Annotated"', () => { + it('Should return TagType.Lightweight when the configuration value is "Lightweight"', () => { // Setup vscode.mockExtensionSettingReturnValue('dialog.addTag.type', 'Lightweight'); @@ -1394,8 +1418,54 @@ describe('Config', () => { }); }); + describe('dialogDefaults.general.referenceInputSpaceSubstitution', () => { + it('Should return NULL when the configuration value is "None"', () => { + // Setup + vscode.mockExtensionSettingReturnValue('dialog.general.referenceInputSpaceSubstitution', 'None'); + + // Run + const value = config.dialogDefaults; + + // Assert + expect(value.general.referenceInputSpaceSubstitution).toBe(null); + }); + + it('Should return "-" when the configuration value is "Hyphen"', () => { + // Setup + vscode.mockExtensionSettingReturnValue('dialog.general.referenceInputSpaceSubstitution', 'Hyphen'); + + // Run + const value = config.dialogDefaults; + + // Assert + expect(value.general.referenceInputSpaceSubstitution).toBe('-'); + }); + + it('Should return "_" when the configuration value is "Underscore"', () => { + // Setup + vscode.mockExtensionSettingReturnValue('dialog.general.referenceInputSpaceSubstitution', 'Underscore'); + + // Run + const value = config.dialogDefaults; + + // Assert + expect(value.general.referenceInputSpaceSubstitution).toBe('_'); + }); + + it('Should return the default value (NULL) when the configuration value is invalid', () => { + // Setup + vscode.mockExtensionSettingReturnValue('dialog.general.referenceInputSpaceSubstitution', 'invalid'); + + // Run + const value = config.dialogDefaults; + + // Assert + expect(value.general.referenceInputSpaceSubstitution).toBe(null); + }); + }); + describe('dialogDefaults.resetCommit.mode', () => { - it('Should return GitResetMode.Hard the configuration value is "Hard"', () => { + it('Should return GitResetMode.Hard when the configuration value is "Hard"', () => { // Setup vscode.mockExtensionSettingReturnValue('dialog.resetCurrentBranchToCommit.mode', 'Hard'); @@ -1406,7 +1476,7 @@ describe('Config', () => { expect(value.resetCommit.mode).toBe(GitResetMode.Hard); }); - it('Should return GitResetMode.Mixed the configuration value is "Mixed"', () => { + it('Should return GitResetMode.Mixed when the configuration value is "Mixed"', () => { // Setup vscode.mockExtensionSettingReturnValue('dialog.resetCurrentBranchToCommit.mode', 'Mixed'); @@ -1417,7 +1487,7 @@ describe('Config', () => { expect(value.resetCommit.mode).toBe(GitResetMode.Mixed); }); - it('Should return GitResetMode.Soft the configuration value is "Soft"', () => { + it('Should return GitResetMode.Soft when the configuration value is "Soft"', () => { // Setup vscode.mockExtensionSettingReturnValue('dialog.resetCurrentBranchToCommit.mode', 'Soft'); @@ -1430,7 +1500,7 @@ describe('Config', () => { }); describe('dialogDefaults.resetUncommitted.mode', () => { - it('Should return GitResetMode.Hard the configuration value is "Hard"', () => { + it('Should return GitResetMode.Hard when the configuration value is "Hard"', () => { // Setup vscode.mockExtensionSettingReturnValue('dialog.resetUncommittedChanges.mode', 'Hard'); @@ -1441,7 +1511,7 @@ describe('Config', () => { expect(value.resetUncommitted.mode).toBe(GitResetMode.Hard); }); - it('Should return GitResetMode.Mixed the configuration value is "Mixed"', () => { + it('Should return GitResetMode.Mixed when the configuration value is "Mixed"', () => { // Setup vscode.mockExtensionSettingReturnValue('dialog.resetUncommittedChanges.mode', 'Mixed'); diff --git a/web/dialog.ts b/web/dialog.ts index 9992b386..e1385a99 100644 --- a/web/dialog.ts +++ b/web/dialog.ts @@ -91,6 +91,8 @@ class Dialog { private type: DialogType | null = null; private customSelects: { [inputIndex: string]: CustomSelect } = {}; + private static readonly WHITESPACE_REGEXP = /\s/gu; + /** * Show a confirmation dialog to the user. * @param message A message outlining what the user is being asked to confirm. @@ -269,7 +271,13 @@ class Dialog { if (dialogInput.value === '') this.elem!.classList.add(CLASS_DIALOG_NO_INPUT); dialogInput.addEventListener('keyup', () => { if (this.elem === null) return; - let noInput = dialogInput.value === '', invalidInput = dialogInput.value.match(REF_INVALID_REGEX) !== null; + if (initialState.config.dialogDefaults.general.referenceInputSpaceSubstitution !== null) { + const selectionStart = dialogInput.selectionStart, selectionEnd = dialogInput.selectionEnd; + dialogInput.value = dialogInput.value.replace(Dialog.WHITESPACE_REGEXP, initialState.config.dialogDefaults.general.referenceInputSpaceSubstitution); + dialogInput.selectionStart = selectionStart; + dialogInput.selectionEnd = selectionEnd; + } + const noInput = dialogInput.value === '', invalidInput = dialogInput.value.match(REF_INVALID_REGEX) !== null; alterClass(this.elem, CLASS_DIALOG_NO_INPUT, noInput); if (alterClass(this.elem, CLASS_DIALOG_INPUT_INVALID, !noInput && invalidInput)) { dialogAction.title = invalidInput ? 'Unable to ' + actionName + ', one or more invalid characters entered.' : '';