From e18bc3c7f83900a87518a442ab117ffce4494991 Mon Sep 17 00:00:00 2001 From: Opisek Date: Sun, 3 Dec 2023 15:08:20 +0100 Subject: [PATCH 01/14] added langmap remapping logic (#145) --- src/vim.js | 100 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 99 insertions(+), 1 deletion(-) diff --git a/src/vim.js b/src/vim.js index 3446a15..f867104 100644 --- a/src/vim.js +++ b/src/vim.js @@ -303,6 +303,17 @@ export function initVim(CodeMirror) { { name: 'global', shortName: 'g' } ]; + /** + * Langmap + * Determines how to interpret keystrokes in Normal and Visual mode. + * Useful for people who use a different keyboard layout than QWERTY + */ + var langmap; + updateLangmap(''); + + // dvorak langmap for testing purposes: + updateLangmap("'q,\\,w,.e,pr,yt,fy,gu,ci,ro,lp,/[,=],aa,os,ed,uf,ig,dh,hj,tk,nl,s\\;,-',\\;z,qx,jc,kv,xb,bn,mm,w\\,,v.,z/,[-,]=,\"Q,E,PR,YT,FY,GU,CI,RO,LP,?{,+},AA,OS,ED,UF,IG,DH,HJ,TK,NL,S:,_\",:Z,QX,JC,KV,XB,BN,MM,W<,V>,Z?"); + function enterVimMode(cm) { cm.setOption('disableInput', true); cm.setOption('showCursorWhenSelecting', false); @@ -868,8 +879,12 @@ export function initVim(CodeMirror) { * execute the bound command if a a key is matched. The function always * returns true. */ - findKey: function(cm, key, origin) { + findKey: function(cm, rawKey, origin) { var vim = maybeInitVimState(cm); + + let key = langmapRemapKey(rawKey); + console.log(`${rawKey} -> ${key}`); + function handleMacroRecording() { var macroModeState = vimGlobalState.macroModeState; if (macroModeState.isRecording) { @@ -6322,5 +6337,88 @@ export function initVim(CodeMirror) { } resetVimGlobalState(); + // langmap support + function updateLangmap(langmapString) { + if (langmap != null && langmap.string == langmapString) return; + langmap = parseLangmap(langmapString); + } + function langmapIsLiteralMode(vim) { + // Determine if keystrokes should be interpreted literally + return vim.insertMode; + } + function parseLangmap(langmapString) { + // From :help langmap + /* + The 'langmap' option is a list of parts, separated with commas. Each + part can be in one of two forms: + 1. A list of pairs. Each pair is a "from" character immediately + followed by the "to" character. Examples: "aA", "aAbBcC". + 2. A list of "from" characters, a semi-colon and a list of "to" + characters. Example: "abc;ABC" + */ + + // Step 0: Shortcut for empty langmap + let toQwerty = {}; + let fromQwerty = {}; + if (langmapString === '') return { toQwerty: toQwerty, fromQwerty: fromQwerty, string: '' }; + + // Step 1: Separate into parts. + // Technically the regex /(? x.index); + const parts = separators.map((separatorIndex, arrayIndex) => + langmapString.substring( + arrayIndex === 0 ? 0 : (separators[arrayIndex - 1]) + 1, + separatorIndex, + ), + ); + + // Step 2: Parse each part + function getEscaped(list) { + const characters = []; + let escaped = false; + for (const character of list) { + if (character === '\\') { + escaped = !escaped; + if (escaped) continue; + } + characters.push(character); + } + return characters; + } + + for (const part of parts) { + const semicolon = [...part.matchAll(/(? x.index); + if (semicolon.length > 1) continue; // skip over malformed part + if (semicolon.length === 0) { + // List of pairs of "from" and "to" characters + const pairs = getEscaped(part); + if (pairs.length % 2 !== 0) continue; // skip over malformed part + for (let i = 0; i < pairs.length; i += 2) toQwerty[pairs[i]] = pairs[i + 1]; + } else { + // List of "from" characters and list of "to" characters + const from = getEscaped(part.substring(0, semicolon[0])); + const to = getEscaped(part.substring((semicolon[0]) + 1)); + if (from.length !== to.length) continue; // skip over malformed part + for (let i = 0; i < from.length; ++i) toQwerty[from[i]] = to[i]; + } + } + + // Step 3: Reverse mapping + Object.fromEntries(Object.entries(toQwerty).map((x) => [x[1], x[0]])); + + return { toQwerty: toQwerty, fromQwerty: fromQwerty, string: langmapString }; + } + function _langmapMap(map, key) { + return (key.length !== 1 || !(key in map)) ? key : map[key]; + + } + function langmapRemapKey(key) { + return _langmapMap(langmap.toQwerty, key) + } + function langmapUnmapKey(key) { + return _langmapMap(langmap.fromQwerty, key) + } + return vimApi; }; From 4e78b2acd520ea4c4449ded2154a13936d72483a Mon Sep 17 00:00:00 2001 From: Opisek Date: Wed, 6 Dec 2023 12:35:52 +0100 Subject: [PATCH 02/14] added literal key detection for langmaps (#145) --- src/vim.js | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/vim.js b/src/vim.js index f867104..ca46eea 100644 --- a/src/vim.js +++ b/src/vim.js @@ -744,7 +744,11 @@ export function initVim(CodeMirror) { lastPastedText: null, sel: {}, // Buffer-local/window-local values of vim options. - options: {} + options: {}, + // Whether the next character should be interpreted literally + // Necassary for correct implementation of f, r etc. + // in terms of langmaps. + expectLiteralNext: false }; } return cm.state.vim; @@ -882,8 +886,7 @@ export function initVim(CodeMirror) { findKey: function(cm, rawKey, origin) { var vim = maybeInitVimState(cm); - let key = langmapRemapKey(rawKey); - console.log(`${rawKey} -> ${key}`); + let key = vim.expectLiteralNext ? rawKey : langmapRemapKey(rawKey); function handleMacroRecording() { var macroModeState = vimGlobalState.macroModeState; @@ -925,6 +928,7 @@ export function initVim(CodeMirror) { if (match.type == 'none') { clearInputState(cm); return false; } else if (match.type == 'partial') { + if (match.expectLiteralNext) vim.expectLiteralNext = true; if (lastInsertModeKeyTimer) { window.clearTimeout(lastInsertModeKeyTimer); } lastInsertModeKeyTimer = keysAreChars && window.setTimeout( function() { if (vim.insertMode && vim.inputState.keyBuffer.length) { clearInputState(cm); } }, @@ -943,6 +947,7 @@ export function initVim(CodeMirror) { } return !keysAreChars; } + vim.expectLiteralNext = false; if (lastInsertModeKeyTimer) { window.clearTimeout(lastInsertModeKeyTimer); } if (match.command && changeQueue) { @@ -976,7 +981,10 @@ export function initVim(CodeMirror) { } var match = commandDispatcher.matchCommand(mainKey, defaultKeymap, vim.inputState, context); if (match.type == 'none') { clearInputState(cm); return false; } - else if (match.type == 'partial') { return true; } + else if (match.type == 'partial') { + if (match.expectLiteralNext) vim.expectLiteralNext = true; + return true; + } else if (match.type == 'clear') { clearInputState(cm); return true; } vim.inputState.keyBuffer.length = 0; @@ -1174,6 +1182,7 @@ export function initVim(CodeMirror) { function clearInputState(cm, reason) { cm.state.vim.inputState = new InputState(); + cm.state.vim.expectLiteralNext = false; CodeMirror.signal(cm, 'vim-command-done', reason); } @@ -1381,7 +1390,10 @@ export function initVim(CodeMirror) { if (!matches.full && !matches.partial) { return {type: 'none'}; } else if (!matches.full && matches.partial) { - return {type: 'partial'}; + return { + type: 'partial', + expectLiteralNext: matches.partial.length == 1 && matches.partial[0].keys.slice(-11) == '' // langmap literal logic + }; } var bestMatch; From 0a8a84ec0072ebe5616bcd6eab2f4e805fb01604 Mon Sep 17 00:00:00 2001 From: Opisek Date: Wed, 6 Dec 2023 12:35:52 +0100 Subject: [PATCH 03/14] added missing expectNextLiteral reset (#145) --- src/vim.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vim.js b/src/vim.js index ca46eea..010ebdf 100644 --- a/src/vim.js +++ b/src/vim.js @@ -986,6 +986,7 @@ export function initVim(CodeMirror) { return true; } else if (match.type == 'clear') { clearInputState(cm); return true; } + vim.expectLiteralNext = false; vim.inputState.keyBuffer.length = 0; keysMatcher = /^(\d*)(.*)$/.exec(keys); From 3abe3c12aaa9e15ee277ec2597c3b57ba16a2773 Mon Sep 17 00:00:00 2001 From: Opisek Date: Wed, 6 Dec 2023 12:35:52 +0100 Subject: [PATCH 04/14] added macro support for langmap (#145) --- src/vim.js | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/src/vim.js b/src/vim.js index 010ebdf..6a45f68 100644 --- a/src/vim.js +++ b/src/vim.js @@ -150,8 +150,8 @@ export function initVim(CodeMirror) { { keys: 'T', type: 'motion', motion: 'moveTillCharacter', motionArgs: { forward: false }}, { keys: ';', type: 'motion', motion: 'repeatLastCharacterSearch', motionArgs: { forward: true }}, { keys: ',', type: 'motion', motion: 'repeatLastCharacterSearch', motionArgs: { forward: false }}, - { keys: '\'', type: 'motion', motion: 'goToMark', motionArgs: {toJumplist: true, linewise: true}}, - { keys: '`', type: 'motion', motion: 'goToMark', motionArgs: {toJumplist: true}}, + { keys: '\'', type: 'motion', motion: 'goToMark', motionArgs: {toJumplist: true, linewise: true}}, + { keys: '`', type: 'motion', motion: 'goToMark', motionArgs: {toJumplist: true}}, { keys: ']`', type: 'motion', motion: 'jumpToMark', motionArgs: { forward: true } }, { keys: '[`', type: 'motion', motion: 'jumpToMark', motionArgs: { forward: false } }, { keys: ']\'', type: 'motion', motion: 'jumpToMark', motionArgs: { forward: true, linewise: true } }, @@ -220,8 +220,8 @@ export function initVim(CodeMirror) { { keys: 'p', type: 'action', action: 'paste', isEdit: true, actionArgs: { after: true, isEdit: true }}, { keys: 'P', type: 'action', action: 'paste', isEdit: true, actionArgs: { after: false, isEdit: true }}, { keys: 'r', type: 'action', action: 'replace', isEdit: true }, - { keys: '@', type: 'action', action: 'replayMacro' }, - { keys: 'q', type: 'action', action: 'enterMacroRecordMode' }, + { keys: '@', type: 'action', action: 'replayMacro' }, + { keys: 'q', type: 'action', action: 'enterMacroRecordMode' }, // Handle Replace-mode as a special case of insert mode. { keys: 'R', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { replace: true }, context: 'normal'}, { keys: 'R', type: 'operator', operator: 'change', operatorArgs: { linewise: true, fullLine: true }, context: 'visual', exitVisualBlock: true}, @@ -229,9 +229,9 @@ export function initVim(CodeMirror) { { keys: 'u', type: 'operator', operator: 'changeCase', operatorArgs: {toLower: true}, context: 'visual', isEdit: true }, { keys: 'U', type: 'operator', operator: 'changeCase', operatorArgs: {toLower: false}, context: 'visual', isEdit: true }, { keys: '', type: 'action', action: 'redo' }, - { keys: 'm', type: 'action', action: 'setMark' }, - { keys: '"', type: 'action', action: 'setRegister' }, - { keys: '', type: 'action', action: 'insertRegister', context: 'insert', isEdit: true }, + { keys: 'm', type: 'action', action: 'setMark' }, + { keys: '"', type: 'action', action: 'setRegister' }, + { keys: '', type: 'action', action: 'insertRegister', context: 'insert', isEdit: true }, { keys: '', type: 'action', action: 'oneNormalCommand', context: 'insert' }, { keys: 'zz', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'center' }}, { keys: 'z.', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'center' }, motion: 'moveToFirstNonWhiteSpaceCharacter' }, @@ -1404,7 +1404,7 @@ export function initVim(CodeMirror) { bestMatch = match; } } - if (bestMatch.keys.slice(-11) == '') { + if (bestMatch.keys.slice(-11) == '' || bestMatch.keys.slice(-10) == '') { var character = lastChar(keys); if (!character || character.length > 1) return {type: 'clear'}; inputState.selectedCharacter = character; @@ -2630,6 +2630,10 @@ export function initVim(CodeMirror) { cm.scrollTo(null, y); }, replayMacro: function(cm, actionArgs, vim) { + // when replaying a macro, we must not "double remap" characters + const savedLangMap = langmap; + langmap = parseLangmap(''); + var registerName = actionArgs.selectedCharacter; var repeat = actionArgs.repeat; var macroModeState = vimGlobalState.macroModeState; @@ -2641,6 +2645,9 @@ export function initVim(CodeMirror) { while(repeat--){ executeMacroRegister(cm, vim, macroModeState, registerName); } + + // restore langmap + langmap = savedLangMap; }, enterMacroRecordMode: function(cm, actionArgs) { var macroModeState = vimGlobalState.macroModeState; @@ -3225,9 +3232,11 @@ export function initVim(CodeMirror) { }; } function commandMatch(pressed, mapped) { - if (mapped.slice(-11) == '') { + const isLastCharacter = mapped.slice(-11) == ''; + const isLastRegister = mapped.slice(-10) == ''; + if (isLastCharacter || isLastRegister) { // Last character matches anything. - var prefixLen = mapped.length - 11; + var prefixLen = mapped.length - (isLastCharacter ? 11 : 10); var pressedPrefix = pressed.slice(0, prefixLen); var mappedPrefix = mapped.slice(0, prefixLen); return pressedPrefix == mappedPrefix && pressed.length > prefixLen ? 'full' : From a9cd530a989c050257c2059f2d32600357e619fa Mon Sep 17 00:00:00 2001 From: Opisek Date: Wed, 6 Dec 2023 12:35:52 +0100 Subject: [PATCH 05/14] moved langmap code to respect the general code structure (#145) --- src/vim.js | 166 ++++++++++++++++++++++++++--------------------------- 1 file changed, 83 insertions(+), 83 deletions(-) diff --git a/src/vim.js b/src/vim.js index 6a45f68..bb6aa37 100644 --- a/src/vim.js +++ b/src/vim.js @@ -1147,6 +1147,89 @@ export function initVim(CodeMirror) { } } + // langmap support + function updateLangmap(langmapString) { + if (langmap != null && langmap.string == langmapString) return; + langmap = parseLangmap(langmapString); + } + function langmapIsLiteralMode(vim) { + // Determine if keystrokes should be interpreted literally + return vim.insertMode; + } + function parseLangmap(langmapString) { + // From :help langmap + /* + The 'langmap' option is a list of parts, separated with commas. Each + part can be in one of two forms: + 1. A list of pairs. Each pair is a "from" character immediately + followed by the "to" character. Examples: "aA", "aAbBcC". + 2. A list of "from" characters, a semi-colon and a list of "to" + characters. Example: "abc;ABC" + */ + + // Step 0: Shortcut for empty langmap + let toQwerty = {}; + let fromQwerty = {}; + if (langmapString === '') return { toQwerty: toQwerty, fromQwerty: fromQwerty, string: '' }; + + // Step 1: Separate into parts. + // Technically the regex /(? x.index); + const parts = separators.map((separatorIndex, arrayIndex) => + langmapString.substring( + arrayIndex === 0 ? 0 : (separators[arrayIndex - 1]) + 1, + separatorIndex, + ), + ); + + // Step 2: Parse each part + function getEscaped(list) { + const characters = []; + let escaped = false; + for (const character of list) { + if (character === '\\') { + escaped = !escaped; + if (escaped) continue; + } + characters.push(character); + } + return characters; + } + + for (const part of parts) { + const semicolon = [...part.matchAll(/(? x.index); + if (semicolon.length > 1) continue; // skip over malformed part + if (semicolon.length === 0) { + // List of pairs of "from" and "to" characters + const pairs = getEscaped(part); + if (pairs.length % 2 !== 0) continue; // skip over malformed part + for (let i = 0; i < pairs.length; i += 2) toQwerty[pairs[i]] = pairs[i + 1]; + } else { + // List of "from" characters and list of "to" characters + const from = getEscaped(part.substring(0, semicolon[0])); + const to = getEscaped(part.substring((semicolon[0]) + 1)); + if (from.length !== to.length) continue; // skip over malformed part + for (let i = 0; i < from.length; ++i) toQwerty[from[i]] = to[i]; + } + } + + // Step 3: Reverse mapping + Object.fromEntries(Object.entries(toQwerty).map((x) => [x[1], x[0]])); + + return { toQwerty: toQwerty, fromQwerty: fromQwerty, string: langmapString }; + } + function _langmapMap(map, key) { + return (key.length !== 1 || !(key in map)) ? key : map[key]; + + } + function langmapRemapKey(key) { + return _langmapMap(langmap.toQwerty, key) + } + function langmapUnmapKey(key) { + return _langmapMap(langmap.fromQwerty, key) + } + // Represents the current input state. function InputState() { this.prefixRepeat = []; @@ -6359,88 +6442,5 @@ export function initVim(CodeMirror) { } resetVimGlobalState(); - // langmap support - function updateLangmap(langmapString) { - if (langmap != null && langmap.string == langmapString) return; - langmap = parseLangmap(langmapString); - } - function langmapIsLiteralMode(vim) { - // Determine if keystrokes should be interpreted literally - return vim.insertMode; - } - function parseLangmap(langmapString) { - // From :help langmap - /* - The 'langmap' option is a list of parts, separated with commas. Each - part can be in one of two forms: - 1. A list of pairs. Each pair is a "from" character immediately - followed by the "to" character. Examples: "aA", "aAbBcC". - 2. A list of "from" characters, a semi-colon and a list of "to" - characters. Example: "abc;ABC" - */ - - // Step 0: Shortcut for empty langmap - let toQwerty = {}; - let fromQwerty = {}; - if (langmapString === '') return { toQwerty: toQwerty, fromQwerty: fromQwerty, string: '' }; - - // Step 1: Separate into parts. - // Technically the regex /(? x.index); - const parts = separators.map((separatorIndex, arrayIndex) => - langmapString.substring( - arrayIndex === 0 ? 0 : (separators[arrayIndex - 1]) + 1, - separatorIndex, - ), - ); - - // Step 2: Parse each part - function getEscaped(list) { - const characters = []; - let escaped = false; - for (const character of list) { - if (character === '\\') { - escaped = !escaped; - if (escaped) continue; - } - characters.push(character); - } - return characters; - } - - for (const part of parts) { - const semicolon = [...part.matchAll(/(? x.index); - if (semicolon.length > 1) continue; // skip over malformed part - if (semicolon.length === 0) { - // List of pairs of "from" and "to" characters - const pairs = getEscaped(part); - if (pairs.length % 2 !== 0) continue; // skip over malformed part - for (let i = 0; i < pairs.length; i += 2) toQwerty[pairs[i]] = pairs[i + 1]; - } else { - // List of "from" characters and list of "to" characters - const from = getEscaped(part.substring(0, semicolon[0])); - const to = getEscaped(part.substring((semicolon[0]) + 1)); - if (from.length !== to.length) continue; // skip over malformed part - for (let i = 0; i < from.length; ++i) toQwerty[from[i]] = to[i]; - } - } - - // Step 3: Reverse mapping - Object.fromEntries(Object.entries(toQwerty).map((x) => [x[1], x[0]])); - - return { toQwerty: toQwerty, fromQwerty: fromQwerty, string: langmapString }; - } - function _langmapMap(map, key) { - return (key.length !== 1 || !(key in map)) ? key : map[key]; - - } - function langmapRemapKey(key) { - return _langmapMap(langmap.toQwerty, key) - } - function langmapUnmapKey(key) { - return _langmapMap(langmap.fromQwerty, key) - } - return vimApi; }; From 3c14f11fde745dc162dc22985e1d1e9c4326aa60 Mon Sep 17 00:00:00 2001 From: Opisek Date: Wed, 6 Dec 2023 12:35:52 +0100 Subject: [PATCH 06/14] expose langmap in the vim api (#145) --- src/vim.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/vim.js b/src/vim.js index bb6aa37..c2e86b5 100644 --- a/src/vim.js +++ b/src/vim.js @@ -312,7 +312,7 @@ export function initVim(CodeMirror) { updateLangmap(''); // dvorak langmap for testing purposes: - updateLangmap("'q,\\,w,.e,pr,yt,fy,gu,ci,ro,lp,/[,=],aa,os,ed,uf,ig,dh,hj,tk,nl,s\\;,-',\\;z,qx,jc,kv,xb,bn,mm,w\\,,v.,z/,[-,]=,\"Q,E,PR,YT,FY,GU,CI,RO,LP,?{,+},AA,OS,ED,UF,IG,DH,HJ,TK,NL,S:,_\",:Z,QX,JC,KV,XB,BN,MM,W<,V>,Z?"); + //updateLangmap("'q,\\,w,.e,pr,yt,fy,gu,ci,ro,lp,/[,=],aa,os,ed,uf,ig,dh,hj,tk,nl,s\\;,-',\\;z,qx,jc,kv,xb,bn,mm,w\\,,v.,z/,[-,]=,\"Q,E,PR,YT,FY,GU,CI,RO,LP,?{,+},AA,OS,ED,UF,IG,DH,HJ,TK,NL,S:,_\",:Z,QX,JC,KV,XB,BN,MM,W<,V>,Z?"); function enterVimMode(cm) { cm.setOption('disableInput', true); @@ -851,6 +851,9 @@ export function initVim(CodeMirror) { } } }, + langmap: function(langmapString) { + updateLangmap(langmapString); + }, // TODO: Expose setOption and getOption as instance methods. Need to decide how to namespace // them, or somehow make them work with the existing CodeMirror setOption/getOption API. setOption: setOption, From cccf987524cbc07841a084b90f99d8206c78efec Mon Sep 17 00:00:00 2001 From: Opisek Date: Wed, 6 Dec 2023 12:35:52 +0100 Subject: [PATCH 07/14] added tests for langmap (#145) --- test/vim_test.js | 78 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/test/vim_test.js b/test/vim_test.js index e50b7e0..fd5b3ee 100644 --- a/test/vim_test.js +++ b/test/vim_test.js @@ -5557,6 +5557,84 @@ testVim('_insert_mode', function(cm, vim, helpers) { eq('456 123 ', cm.getValue()); }, { value: '123 456 ' }); +// +// test correct langmap function +// +const dvorakLangmap = "'q,\\,w,.e,pr,yt,fy,gu,ci,ro,lp,/[,=],aa,os,ed,uf,ig,dh,hj,tk,nl,s\\;,-',\\;z,qx,jc,kv,xb,bn,mm,w\\,,v.,z/,[-,]=,\"Q,E,PR,YT,FY,GU,CI,RO,LP,?{,+},AA,OS,ED,UF,IG,DH,HJ,TK,NL,S:,_\",:Z,QX,JC,KV,XB,BN,MM,W<,V>,Z?"; +// this test makes sure that remapping works on an example binding +testVim('langmap_dd', function(cm, vim, helpers) { + CodeMirror.Vim.langmap(dvorakLangmap); + + cm.setCursor(0, 3); + var expectedBuffer = cm.getRange(new Pos(0, 0), + new Pos(1, 0)); + var expectedLineCount = cm.lineCount() - 1; + + helpers.doKeys('e', 'e'); + + eq(expectedLineCount, cm.lineCount()); + var register = helpers.getRegisterController().getRegister(); + eq(expectedBuffer, register.toString()); + is(register.linewise); + helpers.assertCursorAt(0, lines[1].textStart); +}); +// this test serves two functions: +// - make sure that "dd" is **not** interpreted as delete line (correct unmapping) +// - make sure that "dd" **is** interpreted as move left twice (correct mapping) +testVim('langmap_hh', function(cm, vim, helpers) { + CodeMirror.Vim.langmap(dvorakLangmap); + + const startPos = word1.end; + const endPos = offsetCursor(word1.end, 0, -2); + + cm.setCursor(startPos); + helpers.doKeys('d', 'd'); + helpers.assertCursorAt(endPos); +}); +// this test serves two functions: +// - make sure tha the register is properly remapped so that special registers aren't mixed up +// - make sure that recording and replaying macros works without "double remapping" +testVim('langmap_qqddq@q', function(cm, vim, helpers) { + CodeMirror.Vim.langmap(dvorakLangmap); + + cm.setCursor(0, 3); + var expectedBuffer = cm.getRange(new Pos(1, 0), + new Pos(2, 0)); + var expectedLineCount = cm.lineCount() - 2; + + helpers.doKeys('\'\'', 'e', 'e', '\'', '@\''); + + eq(expectedLineCount, cm.lineCount()); + var register = helpers.getRegisterController().getRegister(); + eq(expectedBuffer, register.toString()); + is(register.linewise); + helpers.assertCursorAt(0, lines[2].textStart); +}); +// this test makes sure that directives are interpreted literally +testVim('langmap_fd', function(cm, vim, helpers) { + CodeMirror.Vim.langmap(dvorakLangmap); + + cm.setCursor(0, 0); + helpers.doKeys('u', 'd'); + helpers.assertCursorAt(0, 4); +}); +// this test makes sure that markers work properly +testVim('langmap_mark', function(cm, vim, helpers) { + CodeMirror.Vim.langmap(dvorakLangmap); + + cm.setCursor(2, 2); + helpers.doKeys('m', '\''); + cm.setCursor(0, 0); + helpers.doKeys('`', '\''); + helpers.assertCursorAt(2, 2); + cm.setCursor(2, 0); + cm.replaceRange(' h', cm.getCursor()); + cm.setCursor(0, 0); + helpers.doKeys('-', '\''); + helpers.assertCursorAt(2, 3); +}); + + async function delay(t) { return await new Promise(resolve => setTimeout(resolve, t)); } From 4d3af7e118132403a6b7bc4cc7515ad53b0d60a5 Mon Sep 17 00:00:00 2001 From: Opisek Date: Wed, 6 Dec 2023 12:35:52 +0100 Subject: [PATCH 08/14] removed testing code (#145) --- src/vim.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/vim.js b/src/vim.js index c2e86b5..113bde2 100644 --- a/src/vim.js +++ b/src/vim.js @@ -311,9 +311,6 @@ export function initVim(CodeMirror) { var langmap; updateLangmap(''); - // dvorak langmap for testing purposes: - //updateLangmap("'q,\\,w,.e,pr,yt,fy,gu,ci,ro,lp,/[,=],aa,os,ed,uf,ig,dh,hj,tk,nl,s\\;,-',\\;z,qx,jc,kv,xb,bn,mm,w\\,,v.,z/,[-,]=,\"Q,E,PR,YT,FY,GU,CI,RO,LP,?{,+},AA,OS,ED,UF,IG,DH,HJ,TK,NL,S:,_\",:Z,QX,JC,KV,XB,BN,MM,W<,V>,Z?"); - function enterVimMode(cm) { cm.setOption('disableInput', true); cm.setOption('showCursorWhenSelecting', false); From c737cdeeffb6b3865ac864e557266fe69f802af2 Mon Sep 17 00:00:00 2001 From: Opisek Date: Wed, 6 Dec 2023 12:39:10 +0100 Subject: [PATCH 09/14] removed legacy code (#145) --- src/vim.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/vim.js b/src/vim.js index 113bde2..b2a5ff8 100644 --- a/src/vim.js +++ b/src/vim.js @@ -1219,15 +1219,8 @@ export function initVim(CodeMirror) { return { toQwerty: toQwerty, fromQwerty: fromQwerty, string: langmapString }; } - function _langmapMap(map, key) { - return (key.length !== 1 || !(key in map)) ? key : map[key]; - - } function langmapRemapKey(key) { - return _langmapMap(langmap.toQwerty, key) - } - function langmapUnmapKey(key) { - return _langmapMap(langmap.fromQwerty, key) + return (key.length !== 1 || !(key in langmap.toQwerty)) ? key : langmap.toQwerty[key]; } // Represents the current input state. From d0f09e5de28c1c86b578a3d67cda8ce5e453ba9b Mon Sep 17 00:00:00 2001 From: Opisek Date: Thu, 7 Dec 2023 12:05:16 +0100 Subject: [PATCH 10/14] changed to for a and i bindings (#145) --- src/vim.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vim.js b/src/vim.js index b2a5ff8..1c32c6b 100644 --- a/src/vim.js +++ b/src/vim.js @@ -245,8 +245,8 @@ export function initVim(CodeMirror) { { keys: '', type: 'action', action: 'indent', actionArgs: { indentRight: true }, context: 'insert' }, { keys: '', type: 'action', action: 'indent', actionArgs: { indentRight: false }, context: 'insert' }, // Text object motions - { keys: 'a', type: 'motion', motion: 'textObjectManipulation' }, - { keys: 'i', type: 'motion', motion: 'textObjectManipulation', motionArgs: { textObjectInner: true }}, + { keys: 'a', type: 'motion', motion: 'textObjectManipulation' }, + { keys: 'i', type: 'motion', motion: 'textObjectManipulation', motionArgs: { textObjectInner: true }}, // Search { keys: '/', type: 'search', searchArgs: { forward: true, querySrc: 'prompt', toJumplist: true }}, { keys: '?', type: 'search', searchArgs: { forward: false, querySrc: 'prompt', toJumplist: true }}, From 9f57e27ac6dba880d661ddda76959ba92bda5674 Mon Sep 17 00:00:00 2001 From: Opisek Date: Thu, 7 Dec 2023 12:54:06 +0100 Subject: [PATCH 11/14] shortened langmap parsing code (#145) --- src/vim.js | 67 ++++++++++++++++-------------------------------------- 1 file changed, 19 insertions(+), 48 deletions(-) diff --git a/src/vim.js b/src/vim.js index 1c32c6b..22a38be 100644 --- a/src/vim.js +++ b/src/vim.js @@ -1167,60 +1167,31 @@ export function initVim(CodeMirror) { characters. Example: "abc;ABC" */ - // Step 0: Shortcut for empty langmap - let toQwerty = {}; - let fromQwerty = {}; - if (langmapString === '') return { toQwerty: toQwerty, fromQwerty: fromQwerty, string: '' }; - - // Step 1: Separate into parts. - // Technically the regex /(? x.index); - const parts = separators.map((separatorIndex, arrayIndex) => - langmapString.substring( - arrayIndex === 0 ? 0 : (separators[arrayIndex - 1]) + 1, - separatorIndex, - ), - ); - - // Step 2: Parse each part - function getEscaped(list) { - const characters = []; - let escaped = false; - for (const character of list) { - if (character === '\\') { - escaped = !escaped; - if (escaped) continue; - } - characters.push(character); - } - return characters; - } + let keymap = {}; + if (langmapString == '') return { keymap: keymap, string: '' }; - for (const part of parts) { - const semicolon = [...part.matchAll(/(? x.index); - if (semicolon.length > 1) continue; // skip over malformed part - if (semicolon.length === 0) { - // List of pairs of "from" and "to" characters + function getEscaped(list) { + return list.split(/\\?(.)/).filter(Boolean); + } + langmapString.split(/((?:[^\\,]|\\.)+),/).map(part => { + if (!part) return; + const semicolon = part.split(/((?:[^\\;]|\\.)+);/); + if (semicolon.length == 3) { + const from = getEscaped(semicolon[1]); + const to = getEscaped(semicolon[2]); + if (from.length !== to.length) return; // skip over malformed part + for (let i = 0; i < from.length; ++i) keymap[from[i]] = to[i]; + } else if (semicolon.length == 1) { const pairs = getEscaped(part); - if (pairs.length % 2 !== 0) continue; // skip over malformed part - for (let i = 0; i < pairs.length; i += 2) toQwerty[pairs[i]] = pairs[i + 1]; - } else { - // List of "from" characters and list of "to" characters - const from = getEscaped(part.substring(0, semicolon[0])); - const to = getEscaped(part.substring((semicolon[0]) + 1)); - if (from.length !== to.length) continue; // skip over malformed part - for (let i = 0; i < from.length; ++i) toQwerty[from[i]] = to[i]; + if (pairs.length % 2 !== 0) return; // skip over malformed part + for (let i = 0; i < pairs.length; i += 2) keymap[pairs[i]] = pairs[i + 1]; } - } - - // Step 3: Reverse mapping - Object.fromEntries(Object.entries(toQwerty).map((x) => [x[1], x[0]])); + }); - return { toQwerty: toQwerty, fromQwerty: fromQwerty, string: langmapString }; + return { keymap: keymap, string: langmapString }; } function langmapRemapKey(key) { - return (key.length !== 1 || !(key in langmap.toQwerty)) ? key : langmap.toQwerty[key]; + return (key.length !== 1 || !(key in langmap.keymap)) ? key : langmap.keymap[key]; } // Represents the current input state. From ce415df5d0fdbb48b898a8c6bbb828dffed45fc1 Mon Sep 17 00:00:00 2001 From: Opisek Date: Mon, 18 Dec 2023 12:01:42 +0100 Subject: [PATCH 12/14] added ctrl remap logic (#145) --- src/vim.js | 23 +++++++++++++++-------- test/vim_test.js | 28 ++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/src/vim.js b/src/vim.js index 22a38be..7eea2eb 100644 --- a/src/vim.js +++ b/src/vim.js @@ -848,8 +848,8 @@ export function initVim(CodeMirror) { } } }, - langmap: function(langmapString) { - updateLangmap(langmapString); + langmap: function(langmapString, remapCtrl = true) { + updateLangmap(langmapString, remapCtrl); }, // TODO: Expose setOption and getOption as instance methods. Need to decide how to namespace // them, or somehow make them work with the existing CodeMirror setOption/getOption API. @@ -1148,15 +1148,15 @@ export function initVim(CodeMirror) { } // langmap support - function updateLangmap(langmapString) { + function updateLangmap(langmapString, remapCtrl = true) { if (langmap != null && langmap.string == langmapString) return; - langmap = parseLangmap(langmapString); + langmap = parseLangmap(langmapString, remapCtrl); } function langmapIsLiteralMode(vim) { // Determine if keystrokes should be interpreted literally return vim.insertMode; } - function parseLangmap(langmapString) { + function parseLangmap(langmapString, remapCtrl) { // From :help langmap /* The 'langmap' option is a list of parts, separated with commas. Each @@ -1168,7 +1168,7 @@ export function initVim(CodeMirror) { */ let keymap = {}; - if (langmapString == '') return { keymap: keymap, string: '' }; + if (langmapString == '') return { keymap: keymap, string: '', remapCtrl: remapCtrl }; function getEscaped(list) { return list.split(/\\?(.)/).filter(Boolean); @@ -1188,10 +1188,17 @@ export function initVim(CodeMirror) { } }); - return { keymap: keymap, string: langmapString }; + return { keymap: keymap, string: langmapString, remapCtrl: remapCtrl }; } function langmapRemapKey(key) { - return (key.length !== 1 || !(key in langmap.keymap)) ? key : langmap.keymap[key]; + console.log(`remapCtrl: ${langmap.remapCtrl}`);; + if (key.length == 1) { + return key in langmap.keymap ? langmap.keymap[key] : key; + } else if (langmap.remapCtrl && key.match(//)) { + return key[3] in langmap.keymap ? `` : key; + } else { + return key; + } } // Represents the current input state. diff --git a/test/vim_test.js b/test/vim_test.js index fd5b3ee..81ae11e 100644 --- a/test/vim_test.js +++ b/test/vim_test.js @@ -5633,6 +5633,34 @@ testVim('langmap_mark', function(cm, vim, helpers) { helpers.doKeys('-', '\''); helpers.assertCursorAt(2, 3); }); +// check that ctrl remapping works properly +testVim('langmap_visual_block', function(cm, vim, helpers) { + CodeMirror.Vim.langmap(dvorakLangmap); + + cm.setCursor(0, 1); + helpers.doKeys('', '2', 'h', 'n', 'n', 'n', 'j'); + helpers.doKeys('hello'); + eq('1hello\n5hello\nahellofg', cm.getValue()); + helpers.doKeys(''); + cm.setCursor(2, 3); + helpers.doKeys('', '2', 't', 'd', 'J'); + helpers.doKeys('world'); + eq('1hworld\n5hworld\nahworld', cm.getValue()); +}, {value: '1234\n5678\nabcdefg'}); +// check that ctrl remapping can be disabled +testVim('langmap_visual_block_no_ctrl_remap', function(cm, vim, helpers) { + CodeMirror.Vim.langmap(dvorakLangmap, false); + + cm.setCursor(0, 1); + helpers.doKeys('', '2', 'h', 'n', 'n', 'n', 'j'); + helpers.doKeys('hello'); + eq('1hello\n5hello\nahellofg', cm.getValue()); + helpers.doKeys(''); + cm.setCursor(2, 3); + helpers.doKeys('', '2', 't', 'd', 'J'); + helpers.doKeys('world'); + eq('1hworld\n5hworld\nahworld', cm.getValue()); +}, {value: '1234\n5678\nabcdefg'}); async function delay(t) { From 9fdd8b828a14408d23020887cd1cec36a2f397ed Mon Sep 17 00:00:00 2001 From: Opisek Date: Mon, 18 Dec 2023 12:22:43 +0100 Subject: [PATCH 13/14] new remapCtrl value must be updated (#145) --- src/vim.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/vim.js b/src/vim.js index 7eea2eb..5b4e24f 100644 --- a/src/vim.js +++ b/src/vim.js @@ -1149,7 +1149,10 @@ export function initVim(CodeMirror) { // langmap support function updateLangmap(langmapString, remapCtrl = true) { - if (langmap != null && langmap.string == langmapString) return; + if (langmap != null && langmap.string == langmapString) { + langmap.remapCtrl = remapCtrl; + return; + } langmap = parseLangmap(langmapString, remapCtrl); } function langmapIsLiteralMode(vim) { From fb951a7cecbde8bd21388bd73324cbf5c8767c56 Mon Sep 17 00:00:00 2001 From: Opisek Date: Mon, 18 Dec 2023 12:34:46 +0100 Subject: [PATCH 14/14] moved remapping logic to index.ts via exposed remap function (#145) --- src/index.ts | 9 ++++++--- src/vim.js | 14 ++++---------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/index.ts b/src/index.ts index 830537c..d652311 100644 --- a/src/index.ts +++ b/src/index.ts @@ -197,12 +197,15 @@ const vimPlugin = ViewPlugin.fromClass( decorations = Decoration.none; waitForCopy = false; handleKey(e: KeyboardEvent, view: EditorView) { - const key = CodeMirror.vimKey(e); - const cm = this.cm; - if (!key) return; + const rawKey = CodeMirror.vimKey(e); + if (!rawKey) return; + const cm = this.cm; let vim = cm.state.vim; if (!vim) return; + + const key = vim.expectLiteralNext ? rawKey : Vim.langmapRemapKey(rawKey); + // clear search highlight if ( key == "" && diff --git a/src/vim.js b/src/vim.js index 7eea2eb..390986f 100644 --- a/src/vim.js +++ b/src/vim.js @@ -851,6 +851,9 @@ export function initVim(CodeMirror) { langmap: function(langmapString, remapCtrl = true) { updateLangmap(langmapString, remapCtrl); }, + langmapRemapKey: function(key) { + return langmapRemapKey(key); + }, // TODO: Expose setOption and getOption as instance methods. Need to decide how to namespace // them, or somehow make them work with the existing CodeMirror setOption/getOption API. setOption: setOption, @@ -883,11 +886,9 @@ export function initVim(CodeMirror) { * execute the bound command if a a key is matched. The function always * returns true. */ - findKey: function(cm, rawKey, origin) { + findKey: function(cm, key, origin) { var vim = maybeInitVimState(cm); - let key = vim.expectLiteralNext ? rawKey : langmapRemapKey(rawKey); - function handleMacroRecording() { var macroModeState = vimGlobalState.macroModeState; if (macroModeState.isRecording) { @@ -2684,10 +2685,6 @@ export function initVim(CodeMirror) { cm.scrollTo(null, y); }, replayMacro: function(cm, actionArgs, vim) { - // when replaying a macro, we must not "double remap" characters - const savedLangMap = langmap; - langmap = parseLangmap(''); - var registerName = actionArgs.selectedCharacter; var repeat = actionArgs.repeat; var macroModeState = vimGlobalState.macroModeState; @@ -2699,9 +2696,6 @@ export function initVim(CodeMirror) { while(repeat--){ executeMacroRegister(cm, vim, macroModeState, registerName); } - - // restore langmap - langmap = savedLangMap; }, enterMacroRecordMode: function(cm, actionArgs) { var macroModeState = vimGlobalState.macroModeState;