stylus/edit/codemirror-editing-hooks.js

447 lines
12 KiB
JavaScript
Raw Normal View History

/*
Rewrite linter system (#487) * Add: implement new linter system * Refactor: pull out editor worker * Switch to new linter and worker * Enable eslint cache * Fix: undefined error * Windows compatibility * Fix: refresh linter if the editor.linter changes * Add: stylelint * Add: getStylelintRules, getCsslintRules * Fix: logic to get correct linter * WIP: linter-report * Fix: toggle hidden state * Add: matain the order of lint report for section editor * Add: unhook event * Add: gotoLintIssue * Fix: shouldn't delete rule.init * Add: linter-help-dialog * Drop linterConfig * Add: linter-config-dialog, cacheFn * Add: use cacheFn * Drop lint.js * Add: refresh. Fix report order * Fix: hide empty table * Add: updateCount. Fix table caption * Switch to new linter/worker * Fix: remove unneeded comment * Fix: cacheFn -> cacheFirstCall * Fix: use cacheFirstCall * Fix: cache metaIndex * Fix: i < trs.length * Fix: drop isEmpty * Fix: expose some simple states to global * Fix: return object code style * Fix: use proxy to reflect API * Fix: eslint-disable-line -> eslint-disable-next-line * Fix: requestId -> id * Fix: one-liner * Fix: one-liner * Fix: move dom event block to top * Fix: pending -> pendingResponse * Fix: onSuccess -> onUpdated * Fix: optimize row removing when i === 0 * Fix: hook/unhook -> enableForEditor/disableForEditor * Fix: linter.refresh -> linter.run * Fix: some shadowing * Fix: simplify getAnnotations * Fix: cacheFirstCall -> memoize * Fix: table.update -> table.updateCaption * Fix: unneeded reassign * Fix: callbacks -> listeners * Fix: don't compose but extend * Refactor: replace linter modules with linter-defaults and linter-engines * Fix: implement linter fallbacks * Fix: linter.onChange -> linter.onLintingUpdated * Fix: cms -> tables * Fix: parseMozFormat is not called correctly * Move csslint-loader to background * Fix: watch config changes * Fix: switch to LINTER_DEFAULTS * Fix: csslint-loader -> parserlib-loader
2018-10-01 14:03:17 +00:00
global CodeMirror loadScript
2018-10-09 15:38:29 +00:00
global editor ownTabId
global messageBox
*/
'use strict';
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',
2017-12-18 06:55:32 +00:00
'find', 'findNext', 'findPrev', 'replace', 'replaceAll',
'colorpicker',
]);
Object.assign(CodeMirror, {
getOption,
setOption,
2017-12-18 06:55:32 +00:00
closestVisible,
});
Object.assign(CodeMirror.prototype, {
getSection,
rerouteHotkeys,
});
2018-10-09 18:43:09 +00:00
Object.assign(CodeMirror.commands, COMMANDS);
rerouteHotkeys(true);
CodeMirror.defineInitHook(cm => {
2018-09-03 17:47:45 +00:00
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');
});
});
2018-10-09 18:43:09 +00:00
// FIXME: pull this into a module
window.rerouteHotkeys = rerouteHotkeys;
2018-10-09 18:43:09 +00:00
prefs.subscribe(null, onPrefChanged);
////////////////////////////////////////////////
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();
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(),
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();
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) {
2018-10-09 18:43:09 +00:00
return editor.nextEditor(cm);
}
function prevEditor(cm) {
2018-10-09 18:43:09 +00:00
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();
});
}
2018-10-09 18:43:09 +00:00
function onPrefChanged(key, value) {
let option = key.replace(/^editor\./, '');
if (!option) {
2018-10-09 18:43:09 +00:00
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';
2018-10-09 18:43:09 +00:00
if (prefs.get(key) !== value) {
prefs.set(key, value);
}
themeLink.href = '';
2018-10-09 18:43:09 +00:00
$('#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':
2018-10-09 18:43:09 +00:00
if (editor) {
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)) {
2017-12-18 06:55:32 +00:00
CodeMirror.commands[name](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();
}
}
////////////////////////////////////////////////
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;
}
}
// closest editor should have at least 2 lines visible
2018-10-09 16:41:07 +00:00
const lineHeight = editor.getEditors()[0].defaultTextHeight();
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;
}
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();
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);
}
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();
}
});