@bhsd/codemirror-mediawiki
Version:
Modified CodeMirror mode based on wikimedia/mediawiki-extensions-CodeMirror
241 lines (240 loc) • 10.4 kB
JavaScript
import { showPanel, EditorView } from '@codemirror/view';
import { nextDiagnostic, setDiagnosticsEffect } from '@codemirror/lint';
import { menuRegistry } from './codemirror';
const optionAll = /* @__PURE__ */ (() => {
const ele = document.createElement('div');
ele.textContent = 'Fix all auto-fixable problems';
return ele;
})();
function getLintMarker(view, severity, menu) {
const marker = document.createElement('div'), icon = document.createElement('div');
marker.className = `cm-status-${severity}`;
if (severity === 'fix') {
icon.className = 'cm-status-fix-disabled';
marker.title = 'Fix all';
marker.append(icon);
if (menu) {
marker.addEventListener('click', ({ clientX, clientY }) => {
if (icon.className === 'cm-status-fix-enabled') {
const { bottom, left } = view.dom.getBoundingClientRect();
menu.style.bottom = `${bottom - clientY + 5}px`;
menu.style.left = `${clientX - 20 - left}px`;
menu.style.display = 'block';
menu.focus();
}
});
}
}
else {
icon.className = `cm-lint-marker-${severity}`;
const count = document.createElement('div');
count.textContent = '0';
marker.append(icon, count);
marker.addEventListener('click', () => {
if (marker.parentElement?.classList.contains('cm-status-worker-enabled')) {
nextDiagnostic(view);
view.focus();
}
});
}
return marker;
}
const updateDiagnosticsCount = (diagnostics, s, marker) => {
marker.lastChild.textContent = String(diagnostics.filter(({ severity }) => severity === s).length);
};
const toggleClass = (classList, enabled) => {
classList.toggle('cm-status-fix-enabled', enabled);
classList.toggle('cm-status-fix-disabled', !enabled);
};
const getDiagnostics = (all, main) => all.filter(({ from, to }) => from <= main.to && to >= main.from);
const updateDiagnosticMessage = (cm, allDiagnostics, main, msg) => {
const diagnostics = getDiagnostics(allDiagnostics, main);
if (diagnostics.length === 0) {
msg.textContent = '';
}
else {
const diagnostic = diagnostics.find(({ from, to }) => from <= main.head && to >= main.head) ?? diagnostics[0], view = cm.view;
msg.textContent = diagnostic.message;
if (diagnostic.actions) {
msg.append(...diagnostic.actions.map(({ name, apply }) => {
const button = document.createElement('button');
button.type = 'button';
button.className = 'cm-diagnosticAction';
button.textContent = name;
button.addEventListener('click', e => {
e.preventDefault();
apply(view, diagnostic.from, diagnostic.to);
});
return button;
}));
}
}
};
const updateMenu = (cm, allDiagnostics, main, classList, menu, fixer) => {
if (menu) {
const actionable = menuRegistry.filter(({ name, isActionable }) => cm.hasPreference(name) && isActionable(cm)), fixable = new Set(fixer && getDiagnostics(allDiagnostics, main).filter(({ actions }) => actions?.some(({ name }) => name === 'fix'
|| name !== 'Fix: Stylelint' && name.startsWith('Fix:'))).map(({ message }) => / \(([^()]+)\)$/u.exec(message)?.[1])
.filter(Boolean));
if (actionable.length === 0 && fixable.size === 0) {
toggleClass(classList, false);
return;
}
toggleClass(classList, true);
const actions = actionable.flatMap(({ getItems }) => getItems(cm)), quickfix = [...fixable].map(rule => {
const option = document.createElement('div');
option.textContent = `Fix all ${rule} problems`;
option.dataset['rule'] = rule;
return option;
});
if (fixable.size > 0) {
quickfix.push(optionAll);
}
menu.replaceChildren(...actions, ...quickfix);
}
};
const panelSelector = '.cm-panel-status', workerSelector = '.cm-status-worker', errorSelector = '.cm-status-error', warningSelector = '.cm-status-warning', enabledSelector = '.cm-status-fix-enabled', disabledSelector = '.cm-status-fix-disabled', menuSelector = '.cm-status-fix-menu', messageSelector = '.cm-status-message';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export default (cm, fixer) => [
showPanel.of(view => {
let diagnostics = [], menu;
if (!view.state.readOnly && (fixer || menuRegistry.length > 0)) {
menu = document.createElement('div');
menu.className = 'cm-status-fix-menu';
menu.tabIndex = -1;
if (fixer) {
menu.addEventListener('click', ({ target }) => {
if (target === menu) {
return;
}
(async () => {
const { doc } = view.state, output = await fixer(doc, target.dataset['rule']);
if (output !== doc.toString()) {
view.dispatch({
changes: { from: 0, to: doc.length, insert: output },
});
}
view.focus();
})();
});
}
menu.addEventListener('focusout', () => {
menu.style.display = 'none';
});
view.dom.append(menu);
}
const dom = document.createElement('div'), worker = document.createElement('div'), message = document.createElement('div'), position = document.createElement('div'), error = getLintMarker(view, 'error'), warning = getLintMarker(view, 'warning'), fix = getLintMarker(view, 'fix', menu), { classList } = fix.firstChild;
worker.className = 'cm-status-worker';
worker.append(error, warning, fix);
message.className = 'cm-status-message';
position.className = 'cm-status-line';
position.textContent = '0:0';
dom.className = 'cm-panel cm-panel-status';
dom.append(worker, message, position);
return {
dom,
update({ state: { selection: { main }, doc }, transactions, docChanged, selectionSet }) {
for (const tr of transactions) {
for (const effect of tr.effects) {
if (effect.is(setDiagnosticsEffect)) {
diagnostics = effect.value;
worker.classList.toggle('cm-status-worker-enabled', diagnostics.length > 0);
updateDiagnosticsCount(diagnostics, 'error', error);
updateDiagnosticsCount(diagnostics, 'warning', warning);
updateDiagnosticMessage(cm, diagnostics, main, message);
updateMenu(cm, diagnostics, main, classList, menu, fixer);
}
}
}
if (docChanged || selectionSet) {
updateDiagnosticMessage(cm, diagnostics, main, message);
updateMenu(cm, diagnostics, main, classList, menu, fixer);
const { number, from } = doc.lineAt(main.head);
position.textContent = `${number}:${main.head - from}`;
if (!main.empty) {
position.textContent += ` (${main.to - main.from})`;
}
}
},
};
}),
EditorView.theme({
[panelSelector]: {
lineHeight: 1.4,
},
[`${panelSelector}>div`]: {
padding: '0 .3em',
display: 'table-cell',
},
[workerSelector]: {
WebkitUserSelect: 'none',
userSelect: 'none',
'--fix-icon': "url('data:image/svg+xml,"
+ '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">'
// eslint-disable-next-line @stylistic/max-len
+ '<path d="M8 19a1 1 0 001 1h2a1 1 0 001-1v-1H8zm9-12a7 7 0 10-12 4.9S7 14 7 15v1a1 1 0 001 1h4a1 1 0 001-1v-1c0-1 2-3.1 2-3.1A7 7 0 0017 7"/>'
+ '</svg>'
+ "')",
},
[`${workerSelector}>*`]: {
display: 'table-cell',
whiteSpace: 'nowrap',
},
[`${errorSelector},${warningSelector}`]: {
paddingRight: '8px',
},
[`.cm-status-worker-enabled ${errorSelector},.cm-status-worker-enabled ${warningSelector}`]: {
cursor: 'pointer',
},
[`${workerSelector}>*>div`]: {
display: 'inline-block',
verticalAlign: 'middle',
},
[`${workerSelector}>*>div:first-child`]: {
marginRight: '4px',
width: '1em',
height: '1em',
},
[`${disabledSelector},${enabledSelector}`]: {
WebkitMaskImage: 'var(--fix-icon)',
maskImage: 'var(--fix-icon)',
WebkitMaskSize: '100%',
maskSize: '100%',
WebkitMaskRepeat: 'no-repeat',
maskRepeat: 'no-repeat',
WebkitMaskPosition: 'center',
maskPosition: 'center',
},
[disabledSelector]: {
backgroundColor: '#dadde3',
},
[enabledSelector]: {
backgroundColor: '#ffce31',
cursor: 'pointer',
},
[menuSelector]: {
display: 'none',
position: 'absolute',
zIndex: 301,
border: '1px solid #ddd',
borderRadius: '2px',
outline: 'none',
whiteSpace: 'nowrap',
},
[`${menuSelector}>div`]: {
padding: '1px 5px',
cursor: 'pointer',
},
[messageSelector]: {
borderStyle: 'solid',
borderWidth: '0 1px',
width: '100%',
},
[`${messageSelector} .cm-diagnosticAction`]: {
paddingTop: 0,
paddingBottom: 0,
},
'.cm-status-line': {
whiteSpace: 'nowrap',
},
}),
];