Skip to content

Commit

Permalink
Support tag text objects in xml / htmlmixed mode.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
howardjing committed Jul 23, 2020
1 parent 772d09e commit a0a533e
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 3 deletions.
47 changes: 46 additions & 1 deletion keymap/vim.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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:
* ```
* <!-- <div>broken</div> -->
* ```
* - Does not work when tags have different cases:
* ```
* <div>broken</DIV>
* ```
* - Does not work when cursor is inside a broken tag:
* ```
* <div><brok><en></div>
* ```
*/
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);
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions test/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ <h2>Test Suite</h2>
<script src="../addon/mode/multiplex_test.js"></script>
<script src="../addon/fold/foldcode.js"></script>
<script src="../addon/fold/brace-fold.js"></script>
<script src="../addon/fold/xml-fold.js"></script>

<script src="emacs_test.js"></script>
<script src="sql-hint-test.js"></script>
Expand Down
30 changes: 28 additions & 2 deletions test/vim_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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', '<outer><inner>hello</inner></outer>', /n/, 'dat', '<outer><inner>hello</inner></outer>');
testEdit('dat_open_tag', '<outer><inner>hello</inner></outer>', /n/, 'dat', '<outer></outer>', function(cm) {
cm.setOption('mode', 'xml');
});
testEdit('dat_inside_tag', '<outer><inner>hello</inner></outer>', /l/, 'dat', '<outer></outer>', function(cm) {
cm.setOption('mode', 'xml');
});
testEdit('dat_close_tag', '<outer><inner>hello</inner></outer>', /\//, 'dat', '<outer></outer>', function(cm) {
cm.setOption('mode', 'xml');
});

testEdit('dit_open_tag', '<outer><inner>hello</inner></outer>', /n/, 'dit', '<outer><inner></inner></outer>', function(cm) {
cm.setOption('mode', 'xml');
});
testEdit('dit_inside_tag', '<outer><inner>hello</inner></outer>', /l/, 'dit', '<outer><inner></inner></outer>', function(cm) {
cm.setOption('mode', 'xml');
});
testEdit('dit_close_tag', '<outer><inner>hello</inner></outer>', /\//, 'dit', '<outer><inner></inner></outer>', 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)
Expand Down

0 comments on commit a0a533e

Please sign in to comment.