Add: codemirror-factory
This commit is contained in:
parent
15a1f552f6
commit
d26ce3238e
|
@ -228,69 +228,48 @@
|
||||||
return isBlank;
|
return isBlank;
|
||||||
});
|
});
|
||||||
|
|
||||||
// doubleclick option
|
// editor commands
|
||||||
if (typeof editors !== 'undefined') {
|
for (const name of ['save', 'toggleStyle', 'nextEditor', 'prevEditor']) {
|
||||||
const fn = (cm, repeat) =>
|
CodeMirror.commands[name] = () => editor[name]();
|
||||||
repeat === 'double' ?
|
|
||||||
{unit: selectTokenOnDoubleclick} :
|
|
||||||
{};
|
|
||||||
const configure = (_, enabled) => {
|
|
||||||
editors.forEach(cm => cm.setOption('configureMouse', enabled ? fn : null));
|
|
||||||
CodeMirror.defaults.configureMouse = enabled ? fn : null;
|
|
||||||
};
|
|
||||||
configure(null, prefs.get('editor.selectByTokens'));
|
|
||||||
prefs.subscribe(['editor.selectByTokens'], configure);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectTokenOnDoubleclick(cm, pos) {
|
// CodeMirror convenience commands
|
||||||
let {ch} = pos;
|
Object.assign(CodeMirror.commands, {
|
||||||
const {line, sticky} = pos;
|
toggleEditorFocus,
|
||||||
const {text, styles} = cm.getLineHandle(line);
|
jumpToLine,
|
||||||
|
commentSelection,
|
||||||
|
});
|
||||||
|
|
||||||
const execAt = (rx, i) => (rx.lastIndex = i) && null || rx.exec(text);
|
function jumpToLine(cm) {
|
||||||
const at = (rx, i) => (rx.lastIndex = i) && null || rx.test(text);
|
const cur = cm.getCursor();
|
||||||
const atWord = ch => at(/\w/y, ch);
|
const oldDialog = $('.CodeMirror-dialog', cm.display.wrapper);
|
||||||
const atSpace = ch => at(/\s/y, ch);
|
if (oldDialog) {
|
||||||
|
// close the currently opened minidialog
|
||||||
const atTokenEnd = styles.indexOf(ch, 1);
|
cm.focus();
|
||||||
ch += atTokenEnd < 0 ? 0 : sticky === 'before' && atWord(ch - 1) ? 0 : atSpace(ch + 1) ? 0 : 1;
|
|
||||||
ch = Math.min(text.length, ch);
|
|
||||||
const type = cm.getTokenTypeAt({line, ch: ch + (sticky === 'after' ? 1 : 0)});
|
|
||||||
if (atTokenEnd > 0) ch--;
|
|
||||||
|
|
||||||
const isCss = type && !/^(comment|string)/.test(type);
|
|
||||||
const isNumber = type === 'number';
|
|
||||||
const isSpace = atSpace(ch);
|
|
||||||
let wordChars =
|
|
||||||
isNumber ? /[-+\w.%]/y :
|
|
||||||
isCss ? /[-\w@]/y :
|
|
||||||
isSpace ? /\s/y :
|
|
||||||
atWord(ch) ? /\w/y : /[^\w\s]/y;
|
|
||||||
|
|
||||||
let a = ch;
|
|
||||||
while (a && at(wordChars, a)) a--;
|
|
||||||
a += !a && at(wordChars, a) || isCss && at(/[.!#@]/y, a) ? 0 : at(wordChars, a + 1);
|
|
||||||
|
|
||||||
let b, found;
|
|
||||||
|
|
||||||
if (isNumber) {
|
|
||||||
b = a + execAt(/[+-]?[\d.]+(e\d+)?|$/yi, a)[0].length;
|
|
||||||
found = b >= ch;
|
|
||||||
if (!found) {
|
|
||||||
a = b;
|
|
||||||
ch = a;
|
|
||||||
}
|
}
|
||||||
|
// make sure to focus the input in newly opened minidialog
|
||||||
|
// setTimeout(() => {
|
||||||
|
// $('.CodeMirror-dialog', section).focus();
|
||||||
|
// });
|
||||||
|
cm.openDialog(template.jumpToLine.cloneNode(true), str => {
|
||||||
|
const m = str.match(/^\s*(\d+)(?:\s*:\s*(\d+))?\s*$/);
|
||||||
|
if (m) {
|
||||||
|
cm.setCursor(m[1] - 1, m[2] ? m[2] - 1 : cur.ch);
|
||||||
|
}
|
||||||
|
}, {value: cur.line + 1});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!found) {
|
function commentSelection(cm) {
|
||||||
wordChars = isCss ? /[-\w]*/y : new RegExp(wordChars.source + '*', 'uy');
|
cm.blockComment(cm.getCursor('from'), cm.getCursor('to'), {fullLines: false});
|
||||||
b = ch + execAt(wordChars, ch)[0].length;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
function toggleEditorFocus(cm) {
|
||||||
from: {line, ch: a},
|
if (!cm) return;
|
||||||
to: {line, ch: b},
|
if (cm.hasFocus()) {
|
||||||
};
|
setTimeout(() => cm.display.input.blur());
|
||||||
|
} else {
|
||||||
|
cm.focus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
|
@ -6,356 +6,13 @@ global messageBox
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
onDOMscriptReady('/codemirror.js').then(() => {
|
onDOMscriptReady('/codemirror.js').then(() => {
|
||||||
const COMMANDS = {
|
|
||||||
save,
|
|
||||||
toggleStyle,
|
|
||||||
toggleEditorFocus,
|
|
||||||
jumpToLine,
|
|
||||||
nextEditor, prevEditor,
|
|
||||||
commentSelection,
|
|
||||||
};
|
|
||||||
const ORIGINAL_COMMANDS = {
|
|
||||||
insertTab: CodeMirror.commands.insertTab,
|
|
||||||
};
|
|
||||||
// reroute handling to nearest editor when keypress resolves to one of these commands
|
|
||||||
const REROUTED = new Set([
|
|
||||||
'save',
|
|
||||||
'toggleStyle',
|
|
||||||
'jumpToLine',
|
|
||||||
'nextEditor', 'prevEditor',
|
|
||||||
'toggleEditorFocus',
|
|
||||||
'find', 'findNext', 'findPrev', 'replace', 'replaceAll',
|
|
||||||
'colorpicker',
|
|
||||||
]);
|
|
||||||
Object.assign(CodeMirror.prototype, {
|
|
||||||
// getSection,
|
|
||||||
rerouteHotkeys,
|
|
||||||
});
|
|
||||||
Object.assign(CodeMirror.commands, COMMANDS);
|
|
||||||
rerouteHotkeys(true);
|
|
||||||
|
|
||||||
CodeMirror.defineInitHook(cm => {
|
|
||||||
if (!cm.display.wrapper.closest('#sections')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (prefs.get('editor.autocompleteOnTyping')) {
|
|
||||||
setupAutocomplete(cm);
|
|
||||||
}
|
|
||||||
const wrapper = cm.display.wrapper;
|
|
||||||
cm.on('blur', () => {
|
|
||||||
cm.rerouteHotkeys(true);
|
|
||||||
setTimeout(() => {
|
|
||||||
wrapper.classList.toggle('CodeMirror-active', wrapper.contains(document.activeElement));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
cm.on('focus', () => {
|
|
||||||
cm.rerouteHotkeys(false);
|
|
||||||
wrapper.classList.add('CodeMirror-active');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// FIXME: pull this into a module
|
|
||||||
window.rerouteHotkeys = rerouteHotkeys;
|
|
||||||
|
|
||||||
prefs.subscribe(null, onPrefChanged);
|
|
||||||
|
|
||||||
////////////////////////////////////////////////
|
|
||||||
|
|
||||||
function getOption(o) {
|
|
||||||
return CodeMirror.defaults[o];
|
|
||||||
}
|
|
||||||
|
|
||||||
function setOption(o, v) {
|
|
||||||
CodeMirror.defaults[o] = v;
|
|
||||||
if (!editor) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const editors = editor.getEditors();
|
|
||||||
if (editors.length > 4 && (o === 'theme' || o === 'lineWrapping')) {
|
|
||||||
throttleSetOption({key: o, value: v, index: 0});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
editors.forEach(editor => {
|
|
||||||
editor.setOption(o, v);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function throttleSetOption({
|
|
||||||
key,
|
|
||||||
value,
|
|
||||||
index,
|
|
||||||
timeStart = performance.now(),
|
|
||||||
cmStart = editor.getLastActivatedEditor(),
|
|
||||||
editorsCopy = editor.getEditors().slice(),
|
|
||||||
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;
|
|
||||||
const editors = editor.getEditors();
|
|
||||||
while (index < total) {
|
|
||||||
const cm = editorsCopy[index++];
|
|
||||||
if (cm === cmStart ||
|
|
||||||
cm !== editors[index] && !editors.includes(cm)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
cm.setOption(key, value);
|
|
||||||
if (performance.now() - t0 > THROTTLE_AFTER_MS) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (index >= total) {
|
|
||||||
$.remove(progress);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSection() {
|
|
||||||
return this.display.wrapper.parentNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
function nextEditor(cm) {
|
|
||||||
return editor.nextEditor(cm);
|
|
||||||
}
|
|
||||||
|
|
||||||
function prevEditor(cm) {
|
|
||||||
return editor.prevEditor(cm);
|
|
||||||
}
|
|
||||||
|
|
||||||
function jumpToLine(cm) {
|
|
||||||
const cur = cm.getCursor();
|
|
||||||
refocusMinidialog(cm);
|
|
||||||
cm.openDialog(template.jumpToLine.cloneNode(true), str => {
|
|
||||||
const m = str.match(/^\s*(\d+)(?:\s*:\s*(\d+))?\s*$/);
|
|
||||||
if (m) {
|
|
||||||
cm.setCursor(m[1] - 1, m[2] ? m[2] - 1 : cur.ch);
|
|
||||||
}
|
|
||||||
}, {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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function refocusMinidialog(cm) {
|
|
||||||
const section = cm.getSection();
|
|
||||||
if (!$('.CodeMirror-dialog', section)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// close the currently opened minidialog
|
|
||||||
cm.focus();
|
|
||||||
// make sure to focus the input in newly opened minidialog
|
|
||||||
setTimeout(() => {
|
|
||||||
$('.CodeMirror-dialog', section).focus();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onPrefChanged(key, value) {
|
|
||||||
let option = key.replace(/^editor\./, '');
|
|
||||||
if (!option) {
|
|
||||||
console.error('no "cm_option"', key);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
switch (option) {
|
|
||||||
case 'tabSize':
|
|
||||||
value = Number(value);
|
|
||||||
CodeMirror.setOption('indentUnit', value);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'indentWithTabs':
|
|
||||||
CodeMirror.commands.insertTab = value ?
|
|
||||||
ORIGINAL_COMMANDS.insertTab :
|
|
||||||
CodeMirror.commands.insertSoftTab;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'theme': {
|
|
||||||
const themeLink = $('#cm-theme');
|
|
||||||
// use non-localized 'default' internally
|
|
||||||
if (!value || value === 'default' || value === t('defaultTheme')) {
|
|
||||||
value = 'default';
|
|
||||||
if (prefs.get(key) !== value) {
|
|
||||||
prefs.set(key, value);
|
|
||||||
}
|
|
||||||
themeLink.href = '';
|
|
||||||
$('#editor.theme').value = value;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
const url = chrome.runtime.getURL('vendor/codemirror/theme/' + value + '.css');
|
|
||||||
if (themeLink.href === url) {
|
|
||||||
// preloaded in initCodeMirror()
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// avoid flicker: wait for the second stylesheet to load, then apply the theme
|
|
||||||
document.head.appendChild($create('link#cm-theme2', {rel: 'stylesheet', href: url}));
|
|
||||||
setTimeout(() => {
|
|
||||||
CodeMirror.setOption(option, value);
|
|
||||||
themeLink.remove();
|
|
||||||
$('#cm-theme2').id = 'cm-theme';
|
|
||||||
}, 100);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'autocompleteOnTyping':
|
|
||||||
if (editor) {
|
|
||||||
// FIXME: this won't work with removed sections
|
|
||||||
editor.getEditors().forEach(cm => setupAutocomplete(cm, value));
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
|
|
||||||
case 'autoCloseBrackets':
|
|
||||||
Promise.resolve(value && loadScript('/vendor/codemirror/addon/edit/closebrackets.js')).then(() => {
|
|
||||||
CodeMirror.setOption(option, value);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
|
|
||||||
case 'matchHighlight':
|
|
||||||
switch (value) {
|
|
||||||
case 'token':
|
|
||||||
case 'selection':
|
|
||||||
document.body.dataset[option] = value;
|
|
||||||
value = {showToken: value === 'token' && /[#.\-\w]/, annotateScrollbar: true};
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
value = null;
|
|
||||||
}
|
|
||||||
option = 'highlightSelectionMatches';
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'colorpicker':
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
CodeMirror.setOption(option, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////
|
|
||||||
|
|
||||||
function rerouteHotkeys(enable, immediately) {
|
|
||||||
if (!immediately) {
|
|
||||||
debounce(rerouteHotkeys, 0, enable, true);
|
|
||||||
} else if (enable) {
|
|
||||||
document.addEventListener('keydown', rerouteHandler);
|
|
||||||
} else {
|
|
||||||
document.removeEventListener('keydown', rerouteHandler);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function rerouteHandler(event) {
|
|
||||||
const keyName = CodeMirror.keyName(event);
|
|
||||||
if (!keyName) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const rerouteCommand = name => {
|
|
||||||
if (REROUTED.has(name)) {
|
|
||||||
CodeMirror.commands[name](editor.closestVisible(event.target));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (CodeMirror.lookupKey(keyName, CodeMirror.defaults.keyMap, rerouteCommand) === 'handled' ||
|
|
||||||
CodeMirror.lookupKey(keyName, CodeMirror.defaults.extraKeys, rerouteCommand) === 'handled') {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////
|
////////////////////////////////////////////////
|
||||||
|
|
||||||
////////////////////////////////////////////////
|
////////////////////////////////////////////////
|
||||||
|
|
||||||
function setupAutocomplete(cm, enable = true) {
|
////////////////////////////////////////////////
|
||||||
const onOff = enable ? 'on' : 'off';
|
|
||||||
cm[onOff]('changes', autocompleteOnTyping);
|
|
||||||
cm[onOff]('pick', autocompletePicked);
|
|
||||||
}
|
|
||||||
|
|
||||||
function autocompleteOnTyping(cm, [info], debounced) {
|
|
||||||
if (
|
|
||||||
cm.state.completionActive ||
|
|
||||||
info.origin && !info.origin.includes('input') ||
|
|
||||||
!info.text.last
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (cm.state.autocompletePicked) {
|
|
||||||
cm.state.autocompletePicked = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!debounced) {
|
|
||||||
debounce(autocompleteOnTyping, 100, cm, [info], true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (info.text.last.match(/[-a-z!]+$/i)) {
|
|
||||||
cm.state.autocompletePicked = false;
|
|
||||||
cm.options.hintOptions.completeSingle = false;
|
|
||||||
cm.execCommand('autocomplete');
|
|
||||||
setTimeout(() => {
|
|
||||||
cm.options.hintOptions.completeSingle = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function autocompletePicked(cm) {
|
|
||||||
cm.state.autocompletePicked = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function save() {
|
|
||||||
editor.save();
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleStyle() {
|
|
||||||
editor.toggleStyle();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
311
edit/codemirror-factory.js
Normal file
311
edit/codemirror-factory.js
Normal file
|
@ -0,0 +1,311 @@
|
||||||
|
/* global CodeMirror loadScript rerouteHotkeys */
|
||||||
|
'use strict';
|
||||||
|
/*
|
||||||
|
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
|
||||||
|
when the instance is not used anymore.
|
||||||
|
*/
|
||||||
|
const cmFactory = (() => {
|
||||||
|
const editors = new Set();
|
||||||
|
// used by `indentWithTabs` option
|
||||||
|
const INSERT_TAB_COMMAND = CodeMirror.commands.insertTab;
|
||||||
|
const INSERT_SOFT_TAB_COMMAND = CodeMirror.commands.insertSoftTab;
|
||||||
|
prefs.subscribe(null, onPrefChanged);
|
||||||
|
return {create, destroy, setOption};
|
||||||
|
|
||||||
|
function onPrefChanged(key, value) {
|
||||||
|
let option = key.replace(/^editor\./, '');
|
||||||
|
if (!option) {
|
||||||
|
console.error('no "cm_option"', key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (option) {
|
||||||
|
case 'tabSize':
|
||||||
|
value = Number(value);
|
||||||
|
setOption('indentUnit', value);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'indentWithTabs':
|
||||||
|
CodeMirror.commands.insertTab = value ?
|
||||||
|
INSERT_TAB_COMMAND :
|
||||||
|
INSERT_SOFT_TAB_COMMAND;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'theme': {
|
||||||
|
const themeLink = $('#cm-theme');
|
||||||
|
// use non-localized 'default' internally
|
||||||
|
if (!value || value === 'default' || value === t('defaultTheme')) {
|
||||||
|
value = 'default';
|
||||||
|
if (prefs.get(key) !== value) {
|
||||||
|
prefs.set(key, value);
|
||||||
|
}
|
||||||
|
themeLink.href = '';
|
||||||
|
$('#editor.theme').value = value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const url = chrome.runtime.getURL('vendor/codemirror/theme/' + value + '.css');
|
||||||
|
if (themeLink.href === url) {
|
||||||
|
// preloaded in initCodeMirror()
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// avoid flicker: wait for the second stylesheet to load, then apply the theme
|
||||||
|
document.head.appendChild($create('link#cm-theme2', {rel: 'stylesheet', href: url}));
|
||||||
|
setTimeout(() => {
|
||||||
|
setOption(option, value);
|
||||||
|
themeLink.remove();
|
||||||
|
$('#cm-theme2').id = 'cm-theme';
|
||||||
|
}, 100);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'autocompleteOnTyping':
|
||||||
|
for (const cm of editors) {
|
||||||
|
setupAutocomplete(cm, value);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
|
||||||
|
case 'autoCloseBrackets':
|
||||||
|
Promise.resolve(value && loadScript('/vendor/codemirror/addon/edit/closebrackets.js')).then(() => {
|
||||||
|
setOption(option, value);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
|
||||||
|
case 'matchHighlight':
|
||||||
|
switch (value) {
|
||||||
|
case 'token':
|
||||||
|
case 'selection':
|
||||||
|
document.body.dataset[option] = value;
|
||||||
|
value = {showToken: value === 'token' && /[#.\-\w]/, annotateScrollbar: true};
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
value = null;
|
||||||
|
}
|
||||||
|
option = 'highlightSelectionMatches';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'colorpicker':
|
||||||
|
// FIXME: this is implemented in `colorpicker-helper.js`.
|
||||||
|
return;
|
||||||
|
|
||||||
|
case 'selectByTokens':
|
||||||
|
option = 'configureMouse';
|
||||||
|
value = value ? configureMouseFn : null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
setOption(option, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function configureMouseFn(cm, repeat) {
|
||||||
|
return repeat === 'double' ?
|
||||||
|
{unit: selectTokenOnDoubleclick} :
|
||||||
|
{};
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectTokenOnDoubleclick(cm, pos) {
|
||||||
|
let {ch} = pos;
|
||||||
|
const {line, sticky} = pos;
|
||||||
|
const {text, styles} = cm.getLineHandle(line);
|
||||||
|
|
||||||
|
const execAt = (rx, i) => (rx.lastIndex = i) && null || rx.exec(text);
|
||||||
|
const at = (rx, i) => (rx.lastIndex = i) && null || rx.test(text);
|
||||||
|
const atWord = ch => at(/\w/y, ch);
|
||||||
|
const atSpace = ch => at(/\s/y, ch);
|
||||||
|
|
||||||
|
const atTokenEnd = styles.indexOf(ch, 1);
|
||||||
|
ch += atTokenEnd < 0 ? 0 : sticky === 'before' && atWord(ch - 1) ? 0 : atSpace(ch + 1) ? 0 : 1;
|
||||||
|
ch = Math.min(text.length, ch);
|
||||||
|
const type = cm.getTokenTypeAt({line, ch: ch + (sticky === 'after' ? 1 : 0)});
|
||||||
|
if (atTokenEnd > 0) ch--;
|
||||||
|
|
||||||
|
const isCss = type && !/^(comment|string)/.test(type);
|
||||||
|
const isNumber = type === 'number';
|
||||||
|
const isSpace = atSpace(ch);
|
||||||
|
let wordChars =
|
||||||
|
isNumber ? /[-+\w.%]/y :
|
||||||
|
isCss ? /[-\w@]/y :
|
||||||
|
isSpace ? /\s/y :
|
||||||
|
atWord(ch) ? /\w/y : /[^\w\s]/y;
|
||||||
|
|
||||||
|
let a = ch;
|
||||||
|
while (a && at(wordChars, a)) a--;
|
||||||
|
a += !a && at(wordChars, a) || isCss && at(/[.!#@]/y, a) ? 0 : at(wordChars, a + 1);
|
||||||
|
|
||||||
|
let b, found;
|
||||||
|
|
||||||
|
if (isNumber) {
|
||||||
|
b = a + execAt(/[+-]?[\d.]+(e\d+)?|$/yi, a)[0].length;
|
||||||
|
found = b >= ch;
|
||||||
|
if (!found) {
|
||||||
|
a = b;
|
||||||
|
ch = a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
wordChars = isCss ? /[-\w]*/y : new RegExp(wordChars.source + '*', 'uy');
|
||||||
|
b = ch + execAt(wordChars, ch)[0].length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
from: {line, ch: a},
|
||||||
|
to: {line, ch: b},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupAutocomplete(cm, enable = true) {
|
||||||
|
const onOff = enable ? 'on' : 'off';
|
||||||
|
cm[onOff]('changes', autocompleteOnTyping);
|
||||||
|
cm[onOff]('pick', autocompletePicked);
|
||||||
|
}
|
||||||
|
|
||||||
|
function autocompleteOnTyping(cm, [info], debounced) {
|
||||||
|
if (
|
||||||
|
cm.state.completionActive ||
|
||||||
|
info.origin && !info.origin.includes('input') ||
|
||||||
|
!info.text.last
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (cm.state.autocompletePicked) {
|
||||||
|
cm.state.autocompletePicked = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!debounced) {
|
||||||
|
debounce(autocompleteOnTyping, 100, cm, [info], true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (info.text.last.match(/[-a-z!]+$/i)) {
|
||||||
|
cm.state.autocompletePicked = false;
|
||||||
|
cm.options.hintOptions.completeSingle = false;
|
||||||
|
cm.execCommand('autocomplete');
|
||||||
|
setTimeout(() => {
|
||||||
|
cm.options.hintOptions.completeSingle = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function autocompletePicked(cm) {
|
||||||
|
cm.state.autocompletePicked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function destroy(cm) {
|
||||||
|
editors.delete(cm);
|
||||||
|
}
|
||||||
|
|
||||||
|
function create(init, options) {
|
||||||
|
const cm = CodeMirror(init, options);
|
||||||
|
if (prefs.get('editor.autocompleteOnTyping')) {
|
||||||
|
setupAutocomplete(cm);
|
||||||
|
}
|
||||||
|
cm.lastActive = 0;
|
||||||
|
const wrapper = cm.display.wrapper;
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
editors.add(cm);
|
||||||
|
cm.distroy = () => editors.delete(cm);
|
||||||
|
return cm;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLastActivated() {
|
||||||
|
let result;
|
||||||
|
for (const cm of editors) {
|
||||||
|
if (!result || result.lastActive < cm.lastActive) {
|
||||||
|
result = cm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setOption(key, value) {
|
||||||
|
CodeMirror.defaults[key] = value;
|
||||||
|
if (editors.size > 4 && (key === 'theme' || key === 'lineWrapping')) {
|
||||||
|
throttleSetOption({key, value, index: 0});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const editor of editors) {
|
||||||
|
editor.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (index >= total) {
|
||||||
|
$.remove(progress);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
|
@ -1,4 +1,4 @@
|
||||||
/* global CodeMirror loadScript editors showHelp */
|
/* global CodeMirror loadScript showHelp cmFactory */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
onDOMscriptReady('/colorview.js').then(() => {
|
onDOMscriptReady('/colorview.js').then(() => {
|
||||||
|
@ -20,7 +20,8 @@ onDOMscriptReady('/colorview.js').then(() => {
|
||||||
defaults.extraKeys[keyName] = 'colorpicker';
|
defaults.extraKeys[keyName] = 'colorpicker';
|
||||||
}
|
}
|
||||||
defaults.colorpicker = {
|
defaults.colorpicker = {
|
||||||
forceUpdate: editors.length > 0,
|
// FIXME: who uses this?
|
||||||
|
// forceUpdate: editor.getEditors().length > 0,
|
||||||
tooltip: t('colorpickerTooltip'),
|
tooltip: t('colorpickerTooltip'),
|
||||||
popup: {
|
popup: {
|
||||||
tooltipForSwitcher: t('colorpickerSwitchFormatTooltip'),
|
tooltipForSwitcher: t('colorpickerSwitchFormatTooltip'),
|
||||||
|
@ -38,8 +39,7 @@ onDOMscriptReady('/colorview.js').then(() => {
|
||||||
delete defaults.extraKeys[keyName];
|
delete defaults.extraKeys[keyName];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// on page load runs before CodeMirror.setOption is defined
|
cmFactory.setOption('colorpicker', defaults.colorpicker);
|
||||||
editors.forEach(cm => cm.setOption('colorpicker', defaults.colorpicker));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerHotkey(id, hotkey) {
|
function registerHotkey(id, hotkey) {
|
||||||
|
|
13
edit/edit.js
13
edit/edit.js
|
@ -5,7 +5,7 @@ global closeCurrentTab regExpTester messageBox
|
||||||
global setupCodeMirror
|
global setupCodeMirror
|
||||||
global beautify
|
global beautify
|
||||||
global sectionsToMozFormat
|
global sectionsToMozFormat
|
||||||
global moveFocus editorWorker msg createSectionEditor
|
global moveFocus editorWorker msg createSectionsEditor rerouteHotkeys
|
||||||
*/
|
*/
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
@ -229,7 +229,7 @@ preinit();
|
||||||
$('#lint').addEventListener('scroll', hideLintHeaderOnScroll, {passive: true});
|
$('#lint').addEventListener('scroll', hideLintHeaderOnScroll, {passive: true});
|
||||||
window.addEventListener('resize', () => debounce(rememberWindowSize, 100));
|
window.addEventListener('resize', () => debounce(rememberWindowSize, 100));
|
||||||
|
|
||||||
editor = usercss ? createSourceEditor(style) : createSectionEditor(style);
|
editor = usercss ? createSourceEditor(style) : createSectionsEditor(style);
|
||||||
if (editor.ready) {
|
if (editor.ready) {
|
||||||
return editor.ready();
|
return editor.ready();
|
||||||
}
|
}
|
||||||
|
@ -362,11 +362,6 @@ function onRuntimeMessage(request) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'prefChanged':
|
|
||||||
if ('editor.smartIndent' in request.prefs) {
|
|
||||||
CodeMirror.setOption('smartIndent', request.prefs['editor.smartIndent']);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'editDeleteText':
|
case 'editDeleteText':
|
||||||
document.execCommand('delete');
|
document.execCommand('delete');
|
||||||
break;
|
break;
|
||||||
|
@ -531,7 +526,7 @@ function showCodeMirrorPopup(title, html, options) {
|
||||||
keyMap: prefs.get('editor.keyMap')
|
keyMap: prefs.get('editor.keyMap')
|
||||||
}, options));
|
}, options));
|
||||||
cm.focus();
|
cm.focus();
|
||||||
cm.rerouteHotkeys(false);
|
rerouteHotkeys(false);
|
||||||
|
|
||||||
document.documentElement.style.pointerEvents = 'none';
|
document.documentElement.style.pointerEvents = 'none';
|
||||||
popup.style.pointerEvents = 'auto';
|
popup.style.pointerEvents = 'auto';
|
||||||
|
@ -550,7 +545,7 @@ function showCodeMirrorPopup(title, html, options) {
|
||||||
window.removeEventListener('closeHelp', _);
|
window.removeEventListener('closeHelp', _);
|
||||||
window.removeEventListener('keydown', onKeyDown, true);
|
window.removeEventListener('keydown', onKeyDown, true);
|
||||||
document.documentElement.style.removeProperty('pointer-events');
|
document.documentElement.style.removeProperty('pointer-events');
|
||||||
cm.rerouteHotkeys(true);
|
rerouteHotkeys(true);
|
||||||
cm = popup.codebox = null;
|
cm = popup.codebox = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -212,7 +212,7 @@ onDOMready().then(() => {
|
||||||
state.activeAppliesTo ||
|
state.activeAppliesTo ||
|
||||||
state.cm);
|
state.cm);
|
||||||
const cmExtra = $('body > :not(#sections) .CodeMirror');
|
const cmExtra = $('body > :not(#sections) .CodeMirror');
|
||||||
state.editors = cmExtra ? [cmExtra.CodeMirror] : editors;
|
state.editors = cmExtra ? [cmExtra.CodeMirror] : editor.getEditors();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
/* global memoize editorWorker showCodeMirrorPopup loadScript messageBox LINTER_DEFAULTS*/
|
/* global memoize editorWorker showCodeMirrorPopup loadScript messageBox
|
||||||
|
LINTER_DEFAULTS rerouteHotkeys */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
(() => {
|
(() => {
|
||||||
|
@ -50,10 +51,10 @@
|
||||||
});
|
});
|
||||||
cm.on('changes', updateButtonState);
|
cm.on('changes', updateButtonState);
|
||||||
|
|
||||||
cm.rerouteHotkeys(false);
|
rerouteHotkeys(false);
|
||||||
window.addEventListener('closeHelp', function _() {
|
window.addEventListener('closeHelp', function _() {
|
||||||
window.removeEventListener('closeHelp', _);
|
window.removeEventListener('closeHelp', _);
|
||||||
cm.rerouteHotkeys(true);
|
rerouteHotkeys(true);
|
||||||
cm = null;
|
cm = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
50
edit/reroute-hotkeys.js
Normal file
50
edit/reroute-hotkeys.js
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
/* global CodeMirror editor */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const rerouteHotkeys = (() => {
|
||||||
|
// reroute handling to nearest editor when keypress resolves to one of these commands
|
||||||
|
const REROUTED = new Set([
|
||||||
|
'save',
|
||||||
|
'toggleStyle',
|
||||||
|
'jumpToLine',
|
||||||
|
'nextEditor', 'prevEditor',
|
||||||
|
'toggleEditorFocus',
|
||||||
|
'find', 'findNext', 'findPrev', 'replace', 'replaceAll',
|
||||||
|
'colorpicker',
|
||||||
|
]);
|
||||||
|
|
||||||
|
rerouteHotkeys(true);
|
||||||
|
|
||||||
|
return rerouteHotkeys;
|
||||||
|
|
||||||
|
function rerouteHotkeys(enable, immediately) {
|
||||||
|
if (!immediately) {
|
||||||
|
debounce(rerouteHotkeys, 0, enable, true);
|
||||||
|
} else if (enable) {
|
||||||
|
document.addEventListener('keydown', rerouteHandler);
|
||||||
|
} else {
|
||||||
|
document.removeEventListener('keydown', rerouteHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function rerouteHandler(event) {
|
||||||
|
if (!editor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const keyName = CodeMirror.keyName(event);
|
||||||
|
if (!keyName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const rerouteCommand = name => {
|
||||||
|
if (REROUTED.has(name)) {
|
||||||
|
CodeMirror.commands[name](editor.closestVisible(event.target));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (CodeMirror.lookupKey(keyName, CodeMirror.defaults.keyMap, rerouteCommand) === 'handled' ||
|
||||||
|
CodeMirror.lookupKey(keyName, CodeMirror.defaults.extraKeys, rerouteCommand) === 'handled') {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
|
@ -134,7 +134,6 @@ function createSectionsEditor(style) {
|
||||||
isDirty: dirty.isDirty,
|
isDirty: dirty.isDirty,
|
||||||
getStyle: () => style,
|
getStyle: () => style,
|
||||||
getEditors,
|
getEditors,
|
||||||
getLastActivatedEditor,
|
|
||||||
scrollToEditor,
|
scrollToEditor,
|
||||||
getStyleId: () => style.id,
|
getStyleId: () => style.id,
|
||||||
getEditorTitle: cm => {
|
getEditorTitle: cm => {
|
||||||
|
@ -162,7 +161,7 @@ function createSectionsEditor(style) {
|
||||||
nearbyElement instanceof CodeMirror ? nearbyElement :
|
nearbyElement instanceof CodeMirror ? nearbyElement :
|
||||||
nearbyElement instanceof Node &&
|
nearbyElement instanceof Node &&
|
||||||
(nearbyElement.closest('#sections > .section') || {}).CodeMirror ||
|
(nearbyElement.closest('#sections > .section') || {}).CodeMirror ||
|
||||||
editor.getLastActivatedEditor();
|
getLastActivatedEditor();
|
||||||
if (nearbyElement instanceof Node && cm) {
|
if (nearbyElement instanceof Node && cm) {
|
||||||
const {left, top} = nearbyElement.getBoundingClientRect();
|
const {left, top} = nearbyElement.getBoundingClientRect();
|
||||||
const bounds = cm.display.wrapper.getBoundingClientRect();
|
const bounds = cm.display.wrapper.getBoundingClientRect();
|
||||||
|
@ -172,7 +171,7 @@ function createSectionsEditor(style) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// closest editor should have at least 2 lines visible
|
// closest editor should have at least 2 lines visible
|
||||||
const lineHeight = editor.getEditors()[0].defaultTextHeight();
|
const lineHeight = sections[0].cm.defaultTextHeight();
|
||||||
const scrollY = window.scrollY;
|
const scrollY = window.scrollY;
|
||||||
const windowBottom = scrollY + window.innerHeight - 2 * lineHeight;
|
const windowBottom = scrollY + window.innerHeight - 2 * lineHeight;
|
||||||
const allSectionsContainerTop = scrollY + $('#sections').getBoundingClientRect().top;
|
const allSectionsContainerTop = scrollY + $('#sections').getBoundingClientRect().top;
|
||||||
|
@ -203,7 +202,7 @@ function createSectionsEditor(style) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function findClosest() {
|
function findClosest() {
|
||||||
const editors = editor.getEditors();
|
const editors = getEditors();
|
||||||
const last = editors.length - 1;
|
const last = editors.length - 1;
|
||||||
let a = 0;
|
let a = 0;
|
||||||
let b = last;
|
let b = last;
|
||||||
|
@ -228,7 +227,7 @@ function createSectionsEditor(style) {
|
||||||
}
|
}
|
||||||
const cm = editors[b];
|
const cm = editors[b];
|
||||||
if (distances[b] > 0) {
|
if (distances[b] > 0) {
|
||||||
editor.scrollToEditor(cm);
|
scrollToEditor(cm);
|
||||||
}
|
}
|
||||||
return cm;
|
return cm;
|
||||||
}
|
}
|
||||||
|
|
|
@ -400,7 +400,6 @@ function createSourceEditor(style) {
|
||||||
isDirty: dirty.isDirty,
|
isDirty: dirty.isDirty,
|
||||||
getStyle: () => style,
|
getStyle: () => style,
|
||||||
getEditors: () => [cm],
|
getEditors: () => [cm],
|
||||||
getLastActivatedEditor: () => cm,
|
|
||||||
scrollToEditor: () => {},
|
scrollToEditor: () => {},
|
||||||
getStyleId: () => style.id,
|
getStyleId: () => style.id,
|
||||||
getEditorTitle: () => '',
|
getEditorTitle: () => '',
|
||||||
|
|
Loading…
Reference in New Issue
Block a user