@codemirror/lint 0.18.6 → 0.19.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,33 @@
1
+ ## 0.19.3 (2021-11-09)
2
+
3
+ ### New features
4
+
5
+ Export a function `lintGutter` which returns an extension that installs a gutter marking lines with diagnostics.
6
+
7
+ The package now exports the effect used to update the diagnostics (`setDiagnosticsEffect`).
8
+
9
+ ## 0.19.2 (2021-09-29)
10
+
11
+ ### Bug fixes
12
+
13
+ Fix a bug where reconfiguring the lint source didn't restart linting.
14
+
15
+ ## 0.19.1 (2021-09-17)
16
+
17
+ ### Bug fixes
18
+
19
+ Prevent decorations that cover just a line break from being invisible by showing a widget instead of range for them.
20
+
21
+ ### New features
22
+
23
+ The `diagnosticCount` method can now be used to determine whether there are active diagnostics.
24
+
25
+ ## 0.19.0 (2021-08-11)
26
+
27
+ ### Breaking changes
28
+
29
+ Update dependencies to 0.19.0
30
+
1
31
  ## 0.18.6 (2021-08-08)
2
32
 
3
33
  ### Bug fixes
package/dist/index.cjs CHANGED
@@ -6,6 +6,8 @@ var view = require('@codemirror/view');
6
6
  var state = require('@codemirror/state');
7
7
  var tooltip = require('@codemirror/tooltip');
8
8
  var panel = require('@codemirror/panel');
9
+ var gutter = require('@codemirror/gutter');
10
+ var rangeset = require('@codemirror/rangeset');
9
11
  var elt = require('crelt');
10
12
 
11
13
  function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
@@ -25,17 +27,18 @@ class LintState {
25
27
  this.panel = panel;
26
28
  this.selected = selected;
27
29
  }
28
- static init(diagnostics, panel) {
30
+ static init(diagnostics, panel, state) {
29
31
  let ranges = view.Decoration.set(diagnostics.map((d) => {
30
- return d.from < d.to
31
- ? view.Decoration.mark({
32
- attributes: { class: "cm-lintRange cm-lintRange-" + d.severity },
33
- diagnostic: d
34
- }).range(d.from, d.to)
35
- : view.Decoration.widget({
32
+ // For zero-length ranges or ranges covering only a line break, create a widget
33
+ return d.from == d.to || (d.from == d.to - 1 && state.doc.lineAt(d.from).to == d.from)
34
+ ? view.Decoration.widget({
36
35
  widget: new DiagnosticWidget(d),
37
36
  diagnostic: d
38
- }).range(d.from);
37
+ }).range(d.from)
38
+ : view.Decoration.mark({
39
+ attributes: { class: "cm-lintRange cm-lintRange-" + d.severity },
40
+ diagnostic: d
41
+ }).range(d.from, d.to);
39
42
  }), true);
40
43
  return new LintState(ranges, panel, findDiagnostic(ranges));
41
44
  }
@@ -50,9 +53,9 @@ function findDiagnostic(diagnostics, diagnostic = null, after = 0) {
50
53
  });
51
54
  return found;
52
55
  }
