@matdata/yasqe 5.20.3 → 6.1.0
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 +1 -1
- package/README.md +76 -59
- package/build/prefixes.json +2169 -2115
- package/build/ts/grammar/tokenizer.d.ts +9 -3
- package/build/ts/grammar/tokenizer.js +34 -6
- package/build/ts/grammar/tokenizer.js.map +1 -1
- package/build/ts/src/autocompleters/index.d.ts +4 -2
- package/build/ts/src/autocompleters/index.js +9 -33
- package/build/ts/src/autocompleters/index.js.map +1 -1
- package/build/ts/src/autocompleters/variables.js +17 -19
- package/build/ts/src/autocompleters/variables.js.map +1 -1
- package/build/ts/src/defaults.d.ts +17 -53
- package/build/ts/src/defaults.js +2 -5
- package/build/ts/src/defaults.js.map +1 -1
- package/build/ts/src/editor/autocompletion.d.ts +32 -0
- package/build/ts/src/editor/autocompletion.js +94 -0
- package/build/ts/src/editor/autocompletion.js.map +1 -0
- package/build/ts/src/editor/doc.d.ts +32 -0
- package/build/ts/src/editor/doc.js +121 -0
- package/build/ts/src/editor/doc.js.map +1 -0
- package/build/ts/src/editor/facade.d.ts +94 -0
- package/build/ts/src/editor/facade.js +392 -0
- package/build/ts/src/editor/facade.js.map +1 -0
- package/build/ts/src/editor/folding.d.ts +16 -0
- package/build/ts/src/editor/folding.js +190 -0
- package/build/ts/src/editor/folding.js.map +1 -0
- package/build/ts/src/editor/gutters.d.ts +8 -0
- package/build/ts/src/editor/gutters.js +113 -0
- package/build/ts/src/editor/gutters.js.map +1 -0
- package/build/ts/src/editor/keymap.d.ts +11 -0
- package/build/ts/src/editor/keymap.js +88 -0
- package/build/ts/src/editor/keymap.js.map +1 -0
- package/build/ts/src/editor/language.d.ts +11 -0
- package/build/ts/src/editor/language.js +19 -0
- package/build/ts/src/editor/language.js.map +1 -0
- package/build/ts/src/editor/tokenizerRunner.d.ts +37 -0
- package/build/ts/src/editor/tokenizerRunner.js +115 -0
- package/build/ts/src/editor/tokenizerRunner.js.map +1 -0
- package/build/ts/src/index.d.ts +49 -100
- package/build/ts/src/index.js +179 -100
- package/build/ts/src/index.js.map +1 -1
- package/build/ts/src/prefixes.json +2191 -2137
- package/build/ts/src/tooltip.js +9 -6
- package/build/ts/src/tooltip.js.map +1 -1
- package/build/yasqe.min.css +1 -1
- package/build/yasqe.min.css.map +3 -3
- package/build/yasqe.min.js +76 -91
- package/build/yasqe.min.js.map +4 -4
- package/grammar/tokenizer.ts +62 -8
- package/package.json +11 -6
- package/src/autocompleters/index.ts +24 -53
- package/src/autocompleters/show-hint.scss +17 -9
- package/src/autocompleters/variables.ts +20 -24
- package/src/defaults.ts +2 -6
- package/src/editor/autocompletion.ts +147 -0
- package/src/editor/doc.ts +151 -0
- package/src/editor/facade.ts +558 -0
- package/src/editor/folding.ts +225 -0
- package/src/editor/gutters.ts +152 -0
- package/src/editor/keymap.ts +133 -0
- package/src/editor/language.ts +47 -0
- package/src/editor/tokenizerRunner.ts +198 -0
- package/src/index.ts +193 -127
- package/src/prefixes.json +2169 -2115
- package/src/scss/buttons.scss +4 -2
- package/src/scss/codemirrorMods.scss +107 -11
- package/src/scss/yasqe.scss +4 -2
- package/src/tooltip.ts +16 -13
- package/build/ts/src/CodeMirror.d.ts +0 -30
- package/build/ts/src/CodeMirror.js +0 -20
- package/build/ts/src/CodeMirror.js.map +0 -1
- package/build/ts/src/prefixFold.d.ts +0 -8
- package/build/ts/src/prefixFold.js +0 -87
- package/build/ts/src/prefixFold.js.map +0 -1
- package/src/CodeMirror.ts +0 -54
- package/src/prefixFold.ts +0 -94
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small CodeMirror 5 `Doc`-compatible facade on top of a CodeMirror 6 `EditorView`.
|
|
3
|
+
* Positions are `{line, ch}` and 0-based, like in CodeMirror 5.
|
|
4
|
+
*/
|
|
5
|
+
import { EditorSelection, Text } from "@codemirror/state";
|
|
6
|
+
import { EditorView } from "@codemirror/view";
|
|
7
|
+
import { Position } from "./tokenizerRunner";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Mirrors CodeMirror 5's `clipPos`: a line before the document clips to the document start, a line past the
|
|
11
|
+
* end of the document clips to the end of the last line, and `ch` is clamped to the line length.
|
|
12
|
+
*/
|
|
13
|
+
export function posToOffset(doc: Text, pos: Position): number {
|
|
14
|
+
if (pos.line < 0) return 0;
|
|
15
|
+
if (pos.line >= doc.lines) return doc.length;
|
|
16
|
+
const line = doc.line(pos.line + 1);
|
|
17
|
+
return line.from + Math.max(0, Math.min(pos.ch, line.length));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function offsetToPos(doc: Text, offset: number): Position {
|
|
21
|
+
const clamped = Math.max(0, Math.min(offset, doc.length));
|
|
22
|
+
const line = doc.lineAt(clamped);
|
|
23
|
+
return { line: line.number - 1, ch: clamped - line.from };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface Selection {
|
|
27
|
+
anchor: Position;
|
|
28
|
+
head: Position;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class DocFacade {
|
|
32
|
+
constructor(private view: EditorView) {}
|
|
33
|
+
|
|
34
|
+
private get doc(): Text {
|
|
35
|
+
return this.view.state.doc;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public getValue(separator = "\n"): string {
|
|
39
|
+
return separator === "\n" ? this.doc.toString() : this.doc.toJSON().join(separator);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public setValue(value: string) {
|
|
43
|
+
this.view.dispatch({
|
|
44
|
+
changes: { from: 0, to: this.doc.length, insert: value },
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public getLine(n: number): string {
|
|
49
|
+
if (n < 0 || n >= this.doc.lines) return "";
|
|
50
|
+
return this.doc.line(n + 1).text;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public lineCount(): number {
|
|
54
|
+
return this.doc.lines;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public firstLine(): number {
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public lastLine(): number {
|
|
62
|
+
return this.doc.lines - 1;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public indexFromPos(pos: Position): number {
|
|
66
|
+
return posToOffset(this.doc, pos);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
public posFromIndex(index: number): Position {
|
|
70
|
+
return offsetToPos(this.doc, index);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
public getCursor(start?: "from" | "to" | "head" | "anchor" | "start" | "end"): Position {
|
|
74
|
+
const range = this.view.state.selection.main;
|
|
75
|
+
let offset: number;
|
|
76
|
+
switch (start) {
|
|
77
|
+
case "from":
|
|
78
|
+
case "start":
|
|
79
|
+
offset = range.from;
|
|
80
|
+
break;
|
|
81
|
+
case "to":
|
|
82
|
+
case "end":
|
|
83
|
+
offset = range.to;
|
|
84
|
+
break;
|
|
85
|
+
case "anchor":
|
|
86
|
+
offset = range.anchor;
|
|
87
|
+
break;
|
|
88
|
+
case "head":
|
|
89
|
+
default:
|
|
90
|
+
offset = range.head;
|
|
91
|
+
}
|
|
92
|
+
return offsetToPos(this.doc, offset);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
public setCursor(pos: Position | number, ch?: number) {
|
|
96
|
+
const position: Position = typeof pos === "number" ? { line: pos, ch: ch ?? 0 } : pos;
|
|
97
|
+
const offset = posToOffset(this.doc, position);
|
|
98
|
+
this.view.dispatch({ selection: { anchor: offset }, scrollIntoView: true });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
public setSelection(anchor: Position, head?: Position) {
|
|
102
|
+
const a = posToOffset(this.doc, anchor);
|
|
103
|
+
const h = head ? posToOffset(this.doc, head) : a;
|
|
104
|
+
this.view.dispatch({ selection: { anchor: a, head: h }, scrollIntoView: true });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
public listSelections(): Selection[] {
|
|
108
|
+
return this.view.state.selection.ranges.map((r) => ({
|
|
109
|
+
anchor: offsetToPos(this.doc, r.anchor),
|
|
110
|
+
head: offsetToPos(this.doc, r.head),
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
public somethingSelected(): boolean {
|
|
115
|
+
return this.view.state.selection.ranges.some((r) => !r.empty);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
public getSelection(lineSep = "\n"): string {
|
|
119
|
+
return this.view.state.selection.ranges
|
|
120
|
+
.filter((r) => !r.empty)
|
|
121
|
+
.map((r) => this.doc.sliceString(r.from, r.to))
|
|
122
|
+
.join(lineSep);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
public replaceSelection(text: string, collapse: "around" | "start" | "end" = "end") {
|
|
126
|
+
this.view.dispatch(
|
|
127
|
+
this.view.state.changeByRange((range) => {
|
|
128
|
+
const end = range.from + text.length;
|
|
129
|
+
let sel: ReturnType<typeof EditorSelection.range>;
|
|
130
|
+
if (collapse === "around") sel = EditorSelection.range(range.from, end);
|
|
131
|
+
else if (collapse === "start") sel = EditorSelection.cursor(range.from);
|
|
132
|
+
else sel = EditorSelection.cursor(end);
|
|
133
|
+
return { changes: { from: range.from, to: range.to, insert: text }, range: sel };
|
|
134
|
+
}),
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
public getRange(from: Position, to: Position): string {
|
|
139
|
+
return this.doc.sliceString(posToOffset(this.doc, from), posToOffset(this.doc, to));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
public replaceRange(text: string, from: Position, to?: Position) {
|
|
143
|
+
const f = posToOffset(this.doc, from);
|
|
144
|
+
const t = to ? posToOffset(this.doc, to) : f;
|
|
145
|
+
this.view.dispatch({ changes: { from: Math.min(f, t), to: Math.max(f, t), insert: text } });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
public selectAll() {
|
|
149
|
+
this.view.dispatch({ selection: { anchor: 0, head: this.doc.length } });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `EditorFacade` wraps a CodeMirror 6 `EditorView` and exposes a CodeMirror 5 flavoured API
|
|
3
|
+
* (getValue/setValue, getDoc(), getTokenAt(), setOption(), gutter markers, events, ...).
|
|
4
|
+
*
|
|
5
|
+
* YASQE (and YASGUI) were written against CodeMirror 5. Keeping this compatibility surface
|
|
6
|
+
* means the rest of the code base — autocompleters, prefix utilities, tab management —
|
|
7
|
+
* keeps working without invasive rewrites, while the actual editor is CodeMirror 6.
|
|
8
|
+
*/
|
|
9
|
+
import { Compartment, EditorState, Extension, StateEffect, StateField } from "@codemirror/state";
|
|
10
|
+
import {
|
|
11
|
+
EditorView,
|
|
12
|
+
ViewUpdate,
|
|
13
|
+
Decoration,
|
|
14
|
+
DecorationSet,
|
|
15
|
+
lineNumbers as lineNumbersExt,
|
|
16
|
+
drawSelection,
|
|
17
|
+
highlightActiveLine,
|
|
18
|
+
highlightActiveLineGutter,
|
|
19
|
+
rectangularSelection,
|
|
20
|
+
crosshairCursor,
|
|
21
|
+
dropCursor,
|
|
22
|
+
} from "@codemirror/view";
|
|
23
|
+
import { history, indentMore, indentLess, selectAll, deleteLine, undo, redo } from "@codemirror/commands";
|
|
24
|
+
import { bracketMatching, indentUnit as indentUnitFacet, getIndentation, indentString } from "@codemirror/language";
|
|
25
|
+
import { closeBrackets } from "@codemirror/autocomplete";
|
|
26
|
+
import { highlightSelectionMatches } from "@codemirror/search";
|
|
27
|
+
import { sparql } from "./language";
|
|
28
|
+
import { sparqlFolding, foldAt, toggleFoldAt } from "./folding";
|
|
29
|
+
import { markerGutters, setGutterMarker, clearGutter, GutterName } from "./gutters";
|
|
30
|
+
import { TokenizerRunner, Token, Position } from "./tokenizerRunner";
|
|
31
|
+
import { extraKeysExtension, tabKeymap, defaultKeymaps, ExtraKeys } from "./keymap";
|
|
32
|
+
import { sparqlAutocompletion, AutocompletionHost, HintFn } from "./autocompletion";
|
|
33
|
+
import { DocFacade, posToOffset, offsetToPos } from "./doc";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Editor related configuration. Mirrors the subset of CodeMirror 5's `EditorConfiguration`
|
|
37
|
+
* that YASQE historically supported.
|
|
38
|
+
*/
|
|
39
|
+
export interface EditorOptions {
|
|
40
|
+
mode?: string;
|
|
41
|
+
value?: string;
|
|
42
|
+
lineNumbers?: boolean;
|
|
43
|
+
lineWrapping?: boolean;
|
|
44
|
+
readOnly?: boolean;
|
|
45
|
+
tabSize?: number;
|
|
46
|
+
indentUnit?: number;
|
|
47
|
+
indentWithTabs?: boolean;
|
|
48
|
+
theme?: string;
|
|
49
|
+
extraKeys?: ExtraKeys<any>;
|
|
50
|
+
tabMode?: string;
|
|
51
|
+
foldGutter?: boolean | object;
|
|
52
|
+
gutters?: string[];
|
|
53
|
+
matchBrackets?: boolean;
|
|
54
|
+
highlightSelectionMatches?: boolean | object;
|
|
55
|
+
autoCloseBrackets?: boolean;
|
|
56
|
+
fixedGutter?: boolean;
|
|
57
|
+
placeholder?: string;
|
|
58
|
+
/** Kept for backwards compatibility; ignored */
|
|
59
|
+
autofocus?: boolean;
|
|
60
|
+
/** Kept for backwards compatibility; ignored */
|
|
61
|
+
viewportMargin?: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type EventHandler = (...args: any[]) => void;
|
|
65
|
+
|
|
66
|
+
export interface CursorCoords {
|
|
67
|
+
left: number;
|
|
68
|
+
top: number;
|
|
69
|
+
bottom: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const THEME_CLASS_PREFIX = "cm-s-";
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Classes applied to the editor root. CM6 owns `view.dom.className` and rewrites it whenever its computed
|
|
76
|
+
* attributes change (e.g. focus), so these must go through `EditorView.editorAttributes` rather than classList.
|
|
77
|
+
* `CodeMirror` is kept as a legacy hook for existing styling / tests.
|
|
78
|
+
*/
|
|
79
|
+
function themeClassExtension(theme: string | undefined): Extension {
|
|
80
|
+
const classes = ["CodeMirror"];
|
|
81
|
+
if (theme) {
|
|
82
|
+
for (const t of theme.split(/\s+/)) {
|
|
83
|
+
if (t) classes.push(THEME_CLASS_PREFIX + t);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return EditorView.editorAttributes.of({ class: classes.join(" ") });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const syntaxErrorTokenMark = Decoration.mark({ class: "cm-syntax-error-token" });
|
|
90
|
+
const setSyntaxErrorHighlightEffect = StateEffect.define<{ from: number; to: number } | null>();
|
|
91
|
+
const syntaxErrorHighlightField = StateField.define<DecorationSet>({
|
|
92
|
+
create() {
|
|
93
|
+
return Decoration.none;
|
|
94
|
+
},
|
|
95
|
+
update(deco, tr) {
|
|
96
|
+
deco = deco.map(tr.changes);
|
|
97
|
+
for (const effect of tr.effects) {
|
|
98
|
+
if (!effect.is(setSyntaxErrorHighlightEffect)) continue;
|
|
99
|
+
const range = effect.value;
|
|
100
|
+
if (!range || range.to <= range.from) {
|
|
101
|
+
deco = Decoration.none;
|
|
102
|
+
} else {
|
|
103
|
+
deco = Decoration.set([syntaxErrorTokenMark.range(range.from, range.to)]);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return deco;
|
|
107
|
+
},
|
|
108
|
+
provide: (field) => EditorView.decorations.from(field),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
export abstract class EditorFacade {
|
|
112
|
+
public readonly view: EditorView;
|
|
113
|
+
public readonly runner: TokenizerRunner;
|
|
114
|
+
protected readonly editorOptions: EditorOptions;
|
|
115
|
+
private readonly doc: DocFacade;
|
|
116
|
+
private readonly eventHandlers: { [event: string]: EventHandler[] } = {};
|
|
117
|
+
private readonly customOptions: { [key: string]: any } = {};
|
|
118
|
+
|
|
119
|
+
private readonly compartments = {
|
|
120
|
+
theme: new Compartment(),
|
|
121
|
+
readOnly: new Compartment(),
|
|
122
|
+
lineNumbers: new Compartment(),
|
|
123
|
+
lineWrapping: new Compartment(),
|
|
124
|
+
tabSize: new Compartment(),
|
|
125
|
+
indentUnit: new Compartment(),
|
|
126
|
+
extraKeys: new Compartment(),
|
|
127
|
+
tabMode: new Compartment(),
|
|
128
|
+
matchBrackets: new Compartment(),
|
|
129
|
+
highlightSelectionMatches: new Compartment(),
|
|
130
|
+
autoCloseBrackets: new Compartment(),
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* @param parent element the editor is appended to
|
|
135
|
+
* @param options editor options
|
|
136
|
+
*/
|
|
137
|
+
constructor(parent: HTMLElement, options: EditorOptions) {
|
|
138
|
+
this.editorOptions = { ...options };
|
|
139
|
+
this.runner = new TokenizerRunner({ tabSize: options.tabSize, indentUnit: options.indentUnit });
|
|
140
|
+
|
|
141
|
+
const host: AutocompletionHost = {
|
|
142
|
+
getHintFn: () => this.getHintFn(),
|
|
143
|
+
onShown: (dom) => this.emit("autocompletionShown", dom),
|
|
144
|
+
onClose: () => this.emit("autocompletionClose"),
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const state = EditorState.create({
|
|
148
|
+
doc: options.value || "",
|
|
149
|
+
extensions: [
|
|
150
|
+
sparql(),
|
|
151
|
+
sparqlFolding(this.runner),
|
|
152
|
+
markerGutters(),
|
|
153
|
+
syntaxErrorHighlightField,
|
|
154
|
+
this.compartments.lineNumbers.of(
|
|
155
|
+
options.lineNumbers === false ? [] : [lineNumbersExt(), highlightActiveLineGutter()],
|
|
156
|
+
),
|
|
157
|
+
this.compartments.lineWrapping.of(options.lineWrapping === false ? [] : EditorView.lineWrapping),
|
|
158
|
+
this.compartments.readOnly.of([
|
|
159
|
+
EditorState.readOnly.of(!!options.readOnly),
|
|
160
|
+
EditorView.editable.of(!options.readOnly),
|
|
161
|
+
]),
|
|
162
|
+
this.compartments.tabSize.of(EditorState.tabSize.of(options.tabSize || 4)),
|
|
163
|
+
this.compartments.indentUnit.of(indentUnitFacet.of(indentUnitString(options))),
|
|
164
|
+
this.compartments.theme.of(themeClassExtension(options.theme)),
|
|
165
|
+
this.compartments.matchBrackets.of(options.matchBrackets === false ? [] : bracketMatching()),
|
|
166
|
+
this.compartments.highlightSelectionMatches.of(
|
|
167
|
+
options.highlightSelectionMatches ? highlightSelectionMatches() : [],
|
|
168
|
+
),
|
|
169
|
+
this.compartments.autoCloseBrackets.of(options.autoCloseBrackets ? closeBrackets() : []),
|
|
170
|
+
history(),
|
|
171
|
+
drawSelection(),
|
|
172
|
+
dropCursor(),
|
|
173
|
+
rectangularSelection(),
|
|
174
|
+
crosshairCursor(),
|
|
175
|
+
highlightActiveLine(),
|
|
176
|
+
EditorState.allowMultipleSelections.of(true),
|
|
177
|
+
sparqlAutocompletion(host),
|
|
178
|
+
this.compartments.extraKeys.of(extraKeysExtension(options.extraKeys, () => this)),
|
|
179
|
+
this.compartments.tabMode.of(tabKeymap(options.tabMode)),
|
|
180
|
+
defaultKeymaps(),
|
|
181
|
+
EditorView.updateListener.of((update) => this.handleUpdate(update)),
|
|
182
|
+
EditorView.domEventHandlers({
|
|
183
|
+
blur: () => {
|
|
184
|
+
this.emit("blur");
|
|
185
|
+
},
|
|
186
|
+
focus: () => {
|
|
187
|
+
this.emit("focus");
|
|
188
|
+
},
|
|
189
|
+
}),
|
|
190
|
+
],
|
|
191
|
+
});
|
|
192
|
+
this.view = new EditorView({ state, parent });
|
|
193
|
+
// Legacy back-reference (CM5 exposed `wrapper.CodeMirror`); used by Matgui theme management
|
|
194
|
+
(this.view.dom as any).CodeMirror = this;
|
|
195
|
+
this.doc = new DocFacade(this.view);
|
|
196
|
+
this.customOptions.theme = options.theme;
|
|
197
|
+
if (options.mode) this.customOptions.mode = options.mode;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Returns the hint function for the current completion session. Implemented by Yasqe. */
|
|
201
|
+
protected abstract getHintFn(): HintFn | undefined;
|
|
202
|
+
|
|
203
|
+
private handleUpdate(update: ViewUpdate) {
|
|
204
|
+
if (update.docChanged) {
|
|
205
|
+
let fromLine = Number.MAX_SAFE_INTEGER;
|
|
206
|
+
update.changes.iterChangedRanges((fromA) => {
|
|
207
|
+
const line = update.startState.doc.lineAt(fromA).number - 1;
|
|
208
|
+
if (line < fromLine) fromLine = line;
|
|
209
|
+
});
|
|
210
|
+
this.runner.invalidate(fromLine === Number.MAX_SAFE_INTEGER ? 0 : fromLine);
|
|
211
|
+
this.emit("change", { origin: "input" });
|
|
212
|
+
this.emit("changes");
|
|
213
|
+
}
|
|
214
|
+
if (update.selectionSet || update.docChanged) {
|
|
215
|
+
this.emit("cursorActivity");
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/* ------------------------------------------------------------------ */
|
|
220
|
+
/* Events */
|
|
221
|
+
/* ------------------------------------------------------------------ */
|
|
222
|
+
|
|
223
|
+
public on(eventName: string, handler: EventHandler): void {
|
|
224
|
+
if (!this.eventHandlers[eventName]) this.eventHandlers[eventName] = [];
|
|
225
|
+
this.eventHandlers[eventName].push(handler);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
public off(eventName: string, handler: EventHandler): void {
|
|
229
|
+
const handlers = this.eventHandlers[eventName];
|
|
230
|
+
if (!handlers) return;
|
|
231
|
+
this.eventHandlers[eventName] = handlers.filter((h) => h !== handler);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Emits an event. Handlers are invoked with `(this, ...data)` like CodeMirror 5 did. */
|
|
235
|
+
public emit(eventName: string, ...data: any[]): void {
|
|
236
|
+
const handlers = this.eventHandlers[eventName];
|
|
237
|
+
if (!handlers || !handlers.length) return;
|
|
238
|
+
for (const handler of handlers.slice()) {
|
|
239
|
+
handler(this, ...data);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/* ------------------------------------------------------------------ */
|
|
244
|
+
/* Document */
|
|
245
|
+
/* ------------------------------------------------------------------ */
|
|
246
|
+
|
|
247
|
+
public getDoc(): DocFacade {
|
|
248
|
+
return this.doc;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
public getValue(): string {
|
|
252
|
+
return this.doc.getValue();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
public setValue(value: string) {
|
|
256
|
+
this.doc.setValue(value);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
public getLine(line: number) {
|
|
260
|
+
return this.doc.getLine(line);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
public lineCount() {
|
|
264
|
+
return this.doc.lineCount();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
public lastLine() {
|
|
268
|
+
return this.doc.lastLine();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
public getCursor(start?: "from" | "to" | "head" | "anchor" | "start" | "end") {
|
|
272
|
+
return this.doc.getCursor(start);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
public setCursor(pos: Position | number, ch?: number) {
|
|
276
|
+
this.doc.setCursor(pos, ch);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
public setSelection(anchor: Position, head?: Position) {
|
|
280
|
+
this.doc.setSelection(anchor, head);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
public somethingSelected() {
|
|
284
|
+
return this.doc.somethingSelected();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
public getSelection() {
|
|
288
|
+
return this.doc.getSelection();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
public replaceSelection(text: string, collapse?: "around" | "start" | "end") {
|
|
292
|
+
this.doc.replaceSelection(text, collapse);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
public getRange(from: Position, to: Position) {
|
|
296
|
+
return this.doc.getRange(from, to);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
public replaceRange(text: string, from: Position, to?: Position) {
|
|
300
|
+
this.doc.replaceRange(text, from, to);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
public indexFromPos(pos: Position) {
|
|
304
|
+
return this.doc.indexFromPos(pos);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
public posFromIndex(index: number) {
|
|
308
|
+
return this.doc.posFromIndex(index);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* CM5 compatibility: batch operations. CodeMirror 6 already batches DOM updates,
|
|
313
|
+
* so the callback is simply executed.
|
|
314
|
+
*/
|
|
315
|
+
public operation<T>(fn: () => T): T {
|
|
316
|
+
return fn();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/* ------------------------------------------------------------------ */
|
|
320
|
+
/* Tokens */
|
|
321
|
+
/* ------------------------------------------------------------------ */
|
|
322
|
+
|
|
323
|
+
public getTokenAt(pos: Position, precise = false): Token {
|
|
324
|
+
return this.runner.getTokenAt(this.view.state.doc, pos, precise);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
public getTokenTypeAt(pos: Position): string | null {
|
|
328
|
+
return this.runner.getTokenTypeAt(this.view.state.doc, pos);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/* ------------------------------------------------------------------ */
|
|
332
|
+
/* Options */
|
|
333
|
+
/* ------------------------------------------------------------------ */
|
|
334
|
+
|
|
335
|
+
public getOption(name: string): any {
|
|
336
|
+
if (name in this.customOptions) return this.customOptions[name];
|
|
337
|
+
return (this.editorOptions as any)[name];
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
public setOption(name: string, value: any) {
|
|
341
|
+
(this.editorOptions as any)[name] = value;
|
|
342
|
+
switch (name) {
|
|
343
|
+
case "theme":
|
|
344
|
+
this.setTheme(value);
|
|
345
|
+
break;
|
|
346
|
+
case "readOnly": {
|
|
347
|
+
const readOnly = !!value && value !== "nocursor" ? true : !!value;
|
|
348
|
+
this.view.dispatch({
|
|
349
|
+
effects: this.compartments.readOnly.reconfigure([
|
|
350
|
+
EditorState.readOnly.of(readOnly),
|
|
351
|
+
EditorView.editable.of(!readOnly),
|
|
352
|
+
]),
|
|
353
|
+
});
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
case "lineNumbers":
|
|
357
|
+
this.view.dispatch({
|
|
358
|
+
effects: this.compartments.lineNumbers.reconfigure(
|
|
359
|
+
value ? [lineNumbersExt(), highlightActiveLineGutter()] : [],
|
|
360
|
+
),
|
|
361
|
+
});
|
|
362
|
+
break;
|
|
363
|
+
case "lineWrapping":
|
|
364
|
+
this.view.dispatch({
|
|
365
|
+
effects: this.compartments.lineWrapping.reconfigure(value ? EditorView.lineWrapping : []),
|
|
366
|
+
});
|
|
367
|
+
break;
|
|
368
|
+
case "tabSize":
|
|
369
|
+
this.view.dispatch({ effects: this.compartments.tabSize.reconfigure(EditorState.tabSize.of(value || 4)) });
|
|
370
|
+
break;
|
|
371
|
+
case "indentUnit":
|
|
372
|
+
case "indentWithTabs":
|
|
373
|
+
this.view.dispatch({
|
|
374
|
+
effects: this.compartments.indentUnit.reconfigure(indentUnitFacet.of(indentUnitString(this.editorOptions))),
|
|
375
|
+
});
|
|
376
|
+
break;
|
|
377
|
+
case "extraKeys":
|
|
378
|
+
this.view.dispatch({
|
|
379
|
+
effects: this.compartments.extraKeys.reconfigure(extraKeysExtension(value, () => this)),
|
|
380
|
+
});
|
|
381
|
+
break;
|
|
382
|
+
case "tabMode":
|
|
383
|
+
this.view.dispatch({ effects: this.compartments.tabMode.reconfigure(tabKeymap(value)) });
|
|
384
|
+
break;
|
|
385
|
+
case "matchBrackets":
|
|
386
|
+
this.view.dispatch({ effects: this.compartments.matchBrackets.reconfigure(value ? bracketMatching() : []) });
|
|
387
|
+
break;
|
|
388
|
+
case "highlightSelectionMatches":
|
|
389
|
+
this.view.dispatch({
|
|
390
|
+
effects: this.compartments.highlightSelectionMatches.reconfigure(value ? highlightSelectionMatches() : []),
|
|
391
|
+
});
|
|
392
|
+
break;
|
|
393
|
+
case "autoCloseBrackets":
|
|
394
|
+
this.view.dispatch({ effects: this.compartments.autoCloseBrackets.reconfigure(value ? closeBrackets() : []) });
|
|
395
|
+
break;
|
|
396
|
+
default:
|
|
397
|
+
this.customOptions[name] = value;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
private setTheme(theme: string | undefined) {
|
|
402
|
+
this.view.dispatch({ effects: this.compartments.theme.reconfigure(themeClassExtension(theme)) });
|
|
403
|
+
this.customOptions.theme = theme;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/* ------------------------------------------------------------------ */
|
|
407
|
+
/* Gutters */
|
|
408
|
+
/* ------------------------------------------------------------------ */
|
|
409
|
+
|
|
410
|
+
public setGutterMarker(line: number, gutterName: GutterName, element: HTMLElement | null) {
|
|
411
|
+
setGutterMarker(this.view, line, gutterName, element);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
public clearGutter(gutterName: GutterName) {
|
|
415
|
+
clearGutter(this.view, gutterName);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** Highlight one syntax error token range (replaces any existing syntax error highlight). */
|
|
419
|
+
public setSyntaxErrorHighlight(from: Position, to: Position) {
|
|
420
|
+
const fromOffset = posToOffset(this.view.state.doc, from);
|
|
421
|
+
const toOffset = posToOffset(this.view.state.doc, to);
|
|
422
|
+
this.view.dispatch({ effects: setSyntaxErrorHighlightEffect.of({ from: fromOffset, to: toOffset }) });
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** Clear syntax error token highlight. */
|
|
426
|
+
public clearSyntaxErrorHighlight() {
|
|
427
|
+
this.view.dispatch({ effects: setSyntaxErrorHighlightEffect.of(null) });
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/* ------------------------------------------------------------------ */
|
|
431
|
+
/* Commands / editing helpers */
|
|
432
|
+
/* ------------------------------------------------------------------ */
|
|
433
|
+
|
|
434
|
+
public execCommand(command: string): void {
|
|
435
|
+
switch (command) {
|
|
436
|
+
case "selectAll":
|
|
437
|
+
selectAll(this.view);
|
|
438
|
+
break;
|
|
439
|
+
case "indentMore":
|
|
440
|
+
indentMore(this.view);
|
|
441
|
+
break;
|
|
442
|
+
case "indentLess":
|
|
443
|
+
indentLess(this.view);
|
|
444
|
+
break;
|
|
445
|
+
case "deleteLine":
|
|
446
|
+
deleteLine(this.view);
|
|
447
|
+
break;
|
|
448
|
+
case "undo":
|
|
449
|
+
undo(this.view);
|
|
450
|
+
break;
|
|
451
|
+
case "redo":
|
|
452
|
+
redo(this.view);
|
|
453
|
+
break;
|
|
454
|
+
default:
|
|
455
|
+
// eslint-disable-next-line no-console
|
|
456
|
+
console.warn(`Unknown editor command: ${command}`);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Re-indents the given (0-based) line using the language's indentation rules
|
|
462
|
+
* (CM5 `indentLine(line, "smart")`).
|
|
463
|
+
*/
|
|
464
|
+
public indentLine(lineNumber: number) {
|
|
465
|
+
const line = this.view.state.doc.line(lineNumber + 1);
|
|
466
|
+
const indentation = getIndentation(this.view.state, line.from);
|
|
467
|
+
if (indentation === null) return;
|
|
468
|
+
const currentWs = /^\s*/.exec(line.text)?.[0] || "";
|
|
469
|
+
const newWs = indentString(this.view.state, indentation);
|
|
470
|
+
if (currentWs === newWs) return;
|
|
471
|
+
this.view.dispatch({ changes: { from: line.from, to: line.from + currentWs.length, insert: newWs } });
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/** Fold the block starting on the given line. */
|
|
475
|
+
public foldCode(line: number | Position) {
|
|
476
|
+
const pos = typeof line === "number" ? { line, ch: 0 } : line;
|
|
477
|
+
foldAt(this.view, posToOffset(this.view.state.doc, pos));
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/** Toggle folding of the block starting on the given line. */
|
|
481
|
+
public toggleFold(line: number | Position) {
|
|
482
|
+
const pos = typeof line === "number" ? { line, ch: 0 } : line;
|
|
483
|
+
toggleFoldAt(this.view, posToOffset(this.view.state.doc, pos));
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/* ------------------------------------------------------------------ */
|
|
487
|
+
/* DOM / layout */
|
|
488
|
+
/* ------------------------------------------------------------------ */
|
|
489
|
+
|
|
490
|
+
public getWrapperElement(): HTMLElement {
|
|
491
|
+
return this.view.dom;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
public getInputField(): HTMLElement {
|
|
495
|
+
return this.view.contentDOM;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
public getScrollerElement(): HTMLElement {
|
|
499
|
+
return this.view.scrollDOM;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
public focus() {
|
|
503
|
+
this.view.focus();
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
public hasFocus() {
|
|
507
|
+
return this.view.hasFocus;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
public refresh() {
|
|
511
|
+
this.view.requestMeasure();
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
public setSize(width: number | string | null, height: number | string | null) {
|
|
515
|
+
const toCss = (v: number | string) => (typeof v === "number" ? `${v}px` : v);
|
|
516
|
+
if (width !== null && width !== undefined) this.view.dom.style.width = toCss(width);
|
|
517
|
+
if (height !== null && height !== undefined) this.view.dom.style.height = toCss(height);
|
|
518
|
+
this.view.requestMeasure();
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/** Coordinates of a position, relative to the page (CM5 `cursorCoords(where, "page")`). */
|
|
522
|
+
public cursorCoords(where?: Position | boolean): CursorCoords {
|
|
523
|
+
const pos =
|
|
524
|
+
where && typeof where === "object"
|
|
525
|
+
? posToOffset(this.view.state.doc, where)
|
|
526
|
+
: where === false
|
|
527
|
+
? this.view.state.selection.main.anchor
|
|
528
|
+
: this.view.state.selection.main.head;
|
|
529
|
+
const rect = this.view.coordsAtPos(pos);
|
|
530
|
+
if (!rect) {
|
|
531
|
+
const domRect = this.view.dom.getBoundingClientRect();
|
|
532
|
+
return { left: domRect.left, top: domRect.top, bottom: domRect.top };
|
|
533
|
+
}
|
|
534
|
+
return { left: rect.left, top: rect.top, bottom: rect.bottom };
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/** Position for page coordinates (CM5 `coordsChar`). */
|
|
538
|
+
public coordsChar(coords: { left: number; top: number }): Position {
|
|
539
|
+
const offset = this.view.posAtCoords({ x: coords.left, y: coords.top });
|
|
540
|
+
return offsetToPos(this.view.state.doc, offset === null ? this.view.state.doc.length : offset);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/** Adds an extension at runtime (e.g. by plugins). */
|
|
544
|
+
public addExtension(extension: Extension) {
|
|
545
|
+
this.view.dispatch({ effects: StateEffect.appendConfig.of(extension) });
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/** Destroys the editor view. */
|
|
549
|
+
public destroyEditor() {
|
|
550
|
+
this.view.destroy();
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/** Compute the indent unit string from the options (CM5 `indentUnit` + `indentWithTabs`). */
|
|
555
|
+
function indentUnitString(options: EditorOptions) {
|
|
556
|
+
if (options.indentWithTabs) return "\t";
|
|
557
|
+
return " ".repeat(options.indentUnit || 2);
|
|
558
|
+
}
|