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