improve bookmarking + rework codemirror-factory.js
* pull editing-only stuff from codemirror-default.js * switch throttledSetOption to IntersectionObserver
This commit is contained in:
parent
b4ca17c531
commit
657798d219
|
@ -1,3 +1,5 @@
|
||||||
|
/* Built-in CodeMirror and addon customization */
|
||||||
|
|
||||||
.CodeMirror-hints {
|
.CodeMirror-hints {
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
}
|
}
|
||||||
|
@ -20,12 +22,6 @@
|
||||||
.CodeMirror-dialog {
|
.CodeMirror-dialog {
|
||||||
animation: highlight 3s cubic-bezier(.18, .02, 0, .94);
|
animation: highlight 3s cubic-bezier(.18, .02, 0, .94);
|
||||||
}
|
}
|
||||||
.CodeMirror-bookmark {
|
|
||||||
background: linear-gradient(to right, currentColor, transparent);
|
|
||||||
position: absolute;
|
|
||||||
width: 2em;
|
|
||||||
opacity: .5;
|
|
||||||
}
|
|
||||||
.CodeMirror-search-field {
|
.CodeMirror-search-field {
|
||||||
width: 10em;
|
width: 10em;
|
||||||
}
|
}
|
||||||
|
@ -35,10 +31,6 @@
|
||||||
.CodeMirror-search-hint {
|
.CodeMirror-search-hint {
|
||||||
color: #888;
|
color: #888;
|
||||||
}
|
}
|
||||||
.cm-uso-variable {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.CodeMirror-activeline .applies-to:before {
|
.CodeMirror-activeline .applies-to:before {
|
||||||
background-color: hsla(214, 100%, 90%, 0.15);
|
background-color: hsla(214, 100%, 90%, 0.15);
|
||||||
content: "";
|
content: "";
|
||||||
|
@ -49,11 +41,9 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.CodeMirror-activeline .applies-to ul {
|
.CodeMirror-activeline .applies-to ul {
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.CodeMirror-foldgutter-open::after,
|
.CodeMirror-foldgutter-open::after,
|
||||||
.CodeMirror-foldgutter-folded::after {
|
.CodeMirror-foldgutter-folded::after {
|
||||||
top: 5px;
|
top: 5px;
|
||||||
|
@ -65,15 +55,25 @@
|
||||||
opacity: .5;
|
opacity: .5;
|
||||||
left: 1px;
|
left: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.CodeMirror-foldgutter-open::after {
|
.CodeMirror-foldgutter-open::after {
|
||||||
border-width: 5px 3px 0 3px;
|
border-width: 5px 3px 0 3px;
|
||||||
border-color: currentColor transparent transparent transparent;
|
border-color: currentColor transparent transparent transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.CodeMirror-foldgutter-folded::after {
|
.CodeMirror-foldgutter-folded::after {
|
||||||
margin-top: -2px;
|
margin-top: -2px;
|
||||||
margin-left: 1px;
|
margin-left: 1px;
|
||||||
border-width: 4px 0 4px 5px;
|
border-width: 4px 0 4px 5px;
|
||||||
border-color: transparent transparent transparent currentColor;
|
border-color: transparent transparent transparent currentColor;
|
||||||
}
|
}
|
||||||
|
.CodeMirror-linenumber {
|
||||||
|
cursor: pointer; /* for bookmarking */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom stuff we add to CodeMirror */
|
||||||
|
|
||||||
|
.cm-uso-variable {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.gutter-bookmark {
|
||||||
|
background: linear-gradient(0deg, hsla(180, 100%, 30%, .75) 2px, hsla(180, 100%, 30%, .2) 2px);
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
/* global
|
/* global
|
||||||
$
|
$
|
||||||
CodeMirror
|
CodeMirror
|
||||||
editor
|
|
||||||
prefs
|
prefs
|
||||||
t
|
t
|
||||||
*/
|
*/
|
||||||
|
@ -14,8 +13,6 @@
|
||||||
prefs.reset('editor.keyMap');
|
prefs.reset('editor.keyMap');
|
||||||
}
|
}
|
||||||
|
|
||||||
const CM_BOOKMARK = 'CodeMirror-bookmark';
|
|
||||||
const CM_BOOKMARK_GUTTER = CM_BOOKMARK + 'gutter';
|
|
||||||
const defaults = {
|
const defaults = {
|
||||||
autoCloseBrackets: prefs.get('editor.autoCloseBrackets'),
|
autoCloseBrackets: prefs.get('editor.autoCloseBrackets'),
|
||||||
mode: 'css',
|
mode: 'css',
|
||||||
|
@ -23,7 +20,6 @@
|
||||||
lineWrapping: prefs.get('editor.lineWrapping'),
|
lineWrapping: prefs.get('editor.lineWrapping'),
|
||||||
foldGutter: true,
|
foldGutter: true,
|
||||||
gutters: [
|
gutters: [
|
||||||
CM_BOOKMARK_GUTTER,
|
|
||||||
'CodeMirror-linenumbers',
|
'CodeMirror-linenumbers',
|
||||||
'CodeMirror-foldgutter',
|
'CodeMirror-foldgutter',
|
||||||
...(prefs.get('editor.linter') ? ['CodeMirror-lint-markers'] : []),
|
...(prefs.get('editor.linter') ? ['CodeMirror-lint-markers'] : []),
|
||||||
|
@ -35,7 +31,7 @@
|
||||||
theme: prefs.get('editor.theme'),
|
theme: prefs.get('editor.theme'),
|
||||||
keyMap: prefs.get('editor.keyMap'),
|
keyMap: prefs.get('editor.keyMap'),
|
||||||
extraKeys: Object.assign(CodeMirror.defaults.extraKeys || {}, {
|
extraKeys: Object.assign(CodeMirror.defaults.extraKeys || {}, {
|
||||||
// independent of current keyMap
|
// independent of current keyMap; some are implemented only for the edit page
|
||||||
'Alt-Enter': 'toggleStyle',
|
'Alt-Enter': 'toggleStyle',
|
||||||
'Alt-PageDown': 'nextEditor',
|
'Alt-PageDown': 'nextEditor',
|
||||||
'Alt-PageUp': 'prevEditor',
|
'Alt-PageUp': 'prevEditor',
|
||||||
|
@ -46,68 +42,53 @@
|
||||||
|
|
||||||
Object.assign(CodeMirror.defaults, defaults, prefs.get('editor.options'));
|
Object.assign(CodeMirror.defaults, defaults, prefs.get('editor.options'));
|
||||||
|
|
||||||
// 'basic' keymap only has basic keys by design, so we skip it
|
// Adding hotkeys to some keymaps except 'basic' which is primitive by design
|
||||||
|
const KM = CodeMirror.keyMap;
|
||||||
const extraKeysCommands = {};
|
const extras = Object.values(CodeMirror.defaults.extraKeys);
|
||||||
Object.keys(CodeMirror.defaults.extraKeys).forEach(key => {
|
if (!extras.includes('jumpToLine')) {
|
||||||
extraKeysCommands[CodeMirror.defaults.extraKeys[key]] = true;
|
KM.sublime['Ctrl-G'] = 'jumpToLine';
|
||||||
});
|
KM.emacsy['Ctrl-G'] = 'jumpToLine';
|
||||||
if (!extraKeysCommands.jumpToLine) {
|
KM.pcDefault['Ctrl-J'] = 'jumpToLine';
|
||||||
CodeMirror.keyMap.sublime['Ctrl-G'] = 'jumpToLine';
|
KM.macDefault['Cmd-J'] = 'jumpToLine';
|
||||||
CodeMirror.keyMap.emacsy['Ctrl-G'] = 'jumpToLine';
|
|
||||||
CodeMirror.keyMap.pcDefault['Ctrl-J'] = 'jumpToLine';
|
|
||||||
CodeMirror.keyMap.macDefault['Cmd-J'] = 'jumpToLine';
|
|
||||||
}
|
}
|
||||||
if (!extraKeysCommands.autocomplete) {
|
if (!extras.includes('autocomplete')) {
|
||||||
// will be used by 'sublime' on PC via fallthrough
|
// will be used by 'sublime' on PC via fallthrough
|
||||||
CodeMirror.keyMap.pcDefault['Ctrl-Space'] = 'autocomplete';
|
KM.pcDefault['Ctrl-Space'] = 'autocomplete';
|
||||||
// OSX uses Ctrl-Space and Cmd-Space for something else
|
// OSX uses Ctrl-Space and Cmd-Space for something else
|
||||||
CodeMirror.keyMap.macDefault['Alt-Space'] = 'autocomplete';
|
KM.macDefault['Alt-Space'] = 'autocomplete';
|
||||||
// copied from 'emacs' keymap
|
// copied from 'emacs' keymap
|
||||||
CodeMirror.keyMap.emacsy['Alt-/'] = 'autocomplete';
|
KM.emacsy['Alt-/'] = 'autocomplete';
|
||||||
// 'vim' and 'emacs' define their own autocomplete hotkeys
|
// 'vim' and 'emacs' define their own autocomplete hotkeys
|
||||||
}
|
}
|
||||||
if (!extraKeysCommands.blockComment) {
|
if (!extras.includes('blockComment')) {
|
||||||
CodeMirror.keyMap.sublime['Shift-Ctrl-/'] = 'commentSelection';
|
KM.sublime['Shift-Ctrl-/'] = 'commentSelection';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (navigator.appVersion.includes('Windows')) {
|
if (navigator.appVersion.includes('Windows')) {
|
||||||
// 'pcDefault' keymap on Windows should have F3/Shift-F3/Ctrl-R
|
// 'pcDefault' keymap on Windows should have F3/Shift-F3/Ctrl-R
|
||||||
if (!extraKeysCommands.findNext) {
|
if (!extras.includes('findNext')) KM.pcDefault['F3'] = 'findNext';
|
||||||
CodeMirror.keyMap.pcDefault['F3'] = 'findNext';
|
if (!extras.includes('findPrev')) KM.pcDefault['Shift-F3'] = 'findPrev';
|
||||||
}
|
if (!extras.includes('replace')) KM.pcDefault['Ctrl-R'] = 'replace';
|
||||||
if (!extraKeysCommands.findPrev) {
|
// try to remap non-interceptable (Shift-)Ctrl-N/T/W hotkeys
|
||||||
CodeMirror.keyMap.pcDefault['Shift-F3'] = 'findPrev';
|
|
||||||
}
|
|
||||||
if (!extraKeysCommands.replace) {
|
|
||||||
CodeMirror.keyMap.pcDefault['Ctrl-R'] = 'replace';
|
|
||||||
}
|
|
||||||
|
|
||||||
// try to remap non-interceptable Ctrl-(Shift-)N/T/W hotkeys
|
|
||||||
['N', 'T', 'W'].forEach(char => {
|
|
||||||
[
|
|
||||||
{from: 'Ctrl-', to: ['Alt-', 'Ctrl-Alt-']},
|
|
||||||
// Note: modifier order in CodeMirror is S-C-A
|
// Note: modifier order in CodeMirror is S-C-A
|
||||||
|
for (const char of ['N', 'T', 'W']) {
|
||||||
|
for (const remap of [
|
||||||
|
{from: 'Ctrl-', to: ['Alt-', 'Ctrl-Alt-']},
|
||||||
{from: 'Shift-Ctrl-', to: ['Ctrl-Alt-', 'Shift-Ctrl-Alt-']},
|
{from: 'Shift-Ctrl-', to: ['Ctrl-Alt-', 'Shift-Ctrl-Alt-']},
|
||||||
].forEach(remap => {
|
]) {
|
||||||
const oldKey = remap.from + char;
|
const oldKey = remap.from + char;
|
||||||
Object.keys(CodeMirror.keyMap).forEach(keyMapName => {
|
for (const km of Object.values(KM)) {
|
||||||
const keyMap = CodeMirror.keyMap[keyMapName];
|
const command = km[oldKey];
|
||||||
const command = keyMap[oldKey];
|
if (!command) continue;
|
||||||
if (!command) {
|
for (const newMod of remap.to) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
remap.to.some(newMod => {
|
|
||||||
const newKey = newMod + char;
|
const newKey = newMod + char;
|
||||||
if (!(newKey in keyMap)) {
|
if (newKey in km) continue;
|
||||||
delete keyMap[oldKey];
|
km[newKey] = command;
|
||||||
keyMap[newKey] = command;
|
delete km[oldKey];
|
||||||
return true;
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.assign(CodeMirror.mimeModes['text/css'].propertyKeywords, {
|
Object.assign(CodeMirror.mimeModes['text/css'].propertyKeywords, {
|
||||||
|
@ -123,6 +104,7 @@
|
||||||
'lightslategrey': true,
|
'lightslategrey': true,
|
||||||
'slategrey': true,
|
'slategrey': true,
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.assign(CodeMirror.prototype, {
|
Object.assign(CodeMirror.prototype, {
|
||||||
/**
|
/**
|
||||||
* @param {'less' | 'stylus' | ?} [pp] - any value besides `less` or `stylus` sets `css` mode
|
* @param {'less' | 'stylus' | ?} [pp] - any value besides `less` or `stylus` sets `css` mode
|
||||||
|
@ -141,204 +123,30 @@
|
||||||
this.eachLine(({text}) => (filled = text && /\S/.test(text)));
|
this.eachLine(({text}) => (filled = text && /\S/.test(text)));
|
||||||
return !filled;
|
return !filled;
|
||||||
},
|
},
|
||||||
});
|
/**
|
||||||
|
* Sets cursor and centers it in view if `pos` was out of view
|
||||||
// editor commands
|
* @param {CodeMirror.Pos} pos
|
||||||
for (const name of ['save', 'toggleStyle', 'nextEditor', 'prevEditor']) {
|
*/
|
||||||
CodeMirror.commands[name] = (...args) => editor[name](...args);
|
jumpToPos(pos) {
|
||||||
|
const coords = this.cursorCoords(pos, 'page');
|
||||||
|
const b = this.display.wrapper.getBoundingClientRect();
|
||||||
|
if (coords.top < b.top + this.defaultTextHeight() * 2 ||
|
||||||
|
coords.bottom > b.bottom - 100) {
|
||||||
|
this.scrollIntoView(pos, b.height / 2);
|
||||||
}
|
}
|
||||||
|
this.setCursor(pos, null, {scroll: false});
|
||||||
const elBookmark = document.createElement('div');
|
|
||||||
elBookmark.className = CM_BOOKMARK;
|
|
||||||
elBookmark.textContent = '\u00A0';
|
|
||||||
const clearMarker = function () {
|
|
||||||
const line = this.lines[0];
|
|
||||||
delete this.clear; // removing our patch from the instance...
|
|
||||||
this.clear(); // ...and using the original prototype
|
|
||||||
if (!(line.markedSpans || []).some(span => span.marker.sublimeBookmark)) {
|
|
||||||
this.doc.setGutterMarker(line, CM_BOOKMARK_GUTTER, null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const {markText} = CodeMirror.prototype;
|
|
||||||
Object.assign(CodeMirror.prototype, {
|
|
||||||
markText() {
|
|
||||||
const marker = markText.apply(this, arguments);
|
|
||||||
if (marker.sublimeBookmark) {
|
|
||||||
this.doc.setGutterMarker(marker.lines[0], CM_BOOKMARK_GUTTER, elBookmark.cloneNode(true));
|
|
||||||
marker.clear = clearMarker;
|
|
||||||
}
|
|
||||||
return marker;
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// CodeMirror convenience commands
|
|
||||||
Object.assign(CodeMirror.commands, {
|
Object.assign(CodeMirror.commands, {
|
||||||
toggleEditorFocus,
|
jumpToLine(cm) {
|
||||||
jumpToLine,
|
|
||||||
commentSelection,
|
|
||||||
});
|
|
||||||
|
|
||||||
function jumpToLine(cm) {
|
|
||||||
const cur = cm.getCursor();
|
const cur = cm.getCursor();
|
||||||
const oldDialog = $('.CodeMirror-dialog', cm.display.wrapper);
|
const oldDialog = $('.CodeMirror-dialog', cm.display.wrapper);
|
||||||
if (oldDialog) {
|
if (oldDialog) cm.focus(); // close the currently opened minidialog
|
||||||
// close the currently opened minidialog
|
|
||||||
cm.focus();
|
|
||||||
}
|
|
||||||
// make sure to focus the input in newly opened minidialog
|
|
||||||
// setTimeout(() => {
|
|
||||||
// $('.CodeMirror-dialog', section).focus();
|
|
||||||
// });
|
|
||||||
cm.openDialog(t.template.jumpToLine.cloneNode(true), str => {
|
cm.openDialog(t.template.jumpToLine.cloneNode(true), str => {
|
||||||
const m = str.match(/^\s*(\d+)(?:\s*:\s*(\d+))?\s*$/);
|
const [line, ch] = str.match(/^\s*(\d+)(?:\s*:\s*(\d+))?\s*$|$/);
|
||||||
if (m) {
|
if (line) cm.setCursor(line - 1, ch ? ch - 1 : cur.ch);
|
||||||
cm.setCursor(m[1] - 1, m[2] ? m[2] - 1 : cur.ch);
|
|
||||||
}
|
|
||||||
}, {value: cur.line + 1});
|
}, {value: cur.line + 1});
|
||||||
}
|
},
|
||||||
|
});
|
||||||
function commentSelection(cm) {
|
|
||||||
cm.blockComment(cm.getCursor('from'), cm.getCursor('to'), {fullLines: false});
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleEditorFocus(cm) {
|
|
||||||
if (!cm) return;
|
|
||||||
if (cm.hasFocus()) {
|
|
||||||
setTimeout(() => cm.display.input.blur());
|
|
||||||
} else {
|
|
||||||
cm.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-expressions
|
|
||||||
CodeMirror.hint && (() => {
|
|
||||||
const USO_VAR = 'uso-variable';
|
|
||||||
const USO_VALID_VAR = 'variable-3 ' + USO_VAR;
|
|
||||||
const USO_INVALID_VAR = 'error ' + USO_VAR;
|
|
||||||
const RX_IMPORTANT = /(i(m(p(o(r(t(a(nt?)?)?)?)?)?)?)?)?(?=\b|\W|$)/iy;
|
|
||||||
const RX_VAR_KEYWORD = /(^|[^-\w\u0080-\uFFFF])var\(/iy;
|
|
||||||
const RX_END_OF_VAR = /[\s,)]|$/g;
|
|
||||||
const RX_CONSUME_PROP = /[-\w]*\s*:\s?|$/y;
|
|
||||||
|
|
||||||
const originalHelper = CodeMirror.hint.css || (() => {});
|
|
||||||
const helper = cm => {
|
|
||||||
const pos = cm.getCursor();
|
|
||||||
const {line, ch} = pos;
|
|
||||||
const {styles, text} = cm.getLineHandle(line);
|
|
||||||
if (!styles) return originalHelper(cm);
|
|
||||||
const {style, index} = cm.getStyleAtPos({styles, pos: ch}) || {};
|
|
||||||
if (style && (style.startsWith('comment') || style.startsWith('string'))) {
|
|
||||||
return originalHelper(cm);
|
|
||||||
}
|
|
||||||
|
|
||||||
// !important
|
|
||||||
if (text[ch - 1] === '!' && /i|\W|^$/i.test(text[ch] || '')) {
|
|
||||||
RX_IMPORTANT.lastIndex = ch;
|
|
||||||
return {
|
|
||||||
list: ['important'],
|
|
||||||
from: pos,
|
|
||||||
to: {line, ch: ch + RX_IMPORTANT.exec(text)[0].length},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let prev = index > 2 ? styles[index - 2] : 0;
|
|
||||||
let end = styles[index];
|
|
||||||
|
|
||||||
// #hex colors
|
|
||||||
if (text[prev] === '#') {
|
|
||||||
return {list: [], from: pos, to: pos};
|
|
||||||
}
|
|
||||||
|
|
||||||
// adjust cursor position for /*[[ and ]]*/
|
|
||||||
const adjust = text[prev] === '/' ? 4 : 0;
|
|
||||||
prev += adjust;
|
|
||||||
end -= adjust;
|
|
||||||
const leftPart = text.slice(prev, ch);
|
|
||||||
|
|
||||||
// --css-variables
|
|
||||||
const startsWithDoubleDash = text[prev] === '-' && text[prev + 1] === '-';
|
|
||||||
if (startsWithDoubleDash ||
|
|
||||||
leftPart === '(' && testAt(RX_VAR_KEYWORD, Math.max(0, prev - 4), text)) {
|
|
||||||
// simplified regex without CSS escapes
|
|
||||||
const RX_CSS_VAR = new RegExp(
|
|
||||||
'(?:^|[\\s/;{])(' +
|
|
||||||
(leftPart.startsWith('--') ? leftPart : '--') +
|
|
||||||
(leftPart.length <= 2 ? '[a-zA-Z_\u0080-\uFFFF]' : '') +
|
|
||||||
'[-0-9a-zA-Z_\u0080-\uFFFF]*)',
|
|
||||||
'gm');
|
|
||||||
const cursor = cm.getSearchCursor(RX_CSS_VAR, null, {caseFold: false, multiline: false});
|
|
||||||
const list = new Set();
|
|
||||||
while (cursor.findNext()) {
|
|
||||||
list.add(cursor.pos.match[1]);
|
|
||||||
}
|
|
||||||
if (!startsWithDoubleDash) {
|
|
||||||
prev++;
|
|
||||||
}
|
|
||||||
RX_END_OF_VAR.lastIndex = prev;
|
|
||||||
end = RX_END_OF_VAR.exec(text).index;
|
|
||||||
return {
|
|
||||||
list: [...list.keys()].sort(),
|
|
||||||
from: {line, ch: prev},
|
|
||||||
to: {line, ch: end},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!editor || !style || !style.includes(USO_VAR)) {
|
|
||||||
// add ":" after a property name
|
|
||||||
const res = originalHelper(cm);
|
|
||||||
const state = res && cm.getTokenAt(pos).state.state;
|
|
||||||
if (state === 'block' || state === 'maybeprop') {
|
|
||||||
res.list = res.list.map(str => str + ': ');
|
|
||||||
RX_CONSUME_PROP.lastIndex = res.to.ch;
|
|
||||||
res.to.ch += RX_CONSUME_PROP.exec(text)[0].length;
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
// USO vars in usercss mode editor
|
|
||||||
const vars = editor.style.usercssData.vars;
|
|
||||||
const list = vars ?
|
|
||||||
Object.keys(vars).filter(name => name.startsWith(leftPart)) : [];
|
|
||||||
return {
|
|
||||||
list,
|
|
||||||
from: {line, ch: prev},
|
|
||||||
to: {line, ch: end},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
CodeMirror.registerHelper('hint', 'css', helper);
|
|
||||||
CodeMirror.registerHelper('hint', 'stylus', helper);
|
|
||||||
|
|
||||||
const hooks = CodeMirror.mimeModes['text/css'].tokenHooks;
|
|
||||||
const originalCommentHook = hooks['/'];
|
|
||||||
hooks['/'] = tokenizeUsoVariables;
|
|
||||||
|
|
||||||
function tokenizeUsoVariables(stream) {
|
|
||||||
const token = originalCommentHook.apply(this, arguments);
|
|
||||||
if (token[1] !== 'comment') {
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
const {string, start, pos} = stream;
|
|
||||||
// /*[[install-key]]*/
|
|
||||||
// 01234 43210
|
|
||||||
if (string[start + 2] === '[' &&
|
|
||||||
string[start + 3] === '[' &&
|
|
||||||
string[pos - 3] === ']' &&
|
|
||||||
string[pos - 4] === ']') {
|
|
||||||
const vars = typeof editor !== 'undefined' && (editor.style.usercssData || {}).vars;
|
|
||||||
const name = vars && string.slice(start + 4, pos - 4);
|
|
||||||
if (vars && Object.hasOwnProperty.call(vars, name.endsWith('-rgb') ? name.slice(0, -4) : name)) {
|
|
||||||
token[0] = USO_VALID_VAR;
|
|
||||||
} else {
|
|
||||||
token[0] = USO_INVALID_VAR;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
|
|
||||||
function testAt(rx, index, text) {
|
|
||||||
if (!rx) return false;
|
|
||||||
rx.lastIndex = index;
|
|
||||||
return rx.test(text);
|
|
||||||
}
|
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -1,87 +1,190 @@
|
||||||
/* global CodeMirror loadScript rerouteHotkeys prefs $ debounce $create */
|
/* global
|
||||||
/* exported cmFactory */
|
$
|
||||||
|
CodeMirror
|
||||||
|
debounce
|
||||||
|
editor
|
||||||
|
loadScript
|
||||||
|
prefs
|
||||||
|
rerouteHotkeys
|
||||||
|
*/
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
//#region cmFactory
|
||||||
|
(() => {
|
||||||
/*
|
/*
|
||||||
All cm instances created by this module are collected so we can broadcast prefs
|
All cm instances created by this module are collected so we can broadcast prefs
|
||||||
settings to them. You should `cmFactory.destroy(cm)` to unregister the listener
|
settings to them. You should `cmFactory.destroy(cm)` to unregister the listener
|
||||||
when the instance is not used anymore.
|
when the instance is not used anymore.
|
||||||
*/
|
*/
|
||||||
const cmFactory = (() => {
|
const cms = new Set();
|
||||||
const editors = new Set();
|
let lazyOpt;
|
||||||
// used by `indentWithTabs` option
|
|
||||||
const INSERT_TAB_COMMAND = CodeMirror.commands.insertTab;
|
|
||||||
const INSERT_SOFT_TAB_COMMAND = CodeMirror.commands.insertSoftTab;
|
|
||||||
|
|
||||||
CodeMirror.defineOption('tabSize', prefs.get('editor.tabSize'), (cm, value) => {
|
const cmFactory = window.cmFactory = {
|
||||||
|
create(place, options) {
|
||||||
|
const cm = CodeMirror(place, options);
|
||||||
|
const {wrapper} = cm.display;
|
||||||
|
cm.lastActive = 0;
|
||||||
|
cm.on('blur', () => {
|
||||||
|
rerouteHotkeys(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
wrapper.classList.toggle('CodeMirror-active', wrapper.contains(document.activeElement));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
cm.on('focus', () => {
|
||||||
|
rerouteHotkeys(false);
|
||||||
|
wrapper.classList.add('CodeMirror-active');
|
||||||
|
cm.lastActive = Date.now();
|
||||||
|
});
|
||||||
|
cms.add(cm);
|
||||||
|
return cm;
|
||||||
|
},
|
||||||
|
destroy(cm) {
|
||||||
|
cms.delete(cm);
|
||||||
|
},
|
||||||
|
globalSetOption(key, value) {
|
||||||
|
CodeMirror.defaults[key] = value;
|
||||||
|
if (cms.size > 4 && lazyOpt && lazyOpt.names.includes(key)) {
|
||||||
|
lazyOpt.set(key, value);
|
||||||
|
} else {
|
||||||
|
cms.forEach(cm => cm.setOption(key, value));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const handledPrefs = {
|
||||||
|
// handled in colorpicker-helper.js
|
||||||
|
'editor.colorpicker'() {},
|
||||||
|
/** @returns {?Promise<void>} */
|
||||||
|
'editor.theme'(key, value) {
|
||||||
|
const elt = $('#cm-theme');
|
||||||
|
if (value === 'default') {
|
||||||
|
elt.href = '';
|
||||||
|
} else {
|
||||||
|
const url = chrome.runtime.getURL(`vendor/codemirror/theme/${value}.css`);
|
||||||
|
if (url !== elt.href) {
|
||||||
|
// avoid flicker: wait for the second stylesheet to load, then apply the theme
|
||||||
|
return loadScript(url, true).then(([newElt]) => {
|
||||||
|
cmFactory.globalSetOption('theme', value);
|
||||||
|
elt.remove();
|
||||||
|
newElt.id = elt.id;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const pref2opt = k => k.slice('editor.'.length);
|
||||||
|
const mirroredPrefs = Object.keys(prefs.defaults).filter(k =>
|
||||||
|
!handledPrefs[k] &&
|
||||||
|
k.startsWith('editor.') &&
|
||||||
|
Object.hasOwnProperty.call(CodeMirror.defaults, pref2opt(k)));
|
||||||
|
prefs.subscribe(mirroredPrefs, (k, val) => cmFactory.globalSetOption(pref2opt(k), val));
|
||||||
|
prefs.subscribeMany(handledPrefs);
|
||||||
|
|
||||||
|
lazyOpt = window.IntersectionObserver && {
|
||||||
|
names: ['theme', 'lineWrapping'],
|
||||||
|
set(key, value) {
|
||||||
|
const {observer, queue} = lazyOpt;
|
||||||
|
for (const cm of cms) {
|
||||||
|
let opts = queue.get(cm);
|
||||||
|
if (!opts) queue.set(cm, opts = {});
|
||||||
|
opts[key] = value;
|
||||||
|
observer.observe(cm.display.wrapper);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setNow({cm, data}) {
|
||||||
|
cm.operation(() => data.forEach(kv => cm.setOption(...kv)));
|
||||||
|
},
|
||||||
|
onView(entries) {
|
||||||
|
const {queue, observer} = lazyOpt;
|
||||||
|
const delayed = [];
|
||||||
|
for (const e of entries) {
|
||||||
|
const r = e.isIntersecting && e.intersectionRect;
|
||||||
|
if (!r) continue;
|
||||||
|
const cm = e.target.CodeMirror;
|
||||||
|
const data = Object.entries(queue.get(cm) || {});
|
||||||
|
queue.delete(cm);
|
||||||
|
observer.unobserve(e.target);
|
||||||
|
if (!data.every(([key, val]) => cm.getOption(key) === val)) {
|
||||||
|
if (r.bottom > 0 && r.top < window.innerHeight) {
|
||||||
|
lazyOpt.setNow({cm, data});
|
||||||
|
} else {
|
||||||
|
delayed.push({cm, data});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (delayed.length) {
|
||||||
|
setTimeout(() => delayed.forEach(lazyOpt.setNow));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
get observer() {
|
||||||
|
if (!lazyOpt._observer) {
|
||||||
|
// must exceed refreshOnView's 100%
|
||||||
|
lazyOpt._observer = new IntersectionObserver(lazyOpt.onView, {rootMargin: '150%'});
|
||||||
|
lazyOpt.queue = new WeakMap();
|
||||||
|
}
|
||||||
|
return lazyOpt._observer;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region Commands
|
||||||
|
(() => {
|
||||||
|
Object.assign(CodeMirror.commands, {
|
||||||
|
toggleEditorFocus(cm) {
|
||||||
|
if (!cm) return;
|
||||||
|
if (cm.hasFocus()) {
|
||||||
|
setTimeout(() => cm.display.input.blur());
|
||||||
|
} else {
|
||||||
|
cm.focus();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
commentSelection(cm) {
|
||||||
|
cm.blockComment(cm.getCursor('from'), cm.getCursor('to'), {fullLines: false});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
for (const cmd of [
|
||||||
|
'nextEditor',
|
||||||
|
'prevEditor',
|
||||||
|
'save',
|
||||||
|
'toggleStyle',
|
||||||
|
]) {
|
||||||
|
CodeMirror.commands[cmd] = (...args) => editor[cmd](...args);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region CM option handlers
|
||||||
|
(() => {
|
||||||
|
const {insertTab, insertSoftTab} = CodeMirror.commands;
|
||||||
|
Object.entries({
|
||||||
|
tabSize(cm, value) {
|
||||||
cm.setOption('indentUnit', Number(value));
|
cm.setOption('indentUnit', Number(value));
|
||||||
});
|
},
|
||||||
|
indentWithTabs(cm, value) {
|
||||||
CodeMirror.defineOption('indentWithTabs', prefs.get('editor.indentWithTabs'), (cm, value) => {
|
CodeMirror.commands.insertTab = value ? insertTab : insertSoftTab;
|
||||||
CodeMirror.commands.insertTab = value ?
|
},
|
||||||
INSERT_TAB_COMMAND :
|
autocompleteOnTyping(cm, value) {
|
||||||
INSERT_SOFT_TAB_COMMAND;
|
|
||||||
});
|
|
||||||
|
|
||||||
CodeMirror.defineOption('autocompleteOnTyping', prefs.get('editor.autocompleteOnTyping'), (cm, value) => {
|
|
||||||
const onOff = value ? 'on' : 'off';
|
const onOff = value ? 'on' : 'off';
|
||||||
cm[onOff]('changes', autocompleteOnTyping);
|
cm[onOff]('changes', autocompleteOnTyping);
|
||||||
cm[onOff]('pick', autocompletePicked);
|
cm[onOff]('pick', autocompletePicked);
|
||||||
});
|
},
|
||||||
|
matchHighlight(cm, value) {
|
||||||
CodeMirror.defineOption('matchHighlight', prefs.get('editor.matchHighlight'), (cm, value) => {
|
const showToken = value === 'token' && /[#.\-\w]/;
|
||||||
if (value === 'token') {
|
const opt = (showToken || value === 'selection') && {
|
||||||
cm.setOption('highlightSelectionMatches', {
|
showToken,
|
||||||
showToken: /[#.\-\w]/,
|
|
||||||
annotateScrollbar: true,
|
annotateScrollbar: true,
|
||||||
onUpdate: updateMatchHighlightCount,
|
onUpdate: updateMatchHighlightCount,
|
||||||
});
|
};
|
||||||
} else if (value === 'selection') {
|
cm.setOption('highlightSelectionMatches', opt || null);
|
||||||
cm.setOption('highlightSelectionMatches', {
|
},
|
||||||
showToken: false,
|
selectByTokens(cm, value) {
|
||||||
annotateScrollbar: true,
|
|
||||||
onUpdate: updateMatchHighlightCount,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
cm.setOption('highlightSelectionMatches', null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
CodeMirror.defineOption('selectByTokens', prefs.get('editor.selectByTokens'), (cm, value) => {
|
|
||||||
cm.setOption('configureMouse', value ? configureMouseFn : null);
|
cm.setOption('configureMouse', value ? configureMouseFn : null);
|
||||||
|
},
|
||||||
|
}).forEach(([name, fn]) => {
|
||||||
|
CodeMirror.defineOption(name, prefs.get('editor.' + name), fn);
|
||||||
});
|
});
|
||||||
|
|
||||||
prefs.subscribe(null, (key, value) => {
|
|
||||||
const option = key.replace(/^editor\./, '');
|
|
||||||
if (!option) {
|
|
||||||
console.error('no "cm_option"', key);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// FIXME: this is implemented in `colorpicker-helper.js`.
|
|
||||||
if (option === 'colorpicker') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (option === 'theme') {
|
|
||||||
const themeLink = $('#cm-theme');
|
|
||||||
// use non-localized 'default' internally
|
|
||||||
if (value === 'default') {
|
|
||||||
themeLink.href = '';
|
|
||||||
} else {
|
|
||||||
const url = chrome.runtime.getURL('vendor/codemirror/theme/' + value + '.css');
|
|
||||||
if (themeLink.href !== url) {
|
|
||||||
// avoid flicker: wait for the second stylesheet to load, then apply the theme
|
|
||||||
return loadScript(url, true).then(([newThemeLink]) => {
|
|
||||||
setOption(option, value);
|
|
||||||
themeLink.remove();
|
|
||||||
newThemeLink.id = 'cm-theme';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// broadcast option
|
|
||||||
setOption(option, value);
|
|
||||||
});
|
|
||||||
return {create, destroy, setOption};
|
|
||||||
|
|
||||||
function updateMatchHighlightCount(cm, state) {
|
function updateMatchHighlightCount(cm, state) {
|
||||||
cm.display.wrapper.dataset.matchHighlightCount = state.matchesonscroll.matches.length;
|
cm.display.wrapper.dataset.matchHighlightCount = state.matchesonscroll.matches.length;
|
||||||
}
|
}
|
||||||
|
@ -173,121 +276,181 @@ const cmFactory = (() => {
|
||||||
function autocompletePicked(cm) {
|
function autocompletePicked(cm) {
|
||||||
cm.state.autocompletePicked = true;
|
cm.state.autocompletePicked = true;
|
||||||
}
|
}
|
||||||
|
})();
|
||||||
|
//#endregion
|
||||||
|
|
||||||
function destroy(cm) {
|
//#region Autocomplete
|
||||||
editors.delete(cm);
|
(() => {
|
||||||
|
const USO_VAR = 'uso-variable';
|
||||||
|
const USO_VALID_VAR = 'variable-3 ' + USO_VAR;
|
||||||
|
const USO_INVALID_VAR = 'error ' + USO_VAR;
|
||||||
|
const RX_IMPORTANT = /(i(m(p(o(r(t(a(nt?)?)?)?)?)?)?)?)?(?=\b|\W|$)/iy;
|
||||||
|
const RX_VAR_KEYWORD = /(^|[^-\w\u0080-\uFFFF])var\(/iy;
|
||||||
|
const RX_END_OF_VAR = /[\s,)]|$/g;
|
||||||
|
const RX_CONSUME_PROP = /[-\w]*\s*:\s?|$/y;
|
||||||
|
const originalHelper = CodeMirror.hint.css || (() => {});
|
||||||
|
CodeMirror.registerHelper('hint', 'css', helper);
|
||||||
|
CodeMirror.registerHelper('hint', 'stylus', helper);
|
||||||
|
const hooks = CodeMirror.mimeModes['text/css'].tokenHooks;
|
||||||
|
const originalCommentHook = hooks['/'];
|
||||||
|
hooks['/'] = tokenizeUsoVariables;
|
||||||
|
|
||||||
|
function helper(cm) {
|
||||||
|
const pos = cm.getCursor();
|
||||||
|
const {line, ch} = pos;
|
||||||
|
const {styles, text} = cm.getLineHandle(line);
|
||||||
|
if (!styles) {
|
||||||
|
return originalHelper(cm);
|
||||||
|
}
|
||||||
|
const {style, index} = cm.getStyleAtPos({styles, pos: ch}) || {};
|
||||||
|
if (/^(comment|string)/.test(style)) {
|
||||||
|
return originalHelper(cm);
|
||||||
|
}
|
||||||
|
// !important
|
||||||
|
if (text[ch - 1] === '!' && testAt(/i|\W|$/iy, ch, text)) {
|
||||||
|
return {
|
||||||
|
list: ['important'],
|
||||||
|
from: pos,
|
||||||
|
to: {line, ch: ch + execAt(RX_IMPORTANT, ch, text)[0].length},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let prev = index > 2 ? styles[index - 2] : 0;
|
||||||
|
let end = styles[index];
|
||||||
|
// #hex colors
|
||||||
|
if (text[prev] === '#') {
|
||||||
|
return {list: [], from: pos, to: pos};
|
||||||
|
}
|
||||||
|
// adjust cursor position for /*[[ and ]]*/
|
||||||
|
const adjust = text[prev] === '/' ? 4 : 0;
|
||||||
|
prev += adjust;
|
||||||
|
end -= adjust;
|
||||||
|
// --css-variables
|
||||||
|
const leftPart = text.slice(prev, ch);
|
||||||
|
const startsWithDoubleDash = testAt(/--/y, prev, text);
|
||||||
|
if (startsWithDoubleDash ||
|
||||||
|
leftPart === '(' && testAt(RX_VAR_KEYWORD, Math.max(0, prev - 4), text)) {
|
||||||
|
return {
|
||||||
|
list: findAllCssVars(cm, leftPart),
|
||||||
|
from: {line, ch: prev + !startsWithDoubleDash},
|
||||||
|
to: {line, ch: execAt(RX_END_OF_VAR, prev, text).index},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!editor || !style || !style.includes(USO_VAR)) {
|
||||||
|
const res = originalHelper(cm);
|
||||||
|
// add ":" after a property name
|
||||||
|
const state = res && cm.getTokenAt(pos).state.state;
|
||||||
|
if (state === 'block' || state === 'maybeprop') {
|
||||||
|
res.list = res.list.map(str => str + ': ');
|
||||||
|
res.to.ch += execAt(RX_CONSUME_PROP, res.to.ch, text)[0].length;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
// USO vars in usercss mode editor
|
||||||
|
const vars = editor.style.usercssData.vars;
|
||||||
|
return {
|
||||||
|
list: vars ? Object.keys(vars).filter(v => v.startsWith(leftPart)) : [],
|
||||||
|
from: {line, ch: prev},
|
||||||
|
to: {line, ch: end},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function create(init, options) {
|
function findAllCssVars(cm, leftPart) {
|
||||||
const cm = CodeMirror(init, options);
|
// simplified regex without CSS escapes
|
||||||
cm.lastActive = 0;
|
const RX_CSS_VAR = new RegExp(
|
||||||
const wrapper = cm.display.wrapper;
|
'(?:^|[\\s/;{])(' +
|
||||||
cm.on('blur', () => {
|
(leftPart.startsWith('--') ? leftPart : '--') +
|
||||||
rerouteHotkeys(true);
|
(leftPart.length <= 2 ? '[a-zA-Z_\u0080-\uFFFF]' : '') +
|
||||||
setTimeout(() => {
|
'[-0-9a-zA-Z_\u0080-\uFFFF]*)',
|
||||||
wrapper.classList.toggle('CodeMirror-active', wrapper.contains(document.activeElement));
|
'g');
|
||||||
|
const list = new Set();
|
||||||
|
cm.eachLine(({text}) => {
|
||||||
|
for (let m; (m = RX_CSS_VAR.exec(text));) {
|
||||||
|
list.add(m[1]);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
return [...list].sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
function tokenizeUsoVariables(stream) {
|
||||||
|
const token = originalCommentHook.apply(this, arguments);
|
||||||
|
if (token[1] === 'comment') {
|
||||||
|
const {string, start, pos} = stream;
|
||||||
|
if (testAt(/\/\*\[\[/y, start, string) &&
|
||||||
|
testAt(/]]\*\//y, pos - 4, string)) {
|
||||||
|
const vars = (editor.style.usercssData || {}).vars;
|
||||||
|
token[0] =
|
||||||
|
vars && vars.hasOwnProperty(string.slice(start + 4, pos - 4).replace(/-rgb$/, ''))
|
||||||
|
? USO_VALID_VAR
|
||||||
|
: USO_INVALID_VAR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
function execAt(rx, index, text) {
|
||||||
|
rx.lastIndex = index;
|
||||||
|
return rx.exec(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testAt(rx, index, text) {
|
||||||
|
rx.lastIndex = index;
|
||||||
|
return rx.test(text);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region Bookmarks
|
||||||
|
(() => {
|
||||||
|
const CLS = 'gutter-bookmark';
|
||||||
|
const BRAND = 'sublimeBookmark';
|
||||||
|
const CLICK_AREA = 'CodeMirror-linenumbers';
|
||||||
|
const {markText} = CodeMirror.prototype;
|
||||||
|
CodeMirror.defineInitHook(cm => {
|
||||||
|
cm.on('gutterClick', onGutterClick);
|
||||||
|
cm.on('gutterContextMenu', onGutterContextMenu);
|
||||||
});
|
});
|
||||||
cm.on('focus', () => {
|
// TODO: reimplement bookmarking so next/prev order is decided solely by the line numbers
|
||||||
rerouteHotkeys(false);
|
Object.assign(CodeMirror.prototype, {
|
||||||
wrapper.classList.add('CodeMirror-active');
|
markText() {
|
||||||
cm.lastActive = Date.now();
|
const marker = markText.apply(this, arguments);
|
||||||
|
if (marker[BRAND]) {
|
||||||
|
this.doc.addLineClass(marker.lines[0], 'gutter', CLS);
|
||||||
|
marker.clear = clearMarker;
|
||||||
|
}
|
||||||
|
return marker;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
editors.add(cm);
|
function clearMarker() {
|
||||||
return cm;
|
const line = this.lines[0];
|
||||||
}
|
const spans = line.markedSpans;
|
||||||
|
delete this.clear; // removing our patch from the instance...
|
||||||
function getLastActivated() {
|
this.clear(); // ...and using the original prototype
|
||||||
let result;
|
if (!spans || spans.some(span => span.marker[BRAND])) {
|
||||||
for (const cm of editors) {
|
this.doc.removeLineClass(line, 'gutter', CLS);
|
||||||
if (!result || result.lastActive < cm.lastActive) {
|
|
||||||
result = cm;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
function onGutterClick(cm, line, name, e) {
|
||||||
|
switch (name === CLICK_AREA && e.button) {
|
||||||
|
case 0: {
|
||||||
|
// main button: toggle
|
||||||
|
const [mark] = cm.findMarks({line, ch: 0}, {line, ch: 1e9}, m => m[BRAND]);
|
||||||
|
cm.setCursor(mark ? mark.find(-1) : {line, ch: 0});
|
||||||
|
cm.execCommand('toggleBookmark');
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
case 1:
|
||||||
function setOption(key, value) {
|
// middle button: select all marks
|
||||||
CodeMirror.defaults[key] = value;
|
cm.execCommand('selectBookmarks');
|
||||||
if (editors.size > 4 && (key === 'theme' || key === 'lineWrapping')) {
|
|
||||||
throttleSetOption({key, value, index: 0});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const cm of editors) {
|
|
||||||
cm.setOption(key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function throttleSetOption({
|
|
||||||
key,
|
|
||||||
value,
|
|
||||||
index,
|
|
||||||
timeStart = performance.now(),
|
|
||||||
editorsCopy = [...editors],
|
|
||||||
cmStart = getLastActivated(),
|
|
||||||
progress,
|
|
||||||
}) {
|
|
||||||
if (index === 0) {
|
|
||||||
if (!cmStart) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
cmStart.setOption(key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
const THROTTLE_AFTER_MS = 100;
|
|
||||||
const THROTTLE_SHOW_PROGRESS_AFTER_MS = 100;
|
|
||||||
|
|
||||||
const t0 = performance.now();
|
|
||||||
const total = editorsCopy.length;
|
|
||||||
while (index < total) {
|
|
||||||
const cm = editorsCopy[index++];
|
|
||||||
if (cm === cmStart || !editors.has(cm)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
cm.setOption(key, value);
|
|
||||||
if (performance.now() - t0 > THROTTLE_AFTER_MS) {
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (index >= total) {
|
function onGutterContextMenu(cm, line, name, e) {
|
||||||
$.remove(progress);
|
if (name === CLICK_AREA) {
|
||||||
return;
|
cm.setSelection = cm.jumpToPos;
|
||||||
|
cm.execCommand(e.ctrlKey ? 'prevBookmark' : 'nextBookmark');
|
||||||
|
delete cm.setSelection;
|
||||||
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
if (!progress &&
|
|
||||||
index < total / 2 &&
|
|
||||||
t0 - timeStart > THROTTLE_SHOW_PROGRESS_AFTER_MS) {
|
|
||||||
let option = $('#editor.' + key);
|
|
||||||
if (option) {
|
|
||||||
if (option.type === 'checkbox') {
|
|
||||||
option = (option.labels || [])[0] || option.nextElementSibling || option;
|
|
||||||
}
|
|
||||||
progress = document.body.appendChild(
|
|
||||||
$create('.set-option-progress', {targetElement: option}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (progress) {
|
|
||||||
const optionBounds = progress.targetElement.getBoundingClientRect();
|
|
||||||
const bounds = {
|
|
||||||
top: optionBounds.top + window.scrollY + 1,
|
|
||||||
left: optionBounds.left + window.scrollX + 1,
|
|
||||||
width: (optionBounds.width - 2) * index / total | 0,
|
|
||||||
height: optionBounds.height - 2,
|
|
||||||
};
|
|
||||||
const style = progress.style;
|
|
||||||
for (const prop in bounds) {
|
|
||||||
if (bounds[prop] !== parseFloat(style[prop])) {
|
|
||||||
style[prop] = bounds[prop] + 'px';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setTimeout(throttleSetOption, 0, {
|
|
||||||
key,
|
|
||||||
value,
|
|
||||||
index,
|
|
||||||
timeStart,
|
|
||||||
cmStart,
|
|
||||||
editorsCopy,
|
|
||||||
progress,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
//#endregion
|
||||||
|
|
|
@ -5,9 +5,8 @@
|
||||||
onDOMready().then(() => {
|
onDOMready().then(() => {
|
||||||
$('#colorpicker-settings').onclick = configureColorpicker;
|
$('#colorpicker-settings').onclick = configureColorpicker;
|
||||||
});
|
});
|
||||||
prefs.subscribe(['editor.colorpicker.hotkey'], registerHotkey);
|
prefs.subscribe('editor.colorpicker.hotkey', registerHotkey);
|
||||||
prefs.subscribe(['editor.colorpicker'], setColorpickerOption);
|
prefs.subscribe('editor.colorpicker', setColorpickerOption, {now: true});
|
||||||
setColorpickerOption(null, prefs.get('editor.colorpicker'));
|
|
||||||
|
|
||||||
function setColorpickerOption(id, enabled) {
|
function setColorpickerOption(id, enabled) {
|
||||||
const defaults = CodeMirror.defaults;
|
const defaults = CodeMirror.defaults;
|
||||||
|
@ -44,7 +43,7 @@
|
||||||
delete defaults.extraKeys[keyName];
|
delete defaults.extraKeys[keyName];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cmFactory.setOption('colorpicker', defaults.colorpicker);
|
cmFactory.globalSetOption('colorpicker', defaults.colorpicker);
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerHotkey(id, hotkey) {
|
function registerHotkey(id, hotkey) {
|
||||||
|
|
|
@ -290,12 +290,6 @@ input:invalid {
|
||||||
padding: .1rem .25rem 0 0;
|
padding: .1rem .25rem 0 0;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
.set-option-progress {
|
|
||||||
position: absolute;
|
|
||||||
background-color: currentColor;
|
|
||||||
content: "";
|
|
||||||
opacity: .15;
|
|
||||||
}
|
|
||||||
/* footer */
|
/* footer */
|
||||||
.usercss #footer {
|
.usercss #footer {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
|
@ -55,7 +55,7 @@ function SourceEditor() {
|
||||||
const sec = sectionFinder.sections[i];
|
const sec = sectionFinder.sections[i];
|
||||||
if (sec) {
|
if (sec) {
|
||||||
sectionFinder.updatePositions(sec);
|
sectionFinder.updatePositions(sec);
|
||||||
jumpToPos(sec.start);
|
cm.jumpToPos(sec.start);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
closestVisible: () => cm,
|
closestVisible: () => cm,
|
||||||
|
@ -308,17 +308,7 @@ function SourceEditor() {
|
||||||
if (i < 0 && (!dir || CodeMirror.cmpPos(sections[num - 1].start, pos) < 0)) {
|
if (i < 0 && (!dir || CodeMirror.cmpPos(sections[num - 1].start, pos) < 0)) {
|
||||||
i = 0;
|
i = 0;
|
||||||
}
|
}
|
||||||
jumpToPos(sections[(i + dir + num) % num].start);
|
cm.jumpToPos(sections[(i + dir + num) % num].start);
|
||||||
}
|
|
||||||
|
|
||||||
function jumpToPos(pos) {
|
|
||||||
const coords = cm.cursorCoords(pos, 'page');
|
|
||||||
const b = cm.display.wrapper.getBoundingClientRect();
|
|
||||||
if (coords.top < b.top + cm.defaultTextHeight() * 2 ||
|
|
||||||
coords.bottom > b.bottom - 100) {
|
|
||||||
cm.scrollIntoView(pos, b.height / 2);
|
|
||||||
}
|
|
||||||
cm.setCursor(pos, null, {scroll: false});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function headerOnScroll({target, deltaY, deltaMode, shiftKey}) {
|
function headerOnScroll({target, deltaY, deltaMode, shiftKey}) {
|
||||||
|
|
|
@ -610,10 +610,7 @@
|
||||||
const lines = el.title.split('\n')[1].match(/\d+/g).map(Number);
|
const lines = el.title.split('\n')[1].match(/\d+/g).map(Number);
|
||||||
const i = lines.indexOf(cm.getCursor().line + 1) + 1;
|
const i = lines.indexOf(cm.getCursor().line + 1) + 1;
|
||||||
const line = (lines[i] || lines[0]) - 1;
|
const line = (lines[i] || lines[0]) - 1;
|
||||||
const vpm = cm.options.viewportMargin;
|
cm.jumpToPos({line, ch: 0});
|
||||||
const inView = line >= cm.display.viewFrom - vpm && line <= cm.display.viewTo - vpm;
|
|
||||||
cm.scrollIntoView(line, inView ? cm.defaultTextHeight() : cm.display.wrapper.clientHeight / 2);
|
|
||||||
cm.setCursor(line);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//endregion
|
//endregion
|
||||||
|
|
Loading…
Reference in New Issue
Block a user