53
- function maybeEnableLint(state$1, effects, getState) {
56
+ function maybeEnableLint(state$1, effects) {
54
57
  return state$1.field(lintState, false) ? effects : effects.concat(state.StateEffect.appendConfig.of([
55
- lintState.init(getState),
58
+ lintState,
56
59
  view.EditorView.decorations.compute([lintState], state => {
57
60
  let { selected, panel } = state.field(lintState);
58
61
  return !selected || !panel || selected.from == selected.to ? view.Decoration.none : view.Decoration.set([
@@ -65,13 +68,18 @@ function maybeEnableLint(state$1, effects, getState) {
65
68
  }
66
69
  /**
67
70
  Returns a transaction spec which updates the current set of
68
- diagnostics.
71
+ diagnostics, and enables the lint extension if if wasn't already
72
+ active.
69
73
  */
70
74
  function setDiagnostics(state, diagnostics) {
71
75
  return {
72
- effects: maybeEnableLint(state, [setDiagnosticsEffect.of(diagnostics)], () => LintState.init(diagnostics, null))
76
+ effects: maybeEnableLint(state, [setDiagnosticsEffect.of(diagnostics)])
73
77
  };
74
78
  }
79
+ /**
80
+ The state effect that updates the set of active diagnostics. Can
81
+ be useful when writing an extension that needs to track these.
82
+ */
75
83
  const setDiagnosticsEffect = state.StateEffect.define();
76
84
  const togglePanel = state.StateEffect.define();
77
85
  const movePanelSelection = state.StateEffect.define();
@@ -90,7 +98,7 @@ const lintState = state.StateField.define({
90
98
  }
91
99
  for (let effect of tr.effects) {
92
100
  if (effect.is(setDiagnosticsEffect)) {
93
- value = LintState.init(effect.value, value.panel);
101
+ value = LintState.init(effect.value, value.panel, tr.state);
94
102
  }
95
103
  else if (effect.is(togglePanel)) {
96
104
  value = new LintState(value.diagnostics, effect.value ? LintPanel.open : null, value.selected);
@@ -104,6 +112,13 @@ const lintState = state.StateField.define({
104
112
  provide: f => [panel.showPanel.from(f, val => val.panel),
105
113
  view.EditorView.decorations.from(f, s => s.diagnostics)]
106
114
  });
115
+ /**
116
+ Returns the number of active lint diagnostics in the given state.
117
+ */
118
+ function diagnosticCount(state) {
119
+ let lint = state.field(lintState, false);
120
+ return lint ? lint.diagnostics.size : 0;
121
+ }
107
122
  const activeMark = view.Decoration.mark({ class: "cm-lintRange cm-lintRange-active" });
108
123
  function lintTooltip(view, pos, side) {
109
124
  let { diagnostics } = view.state.field(lintState);
@@ -123,17 +138,20 @@ function lintTooltip(view, pos, side) {
123
138
  end: stackEnd,
124
139
  above: view.state.doc.lineAt(stackStart).to < stackEnd,
125
140
  create() {
126
- return { dom: elt__default['default']("ul", { class: "cm-tooltip-lint" }, found.map(d => renderDiagnostic(view, d, false))) };
141
+ return { dom: diagnosticsTooltip(view, found) };
127
142
  }
128
143
  };
129
144
  }
145
+ function diagnosticsTooltip(view, diagnostics) {
146
+ return elt__default['default']("ul", { class: "cm-tooltip-lint" }, diagnostics.map(d => renderDiagnostic(view, d, false)));
147
+ }
130
148
  /**
131
149
  Command to open and focus the lint panel.
132
150
  */
133
151
  const openLintPanel = (view) => {
134
152
  let field = view.state.field(lintState, false);
135
153
  if (!field || !field.panel)
136
- view.dispatch({ effects: maybeEnableLint(view.state, [togglePanel.of(true)], () => LintState.init([], LintPanel.open)) });
154
+ view.dispatch({ effects: maybeEnableLint(view.state, [togglePanel.of(true)]) });
137
155
  let panel$1 = panel.getPanel(view, LintPanel.open);
138
156
  if (panel$1)
139
157
  panel$1.dom.querySelector(".cm-panel-lint ul").focus();
@@ -203,12 +221,12 @@ const lintPlugin = view.ViewPlugin.fromClass(class {
203
221
  }
204
222
  }
205
223
  update(update) {
206
- if (update.docChanged) {
207
- let { delay } = update.state.facet(lintSource);
208
- this.lintTime = Date.now() + delay;
224
+ let source = update.state.facet(lintSource);
225
+ if (update.docChanged || source != update.startState.facet(lintSource)) {
226
+ this.lintTime = Date.now() + source.delay;
209
227
  if (!this.set) {
210
228
  this.set = true;
211
- this.timeout = setTimeout(this.run, delay);
229
+ this.timeout = setTimeout(this.run, source.delay);
212
230
  }
213
231
  }
214
232
  }
@@ -276,6 +294,7 @@ function renderDiagnostic(view, diagnostic, inPanel) {
276
294
  elt__default['default']("u", name.slice(keyIndex, keyIndex + 1)),
277
295
  name.slice(keyIndex + 1)];
278
296
  return elt__default['default']("button", {
297
+ type: "button",
279
298
  class: "cm-diagnosticAction",
280
299
  onclick: click,
281
300
  onmousedown: click,
@@ -354,6 +373,7 @@ class LintPanel {
354
373
  onclick
355
374
  });
356
375
  this.dom = elt__default['default']("div", { class: "cm-panel-lint" }, this.list, elt__default['default']("button", {
376
+ type: "button",
357
377
  name: "close",
358
378
  "aria-label": this.view.state.phrase("close"),
359
379
  onclick: () => closeLintPanel(this.view)
@@ -468,13 +488,11 @@ class LintPanel {
468
488
  }
469
489
  static open(view) { return new LintPanel(view); }
470
490
  }
491
+ function svg(content, attrs = `viewBox="0 0 40 40"`) {
492
+ return `url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" ${attrs}>${encodeURIComponent(content)}</svg>')`;
493
+ }
471
494
  function underline(color) {
472
- if (typeof btoa != "function")
473
- return "none";
474
- let svg = `<svg xmlns="http://www.w3.org/2000/svg" width="6" height="3">
475
- <path d="m0 3 l2 -2 l1 0 l2 2 l1 0" stroke="${color}" fill="none" stroke-width=".7"/>
476
- </svg>`;
477
- return `url('data:image/svg+xml;base64,${btoa(svg)}')`;
495
+ 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"`);
478
496
  }
479
497
  const baseTheme = view.EditorView.baseTheme({
480
498
  ".cm-diagnostic": {
@@ -501,7 +519,8 @@ const baseTheme = view.EditorView.baseTheme({
501
519
  },
502
520
  ".cm-lintRange": {
503
521
  backgroundPosition: "left bottom",
504
- backgroundRepeat: "repeat-x"
522
+ backgroundRepeat: "repeat-x",
523
+ paddingBottom: "0.7px",
505
524
  },
506
525
  ".cm-lintRange-error": { backgroundImage: underline("#d11") },
507
526
  ".cm-lintRange-warning": { backgroundImage: underline("orange") },
@@ -560,11 +579,146 @@ const baseTheme = view.EditorView.baseTheme({
560
579
  }
561
580
  }
562
581
  });
582
+ class LintGutterMarker extends gutter.GutterMarker {
583
+ constructor(diagnostics) {
584
+ super();
585
+ this.diagnostics = diagnostics;
586
+ this.severity = diagnostics.reduce((max, d) => {
587
+ let s = d.severity;
588
+ return s == "error" || s == "warning" && max == "info" ? s : max;
589
+ }, "info");
590
+ }
591
+ toDOM(view) {
592
+ let elt = document.createElement("div");
593
+ elt.className = "cm-lint-marker cm-lint-marker-" + this.severity;
594
+ elt.onmouseover = () => gutterMarkerMouseOver(view, elt, this.diagnostics);
595
+ return elt;
596
+ }
597
+ }
598
+ function trackHoverOn(view, marker) {
599
+ let mousemove = (event) => {
600
+ let rect = marker.getBoundingClientRect();
601
+ if (event.clientX > rect.left - 10 /* Margin */ && event.clientX < rect.right + 10 /* Margin */ &&
602
+ event.clientY > rect.top - 10 /* Margin */ && event.clientY < rect.bottom + 10 /* Margin */)
603
+ return;
604
+ for (let target = event.target; target; target = target.parentNode) {
605
+ if (target.nodeType == 1 && target.classList.contains("cm-tooltip-lint"))
606
+ return;
607
+ }
608
+ window.removeEventListener("mousemove", mousemove);
609
+ if (view.state.field(lintGutterTooltip))
610
+ view.dispatch({ effects: setLintGutterTooltip.of(null) });
611
+ };
612
+ window.addEventListener("mousemove", mousemove);
613
+ }
614
+ function gutterMarkerMouseOver(view, marker, diagnostics) {
615
+ function hovered() {
616
+ let line = view.visualLineAtHeight(marker.getBoundingClientRect().top + 5);
617
+ const linePos = view.coordsAtPos(line.from), markerRect = marker.getBoundingClientRect();
618
+ if (linePos) {
619
+ view.dispatch({ effects: setLintGutterTooltip.of({
620
+ pos: line.from,
621
+ above: false,
622
+ create() {
623
+ return {
624
+ dom: diagnosticsTooltip(view, diagnostics),
625
+ offset: { x: markerRect.left - linePos.left, y: 0 }
626
+ };
627
+ }
628
+ }) });
629
+ }
630
+ marker.onmouseout = marker.onmousemove = null;
631
+ trackHoverOn(view, marker);
632
+ }
633
+ let hoverTimeout = setTimeout(hovered, 600 /* Time */);
634
+ marker.onmouseout = () => {
635
+ clearTimeout(hoverTimeout);
636
+ marker.onmouseout = marker.onmousemove = null;
637
+ };
638
+ marker.onmousemove = () => {
639
+ clearTimeout(hoverTimeout);
640
+ hoverTimeout = setTimeout(hovered, 600 /* Time */);
641
+ };
642
+ }
643
+ function markersForDiagnostics(doc, diagnostics) {
644
+ let byLine = Object.create(null);
645
+ for (let diagnostic of diagnostics) {
646
+ let line = doc.lineAt(diagnostic.from);
647
+ (byLine[line.from] || (byLine[line.from] = [])).push(diagnostic);
648
+ }
649
+ let markers = [];
650
+ for (let line in byLine) {
651
+ markers.push(new LintGutterMarker(byLine[line]).range(+line));
652
+ }
653
+ return rangeset.RangeSet.of(markers, true);
654
+ }
655
+ const lintGutterExtension = gutter.gutter({
656
+ class: "cm-gutter-lint",
657
+ markers: view => view.state.field(lintGutterMarkers),
658
+ });
659
+ const lintGutterMarkers = state.StateField.define({
660
+ create() {
661
+ return rangeset.RangeSet.empty;
662
+ },
663
+ update(markers, tr) {
664
+ markers = markers.map(tr.changes);
665
+ for (let effect of tr.effects)
666
+ if (effect.is(setDiagnosticsEffect)) {
667
+ markers = markersForDiagnostics(tr.state.doc, effect.value);
668
+ }
669
+ return markers;
670
+ }
671
+ });
672
+ const setLintGutterTooltip = state.StateEffect.define();
673
+ const lintGutterTooltip = state.StateField.define({
674
+ create() { return null; },
675
+ update(tooltip, tr) {
676
+ if (tooltip && tr.docChanged)
677
+ tooltip = Object.assign(Object.assign({}, tooltip), { pos: tr.changes.mapPos(tooltip.pos) });
678
+ return tr.effects.reduce((t, e) => e.is(setLintGutterTooltip) ? e.value : t, tooltip);
679
+ },
680
+ provide: field => tooltip.showTooltip.from(field)
681
+ });
682
+ const lintGutterTheme = view.EditorView.baseTheme({
683
+ ".cm-gutter-lint": {
684
+ width: "1.4em",
685
+ "& .cm-gutterElement": {
686
+ padding: "0 .2em",
687
+ display: "flex",
688
+ flexDirection: "column",
689
+ justifyContent: "center",
690
+ }
691
+ },
692
+ ".cm-lint-marker": {
693
+ width: "1em",
694
+ height: "1em",
695
+ },
696
+ ".cm-lint-marker-info": {
697
+ content: svg(`<path fill="#aaf" stroke="#77e" stroke-width="6" stroke-linejoin="round" d="M5 5L35 5L35 35L5 35Z"/>`)
698
+ },
699
+ ".cm-lint-marker-warning": {
700
+ content: svg(`<path fill="#fe8" stroke="#fd7" stroke-width="6" stroke-linejoin="round" d="M20 6L37 35L3 35Z"/>`),
701
+ },
702
+ ".cm-lint-marker-error:before": {
703
+ content: svg(`<circle cx="20" cy="20" r="15" fill="#f87" stroke="#f43" stroke-width="6"/>`)
704
+ },
705
+ });
706
+ /**
707
+ Returns an extension that installs a gutter showing markers for
708
+ each line that has diagnostics, which can be hovered over to see
709
+ the diagnostics.
710
+ */
711
+ function lintGutter() {
712
+ return [lintGutterMarkers, lintGutterExtension, lintGutterTheme, lintGutterTooltip];
713
+ }
563
714
 
564
715
  exports.closeLintPanel = closeLintPanel;
716
+ exports.diagnosticCount = diagnosticCount;
565
717
  exports.forceLinting = forceLinting;
718
+ exports.lintGutter = lintGutter;
566
719
  exports.lintKeymap = lintKeymap;
567
720
  exports.linter = linter;
568
721
  exports.nextDiagnostic = nextDiagnostic;
569
722
  exports.openLintPanel = openLintPanel;
570
723
  exports.setDiagnostics = setDiagnostics;
724
+ exports.setDiagnosticsEffect = setDiagnosticsEffect;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- import { EditorView, Command, KeyBinding } from '@codemirror/view';
1
+ import * as _codemirror_state from '@codemirror/state';
2
2
  import { EditorState, TransactionSpec, Extension } from '@codemirror/state';
3
+ import { EditorView, Command, KeyBinding } from '@codemirror/view';
3
4
 
4
5
  /**
5
6
  Describes a problem or hint for a piece of code.
@@ -52,10 +53,20 @@ interface Action {
52
53
  }
53
54
  /**
54
55
  Returns a transaction spec which updates the current set of
55
- diagnostics.
56
+ diagnostics, and enables the lint extension if if wasn't already
57
+ active.
56
58
  */
57
59
  declare function setDiagnostics(state: EditorState, diagnostics: readonly Diagnostic[]): TransactionSpec;
58
60
  /**
61
+ The state effect that updates the set of active diagnostics. Can
62
+ be useful when writing an extension that needs to track these.
63
+ */
64
+ declare const setDiagnosticsEffect: _codemirror_state.StateEffectType<readonly Diagnostic[]>;
65
+ /**
66
+ Returns the number of active lint diagnostics in the given state.
67
+ */
68
+ declare function diagnosticCount(state: EditorState): number;
69
+ /**
59
70
  Command to open and focus the lint panel.
60
71
  */
61
72
  declare const openLintPanel: Command;
@@ -92,5 +103,11 @@ Forces any linters [configured](https://codemirror.net/6/docs/ref/#lint.linter)
92
103
  editor is idle to run right away.
93
104
  */
94
105
  declare function forceLinting(view: EditorView): void;
106
+ /**
107
+ Returns an extension that installs a gutter showing markers for
108
+ each line that has diagnostics, which can be hovered over to see
109
+ the diagnostics.
110
+ */
111
+ declare function lintGutter(): Extension;
95
112
 
96
- export { Action, Diagnostic, closeLintPanel, forceLinting, lintKeymap, linter, nextDiagnostic, openLintPanel, setDiagnostics };
113
+ export { Action, Diagnostic, closeLintPanel, diagnosticCount, forceLinting, lintGutter, lintKeymap, linter, nextDiagnostic, openLintPanel, setDiagnostics, setDiagnosticsEffect };
package/dist/index.js CHANGED
@@ -1,7 +1,9 @@
1
- import { EditorView, Decoration, ViewPlugin, logException, WidgetType } from '@codemirror/view';
1
+ import { Decoration, EditorView, ViewPlugin, logException, WidgetType } from '@codemirror/view';
2
2
  import { StateEffect, StateField, Facet } from '@codemirror/state';
3
- import { hoverTooltip } from '@codemirror/tooltip';
3
+ import { hoverTooltip, showTooltip } from '@codemirror/tooltip';
4
4
  import { showPanel, getPanel } from '@codemirror/panel';
5
+ import { gutter, GutterMarker } from '@codemirror/gutter';
6
+ import { RangeSet } from '@codemirror/rangeset';
5
7
  import elt from 'crelt';
6
8
 
7
9
  class SelectedDiagnostic {
@@ -17,17 +19,18 @@ class LintState {
17
19
  this.panel = panel;
18
20
  this.selected = selected;
19
21
  }
20
- static init(diagnostics, panel) {
22
+ static init(diagnostics, panel, state) {
21
23
  let ranges = Decoration.set(diagnostics.map((d) => {
22
- return d.from < d.to
23
- ? Decoration.mark({
24
- attributes: { class: "cm-lintRange cm-lintRange-" + d.severity },
25
- diagnostic: d
26
- }).range(d.from, d.to)
27
- : Decoration.widget({
24
+ // For zero-length ranges or ranges covering only a line break, create a widget
25
+ return d.from == d.to || (d.from == d.to - 1 && state.doc.lineAt(d.from).to == d.from)
26
+ ? Decoration.widget({
28
27
  widget: new DiagnosticWidget(d),
29
28
  diagnostic: d
30
- }).range(d.from);
29
+ }).range(d.from)
30
+ : Decoration.mark({
31
+ attributes: { class: "cm-lintRange cm-lintRange-" + d.severity },
32
+ diagnostic: d
33
+ }).range(d.from, d.to);
31
34
  }), true);
32
35
  return new LintState(ranges, panel, findDiagnostic(ranges));
33
36
  }
@@ -42,9 +45,9 @@ function findDiagnostic(diagnostics, diagnostic = null, after = 0) {
42
45
  });
43
46
  return found;
44
47
  }
45
- function maybeEnableLint(state, effects, getState) {
48
+ function maybeEnableLint(state, effects) {
46
49
  return state.field(lintState, false) ? effects : effects.concat(StateEffect.appendConfig.of([
47
- lintState.init(getState),
50
+ lintState,
48
51
  EditorView.decorations.compute([lintState], state => {
49
52
  let { selected, panel } = state.field(lintState);
50
53
  return !selected || !panel || selected.from == selected.to ? Decoration.none : Decoration.set([
@@ -57,13 +60,18 @@ function maybeEnableLint(state, effects, getState) {
57
60
  }
58
61
  /**
59
62
  Returns a transaction spec which updates the current set of
60
- diagnostics.
63
+ diagnostics, and enables the lint extension if if wasn't already
64
+ active.
61
65
  */
62
66
  function setDiagnostics(state, diagnostics) {
63
67
  return {
64
- effects: maybeEnableLint(state, [setDiagnosticsEffect.of(diagnostics)], () => LintState.init(diagnostics, null))
68
+ effects: maybeEnableLint(state, [setDiagnosticsEffect.of(diagnostics)])
65
69
  };
66
70
  }
71
+ /**
72
+ The state effect that updates the set of active diagnostics. Can
73
+ be useful when writing an extension that needs to track these.
74
+ */
67
75
  const setDiagnosticsEffect = /*@__PURE__*/StateEffect.define();
68
76
  const togglePanel = /*@__PURE__*/StateEffect.define();
69
77
  const movePanelSelection = /*@__PURE__*/StateEffect.define();
@@ -82,7 +90,7 @@ const lintState = /*@__PURE__*/StateField.define({
82
90
  }
83
91
  for (let effect of tr.effects) {
84
92
  if (effect.is(setDiagnosticsEffect)) {
85
- value = LintState.init(effect.value, value.panel);
93
+ value = LintState.init(effect.value, value.panel, tr.state);
86
94
  }
87
95
  else if (effect.is(togglePanel)) {
88
96
  value = new LintState(value.diagnostics, effect.value ? LintPanel.open : null, value.selected);
@@ -96,6 +104,13 @@ const lintState = /*@__PURE__*/StateField.define({
96
104
  provide: f => [showPanel.from(f, val => val.panel),
97
105
  EditorView.decorations.from(f, s => s.diagnostics)]
98
106
  });
107
+ /**
108
+ Returns the number of active lint diagnostics in the given state.
109
+ */
110
+ function diagnosticCount(state) {
111
+ let lint = state.field(lintState, false);
112
+ return lint ? lint.diagnostics.size : 0;
113
+ }
99
114
  const activeMark = /*@__PURE__*/Decoration.mark({ class: "cm-lintRange cm-lintRange-active" });
100
115
  function lintTooltip(view, pos, side) {
101
116
  let { diagnostics } = view.state.field(lintState);
@@ -115,17 +130,20 @@ function lintTooltip(view, pos, side) {
115
130
  end: stackEnd,
116
131
  above: view.state.doc.lineAt(stackStart).to < stackEnd,
117
132
  create() {
118
- return { dom: elt("ul", { class: "cm-tooltip-lint" }, found.map(d => renderDiagnostic(view, d, false))) };
133
+ return { dom: diagnosticsTooltip(view, found) };
119
134
  }
120
135
  };
121
136
  }
137
+ function diagnosticsTooltip(view, diagnostics) {
138
+ return elt("ul", { class: "cm-tooltip-lint" }, diagnostics.map(d => renderDiagnostic(view, d, false)));
139
+ }
122
140
  /**
123
141
  Command to open and focus the lint panel.
124
142
  */
125
143
  const openLintPanel = (view) => {
126
144
  let field = view.state.field(lintState, false);
127
145
  if (!field || !field.panel)
128
- view.dispatch({ effects: maybeEnableLint(view.state, [togglePanel.of(true)], () => LintState.init([], LintPanel.open)) });
146
+ view.dispatch({ effects: maybeEnableLint(view.state, [togglePanel.of(true)]) });
129
147
  let panel = getPanel(view, LintPanel.open);
130
148
  if (panel)
131
149
  panel.dom.querySelector(".cm-panel-lint ul").focus();
@@ -195,12 +213,12 @@ const lintPlugin = /*@__PURE__*/ViewPlugin.fromClass(class {
195
213
  }
196
214
  }
197
215
  update(update) {
198
- if (update.docChanged) {
199
- let { delay } = update.state.facet(lintSource);
200
- this.lintTime = Date.now() + delay;
216
+ let source = update.state.facet(lintSource);
217
+ if (update.docChanged || source != update.startState.facet(lintSource)) {
218
+ this.lintTime = Date.now() + source.delay;
201
219
  if (!this.set) {
202
220
  this.set = true;
203
- this.timeout = setTimeout(this.run, delay);
221
+ this.timeout = setTimeout(this.run, source.delay);
204
222
  }
205
223
  }
206
224
  }
@@ -268,6 +286,7 @@ function renderDiagnostic(view, diagnostic, inPanel) {
268
286
  elt("u", name.slice(keyIndex, keyIndex + 1)),
269
287
  name.slice(keyIndex + 1)];
270
288
  return elt("button", {
289
+ type: "button",
271
290
  class: "cm-diagnosticAction",
272
291
  onclick: click,
273
292
  onmousedown: click,
@@ -346,6 +365,7 @@ class LintPanel {
346
365
  onclick
347
366
  });
348
367
  this.dom = elt("div", { class: "cm-panel-lint" }, this.list, elt("button", {
368
+ type: "button",
349
369
  name: "close",
350
370
  "aria-label": this.view.state.phrase("close"),
351
371
  onclick: () => closeLintPanel(this.view)
@@ -460,13 +480,11 @@ class LintPanel {
460
480
  }
461
481
  static open(view) { return new LintPanel(view); }
462
482
  }
483
+ function svg(content, attrs = `viewBox="0 0 40 40"`) {
484
+ return `url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" ${attrs}>${encodeURIComponent(content)}</svg>')`;
485
+ }
463
486
  function underline(color) {
464
- if (typeof btoa != "function")
465
- return "none";
466
- let svg = `<svg xmlns="http://www.w3.org/2000/svg" width="6" height="3">
467
- <path d="m0 3 l2 -2 l1 0 l2 2 l1 0" stroke="${color}" fill="none" stroke-width=".7"/>
468
- </svg>`;
469
- return `url('data:image/svg+xml;base64,${btoa(svg)}')`;
487
+ 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"`);
470
488
  }
471
489
  const baseTheme = /*@__PURE__*/EditorView.baseTheme({
472
490
  ".cm-diagnostic": {
@@ -493,7 +511,8 @@ const baseTheme = /*@__PURE__*/EditorView.baseTheme({
493
511
  },
494
512
  ".cm-lintRange": {
495
513
  backgroundPosition: "left bottom",
496
- backgroundRepeat: "repeat-x"
514
+ backgroundRepeat: "repeat-x",
515
+ paddingBottom: "0.7px",
497
516
  },
498
517
  ".cm-lintRange-error": { backgroundImage: /*@__PURE__*/underline("#d11") },
499
518
  ".cm-lintRange-warning": { backgroundImage: /*@__PURE__*/underline("orange") },
@@ -552,5 +571,137 @@ const baseTheme = /*@__PURE__*/EditorView.baseTheme({
552
571
  }
553
572
  }
554
573
  });
574
+ class LintGutterMarker extends GutterMarker {
575
+ constructor(diagnostics) {
576
+ super();
577
+ this.diagnostics = diagnostics;
578
+ this.severity = diagnostics.reduce((max, d) => {
579
+ let s = d.severity;
580
+ return s == "error" || s == "warning" && max == "info" ? s : max;
581
+ }, "info");
582
+ }
583
+ toDOM(view) {
584
+ let elt = document.createElement("div");
585
+ elt.className = "cm-lint-marker cm-lint-marker-" + this.severity;
586
+ elt.onmouseover = () => gutterMarkerMouseOver(view, elt, this.diagnostics);
587
+ return elt;
588
+ }
589
+ }
590
+ function trackHoverOn(view, marker) {
591
+ let mousemove = (event) => {
592
+ let rect = marker.getBoundingClientRect();
593
+ if (event.clientX > rect.left - 10 /* Margin */ && event.clientX < rect.right + 10 /* Margin */ &&
594
+ event.clientY > rect.top - 10 /* Margin */ && event.clientY < rect.bottom + 10 /* Margin */)
595
+ return;
596
+ for (let target = event.target; target; target = target.parentNode) {
597
+ if (target.nodeType == 1 && target.classList.contains("cm-tooltip-lint"))
598
+ return;
599
+ }
600
+ window.removeEventListener("mousemove", mousemove);
601
+ if (view.state.field(lintGutterTooltip))
602
+ view.dispatch({ effects: setLintGutterTooltip.of(null) });
603
+ };
604
+ window.addEventListener("mousemove", mousemove);
605
+ }
606
+ function gutterMarkerMouseOver(view, marker, diagnostics) {
607
+ function hovered() {
608
+ let line = view.visualLineAtHeight(marker.getBoundingClientRect().top + 5);
609
+ const linePos = view.coordsAtPos(line.from), markerRect = marker.getBoundingClientRect();
610
+ if (linePos) {
611
+ view.dispatch({ effects: setLintGutterTooltip.of({
612
+ pos: line.from,
613
+ above: false,
614
+ create() {
615
+ return {
616
+ dom: diagnosticsTooltip(view, diagnostics),
617
+ offset: { x: markerRect.left - linePos.left, y: 0 }
618
+ };
619
+ }
620
+ }) });
621
+ }
622
+ marker.onmouseout = marker.onmousemove = null;
623
+ trackHoverOn(view, marker);
624
+ }
625
+ let hoverTimeout = setTimeout(hovered, 600 /* Time */);
626
+ marker.onmouseout = () => {
627
+ clearTimeout(hoverTimeout);
628
+ marker.onmouseout = marker.onmousemove = null;
629
+ };
630
+ marker.onmousemove = () => {
631
+ clearTimeout(hoverTimeout);
632
+ hoverTimeout = setTimeout(hovered, 600 /* Time */);
633
+ };
634
+ }
635
+ function markersForDiagnostics(doc, diagnostics) {
636
+ let byLine = Object.create(null);
637
+ for (let diagnostic of diagnostics) {
638
+ let line = doc.lineAt(diagnostic.from);
639
+ (byLine[line.from] || (byLine[line.from] = [])).push(diagnostic);
640
+ }
641
+ let markers = [];
642
+ for (let line in byLine) {
643
+ markers.push(new LintGutterMarker(byLine[line]).range(+line));
644
+ }
645
+ return RangeSet.of(markers, true);
646
+ }
647
+ const lintGutterExtension = /*@__PURE__*/gutter({
648
+ class: "cm-gutter-lint",
649
+ markers: view => view.state.field(lintGutterMarkers),
650
+ });
651
+ const lintGutterMarkers = /*@__PURE__*/StateField.define({
652
+ create() {
653
+ return RangeSet.empty;
654
+ },
655
+ update(markers, tr) {
656
+ markers = markers.map(tr.changes);
657
+ for (let effect of tr.effects)
658
+ if (effect.is(setDiagnosticsEffect)) {
659
+ markers = markersForDiagnostics(tr.state.doc, effect.value);
660
+ }
661
+ return markers;
662
+ }
663
+ });
664
+ const setLintGutterTooltip = /*@__PURE__*/StateEffect.define();
665
+ const lintGutterTooltip = /*@__PURE__*/StateField.define({
666
+ create() { return null; },
667
+ update(tooltip, tr) {
668
+ if (tooltip && tr.docChanged)
669
+ tooltip = Object.assign(Object.assign({}, tooltip), { pos: tr.changes.mapPos(tooltip.pos) });
670
+ return tr.effects.reduce((t, e) => e.is(setLintGutterTooltip) ? e.value : t, tooltip);
671
+ },
672
+ provide: field => showTooltip.from(field)
673
+ });
674
+ const lintGutterTheme = /*@__PURE__*/EditorView.baseTheme({
675
+ ".cm-gutter-lint": {
676
+ width: "1.4em",
677
+ "& .cm-gutterElement": {
678
+ padding: "0 .2em",
679
+ display: "flex",
680
+ flexDirection: "column",
681
+ justifyContent: "center",
682
+ }
683
+ },
684
+ ".cm-lint-marker": {
685
+ width: "1em",
686
+ height: "1em",
687
+ },
688
+ ".cm-lint-marker-info": {
689
+ content: /*@__PURE__*/svg(`<path fill="#aaf" stroke="#77e" stroke-width="6" stroke-linejoin="round" d="M5 5L35 5L35 35L5 35Z"/>`)
690
+ },
691
+ ".cm-lint-marker-warning": {
692
+ content: /*@__PURE__*/svg(`<path fill="#fe8" stroke="#fd7" stroke-width="6" stroke-linejoin="round" d="M20 6L37 35L3 35Z"/>`),
693
+ },
694
+ ".cm-lint-marker-error:before": {
695
+ content: /*@__PURE__*/svg(`<circle cx="20" cy="20" r="15" fill="#f87" stroke="#f43" stroke-width="6"/>`)
696
+ },
697
+ });
698
+ /**
699
+ Returns an extension that installs a gutter showing markers for
700
+ each line that has diagnostics, which can be hovered over to see
701
+ the diagnostics.
702
+ */
703
+ function lintGutter() {
704
+ return [lintGutterMarkers, lintGutterExtension, lintGutterTheme, lintGutterTooltip];
705
+ }
555
706
 
556
- export { closeLintPanel, forceLinting, lintKeymap, linter, nextDiagnostic, openLintPanel, setDiagnostics };
707
+ export { closeLintPanel, diagnosticCount, forceLinting, lintGutter, lintKeymap, linter, nextDiagnostic, openLintPanel, setDiagnostics, setDiagnosticsEffect };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemirror/lint",
3
- "version": "0.18.6",
3
+ "version": "0.19.3",
4
4
  "description": "Linting support for the CodeMirror code editor",
5
5
  "scripts": {
6
6
  "test": "cm-runtests",
@@ -26,10 +26,12 @@
26
26
  "sideEffects": false,
27
27
  "license": "MIT",
28
28
  "dependencies": {
29
- "@codemirror/panel": "^0.18.1",
30
- "@codemirror/state": "^0.18.0",
31
- "@codemirror/tooltip": "^0.18.4",
32
- "@codemirror/view": "^0.18.0",
29
+ "@codemirror/gutter": "^0.19.4",
30
+ "@codemirror/panel": "^0.19.0",
31
+ "@codemirror/rangeset": "^0.19.1",
32
+ "@codemirror/state": "^0.19.4",
33
+ "@codemirror/tooltip": "^0.19.5",
34
+ "@codemirror/view": "^0.19.0",
33
35
  "crelt": "^1.0.5"
34
36
  },
35
37
  "devDependencies": {