Skip to content

Commit b0bfa43

Browse files
Verymagic (#190)
* fix substitution with empty string * fix input dialog overlapping key info * support backreferences in replacement pattern * improve vim regex support * better explain reason why when regex doesn't match * Update src/vim.js Co-authored-by: Faris Masad <[email protected]> * Update src/vim.js Co-authored-by: Faris Masad <[email protected]> * fix tests --------- Co-authored-by: Faris Masad <[email protected]>
1 parent 08d75b9 commit b0bfa43

File tree

4 files changed

+135
-46
lines changed

4 files changed

+135
-46
lines changed

src/cm_adapter.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ export class CodeMirror {
186186
cm6: EditorView
187187
state: {
188188
statusbar?: Element | null,
189-
dialog?: Element | null,
189+
dialog?: HTMLElement | null,
190190
vimPlugin?: any,
191191
vim?: vimState | null,
192192
currentNotificationClose?: Function | null,
@@ -534,6 +534,9 @@ export class CodeMirror {
534534
lastCM5Result.to = posFromIndex(cm.cm6.state.doc, last.to);
535535
}
536536
}
537+
},
538+
get match() {
539+
return lastCM5Result && lastCM5Result.match
537540
}
538541
};
539542
};
@@ -848,9 +851,10 @@ function openNotification(cm: CodeMirror, template: Node, options: NotificationO
848851
}
849852

850853

