Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

[vim keymap] Visual mode rewrite and other changes #1262

Closed
wants to merge 6 commits into from

5 participants

@metatheos

My second batch of changes, containing the remaining additions from my local setup. Should also fix some problems discussed in Issue #1246.

Changes

  1. New moveByLinesToFirstNonWhiteSpaceCharacter, used to implement the motions + (move {repeat} lines down, then to the first non-whitespace-character on that line), - (same for up) and _ (same as + but with repeat reduced by 1, so with a repeat of 1 it behaves like ^).
  2. Some commands require the user to input a character to operate on. Previously pressing any handled special key (like Enter, Space or Down) would pass it on as string (see Issue #1246). These keys are now converted to a printable character if possible and rejected otherwise.
  3. If there is text selected, r{character} replaces all individual characters in the selection by {character}, while leaving linebreaks intact. Special case as by vim help is \n as {character}, which creates only one linebreak for an r operation.
  4. Visual mode is significantly changed, using some of the properties of CodeMirror v3. Notes below.

DIY-Example: Inputting ggOTitle<Esc>yypVr= (with <Esc> being the escape-key) should now behave just as it does in vim.

Notes on moveByLinesToFirstNonWhiteSpaceCharacter

I'm on the fence about whether to keep moveByLinesToFirstNonWhiteSpaceCharacter or merge it with moveToFirstNonWhiteSpaceCharacter. Merging means there would be one less motion, but instead one with an if at the start picking between the two code paths. I also find the name to be obscenely long. @mightyguava what would you prefer here?

Notes on visual mode (selection)

Vim's cursor is not between characters but on one. This also has implications on the selection model. For the beginning of the selection the left side of the cursor is used, while for the end it is the right side. This requires sel.to to be incremented by 1 from the position CodeMirror's setSelection would place it at. Even more of a special case is linewise selection. Here sel.from is moved to the first character of the line, while sel.to is moved behind the last one.

CodeMirror as it is doesn't seem to require sel.from and sel.to to be either sel.anchor or sel.head. I'm not sure if I should expect this special case to be stable and especially whether I really want you to maintain this rather exotic use-case. I am replicating setSelection's behavior for now in an own function (vim.js:1511) and adapt the values at the end. From my testing it seemed to work out quite well. Obviously it cannot deal with folding yet, since I cannot access skipAtomic from my code (a hacky workaround would be calling setCursor(curEnd) or something similar before the custom operation). The only other problem I ran into is how CodeMirror restores selection on undo, which is a separate issue I'll look into as well. Also of course replicating internal code is really bad, I'll try to think of a way that doesn't need much change to the API but still allows me to get the core-code out of my setSelection.

I made one change to codemirror.css. The reason is that setting showCursorWhenSelecting doesn't help if the cursor is covered by the selection (selection has z-index set to 1).

@metatheos metatheos [vim keymap] better visual mode, r, new +/-/_
- visual mode now behaves more vim-like
- handling special keys better for commands requiring a character
- r can operate on selection
- +/-/_ via new moveByLinesToFirstNonWhiteSpaceCharacter motion
7bfb17e
@marijnh
Owner

Great!

@mightyguava I'd like you to glance over this again (if you don't have time for these reviews, let me know, I'll stop notifying you.)

@mightyguava

This should move to firstLine/lastLine instead of not moving? This appears to apply to moveByLines too...

Yeah, it should, and also it should report an error. This error-behavior is actually tested already (dj_end_of_document), since you're not supposed to dj out of the document. This applies to + as well.

@mightyguava

This is here in case the user clicks the document in order to clear the selection. I don't think it should be removed.

I can leave it in, until I have some better handling for mouse-interaction in general. With 7fcfb6d it should work again.

@mightyguava

It would be nice if you could merge the visual mode and normal mode logic here a bit more. Don't feel too strongly, just don't like the duplicated logic.

I can apply the new replacement logic to both cases. Does b74d757 look better?

@mightyguava
Owner

Sorry for the delay. I was away for the long weekend. The new visual mode looks vastly superior to the previous version. Thanks!

If you want to merge moveByLinesToFirstNonWhiteSpaceCharacter, I suggest merging with moveByLines instead, and add an additional argument for which character the cursor lands on the line. It would also make a bit more structural sense and probably less duplication.

Regarding the custom selection function. I strongly agree that that changing CodeMirror's internal state here is bad. A rule I followed implementing vim.js was to not change CodeMirror's internal state in any way. This made some parts of the logic a bit convoluted, but let vim.js be much more future proof. As you said, a better solution would be to refactor CodeMirror's selection API a bit to make it work for the vim use case. Maybe changing setSelection to accept a sel object with anchor, head, from, and to? Defer to @marijnh here.

@metatheos

If you want to merge moveByLinesToFirstNonWhiteSpaceCharacter, I suggest merging with moveByLines instead

Now I feel stupid. I'll do just that. Any idea how I could improve the handling of 0-based repeat on _, while I'm at it? I don't like how I introduce motionArgs.repeatOffset just for that.

Also: would you mind if I rename textObjectManipulation to textObjectRange and invert its inclusive-parameter, renaming it to inner? I found these names rather confusing as they are.

As for the detection for mouse interaction, I've restored that for now, but I hope I can take a stab at solving #1084 the way @marijnh suggests soon. This might result in some tools to handle various mouse-related issues and also help with solving #1038.

@mightyguava
Owner

Now I feel stupid. I'll do just that. Any idea how I could improve the handling of 0-based repeat on _, while I'm at it? I don't like how I introduce motionArgs.repeatOffset just for that.

Sorry, don't have a better idea. I find _ weird by itself, and don't see how you could not special case it.

Also: would you mind if I rename textObjectManipulation to textObjectRange and invert its inclusive-parameter, renaming it to inner? I found these names rather confusing as they are.

Sure. The text object manipulation motion is a bad hack I threw in to carry over the functionality from the original vim.js implementation. Any changes to clean it up is fine by me.

As for the detection for mouse interaction, I've restored that for now, but I hope I can take a stab at solving #1084 the way @marijnh suggests soon. This might result in some tools to handle various mouse-related issues and also help with solving #1038.

Sounds great. I didn't do it earlier because the fix would be v3 specific and somewhat involved. At this point, keeping vim.js v2 compatible is moot.

@metatheos

Any changes to clean it up is fine by me.

I just looked through it while trying to improve cw, which led me to looking into aw and iw. At first "Manipulation" confused me a bit, I think "Limits" gives a better idea what it is there for. Also inclusive has a special meaning in vim that is -- as far as I can tell -- unrelated to the use there, so inner might be more descriptive. I had a patch to improve aw and iw but just noticed that it still needs some tweaks to behavior around the end of lines. The actual fix for cw seems to be even more tricky.

I merged the two motions, and added two small fixes in another patch. One was an oversight by me (gj and gk are not considered linewise in vim) and the other one was t walking back if there was no such character on the line (because it could not tell whether it got an actual coordinate or an error-value).

Since you mention v2, how relevant is support for the version? I'm not too familiar with how CodeMirror is used by others so I hope I'm not ignorantly trampling on anything here.

@marijnh
Owner

I don't have a clear grasp of what the selection problem you're having is like, exactly. Just an idea -- would a beforeSelectionChange event, similar to the beforeChange event added this week, which allows you to check and adjust selection changes as they are made, be helpful for you? If not, please explain the nature of the problem or point me to a previous explanation.

@marijnh
Owner

Should I merge the patches here as they stand, by the way, or is there going to be more adjustment?

@metatheos

The idea of beforeSelectionChange sounds really interesting, maybe that would work even better than an extended api for setSelection().

I don't have a clear grasp of what the selection problem you're having is like, exactly.

As I've written in the notes on visual mode, there are a few differences between vim-selection and the default selection handling. The short version: it covers at least one additional character at the end, and sometimes more in both directions (moving sel.from and sel.to without changing sel.anchor or sel.head).

You can also try the vim-demo if you prefer that, press v to activate visual mode and move around using hjkl/arrow keys to select text. You'll notice that in the current version on the homepage the selection does not include the character under the cursor, while in my version the selection expands under the cursor as well. One notable special situation is that on pressing v, initially sel.to is set to sel.head+1, while sel.anchor and sel.head are still in the same position, which would make setSelection() return early (in the homepage-version there is some flipping of head and anchor to avoid that and I could work around that by moving the cursor before setting selection). Another thing to try is V for linewise visual mode, which now allows to move the cursor freely within the selected lines (so sel.from and sel.to are only moved whenever sel.head changes lines).

While what I do works whenever I am actively setting selection, a few areas are out of reach this way (namely click/drag and undo). The vim-keymap does not register for any events at this point, but I'm planning to try your suggestion from #1084 soon. What I like about beforeSelectionChange is that it might allow me to cover both cases with common code. Do you think it should be safe to modify sel.from and sel.to in general and especially during beforeSelectionChange? In that case I would call the normal setSelection() (which I now emulate instead) and do my final touches in the event handler.

As for the merge it depends on whether @mightyguava likes the current state and the way I implemented his suggestions. Especially he might not want the workaround of reimplementing setSelection() to stay in, which I would understand.

@schanzer

+1 vote for something like beforeSelectionChange - that definitely has uses on my end as well.

@bfrohs

:+1: for beforeSelectionChange -- Would be useful in my project.

@mightyguava
Owner

If it would be safe to modify sel.from and sel.to in a beforeSelectionChange event, without changing sel.anchor and sel.head, then I agree it could make for a great solution.

As for the merge it depends on whether @mightyguava likes the current state and the way I implemented his suggestions. Especially he might not want the workaround of reimplementing setSelection() to stay in, which I would understand.

Ah didn't know you were waiting on me for this. If @marijnh will be adding beforeSelectionChange, then I would rather the code to emulate setSelection to not be merged. As I said before my concern is forward compatibility with possible future changes to CodeMirror's selection model (for example, multi selections #778). I don't mean to make it hard on you but I'd rather not chance breaking visual mode completely down the road. Thanks for bearing with me.

@marijnh
Owner

If it would be safe to modify sel.from and sel.to in a beforeSelectionChange event, without changing sel.anchor and sel.head, then I agree it could make for a great solution.

There's an invariant that from/to always correspond to anchor/head (from to the lesser one, to to the greater). This invariant is not going away.

@marijnh marijnh referenced this pull request from a commit
@marijnh marijnh Add beforeSelectionChange event
Issue #1262
a65e98f
@metatheos

There's an invariant that from/to always correspond to anchor/head (from to the lesser one, to to the greater). This invariant is not going away.

Ok, that kicks my approach out of the window. I understand that you don't want to risk stability issues you'd have to deal with though, I'll see if I can get my changes to visual mode out again so the remaining ones can be merged.

@metatheos

@mightyguava are you ok with how the patch looks right now? In that case I'd suggest a merge as

[vim keymap] better r, t, new +/-/_
- handling special keys better for commands requiring a character
- r can operate on selection
- fixed: t moves back if character not found
- +/-/_ via extension of moveByLines motion

Also I'd like you to take a look at d776f74 if you're interested. There I played around with making the selection right but the (now hidden) cursor wrong.

@marijnh
Owner

I've merged the changes as they stand.

(I somehow missed the horrible things you were doing in your setSelection replacement before. Such an approach is not acceptable. All selection changes are supposed to go through CodeMirror's internal setSelection, and poking at the selection from outside is bound to create all kinds of inconsistencies.)

@marijnh marijnh closed this
@metatheos

You're right, the original commit would have better just been an use-case. I should have opened a normal Issue first to ask whether sel.from/sel.to values not being a permutation of sel.anchor/sel.head is something CodeMirror is designed to handle. If it was, I still could have made a pull-request with proper api-access to the values. Sorry about that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Feb 18, 2013
  1. @metatheos

    [vim keymap] better visual mode, r, new +/-/_

    metatheos authored
    - visual mode now behaves more vim-like
    - handling special keys better for commands requiring a character
    - r can operate on selection
    - +/-/_ via new moveByLinesToFirstNonWhiteSpaceCharacter motion
Commits on Feb 19, 2013
  1. @metatheos
  2. @metatheos
Commits on Feb 20, 2013
  1. @metatheos
  2. @metatheos
Commits on Feb 21, 2013
  1. @metatheos
This page is out of date. Refresh to see the latest.
Showing with 73 additions and 21 deletions.
  1. +64 −19 keymap/vim.js
  2. +1 −0  lib/codemirror.css
  3. +8 −2 test/vim_test.js
View
83 keymap/vim.js
@@ -6,7 +6,7 @@
* gj, gk
* e, E, w, W, b, B, ge, gE
* f<character>, F<character>, t<character>, T<character>
- * $, ^, 0
+ * $, ^, 0, -, +, _
* gg, G
* %
* '<character>, `<character>
@@ -93,10 +93,10 @@
motionArgs: { forward: false, linewise: true }},
{ keys: ['g','j'], type: 'motion',
motion: 'moveByDisplayLines',
- motionArgs: { forward: true, linewise: true }},
+ motionArgs: { forward: true }},
{ keys: ['g','k'], type: 'motion',
motion: 'moveByDisplayLines',
- motionArgs: { forward: false, linewise: true }},
+ motionArgs: { forward: false }},
{ keys: ['w'], type: 'motion',
motion: 'moveByWords',
motionArgs: { forward: true, wordEnd: false }},
@@ -140,6 +140,15 @@
{ keys: ['0'], type: 'motion', motion: 'moveToStartOfLine' },
{ keys: ['^'], type: 'motion',
motion: 'moveToFirstNonWhiteSpaceCharacter' },
+ { keys: ['+'], type: 'motion',
+ motion: 'moveByLines',
+ motionArgs: { forward: true, toFirstChar:true }},
+ { keys: ['-'], type: 'motion',
+ motion: 'moveByLines',
+ motionArgs: { forward: false, toFirstChar:true }},
+ { keys: ['_'], type: 'motion',
+ motion: 'moveByLines',
+ motionArgs: { forward: true, toFirstChar:true, repeatOffset:-1 }},
{ keys: ['$'], type: 'motion',
motion: 'moveToEol',
motionArgs: { inclusive: true }},
@@ -597,6 +606,18 @@
// Matches whole comand. Return the command.
if (command.keys[keys.length - 1] == 'character') {
inputState.selectedCharacter = keys[keys.length - 1];
+ if(inputState.selectedCharacter.length>1){
+ switch(inputState.selectedCharacter){
+ case "Enter":
+ inputState.selectedCharacter='\n';
+ break;
+ case "Space":
+ inputState.selectedCharacter=' ';
+ break;
+ default:
+ continue;
+ }
+ }
}
inputState.keyBuffer = [];
return command;
@@ -947,11 +968,15 @@
default:
vim.lastHPos = endCh;
}
- var repeat = motionArgs.repeat;
+ var repeat = motionArgs.repeat+(motionArgs.repeatOffset||0);
var line = motionArgs.forward ? cur.line + repeat : cur.line - repeat;
if (line < cm.firstLine() || line > cm.lastLine() ) {
return null;
}
+ if(motionArgs.toFirstChar){
+ endCh=findFirstNonWhiteSpaceCharacter(cm.getLine(line));
+ vim.lastHPos = endCh;
+ }
vim.lastHSPos = cm.charCoords({line:line, ch:endCh},"div").left;
return { line: line, ch: endCh };
},
@@ -1007,6 +1032,7 @@
var repeat = motionArgs.repeat;
var curEnd = moveToCharacter(cm, repeat, motionArgs.forward,
motionArgs.selectedCharacter);
+ if(!curEnd)return cm.getCursor();
var increment = motionArgs.forward ? -1 : 1;
curEnd.ch += increment;
return curEnd;
@@ -1014,7 +1040,7 @@
moveToCharacter: function(cm, motionArgs) {
var repeat = motionArgs.repeat;
return moveToCharacter(cm, repeat, motionArgs.forward,
- motionArgs.selectedCharacter);
+ motionArgs.selectedCharacter) || cm.getCursor();
},
moveToColumn: function(cm, motionArgs, vim) {
var repeat = motionArgs.repeat;
@@ -1036,7 +1062,6 @@
// Go to the start of the line where the text begins, or the end for
// whitespace-only lines
var cursor = cm.getCursor();
- var line = cm.getLine(cursor.line);
return { line: cursor.line,
ch: findFirstNonWhiteSpaceCharacter(cm.getLine(cursor.line)) };
},
@@ -1341,21 +1366,41 @@
var markName = actionArgs.selectedCharacter;
updateMark(cm, vim, markName, cm.getCursor());
},
- replace: function(cm, actionArgs) {
+ replace: function(cm, actionArgs, vim) {
var replaceWith = actionArgs.selectedCharacter;
var curStart = cm.getCursor();
- var line = cm.getLine(curStart.line);
- var replaceTo = curStart.ch + actionArgs.repeat;
- if (replaceTo > line.length) {
- return;
- }
- var curEnd = { line: curStart.line, ch: replaceTo };
- var replaceWithStr = '';
- for (var i = 0; i < curEnd.ch - curStart.ch; i++) {
- replaceWithStr += replaceWith;
+ var replaceTo;
+ var curEnd;
+ if(vim.visualMode){
+ curStart=cm.getCursor('start');
+ curEnd=cm.getCursor('end');
+ // workaround to catch the character under the cursor
+ // existing workaround doesn't cover actions
+ curEnd=cm.clipPos({line: curEnd.line, ch: curEnd.ch+1});
+ }else{
+ var line = cm.getLine(curStart.line);
+ replaceTo = curStart.ch + actionArgs.repeat;
+ if (replaceTo > line.length) {
+ replaceTo=line.length;
+ }
+ curEnd = { line: curStart.line, ch: replaceTo };
+ }
+ if(replaceWith=='\n'){
+ if(!vim.visualMode) cm.replaceRange('', curStart, curEnd);
+ // special case, where vim help says to replace by just one line-break
+ (CodeMirror.commands.newlineAndIndentContinueComment || CodeMirror.commands.newlineAndIndent)(cm);
+ }else {
+ var replaceWithStr=cm.getRange(curStart, curEnd);
+ //replace all characters in range by selected, but keep linebreaks
+ replaceWithStr=replaceWithStr.replace(/[^\n]/g,replaceWith);
+ cm.replaceRange(replaceWithStr, curStart, curEnd);
+ if(vim.visualMode){
+ cm.setCursor(curStart);
+ exitVisualMode(cm,vim);
+ }else{
+ cm.setCursor(offsetCursor(curEnd, 0, -1));
+ }
}
- cm.replaceRange(replaceWithStr, curStart, curEnd);
- cm.setCursor(offsetCursor(curEnd, 0, -1));
}
};
@@ -1720,7 +1765,7 @@
var line = cm.getLine(cur.line);
idx = charIdxInLine(start, line, character, forward, true);
if (idx == -1) {
- return cur;
+ return null;
}
start = idx;
}
View
1  lib/codemirror.css
@@ -51,6 +51,7 @@
border: 0;
background: transparent;
background: rgba(0, 200, 0, .4);
+ z-index: 2;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#6600c800, endColorstr=#4c00c800);
}
/* Kludge to turn off filter in ie9+, which also accepts rgba */
View
10 test/vim_test.js
@@ -225,6 +225,9 @@ testMotion('G_repeat', ['3', 'G'], makeCursor(lines[2].line,
// TODO: Make the test code long enough to test Ctrl-F and Ctrl-B.
testMotion('0', '0', makeCursor(0, 0), makeCursor(0, 8));
testMotion('^', '^', makeCursor(0, lines[0].textStart), makeCursor(0, 8));
+testMotion('+', '+', makeCursor(1, lines[1].textStart), makeCursor(0, 8));
+testMotion('-', '-', makeCursor(0, lines[0].textStart), makeCursor(1, 4));
+testMotion('_', ['6','_'], makeCursor(5, lines[5].textStart), makeCursor(0, 8));
testMotion('$', '$', makeCursor(0, lines[0].length - 1), makeCursor(0, 1));
testMotion('$_repeat', ['2', '$'], makeCursor(1, lines[1].length - 1),
makeCursor(0, 3));
@@ -793,9 +796,12 @@ testVim('P_line', function(cm, vim, helpers) {
testVim('r', function(cm, vim, helpers) {
cm.setCursor(0, 1);
helpers.doKeys('3', 'r', 'u');
- eq('wuuuet', cm.getValue());
+ eq('wuuuet\nanother', cm.getValue(),'3r failed');
helpers.assertCursorAt(0, 3);
-}, { value: 'wordet' });
+ cm.setCursor(0, 4);
+ helpers.doKeys('v', 'j', 'h', 'r', 'Space');
+ eq('wuuu \n her', cm.getValue(),'Replacing selection by space-characters failed');
+}, { value: 'wordet\nanother' });
testVim('mark', function(cm, vim, helpers) {
cm.setCursor(2, 2);
helpers.doKeys('m', 't');
Something went wrong with that request. Please try again.