index.cjs 28 KB

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