851-
function showDialog(cm: CodeMirror, dialog: Element) {
854+
function showDialog(cm: CodeMirror, dialog: HTMLElement) {
852855
var oldDialog = cm.state.dialog
853856
cm.state.dialog = dialog;
857+
dialog.style.flex = "1";
854858

855859
if (dialog && oldDialog !== dialog) {
856860
if (oldDialog && oldDialog.contains(document.activeElement))

src/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const vimStyle = EditorView.baseTheme({
3434
padding: "0px 10px",
3535
fontFamily: "monospace",
3636
minHeight: "1.3em",
37+
display: 'flex',
3738
},
3839
".cm-vim-panel input": {
3940
border: "none",
@@ -50,6 +51,7 @@ const vimPlugin = ViewPlugin.fromClass(
5051
class implements PluginValue {
5152
private dom: HTMLElement;
5253
private statusButton: HTMLElement;
54+
private spacer: HTMLElement;
5355
public view: EditorViewExtended;
5456
public cm: CodeMirror;
5557
public status = "";
@@ -93,7 +95,8 @@ const vimPlugin = ViewPlugin.fromClass(
9395
});
9496

9597
this.dom = document.createElement("span");
96-
this.dom.style.cssText = "position: absolute; right: 10px; top: 1px";
98+
this.spacer = document.createElement("span");
99+
this.spacer.style.flex = "1";
97100
this.statusButton = document.createElement("span");
98101
this.statusButton.onclick = (e) => {
99102
Vim.handleKey(this.cm, "<Esc>", "user");
@@ -157,6 +160,7 @@ const vimPlugin = ViewPlugin.fromClass(
157160
if (vim.insertModeReturn) status += "(C-O)"
158161
this.statusButton.textContent = `--${status}--`;
159162
dom.appendChild(this.statusButton);
163+
dom.appendChild(this.spacer);
160164
}
161165

162166
this.dom.textContent = vim.status;

src/vim.js

Lines changed: 96 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1639,6 +1639,7 @@ export function initVim(CodeMirror) {
16391639
var promptPrefix = (forward) ? '/' : '?';
16401640
var originalQuery = getSearchState(cm).getQuery();
16411641
var originalScrollPos = cm.getScrollInfo();
1642+
var lastQuery = ""
16421643
/** @arg {string} query @arg {boolean} ignoreCase @arg {boolean} smartCase */
16431644
function handleQuery(query, ignoreCase, smartCase) {
16441645
vimGlobalState.searchHistoryController.pushInput(query);
@@ -1666,6 +1667,9 @@ export function initVim(CodeMirror) {
16661667
logSearchQuery(macroModeState, query);
16671668
}
16681669
}
1670+
function pcreLabel() {
1671+
return getOption('pcre') ? '(JavaScript regexp: set pcre)' : '(Vim regexp: set nopcre)'
1672+
}
16691673
/**
16701674
* @arg {KeyboardEvent&{target:HTMLInputElement}} e
16711675
* @arg {any} query
@@ -1682,9 +1686,13 @@ export function initVim(CodeMirror) {
16821686
} else if (keyName && keyName != '<Left>' && keyName != '<Right>') {
16831687
vimGlobalState.searchHistoryController.reset();
16841688
}
1689+
lastQuery = query;
1690+
onChange();
1691+
}
1692+
function onChange() {
16851693
var parsedQuery;
16861694
try {
1687-
parsedQuery = updateSearchQuery(cm, query,
1695+
parsedQuery = updateSearchQuery(cm, lastQuery,
16881696
true /** ignoreCase */, true /** smartCase */);
16891697
} catch (e) {
16901698
// Swallow bad regexes for incremental search.
@@ -1728,7 +1736,19 @@ export function initVim(CodeMirror) {
17281736
showPrompt(cm, {
17291737
onClose: onPromptClose,
17301738
prefix: promptPrefix,
1731-
desc: '(JavaScript regexp)',
1739+
desc: dom(
1740+
'span',
1741+
{
1742+
$cursor: 'pointer',
1743+
onmousedown: function(e) {
1744+
e.preventDefault()
1745+
setOption('pcre', !getOption('pcre'));
1746+
this.textContent = pcreLabel();
1747+
onChange();
1748+
}
1749+
},
1750+
pcreLabel()
1751+
),
17321752
onKeyUp: onPromptKeyUp,
17331753
onKeyDown: onPromptKeyDown
17341754
});
@@ -2096,7 +2116,12 @@ export function initVim(CodeMirror) {
20962116
// If search is initiated with ? instead of /, negate direction.
20972117
prev = (state.isReversed()) ? !prev : prev;
20982118
highlightSearchMatches(cm, query);
2099-
return findNext(cm, prev/** prev */, query, motionArgs.repeat);
2119+
var result = findNext(cm, prev/** prev */, query, motionArgs.repeat);
2120+
if (!result) {
2121+
showConfirm(cm, 'No match found ' + query +
2122+
(getOption('pcre') ? ' (set nopcre to use Vim regexps)' : ''));
2123+
}
2124+
return result;
21002125
},
21012126
/**
21022127
* Find and select the next occurrence of the search query. If the cursor is currently
@@ -4930,40 +4955,49 @@ export function initVim(CodeMirror) {
49304955
/** @arg {string} str */
49314956
function translateRegex(str) {
49324957
// When these match, add a '\' if unescaped or remove one if escaped.
4933-
var specials = '|(){';
4934-
// Remove, but never add, a '\' for these.
4935-
var unescape = '}';
4936-
var escapeNextChar = false;
4937-
var out = [];
4938-
for (var i = -1; i < str.length; i++) {
4939-
var c = str.charAt(i) || '';
4940-
var n = str.charAt(i+1) || '';
4941-
var specialComesNext = (n && specials.indexOf(n) != -1);
4942-
if (escapeNextChar) {
4943-
if (c !== '\\' || !specialComesNext) {
4944-
out.push(c);
4958+
var modes = {
4959+
V: '|(){+?*.[$^', // verynomagic
4960+
M: '|(){+?*.[', // nomagic
4961+
m: '|(){+?', // magic
4962+
v: '<>', // verymagic
4963+
};
4964+
var escapes = {
4965+
'>': '(?<=[\\w])(?=[^\\w]|$)',
4966+
'<': '(?<=[^\\w]|^)(?=[\\w])',
4967+
};
4968+
var specials = modes.m;
4969+
var regex = str.replace(/\\.|[\[|(){+*?.$^<>]/g, function(match) {
4970+
if (match[0] === '\\') {
4971+
var nextChar = match[1];
4972+
if (nextChar === '}' || specials.indexOf(nextChar) != -1) {
4973+
return nextChar;
49454974
}
4946-
escapeNextChar = false;
4975+
if (nextChar in modes) {
4976+
specials = modes[nextChar];
4977+
return '';
4978+
}
4979+
if (nextChar in escapes) {
4980+
return escapes[nextChar];
4981+
}
4982+
return match;
49474983
} else {
4948-
if (c === '\\') {
4949-
escapeNextChar = true;
4950-
// Treat the unescape list as special for removing, but not adding '\'.
4951-
if (n && unescape.indexOf(n) != -1) {
4952-
specialComesNext = true;
4953-
}
4954-
// Not passing this test means removing a '\'.
4955-
if (!specialComesNext || n === '\\') {
4956-
out.push(c);
4957-
}
4958-
} else {
4959-
out.push(c);
4960-
if (specialComesNext && n !== '\\') {
4961-
out.push('\\');
4962-
}
4984+
if (specials.indexOf(match) != -1) {
4985+
return escapes[match] || '\\' + match;
49634986
}
4987+
return match;
49644988
}
4989+
});
4990+
4991+
var i = regex.indexOf('\\zs')
4992+
if (i != -1) {
4993+
regex = '(?<=' + regex.slice(0, i) + ')' + regex.slice(i + 3);
49654994
}
4966-
return out.join('');
4995+
i = regex.indexOf('\\ze')
4996+
if (i != -1) {
4997+
regex = regex.slice(0, i) + '(?=' + regex.slice(i + 3) + ')';
4998+
}
4999+
5000+
return regex;
49675001
}
49685002

49695003
// Translates the replace part of a search and replace from ex (vim) syntax into
@@ -5104,6 +5138,7 @@ export function initVim(CodeMirror) {
51045138
else for (var key in a) {
51055139
if (!Object.prototype.hasOwnProperty.call(a, key)) continue;
51065140
if (key[0] === '$') n.style[key.slice(1)] = a[key];
5141+
else if (typeof a[key] == "function") n[key] = a[key];
51075142
else n.setAttribute(key, a[key]);
51085143
}
51095144
}
@@ -5121,15 +5156,15 @@ export function initVim(CodeMirror) {
51215156
}
51225157
cm.state.closeVimNotification = cm.openNotification(pre, {bottom: true, duration: 0});
51235158
} else {
5124-
cm.openNotification(pre, {bottom: true, duration: 5000});
5159+
cm.openNotification(pre, {bottom: true, duration: 15000});
51255160
}
51265161
} else {
51275162
alert(pre.innerText);
51285163
}
51295164
}
51305165
/** @arg {string} prefix @arg {string} desc */
51315166
function makePrompt(prefix, desc) {
5132-
return dom('div', {$display: 'flex'},
5167+
return dom('div', {$display: 'flex', $flex: 1},
51335168
dom('span', {$fontFamily: 'monospace', $whiteSpace: 'pre', $flex: 1, $display: 'flex'},
51345169
prefix,
51355170
dom('input', {type: 'text', autocorrect: 'off',
@@ -6267,6 +6302,7 @@ export function initVim(CodeMirror) {
62676302
// Set up all the functions.
62686303
cm.state.vim.exMode = true;
62696304
var done = false;
6305+
var matches = 0;
62706306

62716307
/** @type {Pos}*/ var lastPos;
62726308
/** @type {number}*/ var modifiedLineNumber
@@ -6281,8 +6317,23 @@ export function initVim(CodeMirror) {
62816317
});
62826318
}
62836319
function replace() {
6284-
var text = cm.getRange(searchCursor.from(), searchCursor.to());
6285-
var newText = text.replace(query, replaceWith);
6320+
var newText = '';
6321+
var match = searchCursor.match || searchCursor.pos && searchCursor.pos.match;
6322+
if (match) {
6323+
newText = replaceWith.replace(/\$(\d{1,3}|[$&])/g, function(_, x) {
6324+
if (x == "$") return "$";
6325+
if (x == '&') return match[0];
6326+
var x1 = x;
6327+
while (parseInt(x1) >= match.length && x1.length > 0) {
6328+
x1 = x1.slice(0, x1.length - 1);
6329+
}
6330+
if (x1) return match[x1] + x.slice(x1.length, x.length);
6331+
return _;
6332+
});
6333+
} else {
6334+
var text = cm.getRange(searchCursor.from(), searchCursor.to());
6335+
newText = text.replace(query, replaceWith);
6336+
}
62866337
var unmodifiedLineNumber = searchCursor.to().line;
62876338
searchCursor.replace(newText);
62886339
modifiedLineNumber = searchCursor.to().line;
@@ -6295,6 +6346,7 @@ export function initVim(CodeMirror) {
62956346
if (match && !match[0] && lastMatchTo && cursorEqual(searchCursor.from(), lastMatchTo)) {
62966347
match = searchCursor.findNext();
62976348
}
6349+
if (match) matches++;
62986350
return match;
62996351
}
63006352
function next() {
@@ -6324,6 +6376,13 @@ export function initVim(CodeMirror) {
63246376
vim.lastHPos = vim.lastHSPos = lastPos.ch;
63256377
}
63266378
if (callback) { callback(); }
6379+
else if (done) {
6380+
showConfirm(cm,
6381+
(matches ? 'Found ' + matches + ' matches' : 'No matches found') +
6382+
' for pattern: ' + query +
6383+
(getOption('pcre') ? ' (set nopcre to use Vim regexps)' : '')
6384+
);
6385+
}
63276386
}
63286387
/** @arg {KeyboardEvent} e @arg {any} _value @arg {any} close */
63296388
function onPromptKeyDown(e, _value, close) {
@@ -6360,7 +6419,7 @@ export function initVim(CodeMirror) {
63606419
// Actually do replace.
63616420
next();
63626421
if (done) {
6363-
showConfirm(cm, 'No matches for ' + query.source);
6422+
showConfirm(cm, 'No matches for ' + query + (getOption('pcre') ? ' (set nopcre to use vim regexps)' : ''));
63646423
return;
63656424
}
63666425
if (!confirm) {

test/vim_test.js

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4488,7 +4488,7 @@ testVim('ex_substitute_javascript', function(cm, vim, helpers) {
44884488
cm.setCursor(1, 0);
44894489
// Throw all the things that javascript likes to treat as special values
44904490
// into the replace part. All should be literal (this is VIM).
4491-
helpers.doEx('s/\\(\\d+\\)/$$ $\' $` $& \\1/g')
4491+
helpers.doEx('s/\\(\\d\\+\\)/$$ $\' $` $& \\1/g')
44924492
eq('a $$ $\' $` $& 0 b', cm.getValue());
44934493

44944494
cm.setValue('W12345678OR12345D');
@@ -4509,6 +4509,28 @@ testVim('ex_substitute_highlight', function(cm,vim,helpers) {
45094509
helpers.doKeys('\n');
45104510
is(!searchHighlighted(vim));
45114511
}, {value: 'a a\na a'});
4512+
testVim('ex_substitute_nopcre_special', function(cm, vim, helpers) {
4513+
CodeMirror.Vim.setOption('pcre', false);
4514+
4515+
cm.setValue('aabb1cxyz$^o aabb2cxyz$^o aabb3cxyz$^o aabb4cxyz$^o ');
4516+
helpers.doEx(
4517+
's/'
4518+
+ '\\v<a*(b|\\d){3}c?[x-z]+\\$\\^.> '
4519+
+ '\\V\\<a\\*\\(b\\|\\d\\)\\{3\\}c\\?\\[x-z]\\+$^\\.\\> '
4520+
+ '\\m\\<a*\\(b\\|\\d\\)\\{3}c\\?[x-z]\\+\\$\\^.\\> '
4521+
+ '\\M\\<a\\*\\(b\\|\\d\\)\\{3}c\\?\\[x-z]\\+\\$\\^\\.\\>'
4522+
+ '/M\\4 m\\3 V\\2 v\\1/'
4523+
);
4524+
eq('M4 m3 V2 v1 ', cm.getValue());
4525+
4526+
cm.setValue('10 12 13 42');
4527+
helpers.doEx('s/\\m\\(1\\)\\v\\ze(\\d+)/\\2\\1 a\\1/g')
4528+
eq('01 a10 21 a12 31 a13 42', cm.getValue());
4529+
helpers.doEx('s/2\\zs\\>/b/g');
4530+
eq('01 a10 21 a12b 31 a13 42b', cm.getValue());
4531+
helpers.doEx('s/\\m\\<\\d\\+ //g');
4532+
eq('a10 a12b a13 42b', cm.getValue());
4533+
}, { value: '' });
45124534

45134535
// More complex substitute tests that test both pcre and nopcre options.
45144536
function testSubstitute(name, options) {
@@ -4533,22 +4555,22 @@ testSubstitute('ex_substitute_capture', {
45334555
// $n is a backreference
45344556
expr: 's/(\\d+)/$1$1/g',
45354557
// \n is a backreference.
4536-
noPcreExpr: 's/\\(\\d+\\)/\\1\\1/g'});
4558+
noPcreExpr: 's/\\(\\d\\+\\)/\\1\\1/g'});
45374559
testSubstitute('ex_substitute_capture2', {
45384560
value: 'a 0 b',
45394561
expectedValue: 'a $00 b',
45404562
expr: 's/(\\d+)/$$$1$1/g',
4541-
noPcreExpr: 's/\\(\\d+\\)/$\\1\\1/g'});
4563+
noPcreExpr: 's/\\(\\d\\+\\)/$\\1\\1/g'});
45424564
testSubstitute('ex_substitute_nocapture', {
45434565
value: 'a11 a12 a13',
45444566
expectedValue: 'a$1$1 a$1$1 a$1$1',
45454567
expr: 's/(\\d+)/$$1$$1/g',
4546-
noPcreExpr: 's/\\(\\d+\\)/$1$1/g'});
4568+
noPcreExpr: 's/\\(\\d\\+\\)/$1$1/g'});
45474569
testSubstitute('ex_substitute_nocapture2', {
45484570
value: 'a 0 b',
45494571
expectedValue: 'a $10 b',
45504572
expr: 's/(\\d+)/$$1$1/g',
4551-
noPcreExpr: 's/\\(\\d+\\)/\\$1\\1/g'});
4573+
noPcreExpr: 's/\\(\\d\\+\\)/\\$1\\1/g'});
45524574
testSubstitute('ex_substitute_nocapture', {
45534575
value: 'a b c',
45544576
expectedValue: 'a $ c',
@@ -4694,7 +4716,7 @@ testSubstitute('ex_substitute_empty_match', {
46944716
value: 'aaa aa\n aa\nbb\n',
46954717
expectedValue: '<aaa> <aa>\n <aa>\nbb<>\n<>',
46964718
expr: '%s/(a+|$)/<$1>/g',
4697-
noPcreExpr: '%s/\\(a+\\|$\\)/<\\1>/g'});
4719+
noPcreExpr: '%s/\\(a\\+\\|$\\)/<\\1>/g'});
46984720
testSubstitute('ex_substitute_empty_or_match', {
46994721
value: '1234\n567\n89\n0\n',
47004722
expectedValue: '<12><34>\n<56>7<>\n<89>\n0<>\n<>',

0 commit comments

Comments
 (0)