Skip to content

Commit fdbc04a

Browse files
authored
[vim bindings] Support tag text objects in xml / htmlmixed mode
User can use `t` to operate on tag text objects. For example, given the following html: ``` <div> <span>hello world!</span> </div> ``` If the user's cursor (denoted by █) is inside "hello world!": ``` <div> <span>hello█world!</span> </div> ``` And they enter `dit` (delete inner tag), then the text inside the enclosing tag is deleted -- the following is the expected result: ``` <div> <span></span> </div> ``` If they enter `dat` (delete around tag), then the surrounding tags are deleted as well: ``` <div> </div> ``` 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: ``` <!-- <div>broken</div> --> ``` - Does not work when tags have different cases: ``` <div>broken</DIV> ``` - Does not work when inside a broken tag: ``` <div><brok><en></div> ``` This addresses codemirror#3828.
1 parent 772d09e commit fdbc04a

3 files changed

Lines changed: 75 additions & 3 deletions

File tree

keymap/vim.js

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2069,6 +2069,8 @@
20692069
if (operatorArgs) { operatorArgs.linewise = true; }
20702070
tmp.end.line--;
20712071
}
2072+
} else if (character === 't') {
2073+
tmp = expandTagUnderCursor(cm, head, inclusive);
20722074
} else {
20732075
// No text object defined for this, don't move.
20742076
return null;
@@ -3295,6 +3297,49 @@
32953297
return { start: Pos(cur.line, start), end: Pos(cur.line, end) };
32963298
}
32973299

3300+
/**
3301+
* Depends on the following:
3302+
*
3303+
* - editor mode should be htmlmixedmode / xml
3304+
* - mode/xml/xml.js should be loaded
3305+
* - addon/fold/xml-fold.js should be loaded
3306+
*
3307+
* If any of the above requirements are not true, this function noops.
3308+
*
3309+
* This is _NOT_ a 100% accurate implementation of vim tag text objects.
3310+
* The following caveats apply (based off cursory testing, I'm sure there
3311+
* are other discrepancies):
3312+
*
3313+
* - Does not work inside comments:
3314+
* ```
3315+
* <!-- <div>broken</div> -->
3316+
* ```
3317+
* - Does not work when tags have different cases:
3318+
* ```
3319+
* <div>broken</DIV>
3320+
* ```
3321+
* - Does not work when cursor is inside a broken tag:
3322+
* ```
3323+
* <div><brok><en></div>
3324+
* ```
3325+
*/
3326+
function expandTagUnderCursor(cm, head, inclusive) {
3327+
var cur = head;
3328+
if (!CodeMirror.findMatchingTag || !CodeMirror.findEnclosingTag) {
3329+
return { start: cur, end: cur };
3330+
}
3331+
3332+
var tags = CodeMirror.findMatchingTag(cm, head) || CodeMirror.findEnclosingTag(cm, head);
3333+
if (!tags || !tags.open || !tags.close) {
3334+
return { start: cur, end: cur };
3335+
}
3336+
3337+
if (inclusive) {
3338+
return { start: tags.open.from, end: tags.close.to };
3339+
}
3340+
return { start: tags.open.to, end: tags.close.from };
3341+
}
3342+
32983343
function recordJumpPosition(cm, oldCur, newCur) {
32993344
if (!cursorEqual(oldCur, newCur)) {
33003345
vimGlobalState.jumpList.add(cm, oldCur, newCur);
@@ -3836,7 +3881,7 @@
38363881
return Pos(curr_index.ln, curr_index.pos);
38373882
}
38383883

3839-
// TODO: perhaps this finagling of start and end positions belonds
3884+
// TODO: perhaps this finagling of start and end positions belongs
38403885
// in codemirror/replaceRange?
38413886
function selectCompanionObject(cm, head, symb, inclusive) {
38423887
var cur = head, start, end;

test/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ <h2>Test Suite</h2>
156156
<script src="../addon/mode/multiplex_test.js"></script>
157157
<script src="../addon/fold/foldcode.js"></script>
158158
<script src="../addon/fold/brace-fold.js"></script>
159+
<script src="../addon/fold/xml-fold.js"></script>
159160

160161
<script src="emacs_test.js"></script>
161162
<script src="sql-hint-test.js"></script>

test/vim_test.js

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1317,9 +1317,13 @@ testVim('=', function(cm, vim, helpers) {
13171317
eq(expectedValue, cm.getValue());
13181318
}, { value: ' word1\n word2\n word3', indentUnit: 2 });
13191319

1320-
// Edit tests
1321-
function testEdit(name, before, pos, edit, after) {
1320+
// Edit tests - configureCm is an optional argument that gives caller
1321+
// access to the cm object.
1322+
function testEdit(name, before, pos, edit, after, configureCm) {
13221323
return testVim(name, function(cm, vim, helpers) {
1324+
if (configureCm) {
1325+
configureCm(cm);
1326+
}
13231327
var ch = before.search(pos)
13241328
var line = before.substring(0, ch).split('\n').length - 1;
13251329
if (line) {
@@ -1424,6 +1428,28 @@ testEdit('di>_middle_spc', 'a\t<\n\tbar\n>b', /r/, 'di>', 'a\t<>b');
14241428
testEdit('da<_middle_spc', 'a\t<\n\tbar\n>b', /r/, 'da<', 'a\tb');
14251429
testEdit('da>_middle_spc', 'a\t<\n\tbar\n>b', /r/, 'da>', 'a\tb');
14261430

1431+
// deleting tag objects
1432+
testEdit('dat_noop', '<outer><inner>hello</inner></outer>', /n/, 'dat', '<outer><inner>hello</inner></outer>');
1433+
testEdit('dat_open_tag', '<outer><inner>hello</inner></outer>', /n/, 'dat', '<outer></outer>', function(cm) {
1434+
cm.setOption('mode', 'xml');
1435+
});
1436+
testEdit('dat_inside_tag', '<outer><inner>hello</inner></outer>', /l/, 'dat', '<outer></outer>', function(cm) {
1437+
cm.setOption('mode', 'xml');
1438+
});
1439+
testEdit('dat_close_tag', '<outer><inner>hello</inner></outer>', /\//, 'dat', '<outer></outer>', function(cm) {
1440+
cm.setOption('mode', 'xml');
1441+
});
1442+
1443+
testEdit('dit_open_tag', '<outer><inner>hello</inner></outer>', /n/, 'dit', '<outer><inner></inner></outer>', function(cm) {
1444+
cm.setOption('mode', 'xml');
1445+
});
1446+
testEdit('dit_inside_tag', '<outer><inner>hello</inner></outer>', /l/, 'dit', '<outer><inner></inner></outer>', function(cm) {
1447+
cm.setOption('mode', 'xml');
1448+
});
1449+
testEdit('dit_close_tag', '<outer><inner>hello</inner></outer>', /\//, 'dit', '<outer><inner></inner></outer>', function(cm) {
1450+
cm.setOption('mode', 'xml');
1451+
});
1452+
14271453
function testSelection(name, before, pos, keys, sel) {
14281454
return testVim(name, function(cm, vim, helpers) {
14291455
var ch = before.search(pos)

0 commit comments

Comments
 (0)