|
| 1 | +import { EditorView, Decoration } from '@codemirror/view'; |
| 2 | +import type { DecorationSet } from '@codemirror/view'; |
| 3 | +import { StateField, StateEffect } from '@codemirror/state'; |
| 4 | +import { SuggestionKind } from 'harper.js'; |
| 5 | +import type { Lint, Suggestion } from 'harper.js'; |
| 6 | + |
| 7 | +export interface Diagnostic { |
| 8 | + from: number; |
| 9 | + to: number; |
| 10 | + title: string; |
| 11 | + message: string; |
| 12 | + actions: DiagnosticAction[]; |
| 13 | +} |
| 14 | + |
| 15 | +export interface DiagnosticAction { |
| 16 | + name: string; |
| 17 | + apply: (view: EditorView, from: number, to: number) => void; |
| 18 | +} |
| 19 | + |
| 20 | +export const setDiagnosticsEffect = StateEffect.define<Diagnostic[]>(); |
| 21 | + |
| 22 | +export const diagnosticsField = StateField.define<{ diagnostics: Diagnostic[]; decorations: DecorationSet }>({ |
| 23 | + create() { |
| 24 | + return { diagnostics: [], decorations: Decoration.none }; |
| 25 | + }, |
| 26 | + update(value, tr) { |
| 27 | + if (tr.docChanged && value.decorations !== Decoration.none) { |
| 28 | + value = { diagnostics: value.diagnostics, decorations: value.decorations.map(tr.changes) }; |
| 29 | + } |
| 30 | + |
| 31 | + for (const effect of tr.effects) { |
| 32 | + if (effect.is(setDiagnosticsEffect)) { |
| 33 | + const diagnostics = effect.value; |
| 34 | + const ranges = diagnostics |
| 35 | + .filter(d => d.from < d.to) |
| 36 | + .map(d => Decoration.mark({ class: 'cm-harper-lint' }).range(d.from, d.to)); |
| 37 | + |
| 38 | + value = { diagnostics, decorations: Decoration.set(ranges, true) }; |
| 39 | + } |
| 40 | + } |
| 41 | + |
| 42 | + return value; |
| 43 | + }, |
| 44 | + provide: f => EditorView.decorations.from(f, val => val.decorations), |
| 45 | +}); |
| 46 | + |
| 47 | +export function lintToDiagnostic(l: Lint): Diagnostic { |
| 48 | + const span = l.span(); |
| 49 | + |
| 50 | + return { |
| 51 | + from: span.start, |
| 52 | + to: span.end, |
| 53 | + title: l.lint_kind_pretty(), |
| 54 | + message: l.message(), |
| 55 | + actions: l.suggestions().map(suggestionToAction), |
| 56 | + }; |
| 57 | +} |
| 58 | + |
| 59 | +function suggestionToAction(sug: Suggestion): DiagnosticAction { |
| 60 | + const kind = sug.kind(); |
| 61 | + const replacement = sug.get_replacement_text(); |
| 62 | + |
| 63 | + let name: string; |
| 64 | + if (kind === SuggestionKind.Remove) { |
| 65 | + name = 'Remove'; |
| 66 | + } else if (kind === SuggestionKind.InsertAfter) { |
| 67 | + name = `Insert "${replacement}"`; |
| 68 | + } else { |
| 69 | + name = replacement; |
| 70 | + } |
| 71 | + |
| 72 | + return { |
| 73 | + name, |
| 74 | + apply(view, from, to) { |
| 75 | + if (kind === SuggestionKind.Remove) { |
| 76 | + view.dispatch({ changes: { from, to, insert: '' }, selection: { anchor: from } }); |
| 77 | + } else if (kind === SuggestionKind.Replace) { |
| 78 | + view.dispatch({ changes: { from, to, insert: replacement }, selection: { anchor: from + replacement.length } }); |
| 79 | + } else if (kind === SuggestionKind.InsertAfter) { |
| 80 | + view.dispatch({ changes: { from: to, to, insert: replacement }, selection: { anchor: to + replacement.length } }); |
| 81 | + } |
| 82 | + }, |
| 83 | + }; |
| 84 | +} |
0 commit comments