@codemirror/lint 0.19.2 → 0.19.5

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,27 @@
1
+ ## 0.19.5 (2022-02-25)
2
+
3
+ ### Bug fixes
4
+
5
+ Make sure the lint gutter tooltips are positioned under their icon, even when the line is wrapped.
6
+
7
+ ## 0.19.4 (2022-02-25)
8
+
9
+ ### Bug fixes
10
+
11
+ Fix an issue where an outdated marker could stick around on the lint gutter after all diagnostics were removed.
12
+
13
+ ### New features
14
+
15
+ Add a `hoverTime` option to the lint gutter. Change default hover time to 300
16
+
17
+ ## 0.19.3 (2021-11-09)
18
+
19
+ ### New features
20
+
21
+ Export a function `lintGutter` which returns an extension that installs a gutter marking lines with diagnostics.
22
+
23
+ The package now exports the effect used to update the diagnostics (`setDiagnosticsEffect`).
24
+
1
25
  ## 0.19.2 (2021-09-29)
2
26
 
3
27
  ### 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 }; }
@@ -51,9 +53,9 @@ function findDiagnostic(diagnostics, diagnostic = null, after = 0) {
51
53
  });
52
54
  return found;
53
55
  }
54
- function maybeEnableLint(state$1, effects, getState) {
56
+ function maybeEnableLint(state$1, effects) {
55
57
  return state$1.field(lintState, false) ? effects : effects.concat(state.StateEffect.appendConfig.of([
56
- lintState.init(getState),
58
+ lintState,
57
59
  view.EditorView.decorations.compute([lintState], state => {
58
60
  let { selected, panel } = state.field(lintState);
59
61
  return !selected || !panel || selected.from == selected.to ? view.Decoration.none : view.Decoration.set([
@@ -66,13 +68,18 @@ function maybeEnableLint(state$1, effects, getState) {
66
68
  }
67
69
  /**
68
70
  Returns a transaction spec which updates the current set of
69
- diagnostics.
71
+ diagnostics, and enables the lint extension if if wasn't already
72
+ active.
70
73
  */
71
74
  function setDiagnostics(state, diagnostics) {
72
75
  return {
73
- effects: maybeEnableLint(state, [setDiagnosticsEffect.of(diagnostics)], () => LintState.init(diagnostics, null, state))
76
+ effects: maybeEnableLint(state, [setDiagnosticsEffect.of(diagnostics)])
74
77
  };
75
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
+ */
76
83
  const setDiagnosticsEffect = state.StateEffect.define();
77
84
  const togglePanel = state.StateEffect.define();
78
85
  const movePanelSelection = state.StateEffect.define();
@@ -131,17 +138,20 @@ function lintTooltip(view, pos, side) {
131
138
  end: stackEnd,
132
139
  above: view.state.doc.lineAt(stackStart).to < stackEnd,
133
140
  create() {
134
- return { dom: elt__default['default']("ul", { class: "cm-tooltip-lint" }, found.map(d => renderDiagnostic(view, d, false))) };
141
+ return { dom: diagnosticsTooltip(view, found) };
135
142
  }
136
143
  };
137
144
  }
145
+ function diagnosticsTooltip(view, diagnostics) {
146
+ return elt__default["default"]("ul", { class: "cm-tooltip-lint" }, diagnostics.map(d => renderDiagnostic(view, d, false)));
147
+ }
138
148
  /**
139
149
  Command to open and focus the lint panel.
140
150
  */
141
151
  const openLintPanel = (view) => {
142
152
  let field = view.state.field(lintState, false);
143
153
  if (!field || !field.panel)
144
- view.dispatch({ effects: maybeEnableLint(view.state, [togglePanel.of(true)], () => LintState.init([], LintPanel.open, view.state)) });
154
+ view.dispatch({ effects: maybeEnableLint(view.state, [togglePanel.of(true)]) });
145
155
  let panel$1 = panel.getPanel(view, LintPanel.open);
146
156
  if (panel$1)
147
157
  panel$1.dom.querySelector(".cm-panel-lint ul").focus();
@@ -202,10 +212,8 @@ const lintPlugin = view.ViewPlugin.fromClass(class {
202
212
  this.set = false;
203
213
  let { state } = this.view, { sources } = state.facet(lintSource);
204
214
  Promise.all(sources.map(source => Promise.resolve(source(this.view)))).then(annotations => {
205
- var _a, _b;
206
215
  let all = annotations.reduce((a, b) => a.concat(b));
207
- if (this.view.state.doc == state.doc &&
208
- (all.length || ((_b = (_a = this.view.state.field(lintState, false)) === null || _a === void 0 ? void 0 : _a.diagnostics) === null || _b === void 0 ? void 0 : _b.size)))
216
+ if (this.view.state.doc == state.doc)
209
217
  this.view.dispatch(setDiagnostics(this.view.state, all));
210
218
  }, error => { view.logException(this.view.state, error); });
211
219
  }
@@ -272,7 +280,7 @@ function assignKeys(actions) {
272
280
  function renderDiagnostic(view, diagnostic, inPanel) {
273
281
  var _a;
274
282
  let keys = inPanel ? assignKeys(diagnostic.actions) : [];
275
- return elt__default['default']("li", { class: "cm-diagnostic cm-diagnostic-" + diagnostic.severity }, elt__default['default']("span", { class: "cm-diagnosticText" }, diagnostic.message), (_a = diagnostic.actions) === null || _a === void 0 ? void 0 : _a.map((action, i) => {
283
+ return elt__default["default"]("li", { class: "cm-diagnostic cm-diagnostic-" + diagnostic.severity }, elt__default["default"]("span", { class: "cm-diagnosticText" }, diagnostic.message), (_a = diagnostic.actions) === null || _a === void 0 ? void 0 : _a.map((action, i) => {
276
284
  let click = (e) => {
277
285
  e.preventDefault();
278
286
  let found = findDiagnostic(view.state.field(lintState).diagnostics, diagnostic);
@@ -281,16 +289,16 @@ function renderDiagnostic(view, diagnostic, inPanel) {
281
289
  };
282
290
  let { name } = action, keyIndex = keys[i] ? name.indexOf(keys[i]) : -1;
283
291
  let nameElt = keyIndex < 0 ? name : [name.slice(0, keyIndex),
284
- elt__default['default']("u", name.slice(keyIndex, keyIndex + 1)),
292
+ elt__default["default"]("u", name.slice(keyIndex, keyIndex + 1)),
285
293
  name.slice(keyIndex + 1)];
286
- return elt__default['default']("button", {
294
+ return elt__default["default"]("button", {
287
295
  type: "button",
288
296
  class: "cm-diagnosticAction",
289
297
  onclick: click,
290
298
  onmousedown: click,
291
299
  "aria-label": ` Action: ${name}${keyIndex < 0 ? "" : ` (access key "${keys[i]})"`}.`
292
300
  }, nameElt);
293
- }), diagnostic.source && elt__default['default']("div", { class: "cm-diagnosticSource" }, diagnostic.source));
301
+ }), diagnostic.source && elt__default["default"]("div", { class: "cm-diagnosticSource" }, diagnostic.source));
294
302
  }
295
303
  class DiagnosticWidget extends view.WidgetType {
296
304
  constructor(diagnostic) {
@@ -299,7 +307,7 @@ class DiagnosticWidget extends view.WidgetType {
299
307
  }
300
308
  eq(other) { return other.diagnostic == this.diagnostic; }
301
309
  toDOM() {
302
- return elt__default['default']("span", { class: "cm-lintPoint cm-lintPoint-" + this.diagnostic.severity });
310
+ return elt__default["default"]("span", { class: "cm-lintPoint cm-lintPoint-" + this.diagnostic.severity });
303
311
  }
304
312
  }
305
313
  class PanelItem {
@@ -355,14 +363,14 @@ class LintPanel {
355
363
  this.moveSelection(i);
356
364
  }
357
365
  };
358
- this.list = elt__default['default']("ul", {
366
+ this.list = elt__default["default"]("ul", {
359
367
  tabIndex: 0,
360
368
  role: "listbox",
361
369
  "aria-label": this.view.state.phrase("Diagnostics"),
362
370
  onkeydown,
363
371
  onclick
364
372
  });
365
- this.dom = elt__default['default']("div", { class: "cm-panel-lint" }, this.list, elt__default['default']("button", {
373
+ this.dom = elt__default["default"]("div", { class: "cm-panel-lint" }, this.list, elt__default["default"]("button", {
366
374
  type: "button",
367
375
  name: "close",
368
376
  "aria-label": this.view.state.phrase("close"),
@@ -478,13 +486,11 @@ class LintPanel {
478
486
  }
479
487
  static open(view) { return new LintPanel(view); }
480
488
  }
489
+ function svg(content, attrs = `viewBox="0 0 40 40"`) {
490
+ return `url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" ${attrs}>${encodeURIComponent(content)}</svg>')`;
491
+ }
481
492
  function underline(color) {
482
- if (typeof btoa != "function")
483
- return "none";
484
- let svg = `<svg xmlns="http://www.w3.org/2000/svg" width="6" height="3">
485
- <path d="m0 3 l2 -2 l1 0 l2 2 l1 0" stroke="${color}" fill="none" stroke-width=".7"/>
486
- </svg>`;
487
- return `url('data:image/svg+xml;base64,${btoa(svg)}')`;
493
+ 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"`);
488
494
  }
489
495
  const baseTheme = view.EditorView.baseTheme({
490
496
  ".cm-diagnostic": {
@@ -511,7 +517,8 @@ const baseTheme = view.EditorView.baseTheme({
511
517
  },
512
518
  ".cm-lintRange": {
513
519
  backgroundPosition: "left bottom",
514
- backgroundRepeat: "repeat-x"
520
+ backgroundRepeat: "repeat-x",
521
+ paddingBottom: "0.7px",
515
522
  },
516
523
  ".cm-lintRange-error": { backgroundImage: underline("#d11") },
517
524
  ".cm-lintRange-warning": { backgroundImage: underline("orange") },
@@ -570,12 +577,151 @@ const baseTheme = view.EditorView.baseTheme({
570
577
  }
571
578
  }
572
579
  });
580
+ class LintGutterMarker extends gutter.GutterMarker {
581
+ constructor(diagnostics) {
582
+ super();
583
+ this.diagnostics = diagnostics;
584
+ this.severity = diagnostics.reduce((max, d) => {
585
+ let s = d.severity;
586
+ return s == "error" || s == "warning" && max == "info" ? s : max;
587
+ }, "info");
588
+ }
589
+ toDOM(view) {
590
+ let elt = document.createElement("div");
591
+ elt.className = "cm-lint-marker cm-lint-marker-" + this.severity;
592
+ elt.onmouseover = () => gutterMarkerMouseOver(view, elt, this.diagnostics);
593
+ return elt;
594
+ }
595
+ }
596
+ function trackHoverOn(view, marker) {
597
+ let mousemove = (event) => {
598
+ let rect = marker.getBoundingClientRect();
599
+ if (event.clientX > rect.left - 10 /* Margin */ && event.clientX < rect.right + 10 /* Margin */ &&
600
+ event.clientY > rect.top - 10 /* Margin */ && event.clientY < rect.bottom + 10 /* Margin */)
601
+ return;
602
+ for (let target = event.target; target; target = target.parentNode) {
603
+ if (target.nodeType == 1 && target.classList.contains("cm-tooltip-lint"))
604
+ return;
605
+ }
606
+ window.removeEventListener("mousemove", mousemove);
607
+ if (view.state.field(lintGutterTooltip))
608
+ view.dispatch({ effects: setLintGutterTooltip.of(null) });
609
+ };
610
+ window.addEventListener("mousemove", mousemove);
611
+ }
612
+ function gutterMarkerMouseOver(view, marker, diagnostics) {
613
+ function hovered() {
614
+ let line = view.visualLineAtHeight(marker.getBoundingClientRect().top + 5 - view.documentTop);
615
+ const linePos = view.coordsAtPos(line.from);
616
+ if (linePos) {
617
+ view.dispatch({ effects: setLintGutterTooltip.of({
618
+ pos: line.from,
619
+ above: false,
620
+ create() {
621
+ return {
622
+ dom: diagnosticsTooltip(view, diagnostics),
623
+ getCoords: () => marker.getBoundingClientRect()
624
+ };
625
+ }
626
+ }) });
627
+ }
628
+ marker.onmouseout = marker.onmousemove = null;
629
+ trackHoverOn(view, marker);
630
+ }
631
+ let { hoverTime } = view.state.facet(lintGutterConfig);
632
+ let hoverTimeout = setTimeout(hovered, hoverTime);
633
+ marker.onmouseout = () => {
634
+ clearTimeout(hoverTimeout);
635
+ marker.onmouseout = marker.onmousemove = null;
636
+ };
637
+ marker.onmousemove = () => {
638
+ clearTimeout(hoverTimeout);
639
+ hoverTimeout = setTimeout(hovered, hoverTime);
640
+ };
641
+ }
642
+ function markersForDiagnostics(doc, diagnostics) {
643
+ let byLine = Object.create(null);
644
+ for (let diagnostic of diagnostics) {
645
+ let line = doc.lineAt(diagnostic.from);
646
+ (byLine[line.from] || (byLine[line.from] = [])).push(diagnostic);
647
+ }
648
+ let markers = [];
649
+ for (let line in byLine) {
650
+ markers.push(new LintGutterMarker(byLine[line]).range(+line));
651
+ }
652
+ return rangeset.RangeSet.of(markers, true);
653
+ }
654
+ const lintGutterExtension = gutter.gutter({
655
+ class: "cm-gutter-lint",
656
+ markers: view => view.state.field(lintGutterMarkers),
657
+ });
658
+ const lintGutterMarkers = state.StateField.define({
659
+ create() {
660
+ return rangeset.RangeSet.empty;
661
+ },
662
+ update(markers, tr) {
663
+ markers = markers.map(tr.changes);
664
+ for (let effect of tr.effects)
665
+ if (effect.is(setDiagnosticsEffect)) {
666
+ markers = markersForDiagnostics(tr.state.doc, effect.value);
667
+ }
668
+ return markers;
669
+ }
670
+ });
671
+ const setLintGutterTooltip = state.StateEffect.define();
672
+ const lintGutterTooltip = state.StateField.define({
673
+ create() { return null; },
674
+ update(tooltip, tr) {
675
+ if (tooltip && tr.docChanged)
676
+ tooltip = Object.assign(Object.assign({}, tooltip), { pos: tr.changes.mapPos(tooltip.pos) });
677
+ return tr.effects.reduce((t, e) => e.is(setLintGutterTooltip) ? e.value : t, tooltip);
678
+ },
679
+ provide: field => tooltip.showTooltip.from(field)
680
+ });
681
+ const lintGutterTheme = view.EditorView.baseTheme({
682
+ ".cm-gutter-lint": {
683
+ width: "1.4em",
684
+ "& .cm-gutterElement": {
685
+ padding: ".2em"
686
+ }
687
+ },
688
+ ".cm-lint-marker": {
689
+ width: "1em",
690
+ height: "1em"
691
+ },
692
+ ".cm-lint-marker-info": {
693
+ content: svg(`<path fill="#aaf" stroke="#77e" stroke-width="6" stroke-linejoin="round" d="M5 5L35 5L35 35L5 35Z"/>`)
694
+ },
695
+ ".cm-lint-marker-warning": {
696
+ content: svg(`<path fill="#fe8" stroke="#fd7" stroke-width="6" stroke-linejoin="round" d="M20 6L37 35L3 35Z"/>`),
697
+ },
698
+ ".cm-lint-marker-error:before": {
699
+ content: svg(`<circle cx="20" cy="20" r="15" fill="#f87" stroke="#f43" stroke-width="6"/>`)
700
+ },
701
+ });
702
+ const lintGutterConfig = state.Facet.define({
703
+ combine(configs) {
704
+ return state.combineConfig(configs, {
705
+ hoverTime: 300 /* Time */,
706
+ });
707
+ }
708
+ });
709
+ /**
710
+ Returns an extension that installs a gutter showing markers for
711
+ each line that has diagnostics, which can be hovered over to see
712
+ the diagnostics.
713
+ */
714
+ function lintGutter(config = {}) {
715
+ return [lintGutterConfig.of(config), lintGutterMarkers, lintGutterExtension, lintGutterTheme, lintGutterTooltip];
716
+ }
573
717
 
574
718
  exports.closeLintPanel = closeLintPanel;
575
719
  exports.diagnosticCount = diagnosticCount;
576
720
  exports.forceLinting = forceLinting;
721
+ exports.lintGutter = lintGutter;
577
722
  exports.lintKeymap = lintKeymap;
578
723
  exports.linter = linter;
579
724
  exports.nextDiagnostic = nextDiagnostic;
580
725
  exports.openLintPanel = openLintPanel;
581
726
  exports.setDiagnostics = setDiagnostics;
727
+ 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.
@@ -50,12 +51,24 @@ interface Action {
50
51
  */
51
52
  apply: (view: EditorView, from: number, to: number) => void;
52
53
  }
54
+ interface LintGutterConfig {
55
+ /**
56
+ The delay before showing a tooltip when hovering over a lint gutter marker.
57
+ */
58
+ hoverTime?: number;
59
+ }
53
60
  /**
54
61
  Returns a transaction spec which updates the current set of
55
- diagnostics.
62
+ diagnostics, and enables the lint extension if if wasn't already
63
+ active.
56
64
  */
57
65
  declare function setDiagnostics(state: EditorState, diagnostics: readonly Diagnostic[]): TransactionSpec;
58
66
  /**
67
+ The state effect that updates the set of active diagnostics. Can
68
+ be useful when writing an extension that needs to track these.
69
+ */
70
+ declare const setDiagnosticsEffect: _codemirror_state.StateEffectType<readonly Diagnostic[]>;
71
+ /**
59
72
  Returns the number of active lint diagnostics in the given state.
60
73
  */
61
74
  declare function diagnosticCount(state: EditorState): number;
@@ -96,5 +109,11 @@ Forces any linters [configured](https://codemirror.net/6/docs/ref/#lint.linter)
96
109
  editor is idle to run right away.
97
110
  */
98
111
  declare function forceLinting(view: EditorView): void;
112
+ /**
113
+ Returns an extension that installs a gutter showing markers for
114
+ each line that has diagnostics, which can be hovered over to see
115
+ the diagnostics.
116
+ */
117
+ declare function lintGutter(config?: LintGutterConfig): Extension;
99
118
 
100
- export { Action, Diagnostic, closeLintPanel, diagnosticCount, forceLinting, lintKeymap, linter, nextDiagnostic, openLintPanel, setDiagnostics };
119
+ export { Action, Diagnostic, closeLintPanel, diagnosticCount, forceLinting, lintGutter, lintKeymap, linter, nextDiagnostic, openLintPanel, setDiagnostics, setDiagnosticsEffect };
package/dist/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import { Decoration, EditorView, ViewPlugin, logException, WidgetType } from '@codemirror/view';
2
- import { StateEffect, StateField, Facet } from '@codemirror/state';
3
- import { hoverTooltip } from '@codemirror/tooltip';
2
+ import { StateEffect, StateField, Facet, combineConfig } from '@codemirror/state';
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 {
@@ -43,9 +45,9 @@ function findDiagnostic(diagnostics, diagnostic = null, after = 0) {
43
45
  });
44
46
  return found;
45
47
  }
46
- function maybeEnableLint(state, effects, getState) {
48
+ function maybeEnableLint(state, effects) {
47
49
  return state.field(lintState, false) ? effects : effects.concat(StateEffect.appendConfig.of([
48
- lintState.init(getState),
50
+ lintState,
49
51
  EditorView.decorations.compute([lintState], state => {
50
52
  let { selected, panel } = state.field(lintState);
51
53
  return !selected || !panel || selected.from == selected.to ? Decoration.none : Decoration.set([
@@ -58,13 +60,18 @@ function maybeEnableLint(state, effects, getState) {
58
60
  }
59
61
  /**
60
62
  Returns a transaction spec which updates the current set of
61
- diagnostics.
63
+ diagnostics, and enables the lint extension if if wasn't already
64
+ active.
62
65
  */
63
66
  function setDiagnostics(state, diagnostics) {
64
67
  return {
65
- effects: maybeEnableLint(state, [setDiagnosticsEffect.of(diagnostics)], () => LintState.init(diagnostics, null, state))
68
+ effects: maybeEnableLint(state, [setDiagnosticsEffect.of(diagnostics)])
66
69
  };
67
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
+ */
68
75
  const setDiagnosticsEffect = /*@__PURE__*/StateEffect.define();
69
76
  const togglePanel = /*@__PURE__*/StateEffect.define();
70
77
  const movePanelSelection = /*@__PURE__*/StateEffect.define();
@@ -123,17 +130,20 @@ function lintTooltip(view, pos, side) {
123
130
  end: stackEnd,
124
131
  above: view.state.doc.lineAt(stackStart).to < stackEnd,
125
132
  create() {
126
- return { dom: elt("ul", { class: "cm-tooltip-lint" }, found.map(d => renderDiagnostic(view, d, false))) };
133
+ return { dom: diagnosticsTooltip(view, found) };
127
134
  }
128
135
  };
129
136
  }
137
+ function diagnosticsTooltip(view, diagnostics) {
138
+ return elt("ul", { class: "cm-tooltip-lint" }, diagnostics.map(d => renderDiagnostic(view, d, false)));
139
+ }
130
140
  /**
131
141
  Command to open and focus the lint panel.
132
142
  */
133
143
  const openLintPanel = (view) => {
134
144
  let field = view.state.field(lintState, false);
135
145
  if (!field || !field.panel)
136
- view.dispatch({ effects: maybeEnableLint(view.state, [togglePanel.of(true)], () => LintState.init([], LintPanel.open, view.state)) });
146
+ view.dispatch({ effects: maybeEnableLint(view.state, [togglePanel.of(true)]) });
137
147
  let panel = getPanel(view, LintPanel.open);
138
148
  if (panel)
139
149
  panel.dom.querySelector(".cm-panel-lint ul").focus();
@@ -194,10 +204,8 @@ const lintPlugin = /*@__PURE__*/ViewPlugin.fromClass(class {
194
204
  this.set = false;
195
205
  let { state } = this.view, { sources } = state.facet(lintSource);
196
206
  Promise.all(sources.map(source => Promise.resolve(source(this.view)))).then(annotations => {
197
- var _a, _b;
198
207
  let all = annotations.reduce((a, b) => a.concat(b));
199
- if (this.view.state.doc == state.doc &&
200
- (all.length || ((_b = (_a = this.view.state.field(lintState, false)) === null || _a === void 0 ? void 0 : _a.diagnostics) === null || _b === void 0 ? void 0 : _b.size)))
208
+ if (this.view.state.doc == state.doc)
201
209
  this.view.dispatch(setDiagnostics(this.view.state, all));
202
210
  }, error => { logException(this.view.state, error); });
203
211
  }
@@ -470,13 +478,11 @@ class LintPanel {
470
478
  }
471
479
  static open(view) { return new LintPanel(view); }
472
480
  }
481
+ function svg(content, attrs = `viewBox="0 0 40 40"`) {
482
+ return `url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" ${attrs}>${encodeURIComponent(content)}</svg>')`;
483
+ }
473
484
  function underline(color) {
474
- if (typeof btoa != "function")
475
- return "none";
476
- let svg = `<svg xmlns="http://www.w3.org/2000/svg" width="6" height="3">
477
- <path d="m0 3 l2 -2 l1 0 l2 2 l1 0" stroke="${color}" fill="none" stroke-width=".7"/>
478
- </svg>`;
479
- return `url('data:image/svg+xml;base64,${btoa(svg)}')`;
485
+ 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"`);
480
486
  }
481
487
  const baseTheme = /*@__PURE__*/EditorView.baseTheme({
482
488
  ".cm-diagnostic": {
@@ -503,7 +509,8 @@ const baseTheme = /*@__PURE__*/EditorView.baseTheme({
503
509
  },
504
510
  ".cm-lintRange": {
505
511
  backgroundPosition: "left bottom",
506
- backgroundRepeat: "repeat-x"
512
+ backgroundRepeat: "repeat-x",
513
+ paddingBottom: "0.7px",
507
514
  },
508
515
  ".cm-lintRange-error": { backgroundImage: /*@__PURE__*/underline("#d11") },
509
516
  ".cm-lintRange-warning": { backgroundImage: /*@__PURE__*/underline("orange") },
@@ -562,5 +569,142 @@ const baseTheme = /*@__PURE__*/EditorView.baseTheme({
562
569
  }
563
570
  }
564
571
  });
572
+ class LintGutterMarker extends GutterMarker {
573
+ constructor(diagnostics) {
574
+ super();
575
+ this.diagnostics = diagnostics;
576
+ this.severity = diagnostics.reduce((max, d) => {
577
+ let s = d.severity;
578
+ return s == "error" || s == "warning" && max == "info" ? s : max;
579
+ }, "info");
580
+ }
581
+ toDOM(view) {
582
+ let elt = document.createElement("div");
583
+ elt.className = "cm-lint-marker cm-lint-marker-" + this.severity;
584
+ elt.onmouseover = () => gutterMarkerMouseOver(view, elt, this.diagnostics);
585
+ return elt;
586
+ }
587
+ }
588
+ function trackHoverOn(view, marker) {
589
+ let mousemove = (event) => {
590
+ let rect = marker.getBoundingClientRect();
591
+ if (event.clientX > rect.left - 10 /* Margin */ && event.clientX < rect.right + 10 /* Margin */ &&
592
+ event.clientY > rect.top - 10 /* Margin */ && event.clientY < rect.bottom + 10 /* Margin */)
593
+ return;
594
+ for (let target = event.target; target; target = target.parentNode) {
595
+ if (target.nodeType == 1 && target.classList.contains("cm-tooltip-lint"))
596
+ return;
597
+ }
598
+ window.removeEventListener("mousemove", mousemove);
599
+ if (view.state.field(lintGutterTooltip))
600
+ view.dispatch({ effects: setLintGutterTooltip.of(null) });
601
+ };
602
+ window.addEventListener("mousemove", mousemove);
603
+ }
604
+ function gutterMarkerMouseOver(view, marker, diagnostics) {
605
+ function hovered() {
606
+ let line = view.visualLineAtHeight(marker.getBoundingClientRect().top + 5 - view.documentTop);
607
+ const linePos = view.coordsAtPos(line.from);
608
+ if (linePos) {
609
+ view.dispatch({ effects: setLintGutterTooltip.of({
610
+ pos: line.from,
611
+ above: false,
612
+ create() {
613
+ return {
614
+ dom: diagnosticsTooltip(view, diagnostics),
615
+ getCoords: () => marker.getBoundingClientRect()
616
+ };
617
+ }
618
+ }) });
619
+ }
620
+ marker.onmouseout = marker.onmousemove = null;
621
+ trackHoverOn(view, marker);
622
+ }
623
+ let { hoverTime } = view.state.facet(lintGutterConfig);
624
+ let hoverTimeout = setTimeout(hovered, hoverTime);
625
+ marker.onmouseout = () => {
626
+ clearTimeout(hoverTimeout);
627
+ marker.onmouseout = marker.onmousemove = null;
628
+ };
629
+ marker.onmousemove = () => {
630
+ clearTimeout(hoverTimeout);
631
+ hoverTimeout = setTimeout(hovered, hoverTime);
632
+ };
633
+ }
634
+ function markersForDiagnostics(doc, diagnostics) {
635
+ let byLine = Object.create(null);
636
+ for (let diagnostic of diagnostics) {
637
+ let line = doc.lineAt(diagnostic.from);
638
+ (byLine[line.from] || (byLine[line.from] = [])).push(diagnostic);
639
+ }
640
+ let markers = [];
641
+ for (let line in byLine) {
642
+ markers.push(new LintGutterMarker(byLine[line]).range(+line));
643
+ }
644
+ return RangeSet.of(markers, true);
645
+ }
646
+ const lintGutterExtension = /*@__PURE__*/gutter({
647
+ class: "cm-gutter-lint",
648
+ markers: view => view.state.field(lintGutterMarkers),
649
+ });
650
+ const lintGutterMarkers = /*@__PURE__*/StateField.define({
651
+ create() {
652
+ return RangeSet.empty;
653
+ },
654
+ update(markers, tr) {
655
+ markers = markers.map(tr.changes);
656
+ for (let effect of tr.effects)
657
+ if (effect.is(setDiagnosticsEffect)) {
658
+ markers = markersForDiagnostics(tr.state.doc, effect.value);
659
+ }
660
+ return markers;
661
+ }
662
+ });
663
+ const setLintGutterTooltip = /*@__PURE__*/StateEffect.define();
664
+ const lintGutterTooltip = /*@__PURE__*/StateField.define({
665
+ create() { return null; },
666
+ update(tooltip, tr) {
667
+ if (tooltip && tr.docChanged)
668
+ tooltip = Object.assign(Object.assign({}, tooltip), { pos: tr.changes.mapPos(tooltip.pos) });
669
+ return tr.effects.reduce((t, e) => e.is(setLintGutterTooltip) ? e.value : t, tooltip);
670
+ },
671
+ provide: field => showTooltip.from(field)
672
+ });
673
+ const lintGutterTheme = /*@__PURE__*/EditorView.baseTheme({
674
+ ".cm-gutter-lint": {
675
+ width: "1.4em",
676
+ "& .cm-gutterElement": {
677
+ padding: ".2em"
678
+ }
679
+ },
680
+ ".cm-lint-marker": {
681
+ width: "1em",
682
+ height: "1em"
683
+ },
684
+ ".cm-lint-marker-info": {
685
+ content: /*@__PURE__*/svg(`<path fill="#aaf" stroke="#77e" stroke-width="6" stroke-linejoin="round" d="M5 5L35 5L35 35L5 35Z"/>`)
686
+ },
687
+ ".cm-lint-marker-warning": {
688
+ content: /*@__PURE__*/svg(`<path fill="#fe8" stroke="#fd7" stroke-width="6" stroke-linejoin="round" d="M20 6L37 35L3 35Z"/>`),
689
+ },
690
+ ".cm-lint-marker-error:before": {
691
+ content: /*@__PURE__*/svg(`<circle cx="20" cy="20" r="15" fill="#f87" stroke="#f43" stroke-width="6"/>`)
692
+ },
693
+ });
694
+ const lintGutterConfig = /*@__PURE__*/Facet.define({
695
+ combine(configs) {
696
+ return combineConfig(configs, {
697
+ hoverTime: 300 /* Time */,
698
+ });
699
+ }
700
+ });
701
+ /**
702
+ Returns an extension that installs a gutter showing markers for
703
+ each line that has diagnostics, which can be hovered over to see
704
+ the diagnostics.
705
+ */
706
+ function lintGutter(config = {}) {
707
+ return [lintGutterConfig.of(config), lintGutterMarkers, lintGutterExtension, lintGutterTheme, lintGutterTooltip];
708
+ }
565
709
 
566
- export { closeLintPanel, diagnosticCount, forceLinting, lintKeymap, linter, nextDiagnostic, openLintPanel, setDiagnostics };
710
+ 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.19.2",
3
+ "version": "0.19.5",
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/gutter": "^0.19.4",
29
30
  "@codemirror/panel": "^0.19.0",
30
- "@codemirror/state": "^0.19.0",
31
- "@codemirror/tooltip": "^0.19.0",
32
- "@codemirror/view": "^0.19.0",
31
+ "@codemirror/rangeset": "^0.19.1",
32
+ "@codemirror/state": "^0.19.4",
33
+ "@codemirror/tooltip": "^0.19.16",
34
+ "@codemirror/view": "^0.19.22",
33
35
  "crelt": "^1.0.5"
34
36
  },
35
37
  "devDependencies": {