mediawiki-extensions-CodeMi.../resources/addon/edit/matchbrackets-wmde.js

318 lines
11 KiB
JavaScript
Raw Normal View History

// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/LICENSE
/**
* Modified by the WMDE Technical Wishes Team
* currently based on
* https://github.com/codemirror/CodeMirror/blob/4823ade863d0306371858394bc1b117aed3043bf/addon/edit/matchbrackets.js
* (from 2021-01-14)
*
* Modifications:
* - Introduced findSurroundingBrackets() along with it's `brackets` map. This is called when no
* `match` is found at the current cursor position.
* - Removed the `style` argument from the `scanForBracket` call. This fixes a compatibility issue
* with the "mediawiki" mode that tokenizes some brackets as `[[` or `{{{` pairs or triplets.
*/
(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) {
var ie_lt8 = /MSIE \d/.test(navigator.userAgent) &&
(document.documentMode == null || document.documentMode < 8);
var Pos = CodeMirror.Pos;
var matching = {"(": ")>", ")": "(<", "[": "]>", "]": "[<", "{": "}>", "}": "{<", "<": ">>", ">": "<<"};
function bracketRegex(config) {
return config && config.bracketRegex || /[(){}[\]]/
}
var surroundingBrackets = {
'(': ')',
')': false,
'[': ']',
']': false,
'{': '}',
'}': false
};
function findSurroundingBrackets( cm, where, config ) {
var from, to, ch,
nestedBracketsToSkip = 0,
lineNo = where.line,
line = cm.getLine( lineNo ),
pos = where.ch,
maxScanLen = ( config && config.maxScanLineLength ) || 10000,
maxScanLines = ( config && config.maxScanLines ) || 1000;
// Check the limit for the current line
if ( line.length > maxScanLen ) {
return null;
}
// Search forward
while ( true ) {
if ( pos >= line.length ) {
lineNo++;
// Give up when to many lines have been scanned
if ( lineNo > cm.lastLine() || lineNo - where.line >= maxScanLines ) {
break;
}
line = cm.getLine( lineNo );
// Give up when the next line is to long
if ( line.length > maxScanLen ) {
return null;
}
pos = 0;
// Continue early to make sure we don't read characters from empty lines
continue;
}
ch = line.charAt( pos );
if ( ch in surroundingBrackets ) {
// Found a closing bracket that's not part of a nested pair
if ( !surroundingBrackets[ch] && nestedBracketsToSkip <= 0 ) {
to = { pos: Pos( lineNo, pos ), char: ch };
break;
}
nestedBracketsToSkip += surroundingBrackets[ch] ? 1 : -1;
}
pos++;
}
nestedBracketsToSkip = 0;
lineNo = where.line;
line = cm.getLine( lineNo );
pos = where.ch;
// Search backwards
while ( true ) {
pos--;
if ( pos < 0 ) {
lineNo--;
// Give up when to many lines have been scanned
if ( lineNo < cm.firstLine() || where.line - lineNo >= maxScanLines ) {
break;
}
line = cm.getLine( lineNo );
// Give up when the next line is to long
if ( line.length > maxScanLen ) {
return null;
}
pos = line.length;
// Continue early to make sure we don't read characters from empty lines
continue;
}
ch = line.charAt( pos );
if ( ch in surroundingBrackets ) {
// Found an opening bracket that's not part of a nested pair
if ( surroundingBrackets[ch] && nestedBracketsToSkip <= 0 ) {
from = { pos: Pos( lineNo, pos ), expectedToChar: surroundingBrackets[ch] };
break;
}
nestedBracketsToSkip += surroundingBrackets[ch] ? -1 : 1;
}
}
if ( from && to ) {
return {
from: from.pos,
to: to.pos,
match: from.expectedToChar === to.char
};
} else if ( from || to ) {
return { from: ( from || to ).pos, match: false };
}
return null;
}
Improve matchbrackets performance when moving the cursor My previous patch Icbb1122 focused on the behavior of the matchbrackets addon when the text is *edited*. This patch here is about moving the cursor without changing the text. I realized the addon re-draws everything every time the cursor moves, even if the highlighted pair of brackets is still the same. This triggers very expensive code in the CodeMirror lib. I had a look at this expensive code, but did not found an easy win. It just is what it is: an expensive re-draw. Instead I introduced a caching layer that remembers the positions of the previously highlighted brackets and bails out as early as possible when nothing changed. The biggest chunk of code is that "did something change?" comparison. It looks expensive, but typically isn't. There are typically only 2 elements in the array for a single opening–closing pair. (Possibly more when there are multiple text selections.) The elements in the two arrays are typically in the same order. (Except the cursor is on the closing bracket.) Which means the nested `every` → `for` loop will typically be executed 2 times only – one time for each of the 2 elements. I won't upload this change upstream because it is only relevant together with our custom "in the middle" bracket highlighting. With our customization we have many, many situations where the highlighted brackets don't change. This (almost) doesn't happen upstream. Bug: T270317 Change-Id: I789b45362388f0818e797f789f6af427a35e3e06
2021-01-19 14:13:59 +00:00
function stillTheSameMarks( marks, config ) {
var same = config.currentMarks &&
// No need to compare the details if it's not even the same amount
config.currentMarks.length === marks.length &&
// We need the flexibility because the order can be closing → opening bracket as well
marks.every( function ( mark ) {
// These are typically only 2 elements for the opening and closing bracket
for ( var i = 0; i < config.currentMarks.length; i++ ) {
// Ordered from "most likely to change" to "least likely" for performance
if ( config.currentMarks[i].from.ch === mark.from.ch &&
config.currentMarks[i].from.line === mark.from.line &&
config.currentMarks[i].match === mark.match
) {
// This assumes all elements are unique, which should be the case
config.currentMarks.splice( i, 1 );
return true;
}
}
return false;
} ) &&
// Must be empty at the end, i.e. all elements must have been found and removed
!config.currentMarks.length;
config.currentMarks = marks;
return same;
}
function findMatchingBracket(cm, where, config) {
var line = cm.getLineHandle(where.line), pos = where.ch - 1;
var afterCursor = config && config.afterCursor
if (afterCursor == null)
afterCursor = /(^| )cm-fat-cursor($| )/.test(cm.getWrapperElement().className)
var re = bracketRegex(config)
// A cursor is defined as between two characters, but in in vim command mode
// (i.e. not insert mode), the cursor is visually represented as a
// highlighted box on top of the 2nd character. Otherwise, we allow matches
// from before or after the cursor.
var match = (!afterCursor && pos >= 0 && re.test(line.text.charAt(pos)) && matching[line.text.charAt(pos)]) ||
re.test(line.text.charAt(pos + 1)) && matching[line.text.charAt(++pos)];
// Note: Modified by WMDE, was `return null` before.
if (!match) return findSurroundingBrackets( cm, where, config );
var dir = match.charAt(1) == ">" ? 1 : -1;
if (config && config.strict && (dir > 0) != (pos == where.ch)) return null;
// Note: Disabled by WMDE for performance reasons. Not used anymore further down.
// var style = cm.getTokenTypeAt(Pos(where.line, pos + 1));
// Note: Modified by WMDE, used `style || null` instead of `undefined` before.
var found = scanForBracket(cm, Pos(where.line, pos + (dir > 0 ? 1 : 0)), dir, undefined, config);
if (found == null) return null;
return {from: Pos(where.line, pos), to: found && found.pos,
match: found && found.ch == match.charAt(0), forward: dir > 0};
}
// bracketRegex is used to specify which type of bracket to scan
// should be a regexp, e.g. /[[\]]/
//
// Note: If "where" is on an open bracket, then this bracket is ignored.
//
// Returns false when no bracket was found, null when it reached
// maxScanLines and gave up
function scanForBracket(cm, where, dir, style, config) {
var maxScanLen = (config && config.maxScanLineLength) || 10000;
var maxScanLines = (config && config.maxScanLines) || 1000;
var stack = [];
var re = bracketRegex(config)
var lineEnd = dir > 0 ? Math.min(where.line + maxScanLines, cm.lastLine() + 1)
: Math.max(cm.firstLine() - 1, where.line - maxScanLines);
for (var lineNo = where.line; lineNo != lineEnd; lineNo += dir) {
var line = cm.getLine(lineNo);
if (!line) continue;
var pos = dir > 0 ? 0 : line.length - 1, end = dir > 0 ? line.length : -1;
if (line.length > maxScanLen) continue;
if (lineNo == where.line) pos = where.ch - (dir < 0 ? 1 : 0);
for (; pos != end; pos += dir) {
var ch = line.charAt(pos);
if (re.test(ch) && (style === undefined || cm.getTokenTypeAt(Pos(lineNo, pos + 1)) == style)) {
var match = matching[ch];
if (match && (match.charAt(1) == ">") == (dir > 0)) stack.push(ch);
else if (!stack.length) return {pos: Pos(lineNo, pos), ch: ch};
else stack.pop();
}
}
}
return lineNo - dir == (dir > 0 ? cm.lastLine() : cm.firstLine()) ? false : null;
}
function matchBrackets(cm, autoclear, config) {
// Disable brace matching in long lines, since it'll cause hugely slow updates
var maxHighlightLen = cm.state.matchBrackets.maxHighlightLineLength || 1000,
highlightNonMatching = config && config.highlightNonMatching;
Improve matchbrackets performance when moving the cursor My previous patch Icbb1122 focused on the behavior of the matchbrackets addon when the text is *edited*. This patch here is about moving the cursor without changing the text. I realized the addon re-draws everything every time the cursor moves, even if the highlighted pair of brackets is still the same. This triggers very expensive code in the CodeMirror lib. I had a look at this expensive code, but did not found an easy win. It just is what it is: an expensive re-draw. Instead I introduced a caching layer that remembers the positions of the previously highlighted brackets and bails out as early as possible when nothing changed. The biggest chunk of code is that "did something change?" comparison. It looks expensive, but typically isn't. There are typically only 2 elements in the array for a single opening–closing pair. (Possibly more when there are multiple text selections.) The elements in the two arrays are typically in the same order. (Except the cursor is on the closing bracket.) Which means the nested `every` → `for` loop will typically be executed 2 times only – one time for each of the 2 elements. I won't upload this change upstream because it is only relevant together with our custom "in the middle" bracket highlighting. With our customization we have many, many situations where the highlighted brackets don't change. This (almost) doesn't happen upstream. Bug: T270317 Change-Id: I789b45362388f0818e797f789f6af427a35e3e06
2021-01-19 14:13:59 +00:00
var markPositions = [], marks = [], ranges = cm.listSelections();
for (var i = 0; i < ranges.length; i++) {
var match = ranges[i].empty() && findMatchingBracket(cm, ranges[i].head, config);
if (match && (match.match || highlightNonMatching !== false) && cm.getLine(match.from.line).length <= maxHighlightLen) {
Improve matchbrackets performance when moving the cursor My previous patch Icbb1122 focused on the behavior of the matchbrackets addon when the text is *edited*. This patch here is about moving the cursor without changing the text. I realized the addon re-draws everything every time the cursor moves, even if the highlighted pair of brackets is still the same. This triggers very expensive code in the CodeMirror lib. I had a look at this expensive code, but did not found an easy win. It just is what it is: an expensive re-draw. Instead I introduced a caching layer that remembers the positions of the previously highlighted brackets and bails out as early as possible when nothing changed. The biggest chunk of code is that "did something change?" comparison. It looks expensive, but typically isn't. There are typically only 2 elements in the array for a single opening–closing pair. (Possibly more when there are multiple text selections.) The elements in the two arrays are typically in the same order. (Except the cursor is on the closing bracket.) Which means the nested `every` → `for` loop will typically be executed 2 times only – one time for each of the 2 elements. I won't upload this change upstream because it is only relevant together with our custom "in the middle" bracket highlighting. With our customization we have many, many situations where the highlighted brackets don't change. This (almost) doesn't happen upstream. Bug: T270317 Change-Id: I789b45362388f0818e797f789f6af427a35e3e06
2021-01-19 14:13:59 +00:00
// Note: Modified by WMDE, now uses an intermediate format to delay the actual highlighting
markPositions.push(match);
if (match.to && cm.getLine(match.to.line).length <= maxHighlightLen)
Improve matchbrackets performance when moving the cursor My previous patch Icbb1122 focused on the behavior of the matchbrackets addon when the text is *edited*. This patch here is about moving the cursor without changing the text. I realized the addon re-draws everything every time the cursor moves, even if the highlighted pair of brackets is still the same. This triggers very expensive code in the CodeMirror lib. I had a look at this expensive code, but did not found an easy win. It just is what it is: an expensive re-draw. Instead I introduced a caching layer that remembers the positions of the previously highlighted brackets and bails out as early as possible when nothing changed. The biggest chunk of code is that "did something change?" comparison. It looks expensive, but typically isn't. There are typically only 2 elements in the array for a single opening–closing pair. (Possibly more when there are multiple text selections.) The elements in the two arrays are typically in the same order. (Except the cursor is on the closing bracket.) Which means the nested `every` → `for` loop will typically be executed 2 times only – one time for each of the 2 elements. I won't upload this change upstream because it is only relevant together with our custom "in the middle" bracket highlighting. With our customization we have many, many situations where the highlighted brackets don't change. This (almost) doesn't happen upstream. Bug: T270317 Change-Id: I789b45362388f0818e797f789f6af427a35e3e06
2021-01-19 14:13:59 +00:00
markPositions.push({from: match.to, match: match.match});
}
}
Improve matchbrackets performance when moving the cursor My previous patch Icbb1122 focused on the behavior of the matchbrackets addon when the text is *edited*. This patch here is about moving the cursor without changing the text. I realized the addon re-draws everything every time the cursor moves, even if the highlighted pair of brackets is still the same. This triggers very expensive code in the CodeMirror lib. I had a look at this expensive code, but did not found an easy win. It just is what it is: an expensive re-draw. Instead I introduced a caching layer that remembers the positions of the previously highlighted brackets and bails out as early as possible when nothing changed. The biggest chunk of code is that "did something change?" comparison. It looks expensive, but typically isn't. There are typically only 2 elements in the array for a single opening–closing pair. (Possibly more when there are multiple text selections.) The elements in the two arrays are typically in the same order. (Except the cursor is on the closing bracket.) Which means the nested `every` → `for` loop will typically be executed 2 times only – one time for each of the 2 elements. I won't upload this change upstream because it is only relevant together with our custom "in the middle" bracket highlighting. With our customization we have many, many situations where the highlighted brackets don't change. This (almost) doesn't happen upstream. Bug: T270317 Change-Id: I789b45362388f0818e797f789f6af427a35e3e06
2021-01-19 14:13:59 +00:00
// There is no config when autoclear is true
if (config && stillTheSameMarks(markPositions, config)) {
return;
}
if (!autoclear && config.currentlyHighlighted) {
config.currentlyHighlighted();
config.currentlyHighlighted = null;
}
for (i = 0; i < markPositions.length; i++) {
marks.push(cm.markText(
markPositions[i].from,
// Note: This assumes there is always exactly 1 character to highlight
Pos(markPositions[i].from.line, markPositions[i].from.ch + 1),
{className: markPositions[i].match ? 'CodeMirror-matchingbracket' : 'CodeMirror-nonmatchingbracket'}
));
}
if (marks.length) {
// Kludge to work around the IE bug from issue #1193, where text
// input stops going to the textarea whenever this fires.
if (ie_lt8 && cm.state.focused) cm.focus();
var clear = function() {
cm.operation(function() {
for (var i = 0; i < marks.length; i++) marks[i].clear();
});
};
if (autoclear) setTimeout(clear, 800);
Improve matchbrackets performance when moving the cursor My previous patch Icbb1122 focused on the behavior of the matchbrackets addon when the text is *edited*. This patch here is about moving the cursor without changing the text. I realized the addon re-draws everything every time the cursor moves, even if the highlighted pair of brackets is still the same. This triggers very expensive code in the CodeMirror lib. I had a look at this expensive code, but did not found an easy win. It just is what it is: an expensive re-draw. Instead I introduced a caching layer that remembers the positions of the previously highlighted brackets and bails out as early as possible when nothing changed. The biggest chunk of code is that "did something change?" comparison. It looks expensive, but typically isn't. There are typically only 2 elements in the array for a single opening–closing pair. (Possibly more when there are multiple text selections.) The elements in the two arrays are typically in the same order. (Except the cursor is on the closing bracket.) Which means the nested `every` → `for` loop will typically be executed 2 times only – one time for each of the 2 elements. I won't upload this change upstream because it is only relevant together with our custom "in the middle" bracket highlighting. With our customization we have many, many situations where the highlighted brackets don't change. This (almost) doesn't happen upstream. Bug: T270317 Change-Id: I789b45362388f0818e797f789f6af427a35e3e06
2021-01-19 14:13:59 +00:00
else config.currentlyHighlighted = clear;
}
}
function doMatchBrackets(cm) {
cm.operation(function() {
Improve matchbrackets performance when moving the cursor My previous patch Icbb1122 focused on the behavior of the matchbrackets addon when the text is *edited*. This patch here is about moving the cursor without changing the text. I realized the addon re-draws everything every time the cursor moves, even if the highlighted pair of brackets is still the same. This triggers very expensive code in the CodeMirror lib. I had a look at this expensive code, but did not found an easy win. It just is what it is: an expensive re-draw. Instead I introduced a caching layer that remembers the positions of the previously highlighted brackets and bails out as early as possible when nothing changed. The biggest chunk of code is that "did something change?" comparison. It looks expensive, but typically isn't. There are typically only 2 elements in the array for a single opening–closing pair. (Possibly more when there are multiple text selections.) The elements in the two arrays are typically in the same order. (Except the cursor is on the closing bracket.) Which means the nested `every` → `for` loop will typically be executed 2 times only – one time for each of the 2 elements. I won't upload this change upstream because it is only relevant together with our custom "in the middle" bracket highlighting. With our customization we have many, many situations where the highlighted brackets don't change. This (almost) doesn't happen upstream. Bug: T270317 Change-Id: I789b45362388f0818e797f789f6af427a35e3e06
2021-01-19 14:13:59 +00:00
matchBrackets(cm, false, cm.state.matchBrackets);
});
}
function clearHighlighted(cm) {
if (cm.state.matchBrackets && cm.state.matchBrackets.currentlyHighlighted) {
cm.state.matchBrackets.currentlyHighlighted();
cm.state.matchBrackets.currentlyHighlighted = null;
}
}
CodeMirror.defineOption("matchBrackets", false, function(cm, val, old) {
if (old && old != CodeMirror.Init) {
cm.off("cursorActivity", doMatchBrackets);
cm.off("focus", doMatchBrackets)
cm.off("blur", clearHighlighted)
clearHighlighted(cm);
}
if (val) {
cm.state.matchBrackets = typeof val == "object" ? val : {};
cm.on("cursorActivity", doMatchBrackets);
cm.on("focus", doMatchBrackets)
cm.on("blur", clearHighlighted)
}
});
CodeMirror.defineExtension("matchBrackets", function() {matchBrackets(this, true);});
CodeMirror.defineExtension("findMatchingBracket", function(pos, config, oldConfig){
// Backwards-compatibility kludge
if (oldConfig || typeof config == "boolean") {
if (!oldConfig) {
config = config ? {strict: true} : null
} else {
oldConfig.strict = config
config = oldConfig
}
}
return findMatchingBracket(this, pos, config)
});
CodeMirror.defineExtension("scanForBracket", function(pos, dir, style, config){
return scanForBracket(this, pos, dir, style, config);
});
});