diff --git a/AUTHORS b/AUTHORS
index f5f569721e..0c2f67c31a 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -18,6 +18,7 @@ Alberto Pose
Albert Xing
Alexander Pavlov
Alexander Schepanovski
+Alexander Shvets
Alexander Solovyov
Alexandre Bique
alexey-k
@@ -168,6 +169,7 @@ Jason Grout
Jason Johnston
Jason San Jose
Jason Siefken
+Jaydeep Solanki
Jean Boussier
jeffkenton
Jeff Pickhardt
@@ -205,6 +207,7 @@ kubelsmieci
Lanny
Laszlo Vidacs
leaf corcoran
+Leonid Khachaturov
Leonya Khachaturov
Liam Newman
LM
@@ -269,6 +272,7 @@ Niels van Groningen
Nikita Beloglazov
Nikita Vasilyev
Nikolay Kostov
+nilp0inter
nlwillia
pablo
Page
@@ -291,7 +295,9 @@ Radek Piórkowski
Rahul
Randy Edmunds
Rasmus Erik Voel Jensen
+Richard van der Meer
Richard Z.H. Wang
+Roberto Abdelkader Martínez Pérez
robertop23
Robert Plummer
Ruslan Osmanov
@@ -299,6 +305,7 @@ Ryan Prior
sabaca
Samuel Ainsworth
sandeepshetty
+Sander AKA Redsandro
santec
Sascha Peilicke
satchmorun
@@ -330,6 +337,7 @@ Thaddee Tyl
think
Thomas Dvornik
Thomas Schmid
+Tim Alby
Tim Baumann
Timothy Farrell
Timothy Hatcher
@@ -349,6 +357,7 @@ Volker Mische
wenli
Wesley Wiser
William Jamieson
+William Stein
Wojtek Ptak
Xavier Mendez
YNH Webdev
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 8938f62046..3624a39e2b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -70,3 +70,7 @@ should be asked on the
- Note that the linter (`bin/lint`) which is run after each commit
complains about unused variables and functions. Prefix their names
with an underscore to muffle it.
+
+- CodeMirror does *not* follow JSHint or JSLint prescribed style.
+ Patches that try to 'fix' code to pass one of these linters will be
+ unceremoniously discarded.
diff --git a/addon/comment/comment.js b/addon/comment/comment.js
index 3ac476452d..cb78340231 100644
--- a/addon/comment/comment.js
+++ b/addon/comment/comment.js
@@ -153,6 +153,17 @@
!/comment/.test(self.getTokenTypeAt(Pos(end, close + 1))))
return false;
+ // Avoid killing block comments completely outside the selection.
+ // Positions of the last startString before the start of the selection, and the first endString after it.
+ var lastStart = startLine.lastIndexOf(startString, from.ch);
+ var firstEnd = lastStart == -1 ? -1 : startLine.slice(0, from.ch).indexOf(endString, lastStart + startString.length);
+ if (lastStart != -1 && firstEnd != -1) return false;
+ // Positions of the first endString after the end of the selection, and the last startString before it.
+ firstEnd = endLine.indexOf(endString, to.ch);
+ var almostLastStart = endLine.slice(to.ch).lastIndexOf(startString, firstEnd - to.ch);
+ lastStart = (firstEnd == -1 || almostLastStart == -1) ? -1 : to.ch + almostLastStart;
+ if (firstEnd != -1 && lastStart != -1) return false;
+
self.operation(function() {
self.replaceRange("", Pos(end, close - (pad && endLine.slice(close - pad.length, close) == pad ? pad.length : 0)),
Pos(end, close + endString.length));
diff --git a/addon/edit/closebrackets.js b/addon/edit/closebrackets.js
index 83d4229f47..1a04f3664e 100644
--- a/addon/edit/closebrackets.js
+++ b/addon/edit/closebrackets.js
@@ -36,6 +36,22 @@
return str.length == 2 ? str : null;
}
+ // Project the token type that will exists after the given char is
+ // typed, and use it to determine whether it would cause the start
+ // of a string token.
+ function enteringString(cm, pos, ch) {
+ var line = cm.getLine(pos.line);
+ var token = cm.getTokenAt(pos);
+ if (/\bstring2?\b/.test(token.type)) return false;
+ var stream = new CodeMirror.StringStream(line.slice(0, pos.ch) + ch + line.slice(pos.ch), 4);
+ stream.pos = stream.start = token.start;
+ for (;;) {
+ var type1 = cm.getMode().token(stream, token.state);
+ if (stream.pos >= pos.ch + 1) return /\bstring2?\b/.test(type1);
+ stream.start = stream.pos;
+ }
+ }
+
function buildKeymap(pairs) {
var map = {
name : "autoCloseBrackets",
@@ -61,8 +77,6 @@
var ranges = cm.listSelections(), type, next;
for (var i = 0; i < ranges.length; i++) {
var range = ranges[i], cur = range.head, curType;
- if (left == "'" && cm.getTokenTypeAt(cur) == "comment")
- return CodeMirror.Pass;
var next = cm.getRange(cur, Pos(cur.line, cur.ch + 1));
if (!range.empty())
curType = "surround";
@@ -75,9 +89,10 @@
cm.getRange(Pos(cur.line, cur.ch - 2), cur) == left + left &&
(cur.ch <= 2 || cm.getRange(Pos(cur.line, cur.ch - 3), Pos(cur.line, cur.ch - 2)) != left))
curType = "addFour";
- else if (left == right && CodeMirror.isWordChar(next))
- return CodeMirror.Pass;
- else if (cm.getLine(cur.line).length == cur.ch || closingBrackets.indexOf(next) >= 0 || SPACE_CHAR_REGEX.test(next))
+ else if (left == '"' || left == "'") {
+ if (!CodeMirror.isWordChar(next) && enteringString(cm, cur, left)) curType = "both";
+ else return CodeMirror.Pass;
+ } else if (cm.getLine(cur.line).length == cur.ch || closingBrackets.indexOf(next) >= 0 || SPACE_CHAR_REGEX.test(next))
curType = "both";
else
return CodeMirror.Pass;
diff --git a/addon/hint/show-hint.js b/addon/hint/show-hint.js
index f43ca00c03..27b770bdef 100644
--- a/addon/hint/show-hint.js
+++ b/addon/hint/show-hint.js
@@ -228,9 +228,9 @@
(completion.options.container || document.body).appendChild(hints);
var box = hints.getBoundingClientRect(), overlapY = box.bottom - winH;
if (overlapY > 0) {
- var height = box.bottom - box.top, curTop = box.top - (pos.bottom - pos.top);
+ var height = box.bottom - box.top, curTop = pos.top - (pos.bottom - box.top);
if (curTop - height > 0) { // Fits above cursor
- hints.style.top = (top = curTop - height) + "px";
+ hints.style.top = (top = pos.top - height) + "px";
below = false;
} else if (height > winH) {
hints.style.height = (winH - 5) + "px";
diff --git a/addon/lint/lint.js b/addon/lint/lint.js
index 604e2e6058..a87e70c09e 100644
--- a/addon/lint/lint.js
+++ b/addon/lint/lint.js
@@ -11,7 +11,6 @@
})(function(CodeMirror) {
"use strict";
var GUTTER_ID = "CodeMirror-lint-markers";
- var SEVERITIES = /^(?:error|warning)$/;
function showTooltip(e, content) {
var tt = document.createElement("div");
@@ -110,7 +109,7 @@
function annotationTooltip(ann) {
var severity = ann.severity;
- if (!SEVERITIES.test(severity)) severity = "error";
+ if (!severity) severity = "error";
var tip = document.createElement("div");
tip.className = "CodeMirror-lint-message-" + severity;
tip.appendChild(document.createTextNode(ann.message));
@@ -141,7 +140,7 @@
for (var i = 0; i < anns.length; ++i) {
var ann = anns[i];
var severity = ann.severity;
- if (!SEVERITIES.test(severity)) severity = "error";
+ if (!severity) severity = "error";
maxSeverity = getMaxSeverity(maxSeverity, severity);
if (options.formatAnnotation) ann = options.formatAnnotation(ann);
diff --git a/addon/search/search.js b/addon/search/search.js
index 3ce7cc95d8..b177dce6ed 100644
--- a/addon/search/search.js
+++ b/addon/search/search.js
@@ -110,6 +110,7 @@
var replacementQueryDialog = 'With: ';
var doReplaceConfirm = "Replace? Yes No Stop ";
function replace(cm, all) {
+ if (cm.getOption("readOnly")) return;
dialog(cm, replaceQueryDialog, "Replace:", cm.getSelection(), function(query) {
if (!query) return;
query = parseQuery(query);
diff --git a/addon/search/searchcursor.js b/addon/search/searchcursor.js
index 2783308858..55c108b5a3 100644
--- a/addon/search/searchcursor.js
+++ b/addon/search/searchcursor.js
@@ -107,7 +107,7 @@
var from = Pos(pos.line, cut);
for (var ln = pos.line + 1, i = 1; i < last; ++i, ++ln)
if (target[i] != fold(doc.getLine(ln))) return;
- if (doc.getLine(ln).slice(0, origTarget[last].length) != target[last]) return;
+ if (fold(doc.getLine(ln).slice(0, origTarget[last].length)) != target[last]) return;
return {from: from, to: Pos(ln, origTarget[last].length)};
}
};
diff --git a/bower.json b/bower.json
index 80b379f14b..8c57fcdd4b 100644
--- a/bower.json
+++ b/bower.json
@@ -1,6 +1,6 @@
{
- "name": "CodeMirror",
- "version":"4.3.0",
+ "name": "codemirror",
+ "version":"4.4.0",
"main": ["lib/codemirror.js", "lib/codemirror.css"],
"ignore": [
"**/.*",
diff --git a/demo/vim.html b/demo/vim.html
index f92ff9e6d8..cc616759f0 100644
--- a/demo/vim.html
+++ b/demo/vim.html
@@ -45,17 +45,32 @@
return (--n >= 0) ? (unsigned char) *bufp++ : EOF;
}
-
-
-
+Key buffer:
The vim keybindings are enabled by
including keymap/vim.js and setting
the vimMode option to true. This will also
automatically change the keyMap option to "vim".
+Features
+
+
+ All common motions and operators, including text objects
+ Operator motion orthogonality
+ Visual mode - characterwise, linewise, partial support for blockwise
+ Full macro support (q, @)
+ Incremental highlighted search (/, ?, #, *, g#, g*)
+ Search/replace with confirm (:substitute, :%s)
+ Search history
+ Jump lists (Ctrl-o, Ctrl-i)
+ Key/command mapping with API (:map, :nmap, :vmap)
+ Sort (:sort)
+ Marks (`, ')
+ :global
+ Insert mode behaves identical to base CodeMirror
+ Cross-buffer yank/paste
+
+
Note that while the vim mode tries to emulate the most useful features of
vim as faithfully as possible, it does not strive to become a complete vim
implementation
@@ -69,13 +84,6 @@
matchBrackets: true,
showCursorWhenSelecting: true
});
- var editor2 = CodeMirror.fromTextArea(document.getElementById("code2"), {
- lineNumbers: true,
- mode: "text/x-csrc",
- vimMode: true,
- matchBrackets: true,
- showCursorWhenSelecting: true
- });
var commandDisplay = document.getElementById('command-display');
var keys = '';
CodeMirror.on(editor, 'vim-keypress', function(key) {
diff --git a/doc/compress.html b/doc/compress.html
index 5762aa442b..859210c45c 100644
--- a/doc/compress.html
+++ b/doc/compress.html
@@ -36,6 +36,7 @@
Version:
HEAD
+ 4.4
4.3
4.2
4.2
@@ -123,6 +124,7 @@
javascript.js
jinja2.js
julia.js
+ kotlin.js
livescript.js
lua.js
markdown.js
diff --git a/doc/manual.html b/doc/manual.html
index 0f77ca8503..31d08f5e99 100644
--- a/doc/manual.html
+++ b/doc/manual.html
@@ -63,7 +63,7 @@
User manual and reference guide
- version 4.3.0
+ version 4.4.0
CodeMirror is a code-editor component that can be embedded in
@@ -797,9 +797,15 @@
Module loaders
deleteLine Ctrl-D (PC), Cmd-D (Mac)
Deletes the whole line under the cursor, including newline at the end.
- delLineLeft Cmd-Backspace (Mac)
+ delLineLeft
Delete the part of the line before the cursor.
+ delWrappedLineLeft Cmd-Backspace (Mac)
+ Delete the part of the line from the left side of the visual line the cursor is on to the cursor.
+
+ delWrappedLineRight Cmd-Delete (Mac)
+ Delete the part of the line from the cursor to the right side of the visual line the cursor is on.
+
undo Ctrl-Z (PC), Cmd-Z (Mac)
Undo the last change.
@@ -815,13 +821,13 @@ Module loaders
Redo the last change to the selection, or the last text change if
no selection changes remain.
- goDocStart Ctrl-Up (PC), Cmd-Up (Mac)
+ goDocStart Ctrl-Up (PC), Cmd-Up (Mac), Cmd-Home (Mac)
Move the cursor to the start of the document.
goDocEnd Ctrl-Down (PC), Cmd-End (Mac), Cmd-Down (Mac)
Move the cursor to the end of the document.
- goLineStart Alt-Left (PC), Cmd-Left (Mac), Ctrl-A (Mac)
+ goLineStart Alt-Left (PC), Ctrl-A (Mac)
Move the cursor to the start of the line.
goLineStartSmart Home
@@ -829,14 +835,14 @@ Module loaders
already there, to the actual start of the line (including
whitespace).
- goLineEnd Alt-Right (PC), Cmd-Right (Mac), Ctrl-E (Mac)
+ goLineEnd Alt-Right (PC), Ctrl-E (Mac)
Move the cursor to the end of the line.
- goLineLeft
+ goLineLeft Cmd-Left (Mac)
Move the cursor to the left side of the visual line it is on. If
this line is wrapped, that may not be the start of the line.
- goLineRight
+ goLineRight Cmd-Right (Mac)
Move the cursor to the right side of the visual line it is on.
goLineUp Up, Ctrl-P (Mac)
diff --git a/doc/realworld.html b/doc/realworld.html
index 6b44c5e67b..d730e411b2 100644
--- a/doc/realworld.html
+++ b/doc/realworld.html
@@ -37,6 +37,7 @@
Cargo Collective (creative publishing platform)
Chrome DevTools
ClickHelp (technical writing tool)
+ CodeWorld (Haskell playground)
Complete.ly playground
CrossUI (cross-platform UI builder)
Cruncher (notepad with calculation features)
@@ -123,6 +124,7 @@
Quivive File Manager
Rascal (tiny computer)
RealTime.io (Internet-of-Things infrastructure)
+ SageMathCloud (interactive mathematical software environment)
ServePHP (PHP code testing in Chrome dev tools)
Shadertoy (shader sharing)
sketchPatch Livecodelab
diff --git a/doc/releases.html b/doc/releases.html
index ea5f98d3d8..354084b0da 100644
--- a/doc/releases.html
+++ b/doc/releases.html
@@ -29,6 +29,20 @@
Version 4.x
+ 21-07-2014: Version 4.4 :
+
+
+ Note: Some events might now fire in slightly
+ different order ("change" is still guaranteed to fire
+ before "cursorActivity")
+ Nested operations in multiple editors are now synced (complete
+ at same time, reducing DOM reflows)
+ Visual block mode for vim (<C-v>) is nearly complete
+ New mode: Kotlin
+ Better multi-selection paste for text copied from multiple CodeMirror selections
+ Full list of patches .
+
+
23-06-2014: Version 4.3 :
diff --git a/index.html b/index.html
index d5cbece193..19f0365a38 100644
--- a/index.html
+++ b/index.html
@@ -85,7 +85,7 @@
DOWNLOAD LATEST RELEASE
-
+
DONATE WITH PAYPAL
diff --git a/keymap/sublime.js b/keymap/sublime.js
index 4d46938415..7a3a1e5f0e 100644
--- a/keymap/sublime.js
+++ b/keymap/sublime.js
@@ -481,7 +481,7 @@
});
};
- function findAndGoTo(cm, forward) {
+ function getTarget(cm) {
var from = cm.getCursor("from"), to = cm.getCursor("to");
if (CodeMirror.cmpPos(from, to) == 0) {
var word = wordAt(cm, from);
@@ -489,9 +489,14 @@
from = word.from;
to = word.to;
}
+ return {from: from, to: to, query: cm.getRange(from, to), word: word};
+ }
- var query = cm.getRange(from, to);
- var cur = cm.getSearchCursor(query, forward ? to : from);
+ function findAndGoTo(cm, forward) {
+ var target = getTarget(cm);
+ if (!target) return;
+ var query = target.query;
+ var cur = cm.getSearchCursor(query, forward ? target.to : target.from);
if (forward ? cur.findNext() : cur.findPrevious()) {
cm.setSelection(cur.from(), cur.to());
@@ -500,12 +505,25 @@
: cm.clipPos(Pos(cm.lastLine())));
if (forward ? cur.findNext() : cur.findPrevious())
cm.setSelection(cur.from(), cur.to());
- else if (word)
- cm.setSelection(from, to);
+ else if (target.word)
+ cm.setSelection(target.from, target.to);
}
};
cmds[map[ctrl + "F3"] = "findUnder"] = function(cm) { findAndGoTo(cm, true); };
cmds[map["Shift-" + ctrl + "F3"] = "findUnderPrevious"] = function(cm) { findAndGoTo(cm,false); };
+ cmds[map["Alt-F3"] = "findAllUnder"] = function(cm) {
+ var target = getTarget(cm);
+ if (!target) return;
+ var cur = cm.getSearchCursor(target.query);
+ var matches = [];
+ var primaryIndex = -1;
+ while (cur.findNext()) {
+ matches.push({anchor: cur.from(), head: cur.to()});
+ if (cur.from().line <= target.from.line && cur.from().ch <= target.from.ch)
+ primaryIndex++;
+ }
+ cm.setSelections(matches, primaryIndex);
+ };
map["Shift-" + ctrl + "["] = "fold";
map["Shift-" + ctrl + "]"] = "unfold";
diff --git a/keymap/vim.js b/keymap/vim.js
index 3a099a0663..a47b005d02 100644
--- a/keymap/vim.js
+++ b/keymap/vim.js
@@ -225,7 +225,8 @@
{ keys: ['|'], type: 'motion',
motion: 'moveToColumn',
motionArgs: { }},
- { keys: ['o'], type: 'motion', motion: 'moveToOtherHighlightedEnd', motionArgs: { },context:'visual'},
+ { keys: ['o'], type: 'motion', motion: 'moveToOtherHighlightedEnd', motionArgs: { }, context:'visual'},
+ { keys: ['O'], type: 'motion', motion: 'moveToOtherHighlightedEnd', motionArgs: {sameLine: true}, context:'visual'},
// Operators
{ keys: ['d'], type: 'operator', operator: 'delete' },
{ keys: ['y'], type: 'operator', operator: 'yank' },
@@ -288,6 +289,8 @@
{ keys: ['v'], type: 'action', action: 'toggleVisualMode' },
{ keys: ['V'], type: 'action', action: 'toggleVisualMode',
actionArgs: { linewise: true }},
+ { keys: ['
'], type: 'action', action: 'toggleVisualMode',
+ actionArgs: { blockwise: true }},
{ keys: ['g', 'v'], type: 'action', action: 'reselectLastSelection' },
{ keys: ['J'], type: 'action', action: 'joinLines', isEdit: true },
{ keys: ['p'], type: 'action', action: 'paste', isEdit: true,
@@ -558,7 +561,9 @@
MacroModeState.prototype = {
exitMacroRecordMode: function() {
var macroModeState = vimGlobalState.macroModeState;
- macroModeState.onRecordingDone(); // close dialog
+ if (macroModeState.onRecordingDone) {
+ macroModeState.onRecordingDone(); // close dialog
+ }
macroModeState.onRecordingDone = undefined;
macroModeState.isRecording = false;
},
@@ -568,8 +573,10 @@
if (register) {
register.clear();
this.latestRegister = registerName;
- this.onRecordingDone = cm.openDialog(
- '(recording)['+registerName+']', null, {bottom:true});
+ if (cm.openDialog) {
+ this.onRecordingDone = cm.openDialog(
+ '(recording)['+registerName+']', null, {bottom:true});
+ }
this.isRecording = true;
}
}
@@ -607,6 +614,7 @@
visualMode: false,
// If we are in visual line mode. No effect if visualMode is false.
visualLine: false,
+ visualBlock: false,
lastSelection: null,
lastPastedText: null
};
@@ -1343,19 +1351,28 @@
if (vim.visualMode) {
// Check if the selection crossed over itself. Will need to shift
// the start point if that happened.
+ // offset is set to -1 or 1 to shift the curEnd
+ // left or right
+ var offset = 0;
if (cursorIsBefore(selectionStart, selectionEnd) &&
(cursorEqual(selectionStart, curEnd) ||
cursorIsBefore(curEnd, selectionStart))) {
// The end of the selection has moved from after the start to
// before the start. We will shift the start right by 1.
selectionStart.ch += 1;
- curEnd.ch -= 1;
+ offset = -1;
} else if (cursorIsBefore(selectionEnd, selectionStart) &&
(cursorEqual(selectionStart, curEnd) ||
cursorIsBefore(selectionStart, curEnd))) {
// The opposite happened. We will shift the start left by 1.
selectionStart.ch -= 1;
- curEnd.ch += 1;
+ offset = 1;
+ }
+ // in case of visual Block selectionStart and curEnd
+ // may not be on the same line,
+ // Also, In case of v_o this should not happen.
+ if (!vim.visualBlock && !(motionResult instanceof Array)) {
+ curEnd.ch += offset;
}
if (vim.lastHPos != Infinity) {
vim.lastHPos = curEnd.ch;
@@ -1375,8 +1392,14 @@
selectionEnd.ch = 0;
selectionStart.ch = lineLength(cm, selectionStart.line);
}
+ } else if (vim.visualBlock) {
+ // Select a block and
+ // return the diagonally opposite end.
+ selectionStart = selectBlock(cm, selectionEnd);
+ }
+ if (!vim.visualBlock) {
+ cm.setSelection(selectionStart, selectionEnd);
}
- cm.setSelection(selectionStart, selectionEnd);
updateMark(cm, vim, '<',
cursorIsBefore(selectionStart, selectionEnd) ? selectionStart
: selectionEnd);
@@ -1392,11 +1415,13 @@
if (operator) {
var inverted = false;
vim.lastMotion = null;
+ var lastSelection = vim.lastSelection;
operatorArgs.repeat = repeat; // Indent in visual mode needs this.
if (vim.visualMode) {
curStart = selectionStart;
curEnd = selectionEnd;
motionArgs.inclusive = true;
+ operatorArgs.shouldMoveCursor = false;
}
// Swap start and end if motion was backward.
if (curEnd && cursorIsBefore(curEnd, curStart)) {
@@ -1417,6 +1442,24 @@
curEnd.line = curStart.line + operatorArgs.selOffset.line;
if (operatorArgs.selOffset.line) {curEnd.ch = operatorArgs.selOffset.ch; }
else { curEnd.ch = curStart.ch + operatorArgs.selOffset.ch; }
+ // In case of blockwise visual
+ if (lastSelection && lastSelection.visualBlock) {
+ var block = lastSelection.visualBlock;
+ var width = block.width;
+ var height = block.height;
+ curEnd = Pos(curStart.line + height, curStart.ch + width);
+ // selectBlock creates a 'proper' rectangular block.
+ // We do not want that in all cases, so we manually set selections.
+ var selections = [];
+ for (var i = curStart.line; i < curEnd.line; i++) {
+ var anchor = Pos(i, curStart.ch);
+ var head = Pos(i, curEnd.ch);
+ var range = {anchor: anchor, head: head};
+ selections.push(range);
+ }
+ cm.setSelections(selections);
+ var blockSelected = true;
+ }
} else if (vim.visualMode) {
var selOffset = Pos();
selOffset.line = curEnd.line - curStart.line;
@@ -1437,6 +1480,9 @@
operatorArgs.registerName = registerName;
// Keep track of linewise as it affects how paste and change behave.
operatorArgs.linewise = linewise;
+ if (!vim.visualBlock && !blockSelected) {
+ cm.setSelection(curStart, curEnd);
+ }
operators[operator](cm, operatorArgs, vim, curStart,
curEnd, curOriginal);
if (vim.visualMode) {
@@ -1499,15 +1545,19 @@
}
return null;
},
- moveToOtherHighlightedEnd: function(cm) {
- var curEnd = copyCursor(cm.getCursor('head'));
- var curStart = copyCursor(cm.getCursor('anchor'));
- if (cursorIsBefore(curStart, curEnd)) {
- curEnd.ch += 1;
- } else if (cursorIsBefore(curEnd, curStart)) {
- curStart.ch -= 1;
- }
- return ([curEnd,curStart]);
+ moveToOtherHighlightedEnd: function(cm, motionArgs, vim) {
+ var ranges = cm.listSelections();
+ var curEnd = cm.getCursor('head');
+ var curStart = ranges[0].anchor;
+ var curIndex = cursorEqual(ranges[0].head, curEnd) ? ranges.length-1 : 0;
+ if (motionArgs.sameLine && vim.visualBlock) {
+ curStart = Pos(curEnd.line, ranges[curIndex].anchor.ch);
+ curEnd = Pos(ranges[curIndex].head.line, curEnd.ch);
+ } else {
+ curStart = ranges[curIndex].anchor;
+ }
+ cm.setCursor(curEnd);
+ return ([curEnd, curStart]);
},
jumpToMark: function(cm, motionArgs, vim) {
var best = cm.getCursor();
@@ -1804,17 +1854,41 @@
};
var operators = {
- change: function(cm, operatorArgs, _vim, curStart, curEnd) {
+ change: function(cm, operatorArgs, vim) {
+ var selections = cm.listSelections();
+ var start = selections[0], end = selections[selections.length-1];
+ var curStart = cursorIsBefore(start.anchor, start.head) ? start.anchor : start.head;
+ var curEnd = cursorIsBefore(end.anchor, end.head) ? end.head : end.anchor;
+ var text = cm.getSelection();
+ var replacement = new Array(selections.length).join('1').split('1');
+ // save the selectionEnd mark
+ var selectionEnd = vim.marks['>'] ? vim.marks['>'].find() : cm.getCursor('head');
vimGlobalState.registerController.pushText(
- operatorArgs.registerName, 'change', cm.getRange(curStart, curEnd),
+ operatorArgs.registerName, 'change', text,
operatorArgs.linewise);
if (operatorArgs.linewise) {
- // Push the next line back down, if there is a next line.
- var replacement = curEnd.line > cm.lastLine() ? '' : '\n';
- cm.replaceRange(replacement, curStart, curEnd);
- cm.indentLine(curStart.line, 'smart');
- // null ch so setCursor moves to end of line.
- curStart.ch = null;
+ // 'C' in visual block extends the block till eol for all lines
+ if (vim.visualBlock){
+ var startLine = curStart.line;
+ while (startLine <= curEnd.line) {
+ var endCh = lineLength(cm, startLine);
+ var head = Pos(startLine, endCh);
+ var anchor = Pos(startLine, curStart.ch);
+ startLine++;
+ cm.replaceRange('', anchor, head);
+ }
+ } else {
+ // Push the next line back down, if there is a next line.
+ replacement = '\n';
+ if (curEnd.line == curStart.line && curEnd.line == cm.lastLine()) {
+ replacement = '';
+ }
+ cm.replaceRange(replacement, curStart, curEnd);
+ cm.indentLine(curStart.line, 'smart');
+ // null ch so setCursor moves to end of line.
+ curStart.ch = null;
+ cm.setCursor(curStart);
+ }
} else {
// Exclude trailing whitespace if the range is not all whitespace.
var text = cm.getRange(curStart, curEnd);
@@ -1824,24 +1898,60 @@
curEnd = offsetCursor(curEnd, 0, - match[0].length);
}
}
- cm.replaceRange('', curStart, curEnd);
+ if (vim.visualBlock) {
+ cm.replaceSelections(replacement);
+ } else {
+ cm.setCursor(curStart);
+ cm.replaceRange('', curStart, curEnd);
+ }
}
+ vim.marks['>'] = cm.setBookmark(selectionEnd);
actions.enterInsertMode(cm, {}, cm.state.vim);
- cm.setCursor(curStart);
},
// delete is a javascript keyword.
- 'delete': function(cm, operatorArgs, _vim, curStart, curEnd) {
+ 'delete': function(cm, operatorArgs, vim) {
+ var selections = cm.listSelections();
+ var start = selections[0], end = selections[selections.length-1];
+ var curStart = cursorIsBefore(start.anchor, start.head) ? start.anchor : start.head;
+ var curEnd = cursorIsBefore(end.anchor, end.head) ? end.head : end.anchor;
+ // Save the '>' mark before cm.replaceRange clears it.
+ var selectionEnd, selectionStart;
+ if (vim.visualMode) {
+ selectionEnd = vim.marks['>'].find();
+ selectionStart = vim.marks['<'].find();
+ } else if (vim.lastSelection) {
+ selectionEnd = vim.lastSelection.curStartMark.find();
+ selectionStart = vim.lastSelection.curEndMark.find();
+ }
+ var text = cm.getSelection();
+ vimGlobalState.registerController.pushText(
+ operatorArgs.registerName, 'delete', text,
+ operatorArgs.linewise);
+ var replacement = new Array(selections.length).join('1').split('1');
// If the ending line is past the last line, inclusive, instead of
// including the trailing \n, include the \n before the starting line
if (operatorArgs.linewise &&
- curEnd.line > cm.lastLine() && curStart.line > cm.firstLine()) {
+ curEnd.line == cm.lastLine() && curStart.line == curEnd.line) {
+ var tmp = copyCursor(curEnd);
curStart.line--;
curStart.ch = lineLength(cm, curStart.line);
+ curEnd = tmp;
+ cm.replaceRange('', curStart, curEnd);
+ } else {
+ cm.replaceSelections(replacement);
+ }
+ // restore the saved bookmark
+ if (selectionEnd) {
+ var curStartMark = cm.setBookmark(selectionStart);
+ var curEndMark = cm.setBookmark(selectionEnd);
+ if (vim.visualMode) {
+ vim.marks['<'] = curStartMark;
+ vim.marks['>'] = curEndMark;
+ } else {
+ vim.lastSelection.curStartMark = curStartMark;
+ vim.lastSelection.curEndMark = curEndMark;
+ }
}
- vimGlobalState.registerController.pushText(
- operatorArgs.registerName, 'delete', cm.getRange(curStart, curEnd),
- operatorArgs.linewise);
- cm.replaceRange('', curStart, curEnd);
if (operatorArgs.linewise) {
cm.setCursor(motions.moveToFirstNonWhiteSpaceCharacter(cm));
} else {
@@ -1868,23 +1978,32 @@
cm.setCursor(curStart);
cm.setCursor(motions.moveToFirstNonWhiteSpaceCharacter(cm));
},
- swapcase: function(cm, operatorArgs, _vim, curStart, curEnd, curOriginal) {
- var toSwap = cm.getRange(curStart, curEnd);
- var swapped = '';
- for (var i = 0; i < toSwap.length; i++) {
- var character = toSwap.charAt(i);
- swapped += isUpperCase(character) ? character.toLowerCase() :
- character.toUpperCase();
- }
- cm.replaceRange(swapped, curStart, curEnd);
+ swapcase: function(cm, operatorArgs, _vim, _curStart, _curEnd, _curOriginal) {
+ var selections = cm.getSelections();
+ var ranges = cm.listSelections();
+ var swapped = [];
+ for (var j = 0; j < selections.length; j++) {
+ var toSwap = selections[j];
+ var text = '';
+ for (var i = 0; i < toSwap.length; i++) {
+ var character = toSwap.charAt(i);
+ text += isUpperCase(character) ? character.toLowerCase() :
+ character.toUpperCase();
+ }
+ swapped.push(text);
+ }
+ cm.replaceSelections(swapped);
+ var curStart = ranges[0].anchor;
+ var curEnd = ranges[0].head;
if (!operatorArgs.shouldMoveCursor) {
- cm.setCursor(curOriginal);
+ cm.setCursor(cursorIsBefore(curStart, curEnd) ? curStart : curEnd);
}
},
- yank: function(cm, operatorArgs, _vim, curStart, curEnd, curOriginal) {
+ yank: function(cm, operatorArgs, _vim, _curStart, _curEnd, curOriginal) {
+ var text = cm.getSelection();
vimGlobalState.registerController.pushText(
operatorArgs.registerName, 'yank',
- cm.getRange(curStart, curEnd), operatorArgs.linewise);
+ text, operatorArgs.linewise);
cm.setCursor(curOriginal);
}
};
@@ -2015,6 +2134,7 @@
var repeat = actionArgs.repeat;
var curStart = cm.getCursor();
var curEnd;
+ var selections = cm.listSelections();
// TODO: The repeat should actually select number of characters/lines
// equal to the repeat times the size of the previous visual
// operation.
@@ -2022,6 +2142,7 @@
cm.on('mousedown', exitVisualMode);
vim.visualMode = true;
vim.visualLine = !!actionArgs.linewise;
+ vim.visualBlock = !!actionArgs.blockwise;
if (vim.visualLine) {
curStart.ch = 0;
curEnd = clipCursorToContent(
@@ -2037,24 +2158,57 @@
} else {
curStart = cm.getCursor('anchor');
curEnd = cm.getCursor('head');
- if (!vim.visualLine && actionArgs.linewise) {
- // Shift-V pressed in characterwise visual mode. Switch to linewise
- // visual mode instead of exiting visual mode.
- vim.visualLine = true;
- curStart.ch = cursorIsBefore(curStart, curEnd) ? 0 :
+ if (vim.visualLine) {
+ if (actionArgs.blockwise) {
+ // This means Ctrl-V pressed in linewise visual
+ vim.visualBlock = true;
+ selectBlock(cm, curEnd);
+ CodeMirror.signal(cm, 'vim-mode-change', {mode: 'visual', subMode: 'blockwise'});
+ } else if (!actionArgs.linewise) {
+ // v pressed in linewise, switch to characterwise visual mode
+ CodeMirror.signal(cm, 'vim-mode-change', {mode: 'visual'});
+ } else {
+ exitVisualMode(cm);
+ }
+ vim.visualLine = false;
+ } else if (vim.visualBlock) {
+ if (actionArgs.linewise) {
+ // Shift-V pressed in blockwise visual mode
+ vim.visualLine = true;
+ curStart = Pos(selections[0].anchor.line, 0);
+ curEnd = Pos(selections[selections.length-1].anchor.line, lineLength(cm, selections[selections.length-1].anchor.line));
+ cm.setSelection(curStart, curEnd);
+ CodeMirror.signal(cm, 'vim-mode-change', {mode: 'visual', subMode: 'linewise'});
+ } else if (!actionArgs.blockwise) {
+ // v pressed in blockwise mode, Switch to characterwise
+ if (curEnd != selections[0].head) {
+ curStart = selections[0].anchor;
+ } else {
+ curStart = selections[selections.length-1].anchor;
+ }
+ cm.setSelection(curStart, curEnd);
+ CodeMirror.signal(cm, 'vim-mode-change', {mode: 'visual'});
+ } else {
+ exitVisualMode(cm);
+ }
+ vim.visualBlock = false;
+ } else if (actionArgs.linewise) {
+ // Shift-V pressed in characterwise visual mode. Switch to linewise
+ // visual mode instead of exiting visual mode.
+ vim.visualLine = true;
+ curStart.ch = cursorIsBefore(curStart, curEnd) ? 0 :
lineLength(cm, curStart.line);
- curEnd.ch = cursorIsBefore(curStart, curEnd) ?
+ curEnd.ch = cursorIsBefore(curStart, curEnd) ?
lineLength(cm, curEnd.line) : 0;
- cm.setSelection(curStart, curEnd);
- CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: "linewise"});
- } else if (vim.visualLine && !actionArgs.linewise) {
- // v pressed in linewise visual mode. Switch to characterwise visual
- // mode instead of exiting visual mode.
- vim.visualLine = false;
- CodeMirror.signal(cm, "vim-mode-change", {mode: "visual"});
- } else {
- exitVisualMode(cm);
- }
+ cm.setSelection(curStart, curEnd);
+ CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: "linewise"});
+ } else if (actionArgs.blockwise) {
+ vim.visualBlock = true;
+ selectBlock(cm, curEnd);
+ CodeMirror.signal(cm, 'vim-mode-change', {mode: 'visual', subMode: 'blockwise'});
+ } else {
+ exitVisualMode(cm);
+ }
}
updateMark(cm, vim, '<', cursorIsBefore(curStart, curEnd) ? curStart
: curEnd);
@@ -2062,27 +2216,40 @@
: curStart);
},
reselectLastSelection: function(cm, _actionArgs, vim) {
+ var curStart = vim.marks['<'].find();
+ var curEnd = vim.marks['>'].find();
var lastSelection = vim.lastSelection;
if (lastSelection) {
- var curStart = lastSelection.curStartMark.find();
- var curEnd = lastSelection.curEndMark.find();
- cm.setSelection(curStart, curEnd);
+ // Set the selections as per last selection
+ var selectionStart = lastSelection.curStartMark.find();
+ var selectionEnd = lastSelection.curEndMark.find();
+ var blockwise = lastSelection.visualBlock;
+ // update last selection
+ updateLastSelection(cm, vim, curStart, curEnd);
+ if (blockwise) {
+ cm.setCursor(selectionStart);
+ selectionStart = selectBlock(cm, selectionEnd);
+ } else {
+ cm.setSelection(selectionStart, selectionEnd);
+ selectionStart = cm.getCursor('anchor');
+ selectionEnd = cm.getCursor('head');
+ }
if (vim.visualMode) {
- updateLastSelection(cm, vim);
- var selectionStart = cm.getCursor('anchor');
- var selectionEnd = cm.getCursor('head');
updateMark(cm, vim, '<', cursorIsBefore(selectionStart, selectionEnd) ? selectionStart
- : selectionEnd);
+ : selectionEnd);
updateMark(cm, vim, '>', cursorIsBefore(selectionStart, selectionEnd) ? selectionEnd
- : selectionStart);
+ : selectionStart);
}
+ // Last selection is updated now
+ vim.visualMode = true;
if (lastSelection.visualLine) {
- vim.visualMode = true;
vim.visualLine = true;
- }
- else {
- vim.visualMode = true;
+ vim.visualBlock = false;
+ } else if (lastSelection.visualBlock) {
vim.visualLine = false;
+ vim.visualBlock = true;
+ } else {
+ vim.visualBlock = vim.visualLine = false;
}
CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: vim.visualLine ? "linewise" : ""});
}
@@ -2224,6 +2391,9 @@
}
}
cm.setCursor(curPosFinal);
+ if (vim.visualMode) {
+ exitVisualMode(cm);
+ }
},
undo: function(cm, actionArgs) {
cm.operation(function() {
@@ -2246,10 +2416,11 @@
var curStart = cm.getCursor();
var replaceTo;
var curEnd;
- if (vim.visualMode){
- curStart=cm.getCursor('start');
- curEnd=cm.getCursor('end');
- }else{
+ var selections = cm.listSelections();
+ if (vim.visualMode) {
+ curStart = cm.getCursor('start');
+ curEnd = cm.getCursor('end');
+ } else {
var line = cm.getLine(curStart.line);
replaceTo = curStart.ch + actionArgs.repeat;
if (replaceTo > line.length) {
@@ -2257,19 +2428,29 @@
}
curEnd = Pos(curStart.line, replaceTo);
}
- if (replaceWith=='\n'){
+ 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);
+ } 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){
+ replaceWithStr = replaceWithStr.replace(/[^\n]/g, replaceWith);
+ if (vim.visualBlock) {
+ // Tabs are split in visua block before replacing
+ var spaces = new Array(cm.options.tabSize+1).join(' ');
+ replaceWithStr = cm.getSelection();
+ replaceWithStr = replaceWithStr.replace(/\t/g, spaces).replace(/[^\n]/g, replaceWith).split('\n');
+ cm.replaceSelections(replaceWithStr);
+ } else {
+ cm.replaceRange(replaceWithStr, curStart, curEnd);
+ }
+ if (vim.visualMode) {
+ curStart = cursorIsBefore(selections[0].anchor, selections[0].head) ?
+ selections[0].anchor : selections[0].head;
cm.setCursor(curStart);
exitVisualMode(cm);
- }else{
+ } else {
cm.setCursor(offsetCursor(curEnd, 0, -1));
}
}
@@ -2314,17 +2495,26 @@
repeatLastEdit(cm, vim, repeat, false /** repeatForInsert */);
},
changeCase: function(cm, actionArgs, vim) {
- var selectedAreaRange = getSelectedAreaRange(cm, vim);
- var selectionStart = selectedAreaRange[0];
- var selectionEnd = selectedAreaRange[1];
+ var selectionStart = getSelectedAreaRange(cm, vim)[0];
+ var text = cm.getSelection();
+ var lastSelectionCurEnd;
+ var blockSelection;
+ if (vim.lastSelection) {
// save the curEnd marker to avoid its removal due to cm.replaceRange
- var lastSelectionCurEnd = vim.lastSelection.curEndMark.find();
+ lastSelectionCurEnd = vim.lastSelection.curEndMark.find();
+ blockSelection = vim.lastSelection.visualBlock;
+ }
var toLower = actionArgs.toLower;
- var text = cm.getRange(selectionStart, selectionEnd);
- cm.replaceRange(toLower ? text.toLowerCase() : text.toUpperCase(), selectionStart, selectionEnd);
+ text = toLower ? text.toLowerCase() : text.toUpperCase();
+ cm.replaceSelections(vim.visualBlock || blockSelection ? text.split('\n') : [text]);
// restore the last selection curEnd marker
- vim.lastSelection.curEndMark = cm.setBookmark(lastSelectionCurEnd);
+ if (lastSelectionCurEnd) {
+ vim.lastSelection.curEndMark = cm.setBookmark(lastSelectionCurEnd);
+ }
cm.setCursor(selectionStart);
+ if (vim.visualMode) {
+ exitVisualMode(cm);
+ }
}
};
@@ -2407,45 +2597,161 @@
function escapeRegex(s) {
return s.replace(/([.?*+$\[\]\/\\(){}|\-])/g, '\\$1');
}
+ // This functions selects a rectangular block
+ // of text with selectionEnd as any of its corner
+ // Height of block:
+ // Difference in selectionEnd.line and first/last selection.line
+ // Width of the block:
+ // Distance between selectionEnd.ch and any(first considered here) selection.ch
+ function selectBlock(cm, selectionEnd) {
+ var selections = [], ranges = cm.listSelections();
+ var firstRange = ranges[0].anchor, lastRange = ranges[ranges.length-1].anchor;
+ var start, end, selectionStart;
+ var curEnd = cm.getCursor('head');
+ var primIndex = getIndex(ranges, curEnd);
+ // sets to true when selectionEnd already lies inside the existing selections
+ var contains = getIndex(ranges, selectionEnd) < 0 ? false : true;
+ selectionEnd = cm.clipPos(selectionEnd);
+ // difference in distance of selectionEnd from each end of the block.
+ var near = Math.abs(firstRange.line - selectionEnd.line) - Math.abs(lastRange.line - selectionEnd.line);
+ if (near > 0) {
+ end = selectionEnd.line;
+ start = firstRange.line;
+ if (lastRange.line == selectionEnd.line && contains) {
+ start = end;
+ }
+ } else if (near < 0) {
+ start = selectionEnd.line;
+ end = lastRange.line;
+ if (firstRange.line == selectionEnd.line && contains) {
+ end = start;
+ }
+ } else {
+ // Case where selectionEnd line is halfway between the 2 ends.
+ // We remove the primary selection in this case
+ if (primIndex == 0) {
+ start = selectionEnd.line;
+ end = lastRange.line;
+ } else {
+ start = firstRange.line;
+ end = selectionEnd.line;
+ }
+ }
+ if (start > end) {
+ var tmp = start;
+ start = end;
+ end = tmp;
+ }
+ selectionStart = (near > 0) ? firstRange : lastRange;
+ while (start <= end) {
+ var anchor = {line: start, ch: selectionStart.ch};
+ var head = {line: start, ch: selectionEnd.ch};
+ // Shift the anchor right or left
+ // as each selection crosses itself.
+ if ((anchor.ch < curEnd.ch) && ((head.ch == anchor.ch) || (anchor.ch - head.ch == 1))) {
+ anchor.ch++;
+ head.ch--;
+ } else if ((anchor.ch > curEnd.ch) && ((head.ch == anchor.ch) || (anchor.ch - head.ch == -1))) {
+ anchor.ch--;
+ head.ch++;
+ }
+ var range = {anchor: anchor, head: head};
+ selections.push(range);
+ if (cursorEqual(head, selectionEnd)) {
+ primIndex = selections.indexOf(range);
+ }
+ start++;
+ }
+ // Update selectionEnd and selectionStart
+ // after selection crossing
+ selectionEnd.ch = selections[0].head.ch;
+ selectionStart.ch = selections[0].anchor.ch;
+ cm.setSelections(selections, primIndex);
+ return selectionStart;
+ }
+ function getIndex(ranges, head) {
+ for (var i = 0; i < ranges.length; i++) {
+ if (cursorEqual(ranges[i].head, head)) {
+ return i;
+ }
+ }
+ return -1;
+ }
function getSelectedAreaRange(cm, vim) {
- var selectionStart = cm.getCursor('anchor');
- var selectionEnd = cm.getCursor('head');
var lastSelection = vim.lastSelection;
- if (!vim.visualMode) {
- var lastSelectionCurStart = vim.lastSelection.curStartMark.find();
- var lastSelectionCurEnd = vim.lastSelection.curEndMark.find();
- var line = lastSelectionCurEnd.line - lastSelectionCurStart.line;
- var ch = line ? lastSelectionCurEnd.ch : lastSelectionCurEnd.ch - lastSelectionCurStart.ch;
- selectionEnd = {line: selectionEnd.line + line, ch: line ? selectionEnd.ch : ch + selectionEnd.ch};
- if (lastSelection.visualLine) {
- return [{line: selectionStart.line, ch: 0}, {line: selectionEnd.line, ch: lineLength(cm, selectionEnd.line)}];
+ var getCurrentSelectedAreaRange = function() {
+ var selections = cm.listSelections();
+ var start = selections[0];
+ var end = selections[selections.length-1];
+ var selectionStart = cursorIsBefore(start.anchor, start.head) ? start.anchor : start.head;
+ var selectionEnd = cursorIsBefore(end.anchor, end.head) ? end.head : end.anchor;
+ return [selectionStart, selectionEnd];
+ };
+ var getLastSelectedAreaRange = function() {
+ var selectionStart = cm.getCursor();
+ var selectionEnd = cm.getCursor();
+ var block = lastSelection.visualBlock;
+ if (block) {
+ var width = block.width;
+ var height = block.height;
+ selectionEnd = Pos(selectionStart.line + height, selectionStart.ch + width);
+ var selections = [];
+ // selectBlock creates a 'proper' rectangular block.
+ // We do not want that in all cases, so we manually set selections.
+ for (var i = selectionStart.line; i < selectionEnd.line; i++) {
+ var anchor = Pos(i, selectionStart.ch);
+ var head = Pos(i, selectionEnd.ch);
+ var range = {anchor: anchor, head: head};
+ selections.push(range);
+ }
+ cm.setSelections(selections);
+ } else {
+ var start = lastSelection.curStartMark.find();
+ var end = lastSelection.curEndMark.find();
+ var line = end.line - start.line;
+ var ch = end.ch - start.ch;
+ selectionEnd = {line: selectionEnd.line + line, ch: line ? selectionEnd.ch : ch + selectionEnd.ch};
+ if (lastSelection.visualLine) {
+ selectionStart = Pos(selectionStart.line, 0);
+ selectionEnd = Pos(selectionEnd.line, lineLength(cm, selectionEnd.line));
+ }
+ cm.setSelection(selectionStart, selectionEnd);
}
+ return [selectionStart, selectionEnd];
+ };
+ if (!vim.visualMode) {
+ // In case of replaying the action.
+ return getLastSelectedAreaRange();
} else {
- if (cursorIsBefore(selectionEnd, selectionStart)) {
- var tmp = selectionStart;
- selectionStart = selectionEnd;
- selectionEnd = tmp;
- }
- exitVisualMode(cm);
+ return getCurrentSelectedAreaRange();
}
- return [selectionStart, selectionEnd];
}
- function updateLastSelection(cm, vim) {
- // We need the vim mark '<' to get the selection in case of yank and put
- var selectionStart = vim.marks['<'].find() || cm.getCursor('anchor');
- var selectionEnd = vim.marks['>'].find() ||cm.getCursor('head');
- // To accommodate the effect lastPastedText in the last selection
+ function updateLastSelection(cm, vim, selectionStart, selectionEnd) {
+ if (!selectionStart || !selectionEnd) {
+ selectionStart = vim.marks['<'].find() || cm.getCursor('anchor');
+ selectionEnd = vim.marks['>'].find() || cm.getCursor('head');
+ }
+ // To accommodate the effect of lastPastedText in the last selection
if (vim.lastPastedText) {
- selectionEnd = cm.posFromIndex(cm.indexFromPos(selectionStart) + vim.lastPastedText.length-1);
+ selectionEnd = cm.posFromIndex(cm.indexFromPos(selectionStart) + vim.lastPastedText.length);
vim.lastPastedText = null;
}
+ var ranges = cm.listSelections();
+ // This check ensures to set the cursor
+ // position where we left off in previous selection
+ var swap = getIndex(ranges, selectionStart) > -1;
+ if (vim.visualBlock) {
+ var height = Math.abs(selectionStart.line - selectionEnd.line)+1;
+ var width = Math.abs(selectionStart.ch - selectionEnd.ch);
+ var block = {height: height, width: width};
+ }
// can't use selection state here because yank has already reset its cursor
// Also, Bookmarks make the visual selections robust to edit operations
- vim.lastSelection = {'curStartMark': cm.setBookmark(selectionStart), 'curEndMark': cm.setBookmark(selectionEnd), 'visualMode': vim.visualMode, 'visualLine': vim.visualLine};
- if (cursorIsBefore(selectionEnd, selectionStart)) {
- vim.lastSelection.curStartMark = cm.setBookmark(selectionEnd);
- vim.lastSelection.curEndMark = cm.setBookmark(selectionStart);
- }
+ vim.lastSelection = {'curStartMark': cm.setBookmark(swap ? selectionEnd : selectionStart),
+ 'curEndMark': cm.setBookmark(swap ? selectionStart : selectionEnd),
+ 'visualMode': vim.visualMode,
+ 'visualLine': vim.visualLine,
+ 'visualBlock': block};
}
function exitVisualMode(cm) {
@@ -2456,6 +2762,7 @@
updateLastSelection(cm, vim);
vim.visualMode = false;
vim.visualLine = false;
+ vim.visualBlock = false;
if (!cursorEqual(selectionStart, selectionEnd)) {
// Clear the selection and set the cursor only if the selection has not
// already been cleared. Otherwise we risk moving the cursor somewhere
diff --git a/lib/codemirror.js b/lib/codemirror.js
index bafc9fa142..95856424c9 100644
--- a/lib/codemirror.js
+++ b/lib/codemirror.js
@@ -108,6 +108,7 @@
for (var opt in optionHandlers) if (optionHandlers.hasOwnProperty(opt))
optionHandlers[opt](cm, options[opt], Init);
+ maybeUpdateLineNumberWidth(cm);
for (var i = 0; i < initHooks.length; ++i) initHooks[i](cm);
});
}
@@ -467,18 +468,18 @@
}
// Compute the lines that are visible in a given viewport (defaults
- // the the current scroll position). viewPort may contain top,
+ // the the current scroll position). viewport may contain top,
// height, and ensure (see op.scrollToPos) properties.
- function visibleLines(display, doc, viewPort) {
- var top = viewPort && viewPort.top != null ? Math.max(0, viewPort.top) : display.scroller.scrollTop;
+ function visibleLines(display, doc, viewport) {
+ var top = viewport && viewport.top != null ? Math.max(0, viewport.top) : display.scroller.scrollTop;
top = Math.floor(top - paddingTop(display));
- var bottom = viewPort && viewPort.bottom != null ? viewPort.bottom : top + display.wrapper.clientHeight;
+ var bottom = viewport && viewport.bottom != null ? viewport.bottom : top + display.wrapper.clientHeight;
var from = lineAtHeight(doc, top), to = lineAtHeight(doc, bottom);
// Ensure is a {from: {line, ch}, to: {line, ch}} object, and
// forces those lines into the viewport (if possible).
- if (viewPort && viewPort.ensure) {
- var ensureFrom = viewPort.ensure.from.line, ensureTo = viewPort.ensure.to.line;
+ if (viewport && viewport.ensure) {
+ var ensureFrom = viewport.ensure.from.line, ensureTo = viewport.ensure.to.line;
if (ensureFrom < from)
return {from: ensureFrom,
to: lineAtHeight(doc, heightAtLine(getLine(doc, ensureFrom)) + display.wrapper.clientHeight)};
@@ -543,83 +544,46 @@
// DISPLAY DRAWING
- // Updates the display, selection, and scrollbars, using the
- // information in display.view to find out which nodes are no longer
- // up-to-date. Tries to bail out early when no changes are needed,
- // unless forced is true.
- // Returns true if an actual update happened, false otherwise.
- function updateDisplay(cm, viewPort, forced) {
- var oldFrom = cm.display.viewFrom, oldTo = cm.display.viewTo, updated;
- var visible = visibleLines(cm.display, cm.doc, viewPort);
- for (var first = true;; first = false) {
- var oldWidth = cm.display.scroller.clientWidth;
- if (!updateDisplayInner(cm, visible, forced)) break;
- updated = true;
-
- // If the max line changed since it was last measured, measure it,
- // and ensure the document's width matches it.
- if (cm.display.maxLineChanged && !cm.options.lineWrapping)
- adjustContentWidth(cm);
-
- var barMeasure = measureForScrollbars(cm);
- updateSelection(cm);
- setDocumentHeight(cm, barMeasure);
- updateScrollbars(cm, barMeasure);
- if (webkit && cm.options.lineWrapping)
- checkForWebkitWidthBug(cm, barMeasure); // (Issue #2420)
- if (webkit && barMeasure.scrollWidth > barMeasure.clientWidth &&
- barMeasure.scrollWidth < barMeasure.clientWidth + 1 &&
- !hScrollbarTakesSpace(cm))
- updateScrollbars(cm); // (Issue #2562)
- if (first && cm.options.lineWrapping && oldWidth != cm.display.scroller.clientWidth) {
- forced = true;
- continue;
- }
- forced = false;
-
- // Clip forced viewport to actual scrollable area.
- if (viewPort && viewPort.top != null)
- viewPort = {top: Math.min(barMeasure.docHeight - scrollerCutOff - barMeasure.clientHeight, viewPort.top)};
- // Updated line heights might result in the drawn area not
- // actually covering the viewport. Keep looping until it does.
- visible = visibleLines(cm.display, cm.doc, viewPort);
- if (visible.from >= cm.display.viewFrom && visible.to <= cm.display.viewTo)
- break;
- }
+ function DisplayUpdate(cm, viewport, force) {
+ var display = cm.display;
- cm.display.updateLineNumbers = null;
- if (updated) {
- signalLater(cm, "update", cm);
- if (cm.display.viewFrom != oldFrom || cm.display.viewTo != oldTo)
- signalLater(cm, "viewportChange", cm, cm.display.viewFrom, cm.display.viewTo);
- }
- return updated;
+ this.viewport = viewport;
+ // Store some values that we'll need later (but don't want to force a relayout for)
+ this.visible = visibleLines(display, cm.doc, viewport);
+ this.editorIsHidden = !display.wrapper.offsetWidth;
+ this.wrapperHeight = display.wrapper.clientHeight;
+ this.oldViewFrom = display.viewFrom; this.oldViewTo = display.viewTo;
+ this.oldScrollerWidth = display.scroller.clientWidth;
+ this.force = force;
+ this.dims = getDimensions(cm);
}
// Does the actual updating of the line display. Bails out
// (returning false) when there is nothing to be done and forced is
// false.
- function updateDisplayInner(cm, visible, forced) {
+ function updateDisplayIfNeeded(cm, update) {
var display = cm.display, doc = cm.doc;
- if (!display.wrapper.offsetWidth) {
+ if (update.editorIsHidden) {
resetView(cm);
- return;
+ return false;
}
// Bail out if the visible area is already rendered and nothing changed.
- if (!forced && visible.from >= display.viewFrom && visible.to <= display.viewTo &&
+ if (!update.force &&
+ update.visible.from >= display.viewFrom && update.visible.to <= display.viewTo &&
(display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo) &&
countDirtyView(cm) == 0)
- return;
+ return false;
- if (maybeUpdateLineNumberWidth(cm))
+ if (maybeUpdateLineNumberWidth(cm)) {
resetView(cm);
- var dims = getDimensions(cm);
+ update.dims = getDimensions(cm);
+ }
// Compute a suitable new viewport (from & to)
var end = doc.first + doc.size;
- var from = Math.max(visible.from - cm.options.viewportMargin, doc.first);
- var to = Math.min(end, visible.to + cm.options.viewportMargin);
+ var from = Math.max(update.visible.from - cm.options.viewportMargin, doc.first);
+ var to = Math.min(end, update.visible.to + cm.options.viewportMargin);
if (display.viewFrom < from && from - display.viewFrom < 20) from = Math.max(doc.first, display.viewFrom);
if (display.viewTo > to && display.viewTo - to < 20) to = Math.min(end, display.viewTo);
if (sawCollapsedSpans) {
@@ -628,7 +592,7 @@
}
var different = from != display.viewFrom || to != display.viewTo ||
- display.lastSizeC != display.wrapper.clientHeight;
+ display.lastSizeC != update.wrapperHeight;
adjustView(cm, from, to);
display.viewOffset = heightAtLine(getLine(cm.doc, display.viewFrom));
@@ -636,13 +600,15 @@
cm.display.mover.style.top = display.viewOffset + "px";
var toUpdate = countDirtyView(cm);
- if (!different && toUpdate == 0 && !forced) return;
+ if (!different && toUpdate == 0 && !update.force &&
+ (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo))
+ return false;
// For big changes, we hide the enclosing element during the
// update, since that speeds up the operations on most browsers.
var focused = activeElt();
if (toUpdate > 4) display.lineDiv.style.display = "none";
- patchDisplay(cm, display.updateLineNumbers, dims);
+ patchDisplay(cm, display.updateLineNumbers, update.dims);
if (toUpdate > 4) display.lineDiv.style.display = "";
// There might have been a widget with a focused element that got
// hidden or updated, if so re-focus it.
@@ -654,24 +620,54 @@
removeChildren(display.selectionDiv);
if (different) {
- display.lastSizeC = display.wrapper.clientHeight;
+ display.lastSizeC = update.wrapperHeight;
startWorker(cm, 400);
}
- updateHeightsInViewport(cm);
+ display.updateLineNumbers = null;
return true;
}
- function adjustContentWidth(cm) {
- var display = cm.display;
- var width = measureChar(cm, display.maxLine, display.maxLine.text.length).left;
- display.maxLineChanged = false;
- var minWidth = Math.max(0, width + 3);
- var maxScrollLeft = Math.max(0, display.sizer.offsetLeft + minWidth + scrollerCutOff - display.scroller.clientWidth);
- display.sizer.style.minWidth = minWidth + "px";
- if (maxScrollLeft < cm.doc.scrollLeft)
- setScrollLeft(cm, Math.min(display.scroller.scrollLeft, maxScrollLeft), true);
+ function postUpdateDisplay(cm, update) {
+ var force = update.force, viewport = update.viewport;
+ for (var first = true;; first = false) {
+ if (first && cm.options.lineWrapping && update.oldScrollerWidth != cm.display.scroller.clientWidth) {
+ force = true;
+ } else {
+ force = false;
+ // Clip forced viewport to actual scrollable area.
+ if (viewport && viewport.top != null)
+ viewport = {top: Math.min(cm.doc.height + paddingVert(cm.display) - scrollerCutOff -
+ cm.display.scroller.clientHeight, viewport.top)};
+ // Updated line heights might result in the drawn area not
+ // actually covering the viewport. Keep looping until it does.
+ update.visible = visibleLines(cm.display, cm.doc, viewport);
+ if (update.visible.from >= cm.display.viewFrom && update.visible.to <= cm.display.viewTo)
+ break;
+ }
+ if (!updateDisplayIfNeeded(cm, update)) break;
+ updateHeightsInViewport(cm);
+ var barMeasure = measureForScrollbars(cm);
+ updateSelection(cm);
+ setDocumentHeight(cm, barMeasure);
+ updateScrollbars(cm, barMeasure);
+ }
+
+ signalLater(cm, "update", cm);
+ if (cm.display.viewFrom != update.oldViewFrom || cm.display.viewTo != update.oldViewTo)
+ signalLater(cm, "viewportChange", cm, cm.display.viewFrom, cm.display.viewTo);
+ }
+
+ function updateDisplaySimple(cm, viewport) {
+ var update = new DisplayUpdate(cm, viewport);
+ if (updateDisplayIfNeeded(cm, update)) {
+ postUpdateDisplay(cm, update);
+ var barMeasure = measureForScrollbars(cm);
+ updateSelection(cm);
+ setDocumentHeight(cm, barMeasure);
+ updateScrollbars(cm, barMeasure);
+ }
}
function setDocumentHeight(cm, measure) {
@@ -1257,10 +1253,10 @@
// SELECTION DRAWING
// Redraw the selection and/or cursor
- function updateSelection(cm) {
- var display = cm.display, doc = cm.doc;
- var curFragment = document.createDocumentFragment();
- var selFragment = document.createDocumentFragment();
+ function drawSelection(cm) {
+ var display = cm.display, doc = cm.doc, result = {};
+ var curFragment = result.cursors = document.createDocumentFragment();
+ var selFragment = result.selection = document.createDocumentFragment();
for (var i = 0; i < doc.sel.ranges.length; i++) {
var range = doc.sel.ranges[i];
@@ -1275,16 +1271,23 @@
if (cm.options.moveInputWithCursor) {
var headPos = cursorCoords(cm, doc.sel.primary().head, "div");
var wrapOff = display.wrapper.getBoundingClientRect(), lineOff = display.lineDiv.getBoundingClientRect();
- var top = Math.max(0, Math.min(display.wrapper.clientHeight - 10,
- headPos.top + lineOff.top - wrapOff.top));
- var left = Math.max(0, Math.min(display.wrapper.clientWidth - 10,
- headPos.left + lineOff.left - wrapOff.left));
- display.inputDiv.style.top = top + "px";
- display.inputDiv.style.left = left + "px";
+ result.teTop = Math.max(0, Math.min(display.wrapper.clientHeight - 10,
+ headPos.top + lineOff.top - wrapOff.top));
+ result.teLeft = Math.max(0, Math.min(display.wrapper.clientWidth - 10,
+ headPos.left + lineOff.left - wrapOff.left));
}
- removeChildrenAndAdd(display.cursorDiv, curFragment);
- removeChildrenAndAdd(display.selectionDiv, selFragment);
+ return result;
+ }
+
+ function updateSelection(cm, drawn) {
+ if (!drawn) drawn = drawSelection(cm);
+ removeChildrenAndAdd(cm.display.cursorDiv, drawn.cursors);
+ removeChildrenAndAdd(cm.display.selectionDiv, drawn.selection);
+ if (drawn.teTop != null) {
+ cm.display.inputDiv.style.top = drawn.teTop + "px";
+ cm.display.inputDiv.style.left = drawn.teLeft + "px";
+ }
}
// Draws a cursor for the given range
@@ -1408,8 +1411,8 @@
if (doc.frontier >= cm.display.viewTo) return;
var end = +new Date + cm.options.workTime;
var state = copyState(doc.mode, getStateBefore(cm, doc.frontier));
+ var changedLines = [];
- runInOp(cm, function() {
doc.iter(doc.frontier, Math.min(doc.first + doc.size, cm.display.viewTo + 500), function(line) {
if (doc.frontier >= cm.display.viewFrom) { // Visible
var oldStyles = line.styles;
@@ -1421,7 +1424,7 @@
var ischange = !oldStyles || oldStyles.length != line.styles.length ||
oldCls != newCls && (!oldCls || !newCls || oldCls.bgClass != newCls.bgClass || oldCls.textClass != newCls.textClass);
for (var i = 0; !ischange && i < oldStyles.length; ++i) ischange = oldStyles[i] != line.styles[i];
- if (ischange) regLineChange(cm, doc.frontier, "text");
+ if (ischange) changedLines.push(doc.frontier);
line.stateAfter = copyState(doc.mode, state);
} else {
processLine(cm, line.text, state);
@@ -1433,6 +1436,9 @@
return true;
}
});
+ if (changedLines.length) runInOp(cm, function() {
+ for (var i = 0; i < changedLines.length; i++)
+ regLineChange(cm, changedLines[i], "text");
});
}
@@ -1665,6 +1671,8 @@
rect = nullRect;
}
+ if (ie && ie_version < 11) rect = maybeUpdateRectForZooming(cm.display.measure, rect);
+
var rtop = rect.top - prepared.rect.top, rbot = rect.bottom - prepared.rect.top;
var mid = (rtop + rbot) / 2;
var heights = prepared.view.measure.heights;
@@ -1676,9 +1684,22 @@
top: top, bottom: bot};
if (!rect.left && !rect.right) result.bogus = true;
if (!cm.options.singleCursorHeightPerLine) { result.rtop = rtop; result.rbottom = rbot; }
+
return result;
}
+ // Work around problem with bounding client rects on ranges being
+ // returned incorrectly when zoomed on IE10 and below.
+ function maybeUpdateRectForZooming(measure, rect) {
+ if (!window.screen || screen.logicalXDPI == null ||
+ screen.logicalXDPI == screen.deviceXDPI || !hasBadZoomedRects(measure))
+ return rect;
+ var scaleX = screen.logicalXDPI / screen.deviceXDPI;
+ var scaleY = screen.logicalYDPI / screen.deviceYDPI;
+ return {left: rect.left * scaleX, right: rect.right * scaleX,
+ top: rect.top * scaleY, bottom: rect.bottom * scaleY};
+ }
+
function clearLineMeasurementCacheFor(lineView) {
if (lineView.measure) {
lineView.measure.cache = {};
@@ -1911,10 +1932,13 @@
// error-prone). Instead, display updates are batched and then all
// combined and executed at once.
+ var operationGroup = null;
+
var nextOpId = 0;
// Start a new operation.
function startOperation(cm) {
cm.curOp = {
+ cm: cm,
viewChanged: false, // Flag that indicates that lines might need to be redrawn
startHeight: cm.doc.height, // Used to detect need to update scrollbar
forceUpdate: false, // Used to force a redraw
@@ -1922,33 +1946,129 @@
typing: false, // Whether this reset should be careful to leave existing text (for compositing)
changeObjs: null, // Accumulated changes, for firing change events
cursorActivityHandlers: null, // Set of handlers to fire cursorActivity on
+ cursorActivityCalled: 0, // Tracks which cursorActivity handlers have been called already
selectionChanged: false, // Whether the selection needs to be redrawn
updateMaxLine: false, // Set when the widest line needs to be determined anew
scrollLeft: null, scrollTop: null, // Intermediate scroll position, not pushed to DOM yet
scrollToPos: null, // Used to scroll to a specific position
id: ++nextOpId // Unique ID
};
- if (!delayedCallbackDepth++) delayedCallbacks = [];
+ if (operationGroup) {
+ operationGroup.ops.push(cm.curOp);
+ } else {
+ cm.curOp.ownsGroup = operationGroup = {
+ ops: [cm.curOp],
+ delayedCallbacks: []
+ };
+ }
+ }
+
+ function fireCallbacksForOps(group) {
+ // Calls delayed callbacks and cursorActivity handlers until no
+ // new ones appear
+ var callbacks = group.delayedCallbacks, i = 0;
+ do {
+ for (; i < callbacks.length; i++)
+ callbacks[i]();
+ for (var j = 0; j < group.ops.length; j++) {
+ var op = group.ops[j];
+ if (op.cursorActivityHandlers)
+ while (op.cursorActivityCalled < op.cursorActivityHandlers.length)
+ op.cursorActivityHandlers[op.cursorActivityCalled++](op.cm);
+ }
+ } while (i < callbacks.length);
}
// Finish an operation, updating the display and signalling delayed events
function endOperation(cm) {
- var op = cm.curOp, doc = cm.doc, display = cm.display;
- cm.curOp = null;
-
+ var op = cm.curOp, group = op.ownsGroup;
+ if (!group) return;
+
+ try { fireCallbacksForOps(group); }
+ finally {
+ operationGroup = null;
+ for (var i = 0; i < group.ops.length; i++)
+ group.ops[i].cm.curOp = null;
+ endOperations(group);
+ }
+ }
+
+ // The DOM updates done when an operation finishes are batched so
+ // that the minimum number of relayouts are required.
+ function endOperations(group) {
+ var ops = group.ops;
+ for (var i = 0; i < ops.length; i++) // Read DOM
+ endOperation_R1(ops[i]);
+ for (var i = 0; i < ops.length; i++) // Write DOM (maybe)
+ endOperation_W1(ops[i]);
+ for (var i = 0; i < ops.length; i++) // Read DOM
+ endOperation_R2(ops[i]);
+ for (var i = 0; i < ops.length; i++) // Write DOM (maybe)
+ endOperation_W2(ops[i]);
+ for (var i = 0; i < ops.length; i++) // Read DOM
+ endOperation_finish(ops[i]);
+ }
+
+ function endOperation_R1(op) {
+ var cm = op.cm, display = cm.display;
if (op.updateMaxLine) findMaxLine(cm);
- // If it looks like an update might be needed, call updateDisplay
- if (op.viewChanged || op.forceUpdate || op.scrollTop != null ||
- op.scrollToPos && (op.scrollToPos.from.line < display.viewFrom ||
- op.scrollToPos.to.line >= display.viewTo) ||
- display.maxLineChanged && cm.options.lineWrapping) {
- var updated = updateDisplay(cm, {top: op.scrollTop, ensure: op.scrollToPos}, op.forceUpdate);
- if (cm.display.scroller.offsetHeight) cm.doc.scrollTop = cm.display.scroller.scrollTop;
+ op.mustUpdate = op.viewChanged || op.forceUpdate || op.scrollTop != null ||
+ op.scrollToPos && (op.scrollToPos.from.line < display.viewFrom ||
+ op.scrollToPos.to.line >= display.viewTo) ||
+ display.maxLineChanged && cm.options.lineWrapping;
+ op.update = op.mustUpdate &&
+ new DisplayUpdate(cm, op.mustUpdate && {top: op.scrollTop, ensure: op.scrollToPos}, op.forceUpdate);
+ }
+
+ function endOperation_W1(op) {
+ op.updatedDisplay = op.mustUpdate && updateDisplayIfNeeded(op.cm, op.update);
+ }
+
+ function endOperation_R2(op) {
+ var cm = op.cm, display = cm.display;
+ if (op.updatedDisplay) updateHeightsInViewport(cm);
+
+ // If the max line changed since it was last measured, measure it,
+ // and ensure the document's width matches it.
+ // updateDisplayIfNeeded will use these properties to do the actual resizing
+ if (display.maxLineChanged && !cm.options.lineWrapping) {
+ op.adjustWidthTo = measureChar(cm, display.maxLine, display.maxLine.text.length).left;
+ op.maxScrollLeft = Math.max(0, display.sizer.offsetLeft + op.adjustWidthTo +
+ scrollerCutOff - display.scroller.clientWidth);
+ }
+
+ op.barMeasure = measureForScrollbars(cm);
+ if (op.updatedDisplay || op.selectionChanged)
+ op.newSelectionNodes = drawSelection(cm);
+ }
+
+ function endOperation_W2(op) {
+ var cm = op.cm;
+
+ if (op.adjustWidthTo != null) {
+ cm.display.sizer.style.minWidth = op.adjustWidthTo + "px";
+ if (op.maxScrollLeft < cm.doc.scrollLeft)
+ setScrollLeft(cm, Math.min(cm.display.scroller.scrollLeft, op.maxScrollLeft), true);
}
- // If no update was run, but the selection changed, redraw that.
- if (!updated && op.selectionChanged) updateSelection(cm);
- if (!updated && op.startHeight != cm.doc.height) updateScrollbars(cm);
+
+ if (op.newSelectionNodes)
+ updateSelection(cm, op.newSelectionNodes);
+ if (op.updatedDisplay)
+ setDocumentHeight(cm, op.barMeasure);
+ if (op.updatedDisplay || op.startHeight != cm.doc.height)
+ updateScrollbars(cm, op.barMeasure);
+
+ if (op.selectionChanged) restartBlink(cm);
+
+ if (cm.state.focused && op.updateInput)
+ resetInput(cm, op.typing);
+ }
+
+ function endOperation_finish(op) {
+ var cm = op.cm, display = cm.display, doc = cm.doc;
+
+ if (op.updatedDisplay) postUpdateDisplay(cm, op.update);
// Abort mouse wheel delta measurement, when scrolling explicitly
if (display.wheelStartX != null && (op.scrollTop != null || op.scrollLeft != null || op.scrollToPos))
@@ -1966,16 +2086,11 @@
}
// If we need to scroll a specific position into view, do so.
if (op.scrollToPos) {
- var coords = scrollPosIntoView(cm, clipPos(cm.doc, op.scrollToPos.from),
- clipPos(cm.doc, op.scrollToPos.to), op.scrollToPos.margin);
+ var coords = scrollPosIntoView(cm, clipPos(doc, op.scrollToPos.from),
+ clipPos(doc, op.scrollToPos.to), op.scrollToPos.margin);
if (op.scrollToPos.isCursor && cm.state.focused) maybeScrollWindow(cm, coords);
}
- if (op.selectionChanged) restartBlink(cm);
-
- if (cm.state.focused && op.updateInput)
- resetInput(cm, op.typing);
-
// Fire events for markers that are hidden/unidden by editing or
// undoing
var hidden = op.maybeHiddenMarkers, unhidden = op.maybeUnhiddenMarkers;
@@ -1984,18 +2099,22 @@
if (unhidden) for (var i = 0; i < unhidden.length; ++i)
if (unhidden[i].lines.length) signal(unhidden[i], "unhide");
- var delayed;
- if (!--delayedCallbackDepth) {
- delayed = delayedCallbacks;
- delayedCallbacks = null;
+ if (display.wrapper.offsetHeight)
+ doc.scrollTop = cm.display.scroller.scrollTop;
+
+ // Apply workaround for two webkit bugs
+ if (op.updatedDisplay && webkit) {
+ if (cm.options.lineWrapping)
+ checkForWebkitWidthBug(cm, op.barMeasure); // (Issue #2420)
+ if (op.barMeasure.scrollWidth > op.barMeasure.clientWidth &&
+ op.barMeasure.scrollWidth < op.barMeasure.clientWidth + 1 &&
+ !hScrollbarTakesSpace(cm))
+ updateScrollbars(cm); // (Issue #2562)
}
+
// Fire change events, and delayed event handlers
if (op.changeObjs)
signal(cm, "changes", cm, op.changeObjs);
- if (delayed) for (var i = 0; i < delayed.length; ++i) delayed[i]();
- if (op.cursorActivityHandlers)
- for (var i = 0; i < op.cursorActivityHandlers.length; i++)
- op.cursorActivityHandlers[i](cm);
}
// Run the given function in an operation
@@ -2247,6 +2366,11 @@
cm.display.poll.set(20, p);
}
+ // This will be set to an array of strings when copying, so that,
+ // when pasting, we know what kind of selections the copied text
+ // was made out of.
+ var lastCopied = null;
+
// Read input from the textarea, and update the document to match.
// When something is selected, it is present in the textarea, and
// selected (unless it is huge, in which case a placeholder is
@@ -2269,8 +2393,11 @@
var text = input.value;
// If nothing changed, bail.
if (text == prevInput && !cm.somethingSelected()) return false;
- // Work around nonsensical selection resetting in IE9/10
- if (ie && ie_version >= 9 && cm.display.inputHasSelection === text) {
+ // Work around nonsensical selection resetting in IE9/10, and
+ // inexplicable appearance of private area unicode characters on
+ // some key combos in Mac (#2689).
+ if (ie && ie_version >= 9 && cm.display.inputHasSelection === text ||
+ mac && /[\uf700-\uf7ff]/.test(text)) {
resetInput(cm);
return false;
}
@@ -2287,7 +2414,13 @@
var inserted = text.slice(same), textLines = splitLines(inserted);
// When pasing N lines into N selections, insert one line per selection
- var multiPaste = cm.state.pasteIncoming && textLines.length > 1 && doc.sel.ranges.length == textLines.length;
+ var multiPaste = null;
+ if (cm.state.pasteIncoming && doc.sel.ranges.length > 1) {
+ if (lastCopied && lastCopied.join("\n") == inserted)
+ multiPaste = doc.sel.ranges.length % lastCopied.length == 0 && map(lastCopied, splitLines);
+ else if (textLines.length == doc.sel.ranges.length)
+ multiPaste = map(textLines, function(l) { return [l]; });
+ }
// Normal behavior is to insert the new text into every selection
for (var i = doc.sel.ranges.length - 1; i >= 0; i--) {
@@ -2300,7 +2433,7 @@
else if (cm.state.overwrite && range.empty() && !cm.state.pasteIncoming)
to = Pos(to.line, Math.min(getLine(doc, to.line).text.length, to.ch + lst(textLines).length));
var updateInput = cm.curOp.updateInput;
- var changeEvent = {from: from, to: to, text: multiPaste ? [textLines[i]] : textLines,
+ var changeEvent = {from: from, to: to, text: multiPaste ? multiPaste[i % multiPaste.length] : textLines,
origin: cm.state.pasteIncoming ? "paste" : cm.state.cutIncoming ? "cut" : "+input"};
makeChange(cm.doc, changeEvent);
signalLater(cm, "inputRead", cm, changeEvent);
@@ -2421,7 +2554,7 @@
// Prevent wrapper from ever scrolling
on(d.wrapper, "scroll", function() { d.wrapper.scrollTop = d.wrapper.scrollLeft = 0; });
- on(d.input, "keyup", operation(cm, onKeyUp));
+ on(d.input, "keyup", function(e) { onKeyUp.call(cm, e); });
on(d.input, "input", function() {
if (ie && ie_version >= 9 && cm.display.inputHasSelection) cm.display.inputHasSelection = null;
fastPoll(cm);
@@ -2467,27 +2600,29 @@
function prepareCopyCut(e) {
if (cm.somethingSelected()) {
+ lastCopied = cm.getSelections();
if (d.inaccurateSelection) {
d.prevInput = "";
d.inaccurateSelection = false;
- d.input.value = cm.getSelection();
+ d.input.value = lastCopied.join("\n");
selectInput(d.input);
}
} else {
- var text = "", ranges = [];
+ var text = [], ranges = [];
for (var i = 0; i < cm.doc.sel.ranges.length; i++) {
var line = cm.doc.sel.ranges[i].head.line;
var lineRange = {anchor: Pos(line, 0), head: Pos(line + 1, 0)};
ranges.push(lineRange);
- text += cm.getRange(lineRange.anchor, lineRange.head);
+ text.push(cm.getRange(lineRange.anchor, lineRange.head));
}
if (e.type == "cut") {
cm.setSelections(ranges, null, sel_dontScroll);
} else {
d.prevInput = "";
- d.input.value = text;
+ d.input.value = text.join("\n");
selectInput(d.input);
}
+ lastCopied = text;
}
if (e.type == "cut") cm.state.cutIncoming = true;
}
@@ -2885,10 +3020,10 @@
function setScrollTop(cm, val) {
if (Math.abs(cm.doc.scrollTop - val) < 2) return;
cm.doc.scrollTop = val;
- if (!gecko) updateDisplay(cm, {top: val});
+ if (!gecko) updateDisplaySimple(cm, {top: val});
if (cm.display.scroller.scrollTop != val) cm.display.scroller.scrollTop = val;
if (cm.display.scrollbarV.scrollTop != val) cm.display.scrollbarV.scrollTop = val;
- if (gecko) updateDisplay(cm);
+ if (gecko) updateDisplaySimple(cm);
startWorker(cm, 100);
}
// Sync scroller and scrollbar, ensure the gutter elements are
@@ -2971,7 +3106,7 @@
var top = cm.doc.scrollTop, bot = top + display.wrapper.clientHeight;
if (pixels < 0) top = Math.max(0, top + pixels - 50);
else bot = Math.min(cm.doc.height, bot + pixels + 50);
- updateDisplay(cm, {top: top, bottom: bot});
+ updateDisplaySimple(cm, {top: top, bottom: bot});
}
if (wheelSamples < 20) {
@@ -3114,13 +3249,13 @@
}
function onKeyUp(e) {
- if (signalDOMEvent(this, e)) return;
if (e.keyCode == 16) this.doc.sel.shift = false;
+ signalDOMEvent(this, e);
}
function onKeyPress(e) {
var cm = this;
- if (signalDOMEvent(cm, e) || e.ctrlKey || mac && e.metaKey) return;
+ if (signalDOMEvent(cm, e) || e.ctrlKey && !e.altKey || mac && e.metaKey) return;
var keyCode = e.keyCode, charCode = e.charCode;
if (presto && keyCode == lastStoppedKey) {lastStoppedKey = null; e_preventDefault(e); return;}
if (((presto && (!e.which || e.which < 10)) || khtml) && handleKeyBinding(cm, e)) return;
@@ -3184,7 +3319,9 @@
"px; left: " + (e.clientX - 5) + "px; z-index: 1000; background: " +
(ie ? "rgba(255, 255, 255, .05)" : "transparent") +
"; outline: none; border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);";
+ if (webkit) var oldScrollY = window.scrollY; // Work around Chrome issue (#2712)
focusInput(cm);
+ if (webkit) window.scrollTo(null, oldScrollY);
resetInput(cm);
// Adds "Select all" to context menu in FF
if (!cm.somethingSelected()) display.input.value = display.prevInput = " ";
@@ -3414,7 +3551,7 @@
antiChanges.push(historyChangeFromChange(doc, change));
- var after = i ? computeSelAfterChange(doc, change, null) : lst(source);
+ var after = i ? computeSelAfterChange(doc, change) : lst(source);
makeChangeSingleDoc(doc, change, after, mergeOldSpans(doc, change));
if (!i && doc.cm) doc.cm.scrollIntoView(change);
var rebased = [];
@@ -3473,7 +3610,7 @@
change.removed = getBetween(doc, change.from, change.to);
- if (!selAfter) selAfter = computeSelAfterChange(doc, change, null);
+ if (!selAfter) selAfter = computeSelAfterChange(doc, change);
if (doc.cm) makeChangeSingleDocInEditor(doc.cm, change, spans);
else updateDoc(doc, change, spans);
setSelectionNoUndo(doc, selAfter, sel_dontScroll);
@@ -3680,7 +3817,7 @@
if (how == "smart") {
// Fall back to "prev" when the mode doesn't have an indentation
// method.
- if (!cm.doc.mode.indent) how = "prev";
+ if (!doc.mode.indent) how = "prev";
else state = getStateBefore(cm, n);
}
@@ -3692,8 +3829,8 @@
indentation = 0;
how = "not";
} else if (how == "smart") {
- indentation = cm.doc.mode.indent(state, line.text.slice(curSpaceString.length), line.text);
- if (indentation == Pass) {
+ indentation = doc.mode.indent(state, line.text.slice(curSpaceString.length), line.text);
+ if (indentation == Pass || indentation > 150) {
if (!aggressive) return;
how = "prev";
}
@@ -3716,7 +3853,7 @@
if (pos < indentation) indentString += spaceStr(indentation - pos);
if (indentString != curSpaceString) {
- replaceRange(cm.doc, indentString, Pos(n, 0), Pos(n, curSpaceString.length), "+input");
+ replaceRange(doc, indentString, Pos(n, 0), Pos(n, curSpaceString.length), "+input");
} else {
// Ensure that, if the cursor was in the whitespace at the start
// of the line, it is moved to the end of that space.
@@ -4133,7 +4270,7 @@
triggerOnKeyDown: methodOp(onKeyDown),
triggerOnKeyPress: methodOp(onKeyPress),
- triggerOnKeyUp: methodOp(onKeyUp),
+ triggerOnKeyUp: onKeyUp,
execCommand: function(cmd) {
if (commands.hasOwnProperty(cmd))
@@ -4568,6 +4705,20 @@
return {from: Pos(range.from().line, 0), to: range.from()};
});
},
+ delWrappedLineLeft: function(cm) {
+ deleteNearSelection(cm, function(range) {
+ var top = cm.charCoords(range.head, "div").top + 5;
+ var leftPos = cm.coordsChar({left: 0, top: top}, "div");
+ return {from: leftPos, to: range.from()};
+ });
+ },
+ delWrappedLineRight: function(cm) {
+ deleteNearSelection(cm, function(range) {
+ var top = cm.charCoords(range.head, "div").top + 5;
+ var rightPos = cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div");
+ return {from: range.from(), to: rightPos };
+ });
+ },
undo: function(cm) {cm.undo();},
redo: function(cm) {cm.redo();},
undoSelection: function(cm) {cm.undoSelection();},
@@ -4705,11 +4856,11 @@
};
keyMap.macDefault = {
"Cmd-A": "selectAll", "Cmd-D": "deleteLine", "Cmd-Z": "undo", "Shift-Cmd-Z": "redo", "Cmd-Y": "redo",
- "Cmd-Up": "goDocStart", "Cmd-End": "goDocEnd", "Cmd-Down": "goDocEnd", "Alt-Left": "goGroupLeft",
- "Alt-Right": "goGroupRight", "Cmd-Left": "goLineStart", "Cmd-Right": "goLineEnd", "Alt-Backspace": "delGroupBefore",
+ "Cmd-Home": "goDocStart", "Cmd-Up": "goDocStart", "Cmd-End": "goDocEnd", "Cmd-Down": "goDocEnd", "Alt-Left": "goGroupLeft",
+ "Alt-Right": "goGroupRight", "Cmd-Left": "goLineLeft", "Cmd-Right": "goLineRight", "Alt-Backspace": "delGroupBefore",
"Ctrl-Alt-Backspace": "delGroupAfter", "Alt-Delete": "delGroupAfter", "Cmd-S": "save", "Cmd-F": "find",
"Cmd-G": "findNext", "Shift-Cmd-G": "findPrev", "Cmd-Alt-F": "replace", "Shift-Cmd-Alt-F": "replaceAll",
- "Cmd-[": "indentLess", "Cmd-]": "indentMore", "Cmd-Backspace": "delLineLeft",
+ "Cmd-[": "indentLess", "Cmd-]": "indentMore", "Cmd-Backspace": "delWrappedLineLeft", "Cmd-Delete": "delWrappedLineRight",
"Cmd-U": "undoSelection", "Shift-Cmd-U": "redoSelection",
fallthrough: ["basic", "emacsy"]
};
@@ -6960,6 +7111,8 @@
for (var i = 0; i < arr.length; ++i) arr[i].apply(null, args);
};
+ var orphanDelayedCallbacks = null;
+
// Often, we want to signal events at a point where we are in the
// middle of some work, but don't want the handler to start calling
// other methods on the editor, which might be in an inconsistent
@@ -6967,25 +7120,26 @@
// signalLater looks whether there are any handlers, and schedules
// them to be executed when the last operation ends, or, if no
// operation is active, when a timeout fires.
- var delayedCallbacks, delayedCallbackDepth = 0;
function signalLater(emitter, type /*, values...*/) {
var arr = emitter._handlers && emitter._handlers[type];
if (!arr) return;
- var args = Array.prototype.slice.call(arguments, 2);
- if (!delayedCallbacks) {
- ++delayedCallbackDepth;
- delayedCallbacks = [];
- setTimeout(fireDelayed, 0);
+ var args = Array.prototype.slice.call(arguments, 2), list;
+ if (operationGroup) {
+ list = operationGroup.delayedCallbacks;
+ } else if (orphanDelayedCallbacks) {
+ list = orphanDelayedCallbacks;
+ } else {
+ list = orphanDelayedCallbacks = [];
+ setTimeout(fireOrphanDelayed, 0);
}
function bnd(f) {return function(){f.apply(null, args);};};
for (var i = 0; i < arr.length; ++i)
- delayedCallbacks.push(bnd(arr[i]));
+ list.push(bnd(arr[i]));
}
- function fireDelayed() {
- --delayedCallbackDepth;
- var delayed = delayedCallbacks;
- delayedCallbacks = null;
+ function fireOrphanDelayed() {
+ var delayed = orphanDelayedCallbacks;
+ orphanDelayedCallbacks = null;
for (var i = 0; i < delayed.length; ++i) delayed[i]();
}
@@ -7331,6 +7485,15 @@
return typeof e.oncopy == "function";
})();
+ var badZoomedRects = null;
+ function hasBadZoomedRects(measure) {
+ if (badZoomedRects != null) return badZoomedRects;
+ var node = removeChildrenAndAdd(measure, elt("span", "x"));
+ var normal = node.getBoundingClientRect();
+ var fromRange = range(node, 0, 1).getBoundingClientRect();
+ return badZoomedRects = Math.abs(normal.left - fromRange.left) > 1;
+ }
+
// KEY NAMES
var keyNames = {3: "Enter", 8: "Backspace", 9: "Tab", 13: "Enter", 16: "Shift", 17: "Ctrl", 18: "Alt",
@@ -7632,7 +7795,7 @@
// THE END
- CodeMirror.version = "4.3.0";
+ CodeMirror.version = "4.4.0";
return CodeMirror;
});
diff --git a/mode/clike/clike.js b/mode/clike/clike.js
index 3e253624b4..2873e3629b 100644
--- a/mode/clike/clike.js
+++ b/mode/clike/clike.js
@@ -369,6 +369,7 @@ CodeMirror.defineMode("clike", function(config, parserConfig) {
),
+ multiLineStrings: true,
blockKeywords: words("catch class do else finally for forSome if match switch try while"),
atoms: words("true false null"),
hooks: {
diff --git a/mode/css/css.js b/mode/css/css.js
index 2678c57d59..2695d08150 100644
--- a/mode/css/css.js
+++ b/mode/css/css.js
@@ -461,13 +461,13 @@ CodeMirror.defineMode("css", function(config, parserConfig) {
"glyph-orientation-vertical", "text-anchor", "writing-mode"
], propertyKeywords = keySet(propertyKeywords_);
- var nonStandardPropertyKeywords = [
+ var nonStandardPropertyKeywords_ = [
"scrollbar-arrow-color", "scrollbar-base-color", "scrollbar-dark-shadow-color",
"scrollbar-face-color", "scrollbar-highlight-color", "scrollbar-shadow-color",
"scrollbar-3d-light-color", "scrollbar-track-color", "shape-inside",
"searchfield-cancel-button", "searchfield-decoration", "searchfield-results-button",
"searchfield-results-decoration", "zoom"
- ], nonStandardPropertyKeywords = keySet(nonStandardPropertyKeywords);
+ ], nonStandardPropertyKeywords = keySet(nonStandardPropertyKeywords_);
var colorKeywords_ = [
"aliceblue", "antiquewhite", "aqua", "aquamarine", "azure", "beige",
@@ -589,7 +589,7 @@ CodeMirror.defineMode("css", function(config, parserConfig) {
], fontProperties = keySet(fontProperties_);
var allWords = mediaTypes_.concat(mediaFeatures_).concat(propertyKeywords_)
- .concat(nonStandardPropertyKeywords).concat(colorKeywords_).concat(valueKeywords_);
+ .concat(nonStandardPropertyKeywords_).concat(colorKeywords_).concat(valueKeywords_);
CodeMirror.registerHelper("hintWords", "css", allWords);
function tokenCComment(stream, state) {
diff --git a/mode/index.html b/mode/index.html
index 91fb92bff8..1aa0b8d821 100644
--- a/mode/index.html
+++ b/mode/index.html
@@ -65,6 +65,7 @@
JavaScript
Jinja2
Julia
+ Kotlin
LESS
LiveScript
Lua
diff --git a/mode/javascript/javascript.js b/mode/javascript/javascript.js
index 315674be74..fdb066eb1f 100644
--- a/mode/javascript/javascript.js
+++ b/mode/javascript/javascript.js
@@ -298,6 +298,8 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
var result = function() {
var state = cx.state, indent = state.indented;
if (state.lexical.type == "stat") indent = state.lexical.indented;
+ else for (var outer = state.lexical; outer && outer.type == ")" && outer.align; outer = outer.prev)
+ indent = outer.indented;
state.lexical = new JSLexical(indent, cx.stream.column(), type, null, state.lexical, info);
};
result.lex = true;
diff --git a/mode/javascript/test.js b/mode/javascript/test.js
index a9cc993d3d..77cc695fef 100644
--- a/mode/javascript/test.js
+++ b/mode/javascript/test.js
@@ -128,6 +128,12 @@
" [keyword else]",
" [number 3];");
+ MT("indent_funarg",
+ "[variable foo]([number 10000],",
+ " [keyword function]([def a]) {",
+ " [keyword debugger];",
+ "};");
+
MT("indent_below_if",
"[keyword for] (;;)",
" [keyword if] ([variable foo])",
diff --git a/mode/kotlin/index.html b/mode/kotlin/index.html
new file mode 100644
index 0000000000..38700f3226
--- /dev/null
+++ b/mode/kotlin/index.html
@@ -0,0 +1,89 @@
+
+
+CodeMirror: Kotlin mode
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Kotlin mode
+
+
+
+package org.wasabi.http
+
+import java.util.concurrent.Executors
+import java.net.InetSocketAddress
+import org.wasabi.app.AppConfiguration
+import io.netty.bootstrap.ServerBootstrap
+import io.netty.channel.nio.NioEventLoopGroup
+import io.netty.channel.socket.nio.NioServerSocketChannel
+import org.wasabi.app.AppServer
+
+public class HttpServer(private val appServer: AppServer) {
+
+ val bootstrap: ServerBootstrap
+ val primaryGroup: NioEventLoopGroup
+ val workerGroup: NioEventLoopGroup
+
+ {
+ // Define worker groups
+ primaryGroup = NioEventLoopGroup()
+ workerGroup = NioEventLoopGroup()
+
+ // Initialize bootstrap of server
+ bootstrap = ServerBootstrap()
+
+ bootstrap.group(primaryGroup, workerGroup)
+ bootstrap.channel(javaClass())
+ bootstrap.childHandler(NettyPipelineInitializer(appServer))
+ }
+
+ public fun start(wait: Boolean = true) {
+ val channel = bootstrap.bind(appServer.configuration.port)?.sync()?.channel()
+
+ if (wait) {
+ channel?.closeFuture()?.sync()
+ }
+ }
+
+ public fun stop() {
+ // Shutdown all event loops
+ primaryGroup.shutdownGracefully()
+ workerGroup.shutdownGracefully()
+
+ // Wait till all threads are terminated
+ primaryGroup.terminationFuture().sync()
+ workerGroup.terminationFuture().sync()
+ }
+}
+
+
+
+ Mode for Kotlin (http://kotlin.jetbrains.org/)
+ Developed by Hadi Hariri (https://github.com/hhariri).
+ MIME type defined: text/x-kotlin.
+
diff --git a/mode/kotlin/kotlin.js b/mode/kotlin/kotlin.js
new file mode 100644
index 0000000000..73c84f6c4f
--- /dev/null
+++ b/mode/kotlin/kotlin.js
@@ -0,0 +1,280 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+"use strict";
+
+CodeMirror.defineMode("kotlin", function (config, parserConfig) {
+ function words(str) {
+ var obj = {}, words = str.split(" ");
+ for (var i = 0; i < words.length; ++i) obj[words[i]] = true;
+ return obj;
+ }
+
+ var multiLineStrings = parserConfig.multiLineStrings;
+
+ var keywords = words(
+ "package continue return object while break class data trait throw super" +
+ " when type this else This try val var fun for is in if do as true false null get set");
+ var softKeywords = words("import" +
+ " where by get set abstract enum open annotation override private public internal" +
+ " protected catch out vararg inline finally final ref");
+ var blockKeywords = words("catch class do else finally for if where try while enum");
+ var atoms = words("null true false this");
+
+ var curPunc;
+
+ function tokenBase(stream, state) {
+ var ch = stream.next();
+ if (ch == '"' || ch == "'") {
+ return startString(ch, stream, state);
+ }
+ // Wildcard import w/o trailing semicolon (import smth.*)
+ if (ch == "." && stream.eat("*")) {
+ return "word";
+ }
+ if (/[\[\]{}\(\),;\:\.]/.test(ch)) {
+ curPunc = ch;
+ return null;
+ }
+ if (/\d/.test(ch)) {
+ if (stream.eat(/eE/)) {
+ stream.eat(/\+\-/);
+ stream.eatWhile(/\d/);
+ }
+ return "number";
+ }
+ if (ch == "/") {
+ if (stream.eat("*")) {
+ state.tokenize.push(tokenComment);
+ return tokenComment(stream, state);
+ }
+ if (stream.eat("/")) {
+ stream.skipToEnd();
+ return "comment";
+ }
+ if (expectExpression(state.lastToken)) {
+ return startString(ch, stream, state);
+ }
+ }
+ // Commented
+ if (ch == "-" && stream.eat(">")) {
+ curPunc = "->";
+ return null;
+ }
+ if (/[\-+*&%=<>!?|\/~]/.test(ch)) {
+ stream.eatWhile(/[\-+*&%=<>|~]/);
+ return "operator";
+ }
+ stream.eatWhile(/[\w\$_]/);
+
+ var cur = stream.current();
+ if (atoms.propertyIsEnumerable(cur)) {
+ return "atom";
+ }
+ if (softKeywords.propertyIsEnumerable(cur)) {
+ if (blockKeywords.propertyIsEnumerable(cur)) curPunc = "newstatement";
+ return "softKeyword";
+ }
+
+ if (keywords.propertyIsEnumerable(cur)) {
+ if (blockKeywords.propertyIsEnumerable(cur)) curPunc = "newstatement";
+ return "keyword";
+ }
+ return "word";
+ }
+
+ tokenBase.isBase = true;
+
+ function startString(quote, stream, state) {
+ var tripleQuoted = false;
+ if (quote != "/" && stream.eat(quote)) {
+ if (stream.eat(quote)) tripleQuoted = true;
+ else return "string";
+ }
+ function t(stream, state) {
+ var escaped = false, next, end = !tripleQuoted;
+
+ while ((next = stream.next()) != null) {
+ if (next == quote && !escaped) {
+ if (!tripleQuoted) {
+ break;
+ }
+ if (stream.match(quote + quote)) {
+ end = true;
+ break;
+ }
+ }
+
+ if (quote == '"' && next == "$" && !escaped && stream.eat("{")) {
+ state.tokenize.push(tokenBaseUntilBrace());
+ return "string";
+ }
+
+ if (next == "$" && !escaped && !stream.eat(" ")) {
+ state.tokenize.push(tokenBaseUntilSpace());
+ return "string";
+ }
+ escaped = !escaped && next == "\\";
+ }
+ if (multiLineStrings)
+ state.tokenize.push(t);
+ if (end) state.tokenize.pop();
+ return "string";
+ }
+
+ state.tokenize.push(t);
+ return t(stream, state);
+ }
+
+ function tokenBaseUntilBrace() {
+ var depth = 1;
+
+ function t(stream, state) {
+ if (stream.peek() == "}") {
+ depth--;
+ if (depth == 0) {
+ state.tokenize.pop();
+ return state.tokenize[state.tokenize.length - 1](stream, state);
+ }
+ } else if (stream.peek() == "{") {
+ depth++;
+ }
+ return tokenBase(stream, state);
+ }
+
+ t.isBase = true;
+ return t;
+ }
+
+ function tokenBaseUntilSpace() {
+ function t(stream, state) {
+ if (stream.eat(/[\w]/)) {
+ var isWord = stream.eatWhile(/[\w]/);
+ if (isWord) {
+ state.tokenize.pop();
+ return "word";
+ }
+ }
+ state.tokenize.pop();
+ return "string";
+ }
+
+ t.isBase = true;
+ return t;
+ }
+
+ function tokenComment(stream, state) {
+ var maybeEnd = false, ch;
+ while (ch = stream.next()) {
+ if (ch == "/" && maybeEnd) {
+ state.tokenize.pop();
+ break;
+ }
+ maybeEnd = (ch == "*");
+ }
+ return "comment";
+ }
+
+ function expectExpression(last) {
+ return !last || last == "operator" || last == "->" || /[\.\[\{\(,;:]/.test(last) ||
+ last == "newstatement" || last == "keyword" || last == "proplabel";
+ }
+
+ function Context(indented, column, type, align, prev) {
+ this.indented = indented;
+ this.column = column;
+ this.type = type;
+ this.align = align;
+ this.prev = prev;
+ }
+
+ function pushContext(state, col, type) {
+ return state.context = new Context(state.indented, col, type, null, state.context);
+ }
+
+ function popContext(state) {
+ var t = state.context.type;
+ if (t == ")" || t == "]" || t == "}")
+ state.indented = state.context.indented;
+ return state.context = state.context.prev;
+ }
+
+ // Interface
+
+ return {
+ startState: function (basecolumn) {
+ return {
+ tokenize: [tokenBase],
+ context: new Context((basecolumn || 0) - config.indentUnit, 0, "top", false),
+ indented: 0,
+ startOfLine: true,
+ lastToken: null
+ };
+ },
+
+ token: function (stream, state) {
+ var ctx = state.context;
+ if (stream.sol()) {
+ if (ctx.align == null) ctx.align = false;
+ state.indented = stream.indentation();
+ state.startOfLine = true;
+ // Automatic semicolon insertion
+ if (ctx.type == "statement" && !expectExpression(state.lastToken)) {
+ popContext(state);
+ ctx = state.context;
+ }
+ }
+ if (stream.eatSpace()) return null;
+ curPunc = null;
+ var style = state.tokenize[state.tokenize.length - 1](stream, state);
+ if (style == "comment") return style;
+ if (ctx.align == null) ctx.align = true;
+ if ((curPunc == ";" || curPunc == ":") && ctx.type == "statement") popContext(state);
+ // Handle indentation for {x -> \n ... }
+ else if (curPunc == "->" && ctx.type == "statement" && ctx.prev.type == "}") {
+ popContext(state);
+ state.context.align = false;
+ }
+ else if (curPunc == "{") pushContext(state, stream.column(), "}");
+ else if (curPunc == "[") pushContext(state, stream.column(), "]");
+ else if (curPunc == "(") pushContext(state, stream.column(), ")");
+ else if (curPunc == "}") {
+ while (ctx.type == "statement") ctx = popContext(state);
+ if (ctx.type == "}") ctx = popContext(state);
+ while (ctx.type == "statement") ctx = popContext(state);
+ }
+ else if (curPunc == ctx.type) popContext(state);
+ else if (ctx.type == "}" || ctx.type == "top" || (ctx.type == "statement" && curPunc == "newstatement"))
+ pushContext(state, stream.column(), "statement");
+ state.startOfLine = false;
+ state.lastToken = curPunc || style;
+ return style;
+ },
+
+ indent: function (state, textAfter) {
+ if (!state.tokenize[state.tokenize.length - 1].isBase) return 0;
+ var firstChar = textAfter && textAfter.charAt(0), ctx = state.context;
+ if (ctx.type == "statement" && !expectExpression(state.lastToken)) ctx = ctx.prev;
+ var closing = firstChar == ctx.type;
+ if (ctx.type == "statement") {
+ return ctx.indented + (firstChar == "{" ? 0 : config.indentUnit);
+ }
+ else if (ctx.align) return ctx.column + (closing ? 0 : 1);
+ else return ctx.indented + (closing ? 0 : config.indentUnit);
+ },
+
+ electricChars: "{}"
+ };
+});
+
+CodeMirror.defineMIME("text/x-kotlin", "kotlin");
+
+});
diff --git a/mode/meta.js b/mode/meta.js
index 281028d7ab..3627cd7470 100644
--- a/mode/meta.js
+++ b/mode/meta.js
@@ -12,101 +12,103 @@
"use strict";
CodeMirror.modeInfo = [
- {name: 'APL', mime: 'text/apl', mode: 'apl'},
- {name: 'Asterisk', mime: 'text/x-asterisk', mode: 'asterisk'},
- {name: 'C', mime: 'text/x-csrc', mode: 'clike'},
- {name: 'C++', mime: 'text/x-c++src', mode: 'clike'},
- {name: 'Cobol', mime: 'text/x-cobol', mode: 'cobol'},
- {name: 'Java', mime: 'text/x-java', mode: 'clike'},
- {name: 'C#', mime: 'text/x-csharp', mode: 'clike'},
- {name: 'Scala', mime: 'text/x-scala', mode: 'clike'},
- {name: 'Clojure', mime: 'text/x-clojure', mode: 'clojure'},
- {name: 'CoffeeScript', mime: 'text/x-coffeescript', mode: 'coffeescript'},
- {name: 'Common Lisp', mime: 'text/x-common-lisp', mode: 'commonlisp'},
- {name: 'Cypher', mime: 'application/x-cypher-query', mode: 'cypher'},
- {name: 'CSS', mime: 'text/css', mode: 'css'},
- {name: 'D', mime: 'text/x-d', mode: 'd'},
- {name: 'diff', mime: 'text/x-diff', mode: 'diff'},
- {name: 'DTD', mime: 'application/xml-dtd', mode: 'dtd'},
- {name: 'Dylan', mime: 'text/x-dylan', mode: 'dylan'},
- {name: 'ECL', mime: 'text/x-ecl', mode: 'ecl'},
- {name: 'Eiffel', mime: 'text/x-eiffel', mode: 'eiffel'},
- {name: 'Erlang', mime: 'text/x-erlang', mode: 'erlang'},
- {name: 'Fortran', mime: 'text/x-fortran', mode: 'fortran'},
- {name: 'F#', mime: 'text/x-fsharp', mode: 'mllike'},
- {name: 'Gas', mime: 'text/x-gas', mode: 'gas'},
- {name: 'Gherkin', mime: 'text/x-feature', mode: 'gherkin'},
- {name: 'GitHub Flavored Markdown', mime: 'text/x-gfm', mode: 'gfm'},
- {name: 'Go', mime: 'text/x-go', mode: 'go'},
- {name: 'Groovy', mime: 'text/x-groovy', mode: 'groovy'},
- {name: 'HAML', mime: 'text/x-haml', mode: 'haml'},
- {name: 'Haskell', mime: 'text/x-haskell', mode: 'haskell'},
- {name: 'Haxe', mime: 'text/x-haxe', mode: 'haxe'},
- {name: 'ASP.NET', mime: 'application/x-aspx', mode: 'htmlembedded'},
- {name: 'Embedded Javascript', mime: 'application/x-ejs', mode: 'htmlembedded'},
- {name: 'JavaServer Pages', mime: 'application/x-jsp', mode: 'htmlembedded'},
- {name: 'HTML', mime: 'text/html', mode: 'htmlmixed'},
- {name: 'HTTP', mime: 'message/http', mode: 'http'},
- {name: 'Jade', mime: 'text/x-jade', mode: 'jade'},
- {name: 'JavaScript', mime: 'text/javascript', mode: 'javascript'},
- {name: 'JSON', mime: 'application/x-json', mode: 'javascript'},
- {name: 'JSON', mime: 'application/json', mode: 'javascript'},
- {name: 'JSON-LD', mime: 'application/ld+json', mode: 'javascript'},
- {name: 'TypeScript', mime: 'application/typescript', mode: 'javascript'},
- {name: 'Jinja2', mime: null, mode: 'jinja2'},
- {name: 'Julia', mime: 'text/x-julia', mode: 'julia'},
- {name: 'LESS', mime: 'text/x-less', mode: 'css'},
- {name: 'LiveScript', mime: 'text/x-livescript', mode: 'livescript'},
- {name: 'Lua', mime: 'text/x-lua', mode: 'lua'},
- {name: 'Markdown (GitHub-flavour)', mime: 'text/x-markdown', mode: 'markdown'},
- {name: 'mIRC', mime: 'text/mirc', mode: 'mirc'},
- {name: 'Nginx', mime: 'text/x-nginx-conf', mode: 'nginx'},
- {name: 'NTriples', mime: 'text/n-triples', mode: 'ntriples'},
- {name: 'OCaml', mime: 'text/x-ocaml', mode: 'mllike'},
- {name: 'Octave', mime: 'text/x-octave', mode: 'octave'},
- {name: 'Pascal', mime: 'text/x-pascal', mode: 'pascal'},
- {name: 'PEG.js', mime: null, mode: 'pegjs'},
- {name: 'Perl', mime: 'text/x-perl', mode: 'perl'},
- {name: 'PHP', mime: 'text/x-php', mode: 'php'},
- {name: 'PHP(HTML)', mime: 'application/x-httpd-php', mode: 'php'},
- {name: 'Pig', mime: 'text/x-pig', mode: 'pig'},
- {name: 'Plain Text', mime: 'text/plain', mode: 'null'},
- {name: 'Properties files', mime: 'text/x-properties', mode: 'properties'},
- {name: 'Python', mime: 'text/x-python', mode: 'python'},
- {name: 'Puppet', mime: 'text/x-puppet', mode: 'puppet'},
- {name: 'Cython', mime: 'text/x-cython', mode: 'python'},
- {name: 'R', mime: 'text/x-rsrc', mode: 'r'},
- {name: 'reStructuredText', mime: 'text/x-rst', mode: 'rst'},
- {name: 'Ruby', mime: 'text/x-ruby', mode: 'ruby'},
- {name: 'Rust', mime: 'text/x-rustsrc', mode: 'rust'},
- {name: 'Sass', mime: 'text/x-sass', mode: 'sass'},
- {name: 'Scheme', mime: 'text/x-scheme', mode: 'scheme'},
- {name: 'SCSS', mime: 'text/x-scss', mode: 'css'},
- {name: 'Shell', mime: 'text/x-sh', mode: 'shell'},
- {name: 'Sieve', mime: 'application/sieve', mode: 'sieve'},
- {name: 'Smalltalk', mime: 'text/x-stsrc', mode: 'smalltalk'},
- {name: 'Smarty', mime: 'text/x-smarty', mode: 'smarty'},
- {name: 'SmartyMixed', mime: 'text/x-smarty', mode: 'smartymixed'},
- {name: 'Solr', mime: 'text/x-solr', mode: 'solr'},
- {name: 'SPARQL', mime: 'application/x-sparql-query', mode: 'sparql'},
- {name: 'SQL', mime: 'text/x-sql', mode: 'sql'},
- {name: 'MariaDB', mime: 'text/x-mariadb', mode: 'sql'},
- {name: 'sTeX', mime: 'text/x-stex', mode: 'stex'},
- {name: 'LaTeX', mime: 'text/x-latex', mode: 'stex'},
- {name: 'SystemVerilog', mime: 'text/x-systemverilog', mode: 'verilog'},
- {name: 'Tcl', mime: 'text/x-tcl', mode: 'tcl'},
- {name: 'TiddlyWiki ', mime: 'text/x-tiddlywiki', mode: 'tiddlywiki'},
- {name: 'Tiki wiki', mime: 'text/tiki', mode: 'tiki'},
- {name: 'TOML', mime: 'text/x-toml', mode: 'toml'},
- {name: 'Turtle', mime: 'text/turtle', mode: 'turtle'},
- {name: 'VB.NET', mime: 'text/x-vb', mode: 'vb'},
- {name: 'VBScript', mime: 'text/vbscript', mode: 'vbscript'},
- {name: 'Velocity', mime: 'text/velocity', mode: 'velocity'},
- {name: 'Verilog', mime: 'text/x-verilog', mode: 'verilog'},
- {name: 'XML', mime: 'application/xml', mode: 'xml'},
- {name: 'XQuery', mime: 'application/xquery', mode: 'xquery'},
- {name: 'YAML', mime: 'text/x-yaml', mode: 'yaml'},
- {name: 'Z80', mime: 'text/x-z80', mode: 'z80'}
+ {name: "APL", mime: "text/apl", mode: "apl"},
+ {name: "Asterisk", mime: "text/x-asterisk", mode: "asterisk"},
+ {name: "C", mime: "text/x-csrc", mode: "clike"},
+ {name: "C++", mime: "text/x-c++src", mode: "clike"},
+ {name: "Cobol", mime: "text/x-cobol", mode: "cobol"},
+ {name: "Java", mime: "text/x-java", mode: "clike"},
+ {name: "C#", mime: "text/x-csharp", mode: "clike"},
+ {name: "Scala", mime: "text/x-scala", mode: "clike"},
+ {name: "Clojure", mime: "text/x-clojure", mode: "clojure"},
+ {name: "CoffeeScript", mime: "text/x-coffeescript", mode: "coffeescript"},
+ {name: "Common Lisp", mime: "text/x-common-lisp", mode: "commonlisp"},
+ {name: "Cypher", mime: "application/x-cypher-query", mode: "cypher"},
+ {name: "CSS", mime: "text/css", mode: "css"},
+ {name: "D", mime: "text/x-d", mode: "d"},
+ {name: "diff", mime: "text/x-diff", mode: "diff"},
+ {name: "DTD", mime: "application/xml-dtd", mode: "dtd"},
+ {name: "Dylan", mime: "text/x-dylan", mode: "dylan"},
+ {name: "ECL", mime: "text/x-ecl", mode: "ecl"},
+ {name: "Eiffel", mime: "text/x-eiffel", mode: "eiffel"},
+ {name: "Erlang", mime: "text/x-erlang", mode: "erlang"},
+ {name: "Fortran", mime: "text/x-fortran", mode: "fortran"},
+ {name: "F#", mime: "text/x-fsharp", mode: "mllike"},
+ {name: "Gas", mime: "text/x-gas", mode: "gas"},
+ {name: "Gherkin", mime: "text/x-feature", mode: "gherkin"},
+ {name: "GitHub Flavored Markdown", mime: "text/x-gfm", mode: "gfm"},
+ {name: "Go", mime: "text/x-go", mode: "go"},
+ {name: "Groovy", mime: "text/x-groovy", mode: "groovy"},
+ {name: "HAML", mime: "text/x-haml", mode: "haml"},
+ {name: "Haskell", mime: "text/x-haskell", mode: "haskell"},
+ {name: "Haxe", mime: "text/x-haxe", mode: "haxe"},
+ {name: "ASP.NET", mime: "application/x-aspx", mode: "htmlembedded"},
+ {name: "Embedded Javascript", mime: "application/x-ejs", mode: "htmlembedded"},
+ {name: "JavaServer Pages", mime: "application/x-jsp", mode: "htmlembedded"},
+ {name: "HTML", mime: "text/html", mode: "htmlmixed"},
+ {name: "HTTP", mime: "message/http", mode: "http"},
+ {name: "Jade", mime: "text/x-jade", mode: "jade"},
+ {name: "JavaScript", mime: "text/javascript", mode: "javascript"},
+ {name: "JavaScript", mime: "application/javascript", mode: "javascript"},
+ {name: "JSON", mime: "application/x-json", mode: "javascript"},
+ {name: "JSON", mime: "application/json", mode: "javascript"},
+ {name: "JSON-LD", mime: "application/ld+json", mode: "javascript"},
+ {name: "TypeScript", mime: "application/typescript", mode: "javascript"},
+ {name: "Jinja2", mime: null, mode: "jinja2"},
+ {name: "Julia", mime: "text/x-julia", mode: "julia"},
+ {name: "Kotlin", mime: "text/x-kotlin", mode: "kotlin"},
+ {name: "LESS", mime: "text/x-less", mode: "css"},
+ {name: "LiveScript", mime: "text/x-livescript", mode: "livescript"},
+ {name: "Lua", mime: "text/x-lua", mode: "lua"},
+ {name: "Markdown (GitHub-flavour)", mime: "text/x-markdown", mode: "markdown"},
+ {name: "mIRC", mime: "text/mirc", mode: "mirc"},
+ {name: "Nginx", mime: "text/x-nginx-conf", mode: "nginx"},
+ {name: "NTriples", mime: "text/n-triples", mode: "ntriples"},
+ {name: "OCaml", mime: "text/x-ocaml", mode: "mllike"},
+ {name: "Octave", mime: "text/x-octave", mode: "octave"},
+ {name: "Pascal", mime: "text/x-pascal", mode: "pascal"},
+ {name: "PEG.js", mime: null, mode: "pegjs"},
+ {name: "Perl", mime: "text/x-perl", mode: "perl"},
+ {name: "PHP", mime: "text/x-php", mode: "php"},
+ {name: "PHP(HTML)", mime: "application/x-httpd-php", mode: "php"},
+ {name: "Pig", mime: "text/x-pig", mode: "pig"},
+ {name: "Plain Text", mime: "text/plain", mode: "null"},
+ {name: "Properties files", mime: "text/x-properties", mode: "properties"},
+ {name: "Python", mime: "text/x-python", mode: "python"},
+ {name: "Puppet", mime: "text/x-puppet", mode: "puppet"},
+ {name: "Cython", mime: "text/x-cython", mode: "python"},
+ {name: "R", mime: "text/x-rsrc", mode: "r"},
+ {name: "reStructuredText", mime: "text/x-rst", mode: "rst"},
+ {name: "Ruby", mime: "text/x-ruby", mode: "ruby"},
+ {name: "Rust", mime: "text/x-rustsrc", mode: "rust"},
+ {name: "Sass", mime: "text/x-sass", mode: "sass"},
+ {name: "Scheme", mime: "text/x-scheme", mode: "scheme"},
+ {name: "SCSS", mime: "text/x-scss", mode: "css"},
+ {name: "Shell", mime: "text/x-sh", mode: "shell"},
+ {name: "Sieve", mime: "application/sieve", mode: "sieve"},
+ {name: "Smalltalk", mime: "text/x-stsrc", mode: "smalltalk"},
+ {name: "Smarty", mime: "text/x-smarty", mode: "smarty"},
+ {name: "SmartyMixed", mime: "text/x-smarty", mode: "smartymixed"},
+ {name: "Solr", mime: "text/x-solr", mode: "solr"},
+ {name: "SPARQL", mime: "application/x-sparql-query", mode: "sparql"},
+ {name: "SQL", mime: "text/x-sql", mode: "sql"},
+ {name: "MariaDB", mime: "text/x-mariadb", mode: "sql"},
+ {name: "sTeX", mime: "text/x-stex", mode: "stex"},
+ {name: "LaTeX", mime: "text/x-latex", mode: "stex"},
+ {name: "SystemVerilog", mime: "text/x-systemverilog", mode: "verilog"},
+ {name: "Tcl", mime: "text/x-tcl", mode: "tcl"},
+ {name: "TiddlyWiki ", mime: "text/x-tiddlywiki", mode: "tiddlywiki"},
+ {name: "Tiki wiki", mime: "text/tiki", mode: "tiki"},
+ {name: "TOML", mime: "text/x-toml", mode: "toml"},
+ {name: "Turtle", mime: "text/turtle", mode: "turtle"},
+ {name: "VB.NET", mime: "text/x-vb", mode: "vb"},
+ {name: "VBScript", mime: "text/vbscript", mode: "vbscript"},
+ {name: "Velocity", mime: "text/velocity", mode: "velocity"},
+ {name: "Verilog", mime: "text/x-verilog", mode: "verilog"},
+ {name: "XML", mime: "application/xml", mode: "xml"},
+ {name: "XQuery", mime: "application/xquery", mode: "xquery"},
+ {name: "YAML", mime: "text/x-yaml", mode: "yaml"},
+ {name: "Z80", mime: "text/x-z80", mode: "z80"}
];
});
diff --git a/mode/puppet/puppet.js b/mode/puppet/puppet.js
index 66698bb6ad..b407ded883 100644
--- a/mode/puppet/puppet.js
+++ b/mode/puppet/puppet.js
@@ -176,7 +176,7 @@ CodeMirror.defineMode("puppet", function () {
// Match characters that we are going to assume
// are trying to be regex
if (ch == '/') {
- stream.match(/.*\//);
+ stream.match(/.*?\//);
return 'variable-3';
}
// Match all the numbers
diff --git a/mode/ruby/ruby.js b/mode/ruby/ruby.js
index b68ee2912a..e7de7b57f1 100644
--- a/mode/ruby/ruby.js
+++ b/mode/ruby/ruby.js
@@ -46,16 +46,30 @@ CodeMirror.defineMode("ruby", function(config) {
var ch = stream.next(), m;
if (ch == "`" || ch == "'" || ch == '"') {
return chain(readQuoted(ch, "string", ch == '"' || ch == "`"), stream, state);
- } else if (ch == "/" && !stream.eol() && stream.peek() != " ") {
- if (stream.eat("=")) return "operator";
- return chain(readQuoted(ch, "string-2", true), stream, state);
+ } else if (ch == "/") {
+ var currentIndex = stream.current().length;
+ if (stream.skipTo("/")) {
+ var search_till = stream.current().length;
+ stream.backUp(stream.current().length - currentIndex);
+ var balance = 0; // balance brackets
+ while (stream.current().length < search_till) {
+ var chchr = stream.next();
+ if (chchr == "(") balance += 1;
+ else if (chchr == ")") balance -= 1;
+ if (balance < 0) break;
+ }
+ stream.backUp(stream.current().length - currentIndex);
+ if (balance == 0)
+ return chain(readQuoted(ch, "string-2", true), stream, state);
+ }
+ return "operator";
} else if (ch == "%") {
var style = "string", embed = true;
if (stream.eat("s")) style = "atom";
else if (stream.eat(/[WQ]/)) style = "string";
else if (stream.eat(/[r]/)) style = "string-2";
else if (stream.eat(/[wxq]/)) { style = "string"; embed = false; }
- var delim = stream.eat(/[^\w\s]/);
+ var delim = stream.eat(/[^\w\s=]/);
if (!delim) return "operator";
if (matching.propertyIsEnumerable(delim)) delim = matching[delim];
return chain(readQuoted(delim, style, embed, true), stream, state);
diff --git a/mode/smartymixed/smartymixed.js b/mode/smartymixed/smartymixed.js
index 31cfd2bdae..3290d17d80 100644
--- a/mode/smartymixed/smartymixed.js
+++ b/mode/smartymixed/smartymixed.js
@@ -9,6 +9,9 @@
* @date 05.07.2013
*/
+// Warning: Don't base other modes on this one. This here is a
+// terrible way to write a mixed mode.
+
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"), require("../htmlmixed/htmlmixed"), require("../smarty/smarty"));
@@ -20,11 +23,10 @@
"use strict";
CodeMirror.defineMode("smartymixed", function(config) {
- var settings, regs, helpers, parsers,
- htmlMixedMode = CodeMirror.getMode(config, "htmlmixed"),
- smartyMode = CodeMirror.getMode(config, "smarty"),
+ var htmlMixedMode = CodeMirror.getMode(config, "htmlmixed");
+ var smartyMode = CodeMirror.getMode(config, "smarty");
- settings = {
+ var settings = {
rightDelimiter: '}',
leftDelimiter: '{'
};
@@ -36,15 +38,18 @@ CodeMirror.defineMode("smartymixed", function(config) {
settings.rightDelimiter = config.rightDelimiter;
}
- regs = {
- smartyComment: new RegExp("^" + settings.leftDelimiter + "\\*"),
- literalOpen: new RegExp(settings.leftDelimiter + "literal" + settings.rightDelimiter),
- literalClose: new RegExp(settings.leftDelimiter + "\/literal" + settings.rightDelimiter),
- hasLeftDelimeter: new RegExp(".*" + settings.leftDelimiter),
- htmlHasLeftDelimeter: new RegExp("[^<>]*" + settings.leftDelimiter)
+ function reEsc(str) { return str.replace(/[^\s\w]/g, "\\$&"); }
+
+ var reLeft = reEsc(settings.leftDelimiter), reRight = reEsc(settings.rightDelimiter);
+ var regs = {
+ smartyComment: new RegExp("^" + reRight + "\\*"),
+ literalOpen: new RegExp(reLeft + "literal" + reRight),
+ literalClose: new RegExp(reLeft + "\/literal" + reRight),
+ hasLeftDelimeter: new RegExp(".*" + reLeft),
+ htmlHasLeftDelimeter: new RegExp("[^<>]*" + reLeft)
};
- helpers = {
+ var helpers = {
chain: function(stream, state, parser) {
state.tokenize = parser;
return parser(stream, state);
@@ -70,7 +75,7 @@ CodeMirror.defineMode("smartymixed", function(config) {
}
};
- parsers = {
+ var parsers = {
html: function(stream, state) {
if (!state.inLiteral && stream.match(regs.htmlHasLeftDelimeter, false) && state.htmlMixedState.htmlState.tagName === null) {
state.tokenize = parsers.smarty;
diff --git a/mode/vbscript/vbscript.js b/mode/vbscript/vbscript.js
index bff2594477..b66df2239a 100644
--- a/mode/vbscript/vbscript.js
+++ b/mode/vbscript/vbscript.js
@@ -291,7 +291,7 @@ CodeMirror.defineMode("vbscript", function(conf, parserConf) {
style = state.tokenize(stream, state);
current = stream.current();
- if (style.substr(0, 8) === 'variable' || style==='builtin' || style==='keyword'){//|| knownWords.indexOf(current.substring(1)) > -1) {
+ if (style && (style.substr(0, 8) === 'variable' || style==='builtin' || style==='keyword')){//|| knownWords.indexOf(current.substring(1)) > -1) {
if (style === 'builtin' || style === 'keyword') style='variable';
if (knownWords.indexOf(current.substr(1)) > -1) style='variable-2';
diff --git a/mode/yaml/yaml.js b/mode/yaml/yaml.js
index 4ebe5c81cf..332aef6a23 100644
--- a/mode/yaml/yaml.js
+++ b/mode/yaml/yaml.js
@@ -80,7 +80,7 @@ CodeMirror.defineMode("yaml", function() {
}
/* pairs (associative arrays) -> key */
- if (!state.pair && stream.match(/^\s*\S+(?=\s*:($|\s))/i)) {
+ if (!state.pair && stream.match(/^\s*(?:[,\[\]{}&*!|>'"%@`][^\s'":]|[^,\[\]{}#&*!|>'"%@`])[^#]*?(?=\s*:($|\s))/)) {
state.pair = true;
state.keyCol = stream.indentation();
return "atom";
diff --git a/package.json b/package.json
index eea46775d5..d3bcbb7556 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "codemirror",
- "version":"4.3.0",
+ "version":"4.4.0",
"main": "lib/codemirror.js",
"description": "In-browser code editing made bearable",
"licenses": [{"type": "MIT",
diff --git a/test/comment_test.js b/test/comment_test.js
index d8ff2c866d..8bd3959ee9 100644
--- a/test/comment_test.js
+++ b/test/comment_test.js
@@ -9,6 +9,9 @@ namespace = "comment_";
}
var simpleProg = "function foo() {\n return bar;\n}";
+ var inlineBlock = "foo(/* bar */ true);";
+ var inlineBlocks = "foo(/* bar */ true, /* baz */ false);";
+ var multiLineInlineBlock = ["above();", "foo(/* bar */ true);", "below();"];
test("block", "javascript", function(cm) {
cm.blockComment(Pos(0, 3), Pos(3, 0), {blockCommentLead: " *"});
@@ -19,6 +22,17 @@ namespace = "comment_";
cm.uncomment(Pos(0, 3), Pos(2, 0), {blockCommentLead: " *"});
}, simpleProg, simpleProg);
+ test("blockToggle2", "javascript", function(cm) {
+ cm.setCursor({line: 0, ch: 7 /* inside the block comment */});
+ cm.execCommand("toggleComment");
+ }, inlineBlock, "foo(bar true);");
+
+ // This test should work but currently fails.
+ // test("blockToggle3", "javascript", function(cm) {
+ // cm.setCursor({line: 0, ch: 7 /* inside the first block comment */});
+ // cm.execCommand("toggleComment");
+ // }, inlineBlocks, "foo(bar true, /* baz */ false);");
+
test("line", "javascript", function(cm) {
cm.lineComment(Pos(1, 1), Pos(1, 1));
}, simpleProg, "function foo() {\n// return bar;\n}");
@@ -36,6 +50,29 @@ namespace = "comment_";
cm.blockComment(Pos(0, 0), Pos(1));
}, "def blah()\n return hah\n", "# def blah()\n# return hah\n");
+ test("ignoreExternalBlockComments", "javascript", function(cm) {
+ cm.execCommand("toggleComment");
+ }, inlineBlocks, "// " + inlineBlocks);
+
+ test("ignoreExternalBlockComments2", "javascript", function(cm) {
+ cm.setCursor({line: 0, ch: null /* eol */});
+ cm.execCommand("toggleComment");
+ }, inlineBlocks, "// " + inlineBlocks);
+
+ test("ignoreExternalBlockCommentsMultiLineAbove", "javascript", function(cm) {
+ cm.setSelection({line: 0, ch: 0}, {line: 1, ch: 1});
+ cm.execCommand("toggleComment");
+ }, multiLineInlineBlock.join("\n"), ["// " + multiLineInlineBlock[0],
+ "// " + multiLineInlineBlock[1],
+ multiLineInlineBlock[2]].join("\n"));
+
+ test("ignoreExternalBlockCommentsMultiLineBelow", "javascript", function(cm) {
+ cm.setSelection({line: 1, ch: 13 /* after end of block comment */}, {line: 2, ch: 1});
+ cm.execCommand("toggleComment");
+ }, multiLineInlineBlock.join("\n"), [multiLineInlineBlock[0],
+ "// " + multiLineInlineBlock[1],
+ "// " + multiLineInlineBlock[2]].join("\n"));
+
test("commentRange", "javascript", function(cm) {
cm.blockComment(Pos(1, 2), Pos(1, 13), {fullLines: false});
}, simpleProg, "function foo() {\n /*return bar;*/\n}");
diff --git a/test/lint/lint.js b/test/lint/lint.js
index ef4c13bd9a..c2c45262a6 100644
--- a/test/lint/lint.js
+++ b/test/lint/lint.js
@@ -19,7 +19,7 @@ var topAllowedGlobals = Object.create(null);
("Error RegExp Number String Array Function Object Math Date undefined " +
"parseInt parseFloat Infinity NaN isNaN " +
"window document navigator prompt alert confirm console " +
- "FileReader Worker postMessage importScripts " +
+ "screen FileReader Worker postMessage importScripts " +
"setInterval clearInterval setTimeout clearTimeout " +
"CodeMirror " +
"test exports require module define")
diff --git a/test/test.js b/test/test.js
index 4ef4ef547d..dcfffb7c07 100644
--- a/test/test.js
+++ b/test/test.js
@@ -1988,3 +1988,38 @@ testCM("resizeLineWidget", function(cm) {
cm.setSize(40);
is(widget.parentNode.offsetWidth < 42);
});
+
+testCM("combinedOperations", function(cm) {
+ var place = document.getElementById("testground");
+ var other = CodeMirror(place, {value: "123"});
+ try {
+ cm.operation(function() {
+ cm.addLineClass(0, "wrap", "foo");
+ other.addLineClass(0, "wrap", "foo");
+ });
+ eq(byClassName(cm.getWrapperElement(), "foo").length, 1);
+ eq(byClassName(other.getWrapperElement(), "foo").length, 1);
+ cm.operation(function() {
+ cm.removeLineClass(0, "wrap", "foo");
+ other.removeLineClass(0, "wrap", "foo");
+ });
+ eq(byClassName(cm.getWrapperElement(), "foo").length, 0);
+ eq(byClassName(other.getWrapperElement(), "foo").length, 0);
+ } finally {
+ place.removeChild(other.getWrapperElement());
+ }
+}, {value: "abc"});
+
+testCM("eventOrder", function(cm) {
+ var seen = [];
+ cm.on("change", function() {
+ if (!seen.length) cm.replaceSelection(".");
+ seen.push("change");
+ });
+ cm.on("cursorActivity", function() {
+ cm.replaceSelection("!");
+ seen.push("activity");
+ });
+ cm.replaceSelection("/");
+ eq(seen.join(","), "change,change,activity,change");
+});
diff --git a/test/vim_test.js b/test/vim_test.js
index 932db70136..7c24634a89 100644
--- a/test/vim_test.js
+++ b/test/vim_test.js
@@ -895,6 +895,20 @@ testVim('cc_append', function(cm, vim, helpers) {
helpers.doKeys('c', 'c');
eq(expectedLineCount, cm.lineCount());
});
+testVim('c_visual_block', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('', '2', 'j', 'l', 'l', 'l', 'c');
+ var replacement = new Array(cm.listSelections().length+1).join('hello ').split(' ');
+ replacement.pop();
+ cm.replaceSelections(replacement);
+ eq('1hello\n5hello\nahellofg', cm.getValue());
+ cm.setCursor(2, 3);
+ helpers.doKeys('', '2', 'k', 'h', 'C');
+ replacement = new Array(cm.listSelections().length+1).join('world ').split(' ');
+ replacement.pop();
+ cm.replaceSelections(replacement);
+ eq('1hworld\n5hworld\nahworld', cm.getValue());
+}, {value: '1234\n5678\nabcdefg'});
// Swapcase commands edit in place and do not modify registers.
testVim('g~w_repeat', function(cm, vim, helpers) {
// Assert that dw does delete newline if it should go to the next line, and
@@ -918,8 +932,34 @@ testVim('g~g~', function(cm, vim, helpers) {
var register = helpers.getRegisterController().getRegister();
eq('', register.toString());
is(!register.linewise);
- eqPos(curStart, cm.getCursor());
+ eqPos({line: curStart.line, ch:0}, cm.getCursor());
}, { value: ' word1\nword2\nword3\nword4\nword5\nword6' });
+testVim('visual_block_~', function(cm, vim, helpers) {
+ cm.setCursor(1, 1);
+ helpers.doKeys('', 'l', 'l', 'j', '~');
+ helpers.assertCursorAt(1, 1);
+ eq('hello\nwoRLd\naBCDe', cm.getValue());
+ cm.setCursor(2, 0);
+ helpers.doKeys('v', 'l', 'l', '~');
+ helpers.assertCursorAt(2, 0);
+ eq('hello\nwoRLd\nAbcDe', cm.getValue());
+},{value: 'hello\nwOrld\nabcde' });
+testVim('._swapCase_visualBlock', function(cm, vim, helpers) {
+ helpers.doKeys('', 'j', 'j', 'l', '~');
+ cm.setCursor(0, 3);
+ helpers.doKeys('.');
+ eq('HelLO\nWorLd\nAbcdE', cm.getValue());
+},{value: 'hEllo\nwOrlD\naBcDe' });
+testVim('._delete_visualBlock', function(cm, vim, helpers) {
+ helpers.doKeys('', 'j', 'x');
+ eq('ive\ne\nsome\nsugar', cm.getValue());
+ helpers.doKeys('.');
+ eq('ve\n\nsome\nsugar', cm.getValue());
+ helpers.doKeys('j', 'j', '.');
+ eq('ve\n\nome\nugar', cm.getValue());
+ helpers.doKeys('u', '', '.');
+ eq('ve\n\nme\ngar', cm.getValue());
+},{value: 'give\nme\nsome\nsugar' });
testVim('>{motion}', function(cm, vim, helpers) {
cm.setCursor(1, 3);
var expectedLineCount = cm.lineCount();
@@ -1307,6 +1347,19 @@ testVim('r', function(cm, vim, helpers) {
helpers.doKeys('v', 'j', 'h', 'r', '');
eq('wuuu \n her', cm.getValue(),'Replacing selection by space-characters failed');
}, { value: 'wordet\nanother' });
+testVim('r_visual_block', function(cm, vim, helpers) {
+ cm.setCursor(2, 3);
+ helpers.doKeys('', 'k', 'k', 'h', 'h', 'r', 'l');
+ eq('1lll\n5lll\nalllefg', cm.getValue());
+ helpers.doKeys('', 'l', 'j', 'r', '');
+ eq('1 l\n5 l\nalllefg', cm.getValue());
+ cm.setCursor(2, 0);
+ helpers.doKeys('o');
+ helpers.doInsertModeKeys('Esc');
+ cm.replaceRange('\t\t', cm.getCursor());
+ helpers.doKeys('', 'h', 'h', 'r', 'r');
+ eq('1 l\n5 l\nalllefg\nrrrrrrrr', cm.getValue());
+}, {value: '1234\n5678\nabcdefg'});
testVim('R', function(cm, vim, helpers) {
cm.setCursor(0, 1);
helpers.doKeys('R');
@@ -1573,6 +1626,50 @@ testVim('visual_line', function(cm, vim, helpers) {
helpers.doKeys('l', 'V', 'l', 'j', 'j', 'd');
eq(' 4\n 5', cm.getValue());
}, { value: ' 1\n 2\n 3\n 4\n 5' });
+testVim('visual_block', function(cm, vim, helpers) {
+ // test the block selection with lines of different length
+ // i.e. extending the selection
+ // till the end of the longest line.
+ helpers.doKeys('', 'l', 'j', 'j', '6', 'l', 'd');
+ helpers.doKeys('d', 'd', 'd', 'd');
+ eq('', cm.getValue());
+ // check for left side selection in case
+ // of moving up to a shorter line.
+ cm.replaceRange('hello world\n{\nthis is\nsparta!', cm.getCursor());
+ cm.setCursor(3, 4);
+ helpers.doKeys('', 'l', 'k', 'k', 'd');
+ eq('hello world\n{\nt is\nsta!', cm.getValue());
+ cm.replaceRange('12345\n67891\nabcde', {line: 0, ch: 0}, {line: cm.lastLine(), ch: 6});
+ cm.setCursor(1, 2);
+ helpers.doKeys('', '2', 'l', 'k');
+ // circle around the anchor
+ // and check the selections
+ var selections = cm.getSelections();
+ eq('345891', selections.join(''));
+ helpers.doKeys('4', 'h');
+ selections = cm.getSelections();
+ eq('123678', selections.join(''));
+ helpers.doKeys('j', 'j');
+ selections = cm.getSelections();
+ eq('678abc', selections.join(''));
+ helpers.doKeys('4', 'l');
+ selections = cm.getSelections();
+ eq('891cde', selections.join(''));
+ // switch between visual modes
+ cm.setCursor(1, 1);
+ // blockwise to characterwise visual
+ helpers.doKeys('', '', 'j', 'l', 'v');
+ selections = cm.getSelections();
+ eq('7891\nabc', selections.join(''));
+ // characterwise to blockwise
+ helpers.doKeys('');
+ selections = cm.getSelections();
+ eq('78bc', selections.join(''));
+ // blockwise to linewise visual
+ helpers.doKeys('V');
+ selections = cm.getSelections();
+ eq('67891\nabcde', selections.join(''));
+}, {value: '1234\n5678\nabcdefg'});
testVim('visual_marks', function(cm, vim, helpers) {
helpers.doKeys('l', 'v', 'l', 'l', 'j', 'j', 'v');
// Test visual mode marks
@@ -1600,7 +1697,8 @@ testVim('reselect_visual', function(cm, vim, helpers) {
eq('123456\n2345\nbar', cm.getValue());
cm.setCursor(0, 0);
helpers.doKeys('g', 'v');
- helpers.assertCursorAt(1, 3);
+ // here the fake cursor is at (1, 3)
+ helpers.assertCursorAt(1, 4);
eqPos(makeCursor(1, 0), cm.getCursor('anchor'));
helpers.doKeys('v');
cm.setCursor(2, 0);
@@ -1613,18 +1711,33 @@ testVim('reselect_visual', function(cm, vim, helpers) {
eq('123456\n2345\nbar', cm.getValue());
}, { value: '123456\nfoo\nbar' });
testVim('reselect_visual_line', function(cm, vim, helpers) {
- helpers.doKeys('l', 'V', 'l', 'j', 'j', 'V', 'g', 'v', 'd');
- eq(' foo\n and\n bar', cm.getValue());
- cm.setCursor(0, 0);
+ helpers.doKeys('l', 'V', 'j', 'j', 'V', 'g', 'v', 'd');
+ eq('foo\nand\nbar', cm.getValue());
+ cm.setCursor(1, 0);
helpers.doKeys('V', 'y', 'j');
helpers.doKeys('V', 'p' , 'g', 'v', 'd');
- eq(' foo\n bar', cm.getValue());
-}, { value: ' hello\n this\n is \n foo\n and\n bar' });
+ eq('foo\nand', cm.getValue());
+}, { value: 'hello\nthis\nis\nfoo\nand\nbar' });
+testVim('reselect_visual_block', function(cm, vim, helpers) {
+ cm.setCursor(1, 2);
+ helpers.doKeys('', 'k', 'h', '');
+ cm.setCursor(2, 1);
+ helpers.doKeys('v', 'l', 'g', 'v');
+ helpers.assertCursorAt(0, 1);
+ // Ensure selection is done with visual block mode rather than one
+ // continuous range.
+ eq(cm.getSelections().join(''), '23oo')
+ helpers.doKeys('g', 'v');
+ helpers.assertCursorAt(2, 3);
+ // Ensure selection of deleted range
+ cm.setCursor(1, 1);
+ helpers.doKeys('v', '', 'j', 'd', 'g', 'v');
+ eq(cm.getSelections().join(''), 'or');
+}, { value: '123456\nfoo\nbar' });
testVim('s_normal', function(cm, vim, helpers) {
cm.setCursor(0, 1);
helpers.doKeys('s');
helpers.doInsertModeKeys('Esc');
- helpers.assertCursorAt(0, 0);
eq('ac', cm.getValue());
}, { value: 'abc'});
testVim('s_visual', function(cm, vim, helpers) {
@@ -1634,19 +1747,28 @@ testVim('s_visual', function(cm, vim, helpers) {
helpers.assertCursorAt(0, 0);
eq('ac', cm.getValue());
}, { value: 'abc'});
-testVim('o_visual', function(cm,vim,helpers) {
+testVim('o_visual', function(cm, vim, helpers) {
cm.setCursor(0,0);
helpers.doKeys('v','l','l','l','o');
helpers.assertCursorAt(0,0);
helpers.doKeys('v','v','j','j','j','o');
helpers.assertCursorAt(0,0);
- helpers.doKeys('o');
+ helpers.doKeys('O');
helpers.doKeys('l','l')
helpers.assertCursorAt(3, 3);
helpers.doKeys('d');
eq('p',cm.getValue());
}, { value: 'abcd\nefgh\nijkl\nmnop'});
-testVim('uppercase/lowercase_visual', function(cm, vim, helpers) {
+testVim('o_visual_block', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('','3','j','l','l', 'o');
+ helpers.assertCursorAt(0, 1);
+ helpers.doKeys('O');
+ helpers.assertCursorAt(0, 4);
+ helpers.doKeys('o');
+ helpers.assertCursorAt(3, 1);
+}, { value: 'abcd\nefgh\nijkl\nmnop'});
+testVim('changeCase_visual', function(cm, vim, helpers) {
cm.setCursor(0, 0);
helpers.doKeys('v', 'l', 'l');
helpers.doKeys('U');
@@ -1665,6 +1787,18 @@ testVim('uppercase/lowercase_visual', function(cm, vim, helpers) {
helpers.doKeys('V', 'U', 'j', '.');
eq('ABCDEF\nGHIJKL\nMnopq\nSHORT LINE\nLONG LINE OF TEXT', cm.getValue());
}, { value: 'abcdef\nghijkl\nmnopq\nshort line\nlong line of text'});
+testVim('changeCase_visual_block', function(cm, vim, helpers) {
+ cm.setCursor(2, 1);
+ helpers.doKeys('', 'k', 'k', 'h', 'U');
+ eq('ABcdef\nGHijkl\nMNopq\nfoo', cm.getValue());
+ cm.setCursor(0, 2);
+ helpers.doKeys('.');
+ eq('ABCDef\nGHIJkl\nMNOPq\nfoo', cm.getValue());
+ // check when last line is shorter.
+ cm.setCursor(2, 2);
+ helpers.doKeys('.');
+ eq('ABCDef\nGHIJkl\nMNOPq\nfoO', cm.getValue());
+}, { value: 'abcdef\nghijkl\nmnopq\nfoo'});
testVim('visual_paste', function(cm, vim, helpers) {
cm.setCursor(0, 0);
helpers.doKeys('v', 'l', 'l', 'y', 'j', 'v', 'l', 'p');
@@ -2013,6 +2147,14 @@ testVim('yank_register', function(cm, vim, helpers) {
});
helpers.doKeys(':');
}, { value: 'foo\nbar'});
+testVim('yank_visual_block', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('', 'l', 'j', '"', 'a', 'y');
+ cm.openNotification = helpers.fakeOpenNotification(function(text) {
+ is(/a\s+oo\nar/.test(text));
+ });
+ helpers.doKeys(':');
+}, { value: 'foo\nbar'});
testVim('yank_append_line_to_line_register', function(cm, vim, helpers) {
cm.setCursor(0, 0);
helpers.doKeys('"', 'a', 'y', 'y');