2017-12-02 20:41:15 +00:00
|
|
|
/*
|
2018-10-01 14:03:17 +00:00
|
|
|
global CodeMirror loadScript
|
2018-10-09 15:38:29 +00:00
|
|
|
global editor ownTabId
|
2018-01-10 18:56:14 +00:00
|
|
|
global messageBox
|
2017-12-02 20:41:15 +00:00
|
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
|
2017-12-08 02:45:27 +00:00
|
|
|
onDOMscriptReady('/codemirror.js').then(() => {
|
2017-12-02 20:41:15 +00:00
|
|
|
const COMMANDS = {
|
|
|
|
save,
|
|
|
|
toggleStyle,
|
2018-03-03 20:30:33 +00:00
|
|
|
toggleEditorFocus,
|
2017-12-02 20:41:15 +00:00
|
|
|
jumpToLine,
|
|
|
|
nextEditor, prevEditor,
|
2018-03-03 20:31:21 +00:00
|
|
|
commentSelection,
|
2017-12-02 20:41:15 +00:00
|
|
|
};
|
2018-05-01 20:05:13 +00:00
|
|
|
const ORIGINAL_COMMANDS = {
|
|
|
|
insertTab: CodeMirror.commands.insertTab,
|
|
|
|
};
|
2017-12-02 20:41:15 +00:00
|
|
|
// reroute handling to nearest editor when keypress resolves to one of these commands
|
|
|
|
const REROUTED = new Set([
|
2018-03-03 20:31:21 +00:00
|
|
|
'save',
|
|
|
|
'toggleStyle',
|
|
|
|
'jumpToLine',
|
|
|
|
'nextEditor', 'prevEditor',
|
|
|
|
'toggleEditorFocus',
|
2017-12-18 06:55:32 +00:00
|
|
|
'find', 'findNext', 'findPrev', 'replace', 'replaceAll',
|
2017-12-02 20:41:15 +00:00
|
|
|
'colorpicker',
|
|
|
|
]);
|
|
|
|
Object.assign(CodeMirror, {
|
|
|
|
getOption,
|
|
|
|
setOption,
|
2017-12-18 06:55:32 +00:00
|
|
|
closestVisible,
|
2017-12-02 20:41:15 +00:00
|
|
|
});
|
|
|
|
Object.assign(CodeMirror.prototype, {
|
|
|
|
getSection,
|
|
|
|
rerouteHotkeys,
|
|
|
|
});
|
2018-10-09 18:43:09 +00:00
|
|
|
Object.assign(CodeMirror.commands, COMMANDS);
|
|
|
|
rerouteHotkeys(true);
|
2017-12-02 20:41:15 +00:00
|
|
|
|
2018-01-14 12:57:29 +00:00
|
|
|
CodeMirror.defineInitHook(cm => {
|
2018-09-03 17:47:45 +00:00
|
|
|
if (!cm.display.wrapper.closest('#sections')) {
|
|
|
|
return;
|
|
|
|
}
|
2018-01-14 12:57:29 +00:00
|
|
|
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');
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2018-10-09 18:43:09 +00:00
|
|
|
// FIXME: pull this into a module
|
|
|
|
window.rerouteHotkeys = rerouteHotkeys;
|
2017-12-08 02:45:27 +00:00
|
|
|
|
2018-10-09 18:43:09 +00:00
|
|
|
prefs.subscribe(null, onPrefChanged);
|
2017-12-02 20:41:15 +00:00
|
|
|
|
|
|
|
////////////////////////////////////////////////
|
|
|
|
|
|
|
|
function getOption(o) {
|
|
|
|
return CodeMirror.defaults[o];
|
|
|
|
}
|
|
|
|
|
|
|
|
function setOption(o, v) {
|
|
|
|
CodeMirror.defaults[o] = v;
|
2018-10-09 18:43:09 +00:00
|
|
|
if (!editor) {
|
|
|
|
return;
|
|
|
|
}
|
2018-10-09 16:41:07 +00:00
|
|
|
const editors = editor.getEditors();
|
2017-12-02 20:41:15 +00:00
|
|
|
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(),
|
2018-10-09 16:41:07 +00:00
|
|
|
cmStart = editor.getLastActivatedEditor(),
|
|
|
|
editorsCopy = editor.getEditors().slice(),
|
2017-12-02 20:41:15 +00:00
|
|
|
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;
|
2018-10-09 16:41:07 +00:00
|
|
|
const editors = editor.getEditors();
|
2017-12-02 20:41:15 +00:00
|
|
|
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;
|
|
|
|
}
|
2017-12-03 21:12:09 +00:00
|
|
|
progress = document.body.appendChild(
|
|
|
|
$create('.set-option-progress', {targetElement: option}));
|
2017-12-02 20:41:15 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
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) {
|
2018-10-09 18:43:09 +00:00
|
|
|
return editor.nextEditor(cm);
|
2017-12-02 20:41:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function prevEditor(cm) {
|
2018-10-09 18:43:09 +00:00
|
|
|
return editor.prevEditor(cm);
|
2017-12-02 20:41:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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});
|
|
|
|
}
|
|
|
|
|
2018-03-03 20:31:21 +00:00
|
|
|
function commentSelection(cm) {
|
|
|
|
cm.blockComment(cm.getCursor('from'), cm.getCursor('to'), {fullLines: false});
|
|
|
|
}
|
|
|
|
|
2018-03-03 20:30:33 +00:00
|
|
|
function toggleEditorFocus(cm) {
|
|
|
|
if (!cm) return;
|
|
|
|
if (cm.hasFocus()) {
|
|
|
|
setTimeout(() => cm.display.input.blur());
|
|
|
|
} else {
|
|
|
|
cm.focus();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-12-02 20:41:15 +00:00
|
|
|
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();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-10-09 18:43:09 +00:00
|
|
|
function onPrefChanged(key, value) {
|
|
|
|
let option = key.replace(/^editor\./, '');
|
2017-12-02 20:41:15 +00:00
|
|
|
if (!option) {
|
2018-10-09 18:43:09 +00:00
|
|
|
console.error('no "cm_option"', key);
|
2017-12-02 20:41:15 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
switch (option) {
|
|
|
|
case 'tabSize':
|
|
|
|
value = Number(value);
|
|
|
|
CodeMirror.setOption('indentUnit', value);
|
|
|
|
break;
|
|
|
|
|
2018-05-01 20:05:13 +00:00
|
|
|
case 'indentWithTabs':
|
|
|
|
CodeMirror.commands.insertTab = value ?
|
|
|
|
ORIGINAL_COMMANDS.insertTab :
|
|
|
|
CodeMirror.commands.insertSoftTab;
|
|
|
|
break;
|
|
|
|
|
2017-12-02 20:41:15 +00:00
|
|
|
case 'theme': {
|
|
|
|
const themeLink = $('#cm-theme');
|
|
|
|
// use non-localized 'default' internally
|
|
|
|
if (!value || value === 'default' || value === t('defaultTheme')) {
|
|
|
|
value = 'default';
|
2018-10-09 18:43:09 +00:00
|
|
|
if (prefs.get(key) !== value) {
|
|
|
|
prefs.set(key, value);
|
2017-12-02 20:41:15 +00:00
|
|
|
}
|
|
|
|
themeLink.href = '';
|
2018-10-09 18:43:09 +00:00
|
|
|
$('#editor.theme').value = value;
|
2017-12-02 20:41:15 +00:00
|
|
|
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
|
2017-12-03 21:12:09 +00:00
|
|
|
document.head.appendChild($create('link#cm-theme2', {rel: 'stylesheet', href: url}));
|
2017-12-02 20:41:15 +00:00
|
|
|
setTimeout(() => {
|
|
|
|
CodeMirror.setOption(option, value);
|
|
|
|
themeLink.remove();
|
|
|
|
$('#cm-theme2').id = 'cm-theme';
|
|
|
|
}, 100);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
case 'autocompleteOnTyping':
|
2018-10-09 18:43:09 +00:00
|
|
|
if (editor) {
|
|
|
|
editor.getEditors().forEach(cm => setupAutocomplete(cm, value));
|
|
|
|
}
|
2017-12-02 20:41:15 +00:00
|
|
|
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)) {
|
2017-12-18 06:55:32 +00:00
|
|
|
CodeMirror.commands[name](closestVisible(event.target));
|
2017-12-02 20:41:15 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
if (CodeMirror.lookupKey(keyName, CodeMirror.defaults.keyMap, rerouteCommand) === 'handled' ||
|
|
|
|
CodeMirror.lookupKey(keyName, CodeMirror.defaults.extraKeys, rerouteCommand) === 'handled') {
|
|
|
|
event.preventDefault();
|
|
|
|
event.stopPropagation();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////
|
|
|
|
|
2017-12-18 06:55:32 +00:00
|
|
|
// priority:
|
|
|
|
// 1. associated CM for applies-to element
|
|
|
|
// 2. last active if visible
|
|
|
|
// 3. first visible
|
|
|
|
function closestVisible(nearbyElement) {
|
|
|
|
const cm =
|
|
|
|
nearbyElement instanceof CodeMirror ? nearbyElement :
|
2018-10-09 15:38:29 +00:00
|
|
|
nearbyElement instanceof Node &&
|
|
|
|
(nearbyElement.closest('#sections > .section') || {}).CodeMirror ||
|
|
|
|
editor.getLastActivatedEditor();
|
2017-12-18 06:55:32 +00:00
|
|
|
if (nearbyElement instanceof Node && cm) {
|
|
|
|
const {left, top} = nearbyElement.getBoundingClientRect();
|
|
|
|
const bounds = cm.display.wrapper.getBoundingClientRect();
|
|
|
|
if (top >= 0 && top >= bounds.top &&
|
|
|
|
left >= 0 && left >= bounds.left) {
|
|
|
|
return cm;
|
|
|
|
}
|
2017-12-02 20:41:15 +00:00
|
|
|
}
|
|
|
|
// closest editor should have at least 2 lines visible
|
2018-10-09 16:41:07 +00:00
|
|
|
const lineHeight = editor.getEditors()[0].defaultTextHeight();
|
2017-12-02 20:41:15 +00:00
|
|
|
const scrollY = window.scrollY;
|
|
|
|
const windowBottom = scrollY + window.innerHeight - 2 * lineHeight;
|
|
|
|
const allSectionsContainerTop = scrollY + $('#sections').getBoundingClientRect().top;
|
|
|
|
const distances = [];
|
|
|
|
const alreadyInView = cm && offscreenDistance(null, cm) === 0;
|
|
|
|
return alreadyInView ? cm : findClosest();
|
|
|
|
|
|
|
|
function offscreenDistance(index, cm) {
|
|
|
|
if (index >= 0 && distances[index] !== undefined) {
|
|
|
|
return distances[index];
|
|
|
|
}
|
2018-10-09 16:41:07 +00:00
|
|
|
const section = cm.display.wrapper.closest('.section');
|
2017-12-18 06:55:32 +00:00
|
|
|
if (!section) {
|
|
|
|
return 1e9;
|
|
|
|
}
|
2017-12-02 20:41:15 +00:00
|
|
|
const top = allSectionsContainerTop + section.offsetTop;
|
|
|
|
if (top < scrollY + lineHeight) {
|
|
|
|
return Math.max(0, scrollY - top - lineHeight);
|
|
|
|
}
|
|
|
|
if (top < windowBottom) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
const distance = top - windowBottom + section.offsetHeight;
|
|
|
|
if (index >= 0) {
|
|
|
|
distances[index] = distance;
|
|
|
|
}
|
|
|
|
return distance;
|
|
|
|
}
|
|
|
|
|
|
|
|
function findClosest() {
|
2018-10-09 16:41:07 +00:00
|
|
|
const editors = editor.getEditors();
|
2017-12-02 20:41:15 +00:00
|
|
|
const last = editors.length - 1;
|
|
|
|
let a = 0;
|
|
|
|
let b = last;
|
|
|
|
let c;
|
|
|
|
let distance;
|
|
|
|
while (a < b - 1) {
|
|
|
|
c = (a + b) / 2 | 0;
|
|
|
|
distance = offscreenDistance(c);
|
|
|
|
if (!distance || !c) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
const distancePrev = offscreenDistance(c - 1);
|
|
|
|
const distanceNext = c < last ? offscreenDistance(c + 1) : 1e20;
|
|
|
|
if (distancePrev <= distance && distance <= distanceNext) {
|
|
|
|
b = c;
|
|
|
|
} else {
|
|
|
|
a = c;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
while (b && offscreenDistance(b - 1) <= offscreenDistance(b)) {
|
|
|
|
b--;
|
|
|
|
}
|
|
|
|
const cm = editors[b];
|
|
|
|
if (distances[b] > 0) {
|
2018-10-09 15:38:29 +00:00
|
|
|
editor.scrollToEditor(cm);
|
2017-12-02 20:41:15 +00:00
|
|
|
}
|
|
|
|
return cm;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////
|
|
|
|
|
2018-10-09 15:38:29 +00:00
|
|
|
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;
|
|
|
|
}
|
2018-10-09 18:43:09 +00:00
|
|
|
|
|
|
|
function save() {
|
|
|
|
editor.save();
|
|
|
|
}
|
|
|
|
|
|
|
|
function toggleStyle() {
|
|
|
|
editor.toggleStyle();
|
|
|
|
}
|
2017-12-02 20:41:15 +00:00
|
|
|
});
|