Diffdown is a real-time collaborative Markdown editor/previewer built on the AT Protocol
diffdown.com
1import { Decoration, showPanel, EditorView, ViewPlugin, gutter, showTooltip, hoverTooltip, getPanel, logException, WidgetType, GutterMarker } from '@codemirror/view';
2import { StateEffect, StateField, Facet, combineConfig, RangeSet, RangeSetBuilder } from '@codemirror/state';
3import elt from 'crelt';
4
5class SelectedDiagnostic {
6 constructor(from, to, diagnostic) {
7 this.from = from;
8 this.to = to;
9 this.diagnostic = diagnostic;
10 }
11}
12class LintState {
13 constructor(diagnostics, panel, selected) {
14 this.diagnostics = diagnostics;
15 this.panel = panel;
16 this.selected = selected;
17 }
18 static init(diagnostics, panel, state) {
19 // Filter the list of diagnostics for which to create markers
20 let diagnosticFilter = state.facet(lintConfig).markerFilter;
21 if (diagnosticFilter)
22 diagnostics = diagnosticFilter(diagnostics, state);
23 let sorted = diagnostics.slice().sort((a, b) => a.from - b.from || a.to - b.to);
24 let deco = new RangeSetBuilder(), active = [], pos = 0;
25 let scan = state.doc.iter(), scanPos = 0, docLen = state.doc.length;
26 for (let i = 0;;) {
27 let next = i == sorted.length ? null : sorted[i];
28 if (!next && !active.length)
29 break;
30 let from, to;
31 if (active.length) {
32 from = pos;
33 to = active.reduce((p, d) => Math.min(p, d.to), next && next.from > from ? next.from : 1e8);
34 }
35 else {
36 from = next.from;
37 if (from > docLen)
38 break;
39 to = next.to;
40 active.push(next);
41 i++;
42 }
43 while (i < sorted.length) {
44 let next = sorted[i];
45 if (next.from == from && (next.to > next.from || next.to == from)) {
46 active.push(next);
47 i++;
48 to = Math.min(next.to, to);
49 }
50 else {
51 to = Math.min(next.from, to);
52 break;
53 }
54 }
55 to = Math.min(to, docLen);
56 let widget = false;
57 if (active.some(d => d.from == from && (d.to == to || to == docLen))) {
58 widget = from == to;
59 if (!widget && to - from < 10) {
60 let behind = from - (scanPos + scan.value.length);
61 if (behind > 0) {
62 scan.next(behind);
63 scanPos = from;
64 }
65 for (let check = from;;) {
66 if (check >= to) {
67 widget = true;
68 break;
69 }
70 if (!scan.lineBreak && scanPos + scan.value.length > check)
71 break;
72 check = scanPos + scan.value.length;
73 scanPos += scan.value.length;
74 scan.next();
75 }
76 }
77 }
78 let sev = maxSeverity(active);
79 if (widget) {
80 deco.add(from, from, Decoration.widget({
81 widget: new DiagnosticWidget(sev),
82 diagnostics: active.slice()
83 }));
84 }
85 else {
86 let markClass = active.reduce((c, d) => d.markClass ? c + " " + d.markClass : c, "");
87 deco.add(from, to, Decoration.mark({
88 class: "cm-lintRange cm-lintRange-" + sev + markClass,
89 diagnostics: active.slice(),
90 inclusiveEnd: active.some(a => a.to > to)
91 }));
92 }
93 pos = to;
94 if (pos == docLen)
95 break;
96 for (let i = 0; i < active.length; i++)
97 if (active[i].to <= pos)
98 active.splice(i--, 1);
99 }
100 let set = deco.finish();
101 return new LintState(set, panel, findDiagnostic(set));
102 }
103}
104function findDiagnostic(diagnostics, diagnostic = null, after = 0) {
105 let found = null;
106 diagnostics.between(after, 1e9, (from, to, { spec }) => {
107 if (diagnostic && spec.diagnostics.indexOf(diagnostic) < 0)
108 return;
109 if (!found)
110 found = new SelectedDiagnostic(from, to, diagnostic || spec.diagnostics[0]);
111 else if (spec.diagnostics.indexOf(found.diagnostic) < 0)
112 return false;
113 else
114 found = new SelectedDiagnostic(found.from, to, found.diagnostic);
115 });
116 return found;
117}
118function hideTooltip(tr, tooltip) {
119 let from = tooltip.pos, to = tooltip.end || from;
120 let result = tr.state.facet(lintConfig).hideOn(tr, from, to);
121 if (result != null)
122 return result;
123 let line = tr.startState.doc.lineAt(tooltip.pos);
124 return !!(tr.effects.some(e => e.is(setDiagnosticsEffect)) || tr.changes.touchesRange(line.from, Math.max(line.to, to)));
125}
126function maybeEnableLint(state, effects) {
127 return state.field(lintState, false) ? effects : effects.concat(StateEffect.appendConfig.of(lintExtensions));
128}
129/**
130Returns a transaction spec which updates the current set of
131diagnostics, and enables the lint extension if if wasn't already
132active.
133*/
134function setDiagnostics(state, diagnostics) {
135 return {
136 effects: maybeEnableLint(state, [setDiagnosticsEffect.of(diagnostics)])
137 };
138}
139/**
140The state effect that updates the set of active diagnostics. Can
141be useful when writing an extension that needs to track these.
142*/
143const setDiagnosticsEffect = /*@__PURE__*/StateEffect.define();
144const togglePanel = /*@__PURE__*/StateEffect.define();
145const movePanelSelection = /*@__PURE__*/StateEffect.define();
146const lintState = /*@__PURE__*/StateField.define({
147 create() {
148 return new LintState(Decoration.none, null, null);
149 },
150 update(value, tr) {
151 if (tr.docChanged && value.diagnostics.size) {
152 let mapped = value.diagnostics.map(tr.changes), selected = null, panel = value.panel;
153 if (value.selected) {
154 let selPos = tr.changes.mapPos(value.selected.from, 1);
155 selected = findDiagnostic(mapped, value.selected.diagnostic, selPos) || findDiagnostic(mapped, null, selPos);
156 }
157 if (!mapped.size && panel && tr.state.facet(lintConfig).autoPanel)
158 panel = null;
159 value = new LintState(mapped, panel, selected);
160 }
161 for (let effect of tr.effects) {
162 if (effect.is(setDiagnosticsEffect)) {
163 let panel = !tr.state.facet(lintConfig).autoPanel ? value.panel : effect.value.length ? LintPanel.open : null;
164 value = LintState.init(effect.value, panel, tr.state);
165 }
166 else if (effect.is(togglePanel)) {
167 value = new LintState(value.diagnostics, effect.value ? LintPanel.open : null, value.selected);
168 }
169 else if (effect.is(movePanelSelection)) {
170 value = new LintState(value.diagnostics, value.panel, effect.value);
171 }
172 }
173 return value;
174 },
175 provide: f => [showPanel.from(f, val => val.panel),
176 EditorView.decorations.from(f, s => s.diagnostics)]
177});
178/**
179Returns the number of active lint diagnostics in the given state.
180*/
181function diagnosticCount(state) {
182 let lint = state.field(lintState, false);
183 return lint ? lint.diagnostics.size : 0;
184}
185const activeMark = /*@__PURE__*/Decoration.mark({ class: "cm-lintRange cm-lintRange-active" });
186function lintTooltip(view, pos, side) {
187 let { diagnostics } = view.state.field(lintState);
188 let found, start = -1, end = -1;
189 diagnostics.between(pos - (side < 0 ? 1 : 0), pos + (side > 0 ? 1 : 0), (from, to, { spec }) => {
190 if (pos >= from && pos <= to &&
191 (from == to || ((pos > from || side > 0) && (pos < to || side < 0)))) {
192 found = spec.diagnostics;
193 start = from;
194 end = to;
195 return false;
196 }
197 });
198 let diagnosticFilter = view.state.facet(lintConfig).tooltipFilter;
199 if (found && diagnosticFilter)
200 found = diagnosticFilter(found, view.state);
201 if (!found)
202 return null;
203 return {
204 pos: start,
205 end: end,
206 above: view.state.doc.lineAt(start).to < end,
207 create() {
208 return { dom: diagnosticsTooltip(view, found) };
209 }
210 };
211}
212function diagnosticsTooltip(view, diagnostics) {
213 return elt("ul", { class: "cm-tooltip-lint" }, diagnostics.map(d => renderDiagnostic(view, d, false)));
214}
215/**
216Command to open and focus the lint panel.
217*/
218const openLintPanel = (view) => {
219 let field = view.state.field(lintState, false);
220 if (!field || !field.panel)
221 view.dispatch({ effects: maybeEnableLint(view.state, [togglePanel.of(true)]) });
222 let panel = getPanel(view, LintPanel.open);
223 if (panel)
224 panel.dom.querySelector(".cm-panel-lint ul").focus();
225 return true;
226};
227/**
228Command to close the lint panel, when open.
229*/
230const closeLintPanel = (view) => {
231 let field = view.state.field(lintState, false);
232 if (!field || !field.panel)
233 return false;
234 view.dispatch({ effects: togglePanel.of(false) });
235 return true;
236};
237/**
238Move the selection to the next diagnostic.
239*/
240const nextDiagnostic = (view) => {
241 let field = view.state.field(lintState, false);
242 if (!field)
243 return false;
244 let sel = view.state.selection.main, next = findDiagnostic(field.diagnostics, null, sel.to + 1);
245 if (!next) {
246 next = findDiagnostic(field.diagnostics, null, 0);
247 if (!next || next.from == sel.from && next.to == sel.to)
248 return false;
249 }
250 view.dispatch({ selection: { anchor: next.from, head: next.to }, scrollIntoView: true });
251 return true;
252};
253/**
254Move the selection to the previous diagnostic.
255*/
256const previousDiagnostic = (view) => {
257 let { state } = view, field = state.field(lintState, false);
258 if (!field)
259 return false;
260 let sel = state.selection.main;
261 let prevFrom, prevTo, lastFrom, lastTo;
262 field.diagnostics.between(0, state.doc.length, (from, to) => {
263 if (to < sel.to && (prevFrom == null || prevFrom < from)) {
264 prevFrom = from;
265 prevTo = to;
266 }
267 if (lastFrom == null || from > lastFrom) {
268 lastFrom = from;
269 lastTo = to;
270 }
271 });
272 if (lastFrom == null || prevFrom == null && lastFrom == sel.from)
273 return false;
274 view.dispatch({ selection: { anchor: prevFrom !== null && prevFrom !== void 0 ? prevFrom : lastFrom, head: prevTo !== null && prevTo !== void 0 ? prevTo : lastTo }, scrollIntoView: true });
275 return true;
276};
277/**
278A set of default key bindings for the lint functionality.
279
280- Ctrl-Shift-m (Cmd-Shift-m on macOS): [`openLintPanel`](https://codemirror.net/6/docs/ref/#lint.openLintPanel)
281- F8: [`nextDiagnostic`](https://codemirror.net/6/docs/ref/#lint.nextDiagnostic)
282*/
283const lintKeymap = [
284 { key: "Mod-Shift-m", run: openLintPanel, preventDefault: true },
285 { key: "F8", run: nextDiagnostic }
286];
287const lintPlugin = /*@__PURE__*/ViewPlugin.fromClass(class {
288 constructor(view) {
289 this.view = view;
290 this.timeout = -1;
291 this.set = true;
292 let { delay } = view.state.facet(lintConfig);
293 this.lintTime = Date.now() + delay;
294 this.run = this.run.bind(this);
295 this.timeout = setTimeout(this.run, delay);
296 }
297 run() {
298 clearTimeout(this.timeout);
299 let now = Date.now();
300 if (now < this.lintTime - 10) {
301 this.timeout = setTimeout(this.run, this.lintTime - now);
302 }
303 else {
304 this.set = false;
305 let { state } = this.view, { sources } = state.facet(lintConfig);
306 if (sources.length)
307 batchResults(sources.map(s => Promise.resolve(s(this.view))), annotations => {
308 if (this.view.state.doc == state.doc)
309 this.view.dispatch(setDiagnostics(this.view.state, annotations.reduce((a, b) => a.concat(b))));
310 }, error => { logException(this.view.state, error); });
311 }
312 }
313 update(update) {
314 let config = update.state.facet(lintConfig);
315 if (update.docChanged || config != update.startState.facet(lintConfig) ||
316 config.needsRefresh && config.needsRefresh(update)) {
317 this.lintTime = Date.now() + config.delay;
318 if (!this.set) {
319 this.set = true;
320 this.timeout = setTimeout(this.run, config.delay);
321 }
322 }
323 }
324 force() {
325 if (this.set) {
326 this.lintTime = Date.now();
327 this.run();
328 }
329 }
330 destroy() {
331 clearTimeout(this.timeout);
332 }
333});
334function batchResults(promises, sink, error) {
335 let collected = [], timeout = -1;
336 for (let p of promises)
337 p.then(value => {
338 collected.push(value);
339 clearTimeout(timeout);
340 if (collected.length == promises.length)
341 sink(collected);
342 else
343 timeout = setTimeout(() => sink(collected), 200);
344 }, error);
345}
346const lintConfig = /*@__PURE__*/Facet.define({
347 combine(input) {
348 return {
349 sources: input.map(i => i.source).filter(x => x != null),
350 ...combineConfig(input.map(i => i.config), {
351 delay: 750,
352 markerFilter: null,
353 tooltipFilter: null,
354 needsRefresh: null,
355 hideOn: () => null,
356 }, {
357 delay: Math.max,
358 markerFilter: combineFilter,
359 tooltipFilter: combineFilter,
360 needsRefresh: (a, b) => !a ? b : !b ? a : u => a(u) || b(u),
361 hideOn: (a, b) => !a ? b : !b ? a : (t, x, y) => a(t, x, y) || b(t, x, y),
362 autoPanel: (a, b) => a || b
363 })
364 };
365 }
366});
367function combineFilter(a, b) {
368 return !a ? b : !b ? a : (d, s) => b(a(d, s), s);
369}
370/**
371Given a diagnostic source, this function returns an extension that
372enables linting with that source. It will be called whenever the
373editor is idle (after its content changed).
374
375Note that settings given here will apply to all linters active in
376the editor. If `null` is given as source, this only configures the
377lint extension.
378*/
379function linter(source, config = {}) {
380 return [
381 lintConfig.of({ source, config }),
382 lintPlugin,
383 lintExtensions
384 ];
385}
386/**
387Forces any linters [configured](https://codemirror.net/6/docs/ref/#lint.linter) to run when the
388editor is idle to run right away.
389*/
390function forceLinting(view) {
391 let plugin = view.plugin(lintPlugin);
392 if (plugin)
393 plugin.force();
394}
395function assignKeys(actions) {
396 let assigned = [];
397 if (actions)
398 actions: for (let { name } of actions) {
399 for (let i = 0; i < name.length; i++) {
400 let ch = name[i];
401 if (/[a-zA-Z]/.test(ch) && !assigned.some(c => c.toLowerCase() == ch.toLowerCase())) {
402 assigned.push(ch);
403 continue actions;
404 }
405 }
406 assigned.push("");
407 }
408 return assigned;
409}
410function renderDiagnostic(view, diagnostic, inPanel) {
411 var _a;
412 let keys = inPanel ? assignKeys(diagnostic.actions) : [];
413 return elt("li", { class: "cm-diagnostic cm-diagnostic-" + diagnostic.severity }, elt("span", { class: "cm-diagnosticText" }, diagnostic.renderMessage ? diagnostic.renderMessage(view) : diagnostic.message), (_a = diagnostic.actions) === null || _a === void 0 ? void 0 : _a.map((action, i) => {
414 let fired = false, click = (e) => {
415 e.preventDefault();
416 if (fired)
417 return;
418 fired = true;
419 let found = findDiagnostic(view.state.field(lintState).diagnostics, diagnostic);
420 if (found)
421 action.apply(view, found.from, found.to);
422 };
423 let { name } = action, keyIndex = keys[i] ? name.indexOf(keys[i]) : -1;
424 let nameElt = keyIndex < 0 ? name : [name.slice(0, keyIndex),
425 elt("u", name.slice(keyIndex, keyIndex + 1)),
426 name.slice(keyIndex + 1)];
427 let markClass = action.markClass ? " " + action.markClass : "";
428 return elt("button", {
429 type: "button",
430 class: "cm-diagnosticAction" + markClass,
431 onclick: click,
432 onmousedown: click,
433 "aria-label": ` Action: ${name}${keyIndex < 0 ? "" : ` (access key "${keys[i]})"`}.`
434 }, nameElt);
435 }), diagnostic.source && elt("div", { class: "cm-diagnosticSource" }, diagnostic.source));
436}
437class DiagnosticWidget extends WidgetType {
438 constructor(sev) {
439 super();
440 this.sev = sev;
441 }
442 eq(other) { return other.sev == this.sev; }
443 toDOM() {
444 return elt("span", { class: "cm-lintPoint cm-lintPoint-" + this.sev });
445 }
446}
447class PanelItem {
448 constructor(view, diagnostic) {
449 this.diagnostic = diagnostic;
450 this.id = "item_" + Math.floor(Math.random() * 0xffffffff).toString(16);
451 this.dom = renderDiagnostic(view, diagnostic, true);
452 this.dom.id = this.id;
453 this.dom.setAttribute("role", "option");
454 }
455}
456class LintPanel {
457 constructor(view) {
458 this.view = view;
459 this.items = [];
460 let onkeydown = (event) => {
461 if (event.ctrlKey || event.altKey || event.metaKey)
462 return;
463 if (event.keyCode == 27) { // Escape
464 closeLintPanel(this.view);
465 this.view.focus();
466 }
467 else if (event.keyCode == 38 || event.keyCode == 33) { // ArrowUp, PageUp
468 this.moveSelection((this.selectedIndex - 1 + this.items.length) % this.items.length);
469 }
470 else if (event.keyCode == 40 || event.keyCode == 34) { // ArrowDown, PageDown
471 this.moveSelection((this.selectedIndex + 1) % this.items.length);
472 }
473 else if (event.keyCode == 36) { // Home
474 this.moveSelection(0);
475 }
476 else if (event.keyCode == 35) { // End
477 this.moveSelection(this.items.length - 1);
478 }
479 else if (event.keyCode == 13) { // Enter
480 this.view.focus();
481 }
482 else if (event.keyCode >= 65 && event.keyCode <= 90 && this.selectedIndex >= 0) { // A-Z
483 let { diagnostic } = this.items[this.selectedIndex], keys = assignKeys(diagnostic.actions);
484 for (let i = 0; i < keys.length; i++)
485 if (keys[i].toUpperCase().charCodeAt(0) == event.keyCode) {
486 let found = findDiagnostic(this.view.state.field(lintState).diagnostics, diagnostic);
487 if (found)
488 diagnostic.actions[i].apply(view, found.from, found.to);
489 }
490 }
491 else {
492 return;
493 }
494 event.preventDefault();
495 };
496 let onclick = (event) => {
497 for (let i = 0; i < this.items.length; i++) {
498 if (this.items[i].dom.contains(event.target))
499 this.moveSelection(i);
500 }
501 };
502 this.list = elt("ul", {
503 tabIndex: 0,
504 role: "listbox",
505 "aria-label": this.view.state.phrase("Diagnostics"),
506 onkeydown,
507 onclick
508 });
509 this.dom = elt("div", { class: "cm-panel-lint" }, this.list, elt("button", {
510 type: "button",
511 name: "close",
512 "aria-label": this.view.state.phrase("close"),
513 onclick: () => closeLintPanel(this.view)
514 }, "×"));
515 this.update();
516 }
517 get selectedIndex() {
518 let selected = this.view.state.field(lintState).selected;
519 if (!selected)
520 return -1;
521 for (let i = 0; i < this.items.length; i++)
522 if (this.items[i].diagnostic == selected.diagnostic)
523 return i;
524 return -1;
525 }
526 update() {
527 let { diagnostics, selected } = this.view.state.field(lintState);
528 let i = 0, needsSync = false, newSelectedItem = null;
529 let seen = new Set();
530 diagnostics.between(0, this.view.state.doc.length, (_start, _end, { spec }) => {
531 for (let diagnostic of spec.diagnostics) {
532 if (seen.has(diagnostic))
533 continue;
534 seen.add(diagnostic);
535 let found = -1, item;
536 for (let j = i; j < this.items.length; j++)
537 if (this.items[j].diagnostic == diagnostic) {
538 found = j;
539 break;
540 }
541 if (found < 0) {
542 item = new PanelItem(this.view, diagnostic);
543 this.items.splice(i, 0, item);
544 needsSync = true;
545 }
546 else {
547 item = this.items[found];
548 if (found > i) {
549 this.items.splice(i, found - i);
550 needsSync = true;
551 }
552 }
553 if (selected && item.diagnostic == selected.diagnostic) {
554 if (!item.dom.hasAttribute("aria-selected")) {
555 item.dom.setAttribute("aria-selected", "true");
556 newSelectedItem = item;
557 }
558 }
559 else if (item.dom.hasAttribute("aria-selected")) {
560 item.dom.removeAttribute("aria-selected");
561 }
562 i++;
563 }
564 });
565 while (i < this.items.length && !(this.items.length == 1 && this.items[0].diagnostic.from < 0)) {
566 needsSync = true;
567 this.items.pop();
568 }
569 if (this.items.length == 0) {
570 this.items.push(new PanelItem(this.view, {
571 from: -1, to: -1,
572 severity: "info",
573 message: this.view.state.phrase("No diagnostics")
574 }));
575 needsSync = true;
576 }
577 if (newSelectedItem) {
578 this.list.setAttribute("aria-activedescendant", newSelectedItem.id);
579 this.view.requestMeasure({
580 key: this,
581 read: () => ({ sel: newSelectedItem.dom.getBoundingClientRect(), panel: this.list.getBoundingClientRect() }),
582 write: ({ sel, panel }) => {
583 let scaleY = panel.height / this.list.offsetHeight;
584 if (sel.top < panel.top)
585 this.list.scrollTop -= (panel.top - sel.top) / scaleY;
586 else if (sel.bottom > panel.bottom)
587 this.list.scrollTop += (sel.bottom - panel.bottom) / scaleY;
588 }
589 });
590 }
591 else if (this.selectedIndex < 0) {
592 this.list.removeAttribute("aria-activedescendant");
593 }
594 if (needsSync)
595 this.sync();
596 }
597 sync() {
598 let domPos = this.list.firstChild;
599 function rm() {
600 let prev = domPos;
601 domPos = prev.nextSibling;
602 prev.remove();
603 }
604 for (let item of this.items) {
605 if (item.dom.parentNode == this.list) {
606 while (domPos != item.dom)
607 rm();
608 domPos = item.dom.nextSibling;
609 }
610 else {
611 this.list.insertBefore(item.dom, domPos);
612 }
613 }
614 while (domPos)
615 rm();
616 }
617 moveSelection(selectedIndex) {
618 if (this.selectedIndex < 0)
619 return;
620 let field = this.view.state.field(lintState);
621 let selection = findDiagnostic(field.diagnostics, this.items[selectedIndex].diagnostic);
622 if (!selection)
623 return;
624 this.view.dispatch({
625 selection: { anchor: selection.from, head: selection.to },
626 scrollIntoView: true,
627 effects: movePanelSelection.of(selection)
628 });
629 }
630 static open(view) { return new LintPanel(view); }
631}
632function svg(content, attrs = `viewBox="0 0 40 40"`) {
633 return `url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" ${attrs}>${encodeURIComponent(content)}</svg>')`;
634}
635function underline(color) {
636 return svg(`<path d="m0 2.5 l2 -1.5 l1 0 l2 1.5 l1 0" stroke="${color}" fill="none" stroke-width=".7"/>`, `width="6" height="3"`);
637}
638const baseTheme = /*@__PURE__*/EditorView.baseTheme({
639 ".cm-diagnostic": {
640 padding: "3px 6px 3px 8px",
641 marginLeft: "-1px",
642 display: "block",
643 whiteSpace: "pre-wrap"
644 },
645 ".cm-diagnostic-error": { borderLeft: "5px solid #d11" },
646 ".cm-diagnostic-warning": { borderLeft: "5px solid orange" },
647 ".cm-diagnostic-info": { borderLeft: "5px solid #999" },
648 ".cm-diagnostic-hint": { borderLeft: "5px solid #66d" },
649 ".cm-diagnosticAction": {
650 font: "inherit",
651 border: "none",
652 padding: "2px 4px",
653 backgroundColor: "#444",
654 color: "white",
655 borderRadius: "3px",
656 marginLeft: "8px",
657 cursor: "pointer"
658 },
659 ".cm-diagnosticSource": {
660 fontSize: "70%",
661 opacity: .7
662 },
663 ".cm-lintRange": {
664 backgroundPosition: "left bottom",
665 backgroundRepeat: "repeat-x",
666 paddingBottom: "0.7px",
667 },
668 ".cm-lintRange-error": { backgroundImage: /*@__PURE__*/underline("#d11") },
669 ".cm-lintRange-warning": { backgroundImage: /*@__PURE__*/underline("orange") },
670 ".cm-lintRange-info": { backgroundImage: /*@__PURE__*/underline("#999") },
671 ".cm-lintRange-hint": { backgroundImage: /*@__PURE__*/underline("#66d") },
672 ".cm-lintRange-active": { backgroundColor: "#ffdd9980" },
673 ".cm-tooltip-lint": {
674 padding: 0,
675 margin: 0
676 },
677 ".cm-lintPoint": {
678 position: "relative",
679 "&:after": {
680 content: '""',
681 position: "absolute",
682 bottom: 0,
683 left: "-2px",
684 borderLeft: "3px solid transparent",
685 borderRight: "3px solid transparent",
686 borderBottom: "4px solid #d11"
687 }
688 },
689 ".cm-lintPoint-warning": {
690 "&:after": { borderBottomColor: "orange" }
691 },
692 ".cm-lintPoint-info": {
693 "&:after": { borderBottomColor: "#999" }
694 },
695 ".cm-lintPoint-hint": {
696 "&:after": { borderBottomColor: "#66d" }
697 },
698 ".cm-panel.cm-panel-lint": {
699 position: "relative",
700 "& ul": {
701 maxHeight: "100px",
702 overflowY: "auto",
703 "& [aria-selected]": {
704 backgroundColor: "#ddd",
705 "& u": { textDecoration: "underline" }
706 },
707 "&:focus [aria-selected]": {
708 background_fallback: "#bdf",
709 backgroundColor: "Highlight",
710 color_fallback: "white",
711 color: "HighlightText"
712 },
713 "& u": { textDecoration: "none" },
714 padding: 0,
715 margin: 0
716 },
717 "& [name=close]": {
718 position: "absolute",
719 top: "0",
720 right: "2px",
721 background: "inherit",
722 border: "none",
723 font: "inherit",
724 padding: 0,
725 margin: 0
726 }
727 },
728 "&dark .cm-lintRange-active": { backgroundColor: "#86714a80" },
729 "&dark .cm-panel.cm-panel-lint ul": {
730 "& [aria-selected]": {
731 backgroundColor: "#2e343e",
732 },
733 }
734});
735function severityWeight(sev) {
736 return sev == "error" ? 4 : sev == "warning" ? 3 : sev == "info" ? 2 : 1;
737}
738function maxSeverity(diagnostics) {
739 let sev = "hint", weight = 1;
740 for (let d of diagnostics) {
741 let w = severityWeight(d.severity);
742 if (w > weight) {
743 weight = w;
744 sev = d.severity;
745 }
746 }
747 return sev;
748}
749class LintGutterMarker extends GutterMarker {
750 constructor(diagnostics) {
751 super();
752 this.diagnostics = diagnostics;
753 this.severity = maxSeverity(diagnostics);
754 }
755 toDOM(view) {
756 let elt = document.createElement("div");
757 elt.className = "cm-lint-marker cm-lint-marker-" + this.severity;
758 let diagnostics = this.diagnostics;
759 let diagnosticsFilter = view.state.facet(lintGutterConfig).tooltipFilter;
760 if (diagnosticsFilter)
761 diagnostics = diagnosticsFilter(diagnostics, view.state);
762 if (diagnostics.length)
763 elt.onmouseover = () => gutterMarkerMouseOver(view, elt, diagnostics);
764 return elt;
765 }
766}
767function trackHoverOn(view, marker) {
768 let mousemove = (event) => {
769 let rect = marker.getBoundingClientRect();
770 if (event.clientX > rect.left - 10 /* Hover.Margin */ && event.clientX < rect.right + 10 /* Hover.Margin */ &&
771 event.clientY > rect.top - 10 /* Hover.Margin */ && event.clientY < rect.bottom + 10 /* Hover.Margin */)
772 return;
773 for (let target = event.target; target; target = target.parentNode) {
774 if (target.nodeType == 1 && target.classList.contains("cm-tooltip-lint"))
775 return;
776 }
777 window.removeEventListener("mousemove", mousemove);
778 if (view.state.field(lintGutterTooltip))
779 view.dispatch({ effects: setLintGutterTooltip.of(null) });
780 };
781 window.addEventListener("mousemove", mousemove);
782}
783function gutterMarkerMouseOver(view, marker, diagnostics) {
784 function hovered() {
785 let line = view.elementAtHeight(marker.getBoundingClientRect().top + 5 - view.documentTop);
786 const linePos = view.coordsAtPos(line.from);
787 if (linePos) {
788 view.dispatch({ effects: setLintGutterTooltip.of({
789 pos: line.from,
790 above: false,
791 clip: false,
792 create() {
793 return {
794 dom: diagnosticsTooltip(view, diagnostics),
795 getCoords: () => marker.getBoundingClientRect()
796 };
797 }
798 }) });
799 }
800 marker.onmouseout = marker.onmousemove = null;
801 trackHoverOn(view, marker);
802 }
803 let { hoverTime } = view.state.facet(lintGutterConfig);
804 let hoverTimeout = setTimeout(hovered, hoverTime);
805 marker.onmouseout = () => {
806 clearTimeout(hoverTimeout);
807 marker.onmouseout = marker.onmousemove = null;
808 };
809 marker.onmousemove = () => {
810 clearTimeout(hoverTimeout);
811 hoverTimeout = setTimeout(hovered, hoverTime);
812 };
813}
814function markersForDiagnostics(doc, diagnostics) {
815 let byLine = Object.create(null);
816 for (let diagnostic of diagnostics) {
817 let line = doc.lineAt(diagnostic.from);
818 (byLine[line.from] || (byLine[line.from] = [])).push(diagnostic);
819 }
820 let markers = [];
821 for (let line in byLine) {
822 markers.push(new LintGutterMarker(byLine[line]).range(+line));
823 }
824 return RangeSet.of(markers, true);
825}
826const lintGutterExtension = /*@__PURE__*/gutter({
827 class: "cm-gutter-lint",
828 markers: view => view.state.field(lintGutterMarkers),
829 widgetMarker: (view, widget, block) => {
830 let diagnostics = [];
831 view.state.field(lintGutterMarkers).between(block.from, block.to, (from, to, value) => {
832 if (from > block.from && from < block.to)
833 diagnostics.push(...value.diagnostics);
834 });
835 return diagnostics.length ? new LintGutterMarker(diagnostics) : null;
836 }
837});
838const lintGutterMarkers = /*@__PURE__*/StateField.define({
839 create() {
840 return RangeSet.empty;
841 },
842 update(markers, tr) {
843 markers = markers.map(tr.changes);
844 let diagnosticFilter = tr.state.facet(lintGutterConfig).markerFilter;
845 for (let effect of tr.effects) {
846 if (effect.is(setDiagnosticsEffect)) {
847 let diagnostics = effect.value;
848 if (diagnosticFilter)
849 diagnostics = diagnosticFilter(diagnostics || [], tr.state);
850 markers = markersForDiagnostics(tr.state.doc, diagnostics.slice(0));
851 }
852 }
853 return markers;
854 }
855});
856const setLintGutterTooltip = /*@__PURE__*/StateEffect.define();
857const lintGutterTooltip = /*@__PURE__*/StateField.define({
858 create() { return null; },
859 update(tooltip, tr) {
860 if (tooltip && tr.docChanged)
861 tooltip = hideTooltip(tr, tooltip) ? null : { ...tooltip, pos: tr.changes.mapPos(tooltip.pos) };
862 return tr.effects.reduce((t, e) => e.is(setLintGutterTooltip) ? e.value : t, tooltip);
863 },
864 provide: field => showTooltip.from(field)
865});
866const lintGutterTheme = /*@__PURE__*/EditorView.baseTheme({
867 ".cm-gutter-lint": {
868 width: "1.4em",
869 "& .cm-gutterElement": {
870 padding: ".2em"
871 }
872 },
873 ".cm-lint-marker": {
874 width: "1em",
875 height: "1em"
876 },
877 ".cm-lint-marker-info": {
878 content: /*@__PURE__*/svg(`<path fill="#aaf" stroke="#77e" stroke-width="6" stroke-linejoin="round" d="M5 5L35 5L35 35L5 35Z"/>`)
879 },
880 ".cm-lint-marker-warning": {
881 content: /*@__PURE__*/svg(`<path fill="#fe8" stroke="#fd7" stroke-width="6" stroke-linejoin="round" d="M20 6L37 35L3 35Z"/>`),
882 },
883 ".cm-lint-marker-error": {
884 content: /*@__PURE__*/svg(`<circle cx="20" cy="20" r="15" fill="#f87" stroke="#f43" stroke-width="6"/>`)
885 },
886});
887const lintExtensions = [
888 lintState,
889 /*@__PURE__*/EditorView.decorations.compute([lintState], state => {
890 let { selected, panel } = state.field(lintState);
891 return !selected || !panel || selected.from == selected.to ? Decoration.none : Decoration.set([
892 activeMark.range(selected.from, selected.to)
893 ]);
894 }),
895 /*@__PURE__*/hoverTooltip(lintTooltip, { hideOn: hideTooltip }),
896 baseTheme
897];
898const lintGutterConfig = /*@__PURE__*/Facet.define({
899 combine(configs) {
900 return combineConfig(configs, {
901 hoverTime: 300 /* Hover.Time */,
902 markerFilter: null,
903 tooltipFilter: null
904 });
905 }
906});
907/**
908Returns an extension that installs a gutter showing markers for
909each line that has diagnostics, which can be hovered over to see
910the diagnostics.
911*/
912function lintGutter(config = {}) {
913 return [lintGutterConfig.of(config), lintGutterMarkers, lintGutterExtension, lintGutterTheme, lintGutterTooltip];
914}
915/**
916Iterate over the marked diagnostics for the given editor state,
917calling `f` for each of them. Note that, if the document changed
918since the diagnostics were created, the `Diagnostic` object will
919hold the original outdated position, whereas the `to` and `from`
920arguments hold the diagnostic's current position.
921*/
922function forEachDiagnostic(state, f) {
923 let lState = state.field(lintState, false);
924 if (lState && lState.diagnostics.size) {
925 let pending = [], pendingStart = [], lastEnd = -1;
926 for (let iter = RangeSet.iter([lState.diagnostics]);; iter.next()) {
927 for (let i = 0; i < pending.length; i++)
928 if (!iter.value || iter.value.spec.diagnostics.indexOf(pending[i]) < 0) {
929 f(pending[i], pendingStart[i], lastEnd);
930 pending.splice(i, 1);
931 pendingStart.splice(i--, 1);
932 }
933 if (!iter.value)
934 break;
935 for (let d of iter.value.spec.diagnostics)
936 if (pending.indexOf(d) < 0) {
937 pending.push(d);
938 pendingStart.push(iter.from);
939 }
940 lastEnd = iter.to;
941 }
942 }
943}
944
945export { closeLintPanel, diagnosticCount, forEachDiagnostic, forceLinting, lintGutter, lintKeymap, linter, nextDiagnostic, openLintPanel, previousDiagnostic, setDiagnostics, setDiagnosticsEffect };