From fdbc04a94a3b0064b896effa6da6544f1c2bb39a Mon Sep 17 00:00:00 2001 From: Howard Date: Thu, 23 Jul 2020 14:45:56 -0400 Subject: [PATCH] [vim bindings] Support tag text objects in xml / htmlmixed mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User can use `t` to operate on tag text objects. For example, given the following html: ```
hello world!
``` If the user's cursor (denoted by █) is inside "hello world!": ```
hello█world!
``` And they enter `dit` (delete inner tag), then the text inside the enclosing tag is deleted -- the following is the expected result: ```
``` If they enter `dat` (delete around tag), then the surrounding tags are deleted as well: ```
``` This logic depends on the following: - mode/xml/xml.js - addon/fold/xml-fold.js - editor is in htmlmixedmode / xml mode Caveats This is _NOT_ a 100% accurate implementation of vim tag text objects. For example, the following cases noop / are inconsistent with vim behavior: - Does not work inside comments: ``` ``` - Does not work when tags have different cases: ```
broken
``` - Does not work when inside a broken tag: ```
``` This addresses #3828. --- keymap/vim.js | 47 ++++++++++++++++++++++++++++++++++++++++++++++- test/index.html | 1 + test/vim_test.js | 30 ++++++++++++++++++++++++++++-- 3 files changed, 75 insertions(+), 3 deletions(-) diff --git a/keymap/vim.js b/keymap/vim.js index bca6d46d72..5a4860c65a 100644 --- a/keymap/vim.js +++ b/keymap/vim.js @@ -2069,6 +2069,8 @@ if (operatorArgs) { operatorArgs.linewise = true; } tmp.end.line--; } + } else if (character === 't') { + tmp = expandTagUnderCursor(cm, head, inclusive); } else { // No text object defined for this, don't move. return null; @@ -3295,6 +3297,49 @@ return { start: Pos(cur.line, start), end: Pos(cur.line, end) }; } + /** + * Depends on the following: + * + * - editor mode should be htmlmixedmode / xml + * - mode/xml/xml.js should be loaded + * - addon/fold/xml-fold.js should be loaded + * + * If any of the above requirements are not true, this function noops. + * + * This is _NOT_ a 100% accurate implementation of vim tag text objects. + * The following caveats apply (based off cursory testing, I'm sure there + * are other discrepancies): + * + * - Does not work inside comments: + * ``` + * + * ``` + * - Does not work when tags have different cases: + * ``` + *
broken
+ * ``` + * - Does not work when cursor is inside a broken tag: + * ``` + *
+ * ``` + */ + function expandTagUnderCursor(cm, head, inclusive) { + var cur = head; + if (!CodeMirror.findMatchingTag || !CodeMirror.findEnclosingTag) { + return { start: cur, end: cur }; + } + + var tags = CodeMirror.findMatchingTag(cm, head) || CodeMirror.findEnclosingTag(cm, head); + if (!tags || !tags.open || !tags.close) { + return { start: cur, end: cur }; + } + + if (inclusive) { + return { start: tags.open.from, end: tags.close.to }; + } + return { start: tags.open.to, end: tags.close.from }; + } + function recordJumpPosition(cm, oldCur, newCur) { if (!cursorEqual(oldCur, newCur)) { vimGlobalState.jumpList.add(cm, oldCur, newCur); @@ -3836,7 +3881,7 @@ return Pos(curr_index.ln, curr_index.pos); } - // TODO: perhaps this finagling of start and end positions belonds + // TODO: perhaps this finagling of start and end positions belongs // in codemirror/replaceRange? function selectCompanionObject(cm, head, symb, inclusive) { var cur = head, start, end; diff --git a/test/index.html b/test/index.html index b68ce18964..6566fc436e 100644 --- a/test/index.html +++ b/test/index.html @@ -156,6 +156,7 @@

Test Suite

+ diff --git a/test/vim_test.js b/test/vim_test.js index 71284bdf59..57d276e871 100644 --- a/test/vim_test.js +++ b/test/vim_test.js @@ -1317,9 +1317,13 @@ testVim('=', function(cm, vim, helpers) { eq(expectedValue, cm.getValue()); }, { value: ' word1\n word2\n word3', indentUnit: 2 }); -// Edit tests -function testEdit(name, before, pos, edit, after) { +// Edit tests - configureCm is an optional argument that gives caller +// access to the cm object. +function testEdit(name, before, pos, edit, after, configureCm) { return testVim(name, function(cm, vim, helpers) { + if (configureCm) { + configureCm(cm); + } var ch = before.search(pos) var line = before.substring(0, ch).split('\n').length - 1; if (line) { @@ -1424,6 +1428,28 @@ testEdit('di>_middle_spc', 'a\t<\n\tbar\n>b', /r/, 'di>', 'a\t<>b'); testEdit('da<_middle_spc', 'a\t<\n\tbar\n>b', /r/, 'da<', 'a\tb'); testEdit('da>_middle_spc', 'a\t<\n\tbar\n>b', /r/, 'da>', 'a\tb'); +// deleting tag objects +testEdit('dat_noop', 'hello', /n/, 'dat', 'hello'); +testEdit('dat_open_tag', 'hello', /n/, 'dat', '', function(cm) { + cm.setOption('mode', 'xml'); +}); +testEdit('dat_inside_tag', 'hello', /l/, 'dat', '', function(cm) { + cm.setOption('mode', 'xml'); +}); +testEdit('dat_close_tag', 'hello', /\//, 'dat', '', function(cm) { + cm.setOption('mode', 'xml'); +}); + +testEdit('dit_open_tag', 'hello', /n/, 'dit', '', function(cm) { + cm.setOption('mode', 'xml'); +}); +testEdit('dit_inside_tag', 'hello', /l/, 'dit', '', function(cm) { + cm.setOption('mode', 'xml'); +}); +testEdit('dit_close_tag', 'hello', /\//, 'dit', '', function(cm) { + cm.setOption('mode', 'xml'); +}); + function testSelection(name, before, pos, keys, sel) { return testVim(name, function(cm, vim, helpers) { var ch = before.search(pos)