index.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732
  1. import { Decoration, showPanel, EditorView, ViewPlugin, hoverTooltip, logException, gutter, showTooltip, getPanel, WidgetType, GutterMarker } from '@codemirror/view';
  2. import { StateEffect, StateField, Facet, combineConfig, RangeSet } from '@codemirror/state';
  3. import elt from 'crelt';
  4. class SelectedDiagnostic {
  5. constructor(from, to, diagnostic) {
  6. this.from = from;
  7. this.to = to;
  8. this.diagnostic = diagnostic;
  9. }
  10. }
  11. class LintState {
  12. constructor(diagnostics, panel, selected) {
  13. this.diagnostics = diagnostics;
  14. this.panel = panel;
  15. this.selected = selected;
  16. }
  17. static init(diagnostics, panel, state) {
  18. // Filter the list of diagnostics for which to create markers
  19. let markedDiagnostics = diagnostics;
  20. let diagnosticFilter = state.facet(lintConfig).markerFilter;
  21. if (diagnosticFilter)
  22. markedDiagnostics = diagnosticFilter(markedDiagnostics);
  23. let ranges = Decoration.set(markedDiagnostics.map((d) => {
  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({
  27. widget: new DiagnosticWidget(d),
  28. diagnostic: d
  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);
  34. }), true);
  35. return new LintState(ranges, panel, findDiagnostic(ranges));
  36. }
  37. }
  38. function findDiagnostic(diagnostics, diagnostic = null, after = 0) {
  39. let found = null;
  40. diagnostics.between(after, 1e9, (from, to, { spec }) => {
  41. if (diagnostic && spec.diagnostic != diagnostic)
  42. return;
  43. found = new SelectedDiagnostic(from, to, spec.diagnostic);
  44. return false;
  45. });
  46. return found;
  47. }
  48. function hideTooltip(tr, tooltip) {
  49. return !!(tr.effects.some(e => e.is(setDiagnosticsEffect)) || tr.changes.touchesRange(tooltip.pos));
  50. }
  51. function maybeEnableLint(state, effects) {
  52. return state.field(lintState, false) ? effects : effects.concat(StateEffect.appendConfig.of([
  53. lintState,
  54. EditorView.decorations.compute([lintState], state => {
  55. let { selected, panel } = state.field(lintState);
  56. return !selected || !panel || selected.from == selected.to ? Decoration.none : Decoration.set([
  57. activeMark.range(selected.from, selected.to)
  58. ]);
  59. }),
  60. hoverTooltip(lintTooltip, { hideOn: hideTooltip }),
  61. baseTheme
  62. ]));
  63. }
  64. /**
  65. Returns a transaction spec which updates the current set of
  66. diagnostics, and enables the lint extension if if wasn't already
  67. active.
  68. */
  69. function setDiagnostics(state, diagnostics) {
  70. return {
  71. effects: maybeEnableLint(state, [setDiagnosticsEffect.of(diagnostics)])
  72. };
  73. }
  74. /**
  75. The state effect that updates the set of active diagnostics. Can
  76. be useful when writing an extension that needs to track these.
  77. */
  78. const setDiagnosticsEffect = /*@__PURE__*/StateEffect.define();
  79. const togglePanel = /*@__PURE__*/StateEffect.define();
  80. const movePanelSelection = /*@__PURE__*/StateEffect.define();
  81. const lintState = /*@__PURE__*/StateField.define({
  82. create() {
  83. return new LintState(Decoration.none, null, null);
  84. },
  85. update(value, tr) {
  86. if (tr.docChanged) {
  87. let mapped = value.diagnostics.map(tr.changes), selected = null;
  88. if (value.selected) {
  89. let selPos = tr.changes.mapPos(value.selected.from, 1);
  90. selected = findDiagnostic(mapped, value.selected.diagnostic, selPos) || findDiagnostic(mapped, null, selPos);
  91. }
  92. value = new LintState(mapped, value.panel, selected);
  93. }
  94. for (let effect of tr.effects) {
  95. if (effect.is(setDiagnosticsEffect)) {
  96. value = LintState.init(effect.value, value.panel, tr.state);
  97. }
  98. else if (effect.is(togglePanel)) {
  99. value = new LintState(value.diagnostics, effect.value ? LintPanel.open : null, value.selected);
  100. }
  101. else if (effect.is(movePanelSelection)) {
  102. value = new LintState(value.diagnostics, value.panel, effect.value);
  103. }
  104. }
  105. return value;
  106. },
  107. provide: f => [showPanel.from(f, val => val.panel),
  108. EditorView.decorations.from(f, s => s.diagnostics)]
  109. });
  110. /**
  111. Returns the number of active lint diagnostics in the given state.
  112. */
  113. function diagnosticCount(state) {
  114. let lint = state.field(lintState, false);
  115. return lint ? lint.diagnostics.size : 0;
  116. }
  117. const activeMark = /*@__PURE__*/Decoration.mark({ class: "cm-lintRange cm-lintRange-active" });
  118. function lintTooltip(view, pos, side) {
  119. let { diagnostics } = view.state.field(lintState);
  120. let found = [], stackStart = 2e8, stackEnd = 0;
  121. diagnostics.between(pos - (side < 0 ? 1 : 0), pos + (side > 0 ? 1 : 0), (from, to, { spec }) => {
  122. if (pos >= from && pos <= to &&
  123. (from == to || ((pos > from || side > 0) && (pos < to || side < 0)))) {
  124. found.push(spec.diagnostic);
  125. stackStart = Math.min(from, stackStart);
  126. stackEnd = Math.max(to, stackEnd);
  127. }
  128. });
  129. let diagnosticFilter = view.state.facet(lintConfig).tooltipFilter;
  130. if (diagnosticFilter)
  131. found = diagnosticFilter(found);
  132. if (!found.length)
  133. return null;
  134. return {
  135. pos: stackStart,
  136. end: stackEnd,
  137. above: view.state.doc.lineAt(stackStart).to < stackEnd,
  138. create() {
  139. return { dom: diagnosticsTooltip(view, found) };
  140. }
  141. };
  142. }
  143. function diagnosticsTooltip(view, diagnostics) {
  144. return elt("ul", { class: "cm-tooltip-lint" }, diagnostics.map(d => renderDiagnostic(view, d, false)));
  145. }
  146. /**
  147. Command to open and focus the lint panel.
  148. */
  149. const openLintPanel = (view) => {
  150. let field = view.state.field(lintState, false);
  151. if (!field || !field.panel)
  152. view.dispatch({ effects: maybeEnableLint(view.state, [togglePanel.of(true)]) });
  153. let panel = getPanel(view, LintPanel.open);
  154. if (panel)
  155. panel.dom.querySelector(".cm-panel-lint ul").focus();
  156. return true;
  157. };
  158. /**
  159. Command to close the lint panel, when open.
  160. */
  161. const closeLintPanel = (view) => {
  162. let field = view.state.field(lintState, false);
  163. if (!field || !field.panel)
  164. return false;
  165. view.dispatch({ effects: togglePanel.of(false) });
  166. return true;
  167. };
  168. /**
  169. Move the selection to the next diagnostic.
  170. */
  171. const nextDiagnostic = (view) => {
  172. let field = view.state.field(lintState, false);
  173. if (!field)
  174. return false;
  175. let sel = view.state.selection.main, next = field.diagnostics.iter(sel.to + 1);
  176. if (!next.value) {
  177. next = field.diagnostics.iter(0);
  178. if (!next.value || next.from == sel.from && next.to == sel.to)
  179. return false;
  180. }
  181. view.dispatch({ selection: { anchor: next.from, head: next.to }, scrollIntoView: true });
  182. return true;
  183. };
  184. /**
  185. A set of default key bindings for the lint functionality.
  186. - Ctrl-Shift-m (Cmd-Shift-m on macOS): [`openLintPanel`](https://codemirror.net/6/docs/ref/#lint.openLintPanel)
  187. - F8: [`nextDiagnostic`](https://codemirror.net/6/docs/ref/#lint.nextDiagnostic)
  188. */
  189. const lintKeymap = [
  190. { key: "Mod-Shift-m", run: openLintPanel },
  191. { key: "F8", run: nextDiagnostic }
  192. ];
  193. const lintPlugin = /*@__PURE__*/ViewPlugin.fromClass(class {
  194. constructor(view) {
  195. this.view = view;
  196. this.timeout = -1;
  197. this.set = true;
  198. let { delay } = view.state.facet(lintConfig);
  199. this.lintTime = Date.now() + delay;
  200. this.run = this.run.bind(this);
  201. this.timeout = setTimeout(this.run, delay);
  202. }
  203. run() {
  204. let now = Date.now();
  205. if (now < this.lintTime - 10) {
  206. setTimeout(this.run, this.lintTime - now);
  207. }
  208. else {
  209. this.set = false;
  210. let { state } = this.view, { sources } = state.facet(lintConfig);
  211. Promise.all(sources.map(source => Promise.resolve(source(this.view)))).then(annotations => {
  212. let all = annotations.reduce((a, b) => a.concat(b));
  213. if (this.view.state.doc == state.doc)
  214. this.view.dispatch(setDiagnostics(this.view.state, all));
  215. }, error => { logException(this.view.state, error); });
  216. }
  217. }
  218. update(update) {
  219. let config = update.state.facet(lintConfig);
  220. if (update.docChanged || config != update.startState.facet(lintConfig)) {
  221. this.lintTime = Date.now() + config.delay;
  222. if (!this.set) {
  223. this.set = true;
  224. this.timeout = setTimeout(this.run, config.delay);
  225. }
  226. }
  227. }
  228. force() {
  229. if (this.set) {
  230. this.lintTime = Date.now();
  231. this.run();
  232. }
  233. }
  234. destroy() {
  235. clearTimeout(this.timeout);
  236. }
  237. });
  238. const lintConfig = /*@__PURE__*/Facet.define({
  239. combine(input) {
  240. return Object.assign({ sources: input.map(i => i.source) }, combineConfig(input.map(i => i.config), {
  241. delay: 750,
  242. markerFilter: null,
  243. tooltipFilter: null
  244. }));
  245. },
  246. enables: lintPlugin
  247. });
  248. /**
  249. Given a diagnostic source, this function returns an extension that
  250. enables linting with that source. It will be called whenever the
  251. editor is idle (after its content changed).
  252. */
  253. function linter(source, config = {}) {
  254. return lintConfig.of({ source, config });
  255. }
  256. /**
  257. Forces any linters [configured](https://codemirror.net/6/docs/ref/#lint.linter) to run when the
  258. editor is idle to run right away.
  259. */
  260. function forceLinting(view) {
  261. let plugin = view.plugin(lintPlugin);
  262. if (plugin)
  263. plugin.force();
  264. }
  265. function assignKeys(actions) {
  266. let assigned = [];
  267. if (actions)
  268. actions: for (let { name } of actions) {
  269. for (let i = 0; i < name.length; i++) {
  270. let ch = name[i];
  271. if (/[a-zA-Z]/.test(ch) && !assigned.some(c => c.toLowerCase() == ch.toLowerCase())) {
  272. assigned.push(ch);
  273. continue actions;
  274. }
  275. }
  276. assigned.push("");
  277. }
  278. return assigned;
  279. }
  280. function renderDiagnostic(view, diagnostic, inPanel) {
  281. var _a;
  282. let keys = inPanel ? assignKeys(diagnostic.actions) : [];
  283. return elt("li", { class: "cm-diagnostic cm-diagnostic-" + diagnostic.severity }, elt("span", { class: "cm-diagnosticText" }, diagnostic.renderMessage ? diagnostic.renderMessage() : diagnostic.message), (_a = diagnostic.actions) === null || _a === void 0 ? void 0 : _a.map((action, i) => {
  284. let click = (e) => {
  285. e.preventDefault();
  286. let found = findDiagnostic(view.state.field(lintState).diagnostics, diagnostic);
  287. if (found)
  288. action.apply(view, found.from, found.to);
  289. };
  290. let { name } = action, keyIndex = keys[i] ? name.indexOf(keys[i]) : -1;
  291. let nameElt = keyIndex < 0 ? name : [name.slice(0, keyIndex),
  292. elt("u", name.slice(keyIndex, keyIndex + 1)),
  293. name.slice(keyIndex + 1)];
  294. return elt("button", {
  295. type: "button",
  296. class: "cm-diagnosticAction",
  297. onclick: click,
  298. onmousedown: click,
  299. "aria-label": ` Action: ${name}${keyIndex < 0 ? "" : ` (access key "${keys[i]})"`}.`
  300. }, nameElt);
  301. }), diagnostic.source && elt("div", { class: "cm-diagnosticSource" }, diagnostic.source));
  302. }
  303. class DiagnosticWidget extends WidgetType {
  304. constructor(diagnostic) {
  305. super();
  306. this.diagnostic = diagnostic;
  307. }
  308. eq(other) { return other.diagnostic == this.diagnostic; }
  309. toDOM() {
  310. return elt("span", { class: "cm-lintPoint cm-lintPoint-" + this.diagnostic.severity });
  311. }
  312. }
  313. class PanelItem {
  314. constructor(view, diagnostic) {
  315. this.diagnostic = diagnostic;
  316. this.id = "item_" + Math.floor(Math.random() * 0xffffffff).toString(16);
  317. this.dom = renderDiagnostic(view, diagnostic, true);
  318. this.dom.id = this.id;
  319. this.dom.setAttribute("role", "option");
  320. }
  321. }
  322. class LintPanel {
  323. constructor(view) {
  324. this.view = view;
  325. this.items = [];
  326. let onkeydown = (event) => {
  327. if (event.keyCode == 27) { // Escape
  328. closeLintPanel(this.view);
  329. this.view.focus();
  330. }
  331. else if (event.keyCode == 38 || event.keyCode == 33) { // ArrowUp, PageUp
  332. this.moveSelection((this.selectedIndex - 1 + this.items.length) % this.items.length);
  333. }
  334. else if (event.keyCode == 40 || event.keyCode == 34) { // ArrowDown, PageDown
  335. this.moveSelection((this.selectedIndex + 1) % this.items.length);
  336. }
  337. else if (event.keyCode == 36) { // Home
  338. this.moveSelection(0);
  339. }
  340. else if (event.keyCode == 35) { // End
  341. this.moveSelection(this.items.length - 1);
  342. }
  343. else if (event.keyCode == 13) { // Enter
  344. this.view.focus();
  345. }
  346. else if (event.keyCode >= 65 && event.keyCode <= 90 && this.selectedIndex >= 0) { // A-Z
  347. let { diagnostic } = this.items[this.selectedIndex], keys = assignKeys(diagnostic.actions);
  348. for (let i = 0; i < keys.length; i++)
  349. if (keys[i].toUpperCase().charCodeAt(0) == event.keyCode) {
  350. let found = findDiagnostic(this.view.state.field(lintState).diagnostics, diagnostic);
  351. if (found)
  352. diagnostic.actions[i].apply(view, found.from, found.to);
  353. }
  354. }
  355. else {
  356. return;
  357. }
  358. event.preventDefault();
  359. };
  360. let onclick = (event) => {
  361. for (let i = 0; i < this.items.length; i++) {
  362. if (this.items[i].dom.contains(event.target))
  363. this.moveSelection(i);
  364. }
  365. };
  366. this.list = elt("ul", {
  367. tabIndex: 0,
  368. role: "listbox",
  369. "aria-label": this.view.state.phrase("Diagnostics"),
  370. onkeydown,
  371. onclick
  372. });
  373. this.dom = elt("div", { class: "cm-panel-lint" }, this.list, elt("button", {
  374. type: "button",
  375. name: "close",
  376. "aria-label": this.view.state.phrase("close"),
  377. onclick: () => closeLintPanel(this.view)
  378. }, "×"));
  379. this.update();
  380. }
  381. get selectedIndex() {
  382. let selected = this.view.state.field(lintState).selected;
  383. if (!selected)
  384. return -1;
  385. for (let i = 0; i < this.items.length; i++)
  386. if (this.items[i].diagnostic == selected.diagnostic)
  387. return i;
  388. return -1;
  389. }
  390. update() {
  391. let { diagnostics, selected } = this.view.state.field(lintState);
  392. let i = 0, needsSync = false, newSelectedItem = null;
  393. diagnostics.between(0, this.view.state.doc.length, (_start, _end, { spec }) => {
  394. let found = -1, item;
  395. for (let j = i; j < this.items.length; j++)
  396. if (this.items[j].diagnostic == spec.diagnostic) {
  397. found = j;
  398. break;
  399. }
  400. if (found < 0) {
  401. item = new PanelItem(this.view, spec.diagnostic);
  402. this.items.splice(i, 0, item);
  403. needsSync = true;
  404. }
  405. else {
  406. item = this.items[found];
  407. if (found > i) {
  408. this.items.splice(i, found - i);
  409. needsSync = true;
  410. }
  411. }
  412. if (selected && item.diagnostic == selected.diagnostic) {
  413. if (!item.dom.hasAttribute("aria-selected")) {
  414. item.dom.setAttribute("aria-selected", "true");
  415. newSelectedItem = item;
  416. }
  417. }
  418. else if (item.dom.hasAttribute("aria-selected")) {
  419. item.dom.removeAttribute("aria-selected");
  420. }
  421. i++;
  422. });
  423. while (i < this.items.length && !(this.items.length == 1 && this.items[0].diagnostic.from < 0)) {
  424. needsSync = true;
  425. this.items.pop();
  426. }
  427. if (this.items.length == 0) {
  428. this.items.push(new PanelItem(this.view, {
  429. from: -1, to: -1,
  430. severity: "info",
  431. message: this.view.state.phrase("No diagnostics")
  432. }));
  433. needsSync = true;
  434. }
  435. if (newSelectedItem) {
  436. this.list.setAttribute("aria-activedescendant", newSelectedItem.id);
  437. this.view.requestMeasure({
  438. key: this,
  439. read: () => ({ sel: newSelectedItem.dom.getBoundingClientRect(), panel: this.list.getBoundingClientRect() }),
  440. write: ({ sel, panel }) => {
  441. if (sel.top < panel.top)
  442. this.list.scrollTop -= panel.top - sel.top;
  443. else if (sel.bottom > panel.bottom)
  444. this.list.scrollTop += sel.bottom - panel.bottom;
  445. }
  446. });
  447. }
  448. else if (this.selectedIndex < 0) {
  449. this.list.removeAttribute("aria-activedescendant");
  450. }
  451. if (needsSync)
  452. this.sync();
  453. }
  454. sync() {
  455. let domPos = this.list.firstChild;
  456. function rm() {
  457. let prev = domPos;
  458. domPos = prev.nextSibling;
  459. prev.remove();
  460. }
  461. for (let item of this.items) {
  462. if (item.dom.parentNode == this.list) {
  463. while (domPos != item.dom)
  464. rm();
  465. domPos = item.dom.nextSibling;
  466. }
  467. else {
  468. this.list.insertBefore(item.dom, domPos);
  469. }
  470. }
  471. while (domPos)
  472. rm();
  473. }
  474. moveSelection(selectedIndex) {
  475. if (this.selectedIndex < 0)
  476. return;
  477. let field = this.view.state.field(lintState);
  478. let selection = findDiagnostic(field.diagnostics, this.items[selectedIndex].diagnostic);
  479. if (!selection)
  480. return;
  481. this.view.dispatch({
  482. selection: { anchor: selection.from, head: selection.to },
  483. scrollIntoView: true,
  484. effects: movePanelSelection.of(selection)
  485. });
  486. }
  487. static open(view) { return new LintPanel(view); }
  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. }
  492. function underline(color) {
  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"`);
  494. }
  495. const baseTheme = /*@__PURE__*/EditorView.baseTheme({
  496. ".cm-diagnostic": {
  497. padding: "3px 6px 3px 8px",
  498. marginLeft: "-1px",
  499. display: "block",
  500. whiteSpace: "pre-wrap"
  501. },
  502. ".cm-diagnostic-error": { borderLeft: "5px solid #d11" },
  503. ".cm-diagnostic-warning": { borderLeft: "5px solid orange" },
  504. ".cm-diagnostic-info": { borderLeft: "5px solid #999" },
  505. ".cm-diagnosticAction": {
  506. font: "inherit",
  507. border: "none",
  508. padding: "2px 4px",
  509. backgroundColor: "#444",
  510. color: "white",
  511. borderRadius: "3px",
  512. marginLeft: "8px"
  513. },
  514. ".cm-diagnosticSource": {
  515. fontSize: "70%",
  516. opacity: .7
  517. },
  518. ".cm-lintRange": {
  519. backgroundPosition: "left bottom",
  520. backgroundRepeat: "repeat-x",
  521. paddingBottom: "0.7px",
  522. },
  523. ".cm-lintRange-error": { backgroundImage: /*@__PURE__*/underline("#d11") },
  524. ".cm-lintRange-warning": { backgroundImage: /*@__PURE__*/underline("orange") },
  525. ".cm-lintRange-info": { backgroundImage: /*@__PURE__*/underline("#999") },
  526. ".cm-lintRange-active": { backgroundColor: "#ffdd9980" },
  527. ".cm-tooltip-lint": {
  528. padding: 0,
  529. margin: 0
  530. },
  531. ".cm-lintPoint": {
  532. position: "relative",
  533. "&:after": {
  534. content: '""',
  535. position: "absolute",
  536. bottom: 0,
  537. left: "-2px",
  538. borderLeft: "3px solid transparent",
  539. borderRight: "3px solid transparent",
  540. borderBottom: "4px solid #d11"
  541. }
  542. },
  543. ".cm-lintPoint-warning": {
  544. "&:after": { borderBottomColor: "orange" }
  545. },
  546. ".cm-lintPoint-info": {
  547. "&:after": { borderBottomColor: "#999" }
  548. },
  549. ".cm-panel.cm-panel-lint": {
  550. position: "relative",
  551. "& ul": {
  552. maxHeight: "100px",
  553. overflowY: "auto",
  554. "& [aria-selected]": {
  555. backgroundColor: "#ddd",
  556. "& u": { textDecoration: "underline" }
  557. },
  558. "&:focus [aria-selected]": {
  559. background_fallback: "#bdf",
  560. backgroundColor: "Highlight",
  561. color_fallback: "white",
  562. color: "HighlightText"
  563. },
  564. "& u": { textDecoration: "none" },
  565. padding: 0,
  566. margin: 0
  567. },
  568. "& [name=close]": {
  569. position: "absolute",
  570. top: "0",
  571. right: "2px",
  572. background: "inherit",
  573. border: "none",
  574. font: "inherit",
  575. padding: 0,
  576. margin: 0
  577. }
  578. }
  579. });
  580. class LintGutterMarker extends 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. let diagnostics = this.diagnostics;
  593. let diagnosticsFilter = view.state.facet(lintGutterConfig).tooltipFilter;
  594. if (diagnosticsFilter)
  595. diagnostics = diagnosticsFilter(diagnostics);
  596. if (diagnostics.length)
  597. elt.onmouseover = () => gutterMarkerMouseOver(view, elt, diagnostics);
  598. return elt;
  599. }
  600. }
  601. function trackHoverOn(view, marker) {
  602. let mousemove = (event) => {
  603. let rect = marker.getBoundingClientRect();
  604. if (event.clientX > rect.left - 10 /* Margin */ && event.clientX < rect.right + 10 /* Margin */ &&
  605. event.clientY > rect.top - 10 /* Margin */ && event.clientY < rect.bottom + 10 /* Margin */)
  606. return;
  607. for (let target = event.target; target; target = target.parentNode) {
  608. if (target.nodeType == 1 && target.classList.contains("cm-tooltip-lint"))
  609. return;
  610. }
  611. window.removeEventListener("mousemove", mousemove);
  612. if (view.state.field(lintGutterTooltip))
  613. view.dispatch({ effects: setLintGutterTooltip.of(null) });
  614. };
  615. window.addEventListener("mousemove", mousemove);
  616. }
  617. function gutterMarkerMouseOver(view, marker, diagnostics) {
  618. function hovered() {
  619. let line = view.elementAtHeight(marker.getBoundingClientRect().top + 5 - view.documentTop);
  620. const linePos = view.coordsAtPos(line.from);
  621. if (linePos) {
  622. view.dispatch({ effects: setLintGutterTooltip.of({
  623. pos: line.from,
  624. above: false,
  625. create() {
  626. return {
  627. dom: diagnosticsTooltip(view, diagnostics),
  628. getCoords: () => marker.getBoundingClientRect()
  629. };
  630. }
  631. }) });
  632. }
  633. marker.onmouseout = marker.onmousemove = null;
  634. trackHoverOn(view, marker);
  635. }
  636. let { hoverTime } = view.state.facet(lintGutterConfig);
  637. let hoverTimeout = setTimeout(hovered, hoverTime);
  638. marker.onmouseout = () => {
  639. clearTimeout(hoverTimeout);
  640. marker.onmouseout = marker.onmousemove = null;
  641. };
  642. marker.onmousemove = () => {
  643. clearTimeout(hoverTimeout);
  644. hoverTimeout = setTimeout(hovered, hoverTime);
  645. };
  646. }
  647. function markersForDiagnostics(doc, diagnostics) {
  648. let byLine = Object.create(null);
  649. for (let diagnostic of diagnostics) {
  650. let line = doc.lineAt(diagnostic.from);
  651. (byLine[line.from] || (byLine[line.from] = [])).push(diagnostic);
  652. }
  653. let markers = [];
  654. for (let line in byLine) {
  655. markers.push(new LintGutterMarker(byLine[line]).range(+line));
  656. }
  657. return RangeSet.of(markers, true);
  658. }
  659. const lintGutterExtension = /*@__PURE__*/gutter({
  660. class: "cm-gutter-lint",
  661. markers: view => view.state.field(lintGutterMarkers),
  662. });
  663. const lintGutterMarkers = /*@__PURE__*/StateField.define({
  664. create() {
  665. return RangeSet.empty;
  666. },
  667. update(markers, tr) {
  668. markers = markers.map(tr.changes);
  669. let diagnosticFilter = tr.state.facet(lintGutterConfig).markerFilter;
  670. for (let effect of tr.effects) {
  671. if (effect.is(setDiagnosticsEffect)) {
  672. let diagnostics = effect.value;
  673. if (diagnosticFilter)
  674. diagnostics = diagnosticFilter(diagnostics || []);
  675. markers = markersForDiagnostics(tr.state.doc, diagnostics.slice(0));
  676. }
  677. }
  678. return markers;
  679. }
  680. });
  681. const setLintGutterTooltip = /*@__PURE__*/StateEffect.define();
  682. const lintGutterTooltip = /*@__PURE__*/StateField.define({
  683. create() { return null; },
  684. update(tooltip, tr) {
  685. if (tooltip && tr.docChanged)
  686. tooltip = hideTooltip(tr, tooltip) ? null : Object.assign(Object.assign({}, tooltip), { pos: tr.changes.mapPos(tooltip.pos) });
  687. return tr.effects.reduce((t, e) => e.is(setLintGutterTooltip) ? e.value : t, tooltip);
  688. },
  689. provide: field => showTooltip.from(field)
  690. });
  691. const lintGutterTheme = /*@__PURE__*/EditorView.baseTheme({
  692. ".cm-gutter-lint": {
  693. width: "1.4em",
  694. "& .cm-gutterElement": {
  695. padding: ".2em"
  696. }
  697. },
  698. ".cm-lint-marker": {
  699. width: "1em",
  700. height: "1em"
  701. },
  702. ".cm-lint-marker-info": {
  703. content: /*@__PURE__*/svg(`<path fill="#aaf" stroke="#77e" stroke-width="6" stroke-linejoin="round" d="M5 5L35 5L35 35L5 35Z"/>`)
  704. },
  705. ".cm-lint-marker-warning": {
  706. content: /*@__PURE__*/svg(`<path fill="#fe8" stroke="#fd7" stroke-width="6" stroke-linejoin="round" d="M20 6L37 35L3 35Z"/>`),
  707. },
  708. ".cm-lint-marker-error:before": {
  709. content: /*@__PURE__*/svg(`<circle cx="20" cy="20" r="15" fill="#f87" stroke="#f43" stroke-width="6"/>`)
  710. },
  711. });
  712. const lintGutterConfig = /*@__PURE__*/Facet.define({
  713. combine(configs) {
  714. return combineConfig(configs, {
  715. hoverTime: 300 /* Time */,
  716. markerFilter: null,
  717. tooltipFilter: null
  718. });
  719. }
  720. });
  721. /**
  722. Returns an extension that installs a gutter showing markers for
  723. each line that has diagnostics, which can be hovered over to see
  724. the diagnostics.
  725. */
  726. function lintGutter(config = {}) {
  727. return [lintGutterConfig.of(config), lintGutterMarkers, lintGutterExtension, lintGutterTheme, lintGutterTooltip];
  728. }
  729. export { closeLintPanel, diagnosticCount, forceLinting, lintGutter, lintKeymap, linter, nextDiagnostic, openLintPanel, setDiagnostics, setDiagnosticsEffect };