@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,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code folding for SPARQL: folds bracket blocks (`{ ... }`) and the block of PREFIX declarations.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the CodeMirror 5 combination of `fold.brace` and the custom `fold.prefix` helper.
|
|
5
|
+
* Because the SPARQL grammar is a stream parser (no syntax tree with nesting), folding is
|
|
6
|
+
* implemented as a `foldService` working on the tokenizer output.
|
|
7
|
+
*/
|
|
8
|
+
import { EditorState, Extension, Text } from "@codemirror/state";
|
|
9
|
+
import { EditorView } from "@codemirror/view";
|
|
10
|
+
import { foldService, foldGutter, foldEffect, unfoldEffect, foldedRanges, foldable } from "@codemirror/language";
|
|
11
|
+
import { TokenizerRunner, Position } from "./tokenizerRunner";
|
|
12
|
+
|
|
13
|
+
export interface FoldRange {
|
|
14
|
+
from: number;
|
|
15
|
+
to: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const PREFIX_KEYWORD = "PREFIX ";
|
|
19
|
+
|
|
20
|
+
function tokenTypeAt(runner: TokenizerRunner, doc: Text, pos: Position) {
|
|
21
|
+
return runner.getTokenAt(doc, pos).type;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Whether `type` denotes a real bracket/punctuation token (i.e. not a bracket character that is
|
|
26
|
+
* part of a string or comment). The tokenizer styles brackets as `bracket bracket-level-N` /
|
|
27
|
+
* `bracket bracket-paren`, so a plain `=== "punc"` check would miss `{ } ( ) [ ]`.
|
|
28
|
+
*/
|
|
29
|
+
function isBracketToken(type: string | null): boolean {
|
|
30
|
+
return !!type && (type === "punc" || type.split(/\s+/).some((t) => t === "punc" || t.startsWith("bracket")));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Finds the fold range for a block starting with a bracket on the given line. Mirrors the
|
|
35
|
+
* CodeMirror 5 `brace` range finder: the *last* opening bracket on the line is used.
|
|
36
|
+
*/
|
|
37
|
+
export function braceFoldRange(runner: TokenizerRunner, doc: Text, line: number): FoldRange | undefined {
|
|
38
|
+
const lineText = doc.line(line + 1).text;
|
|
39
|
+
const pairs: { [open: string]: string } = { "{": "}", "[": "]", "(": ")" };
|
|
40
|
+
let startCh = -1;
|
|
41
|
+
let open = "";
|
|
42
|
+
for (let ch = lineText.length - 1; ch >= 0; ch--) {
|
|
43
|
+
const c = lineText[ch];
|
|
44
|
+
if (pairs[c] && isBracketToken(tokenTypeAt(runner, doc, { line, ch: ch + 1 }))) {
|
|
45
|
+
startCh = ch;
|
|
46
|
+
open = c;
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (startCh < 0) return;
|
|
51
|
+
const close = pairs[open];
|
|
52
|
+
|
|
53
|
+
// Scan forward for the matching bracket, taking nesting into account and ignoring brackets inside strings/comments
|
|
54
|
+
let depth = 1;
|
|
55
|
+
const lastLine = doc.lines - 1;
|
|
56
|
+
for (let l = line; l <= lastLine; l++) {
|
|
57
|
+
const text = doc.line(l + 1).text;
|
|
58
|
+
let ch = l === line ? startCh + 1 : 0;
|
|
59
|
+
for (; ch < text.length; ch++) {
|
|
60
|
+
const c = text[ch];
|
|
61
|
+
if (c !== open && c !== close) continue;
|
|
62
|
+
if (!isBracketToken(tokenTypeAt(runner, doc, { line: l, ch: ch + 1 }))) continue;
|
|
63
|
+
if (c === open) depth++;
|
|
64
|
+
else if (--depth === 0) {
|
|
65
|
+
if (l === line) return; // opening and closing bracket on the same line: nothing to fold
|
|
66
|
+
return { from: doc.line(line + 1).from + startCh + 1, to: doc.line(l + 1).from + ch };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Returns the char index of the first PREFIX keyword on the line (or undefined) */
|
|
74
|
+
export function findFirstPrefix(
|
|
75
|
+
runner: TokenizerRunner,
|
|
76
|
+
doc: Text,
|
|
77
|
+
line: number,
|
|
78
|
+
startFromCharIndex = 0,
|
|
79
|
+
): number | undefined {
|
|
80
|
+
const lineText = doc.line(line + 1).text;
|
|
81
|
+
if (!lineText) return undefined;
|
|
82
|
+
const charIndex = lineText.toUpperCase().indexOf(PREFIX_KEYWORD, startFromCharIndex);
|
|
83
|
+
if (charIndex >= 0 && tokenTypeAt(runner, doc, { line, ch: charIndex + 1 }) === "keyword") {
|
|
84
|
+
return charIndex;
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Returns the (0-based) line of the first PREFIX declaration in the document */
|
|
90
|
+
export function findFirstPrefixLine(runner: TokenizerRunner, doc: Text): number | undefined {
|
|
91
|
+
for (let i = 0; i < doc.lines; i++) {
|
|
92
|
+
const firstPrefix = findFirstPrefix(runner, doc, i);
|
|
93
|
+
if (firstPrefix !== undefined && firstPrefix >= 0) return i;
|
|
94
|
+
}
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Fold range for the block of PREFIX declarations. Only the first PREFIX declaration of the query
|
|
100
|
+
* starts a foldable block. Ported from the CodeMirror 5 `fold.prefix` helper.
|
|
101
|
+
*/
|
|
102
|
+
export function prefixFoldRange(runner: TokenizerRunner, doc: Text, line: number): FoldRange | undefined {
|
|
103
|
+
// Only the opening prefix declaration is foldable
|
|
104
|
+
for (let i = line - 1; i >= 0; i--) {
|
|
105
|
+
if (
|
|
106
|
+
doc
|
|
107
|
+
.line(i + 1)
|
|
108
|
+
.text.toUpperCase()
|
|
109
|
+
.indexOf(PREFIX_KEYWORD) >= 0
|
|
110
|
+
)
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const prefixStart = findFirstPrefix(runner, doc, line);
|
|
114
|
+
if (prefixStart === undefined) return;
|
|
115
|
+
|
|
116
|
+
const getNextNonWsToken = (l: number, ch: number) => {
|
|
117
|
+
let token = runner.getTokenAt(doc, { line: l, ch });
|
|
118
|
+
if (token.end < ch) return undefined;
|
|
119
|
+
while (token.type === "ws") {
|
|
120
|
+
const next = runner.getTokenAt(doc, { line: l, ch: token.end + 1 });
|
|
121
|
+
if (next.end < token.end + 1) return undefined;
|
|
122
|
+
token = next;
|
|
123
|
+
}
|
|
124
|
+
return token;
|
|
125
|
+
};
|
|
126
|
+
const getLastPrefixPos = (l: number, ch: number): number => {
|
|
127
|
+
const keyword = runner.getTokenAt(doc, { line: l, ch: ch + 1 });
|
|
128
|
+
if (keyword.type !== "keyword") return -1;
|
|
129
|
+
const shortname = getNextNonWsToken(l, keyword.end + 1);
|
|
130
|
+
if (!shortname || shortname.type !== "string-2") return -1;
|
|
131
|
+
const uri = getNextNonWsToken(l, shortname.end + 1);
|
|
132
|
+
if (!uri || uri.type !== "variable-3") return -1;
|
|
133
|
+
return uri.end;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
let prefixEndChar = getLastPrefixPos(line, prefixStart);
|
|
137
|
+
let prefixEndLine = line;
|
|
138
|
+
let stopAtNextLine = false;
|
|
139
|
+
for (let i = line; i < doc.lines; i++) {
|
|
140
|
+
if (stopAtNextLine) break;
|
|
141
|
+
const text = doc.line(i + 1).text;
|
|
142
|
+
let pos = i === line ? prefixStart + 1 : 0;
|
|
143
|
+
for (;;) {
|
|
144
|
+
if (!stopAtNextLine && text.indexOf("{") >= 0) stopAtNextLine = true;
|
|
145
|
+
const nextPrefixDeclaration = text.toUpperCase().indexOf(PREFIX_KEYWORD, pos);
|
|
146
|
+
if (nextPrefixDeclaration < 0) break;
|
|
147
|
+
const endCh = getLastPrefixPos(i, nextPrefixDeclaration);
|
|
148
|
+
if (endCh > 0) {
|
|
149
|
+
prefixEndChar = endCh;
|
|
150
|
+
prefixEndLine = i;
|
|
151
|
+
pos = prefixEndChar;
|
|
152
|
+
}
|
|
153
|
+
pos++;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (prefixEndChar < 0) return;
|
|
157
|
+
const from = doc.line(line + 1).from + prefixStart + PREFIX_KEYWORD.length;
|
|
158
|
+
const to = doc.line(prefixEndLine + 1).from + prefixEndChar;
|
|
159
|
+
if (to <= from) return;
|
|
160
|
+
return { from, to };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Combined range finder (brace first, then prefix block) for a given 0-based line */
|
|
164
|
+
export function foldRangeForLine(runner: TokenizerRunner, doc: Text, line: number): FoldRange | undefined {
|
|
165
|
+
return braceFoldRange(runner, doc, line) || prefixFoldRange(runner, doc, line);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Folding extensions: fold service + fold gutter */
|
|
169
|
+
export function sparqlFolding(runner: TokenizerRunner): Extension {
|
|
170
|
+
return [
|
|
171
|
+
foldService.of((state: EditorState, lineStart: number) => {
|
|
172
|
+
const line = state.doc.lineAt(lineStart).number - 1;
|
|
173
|
+
return foldRangeForLine(runner, state.doc, line) || null;
|
|
174
|
+
}),
|
|
175
|
+
foldGutter({
|
|
176
|
+
markerDOM(open) {
|
|
177
|
+
const el = document.createElement("span");
|
|
178
|
+
el.className = `cm-foldMarker CodeMirror-foldgutter-${open ? "open" : "folded"}`;
|
|
179
|
+
el.title = open ? "Fold" : "Unfold";
|
|
180
|
+
return el;
|
|
181
|
+
},
|
|
182
|
+
}),
|
|
183
|
+
];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Fold (or unfold, when already folded) the block starting at the given position. */
|
|
187
|
+
export function toggleFoldAt(view: EditorView, pos: number) {
|
|
188
|
+
const line = view.state.doc.lineAt(pos);
|
|
189
|
+
// Unfold when the line is (partly) hidden by a fold
|
|
190
|
+
let unfolded = false;
|
|
191
|
+
foldedRanges(view.state).between(line.from, line.to, (from, to) => {
|
|
192
|
+
if (from >= line.from && from <= line.to) {
|
|
193
|
+
view.dispatch({ effects: unfoldEffect.of({ from, to }) });
|
|
194
|
+
unfolded = true;
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
return;
|
|
198
|
+
});
|
|
199
|
+
if (unfolded) return;
|
|
200
|
+
const range = foldable(view.state, line.from, line.to);
|
|
201
|
+
if (range) view.dispatch({ effects: foldEffect.of(range) });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Fold the block starting at the given position (no-op if nothing foldable or already folded) */
|
|
205
|
+
export function foldAt(view: EditorView, pos: number) {
|
|
206
|
+
const line = view.state.doc.lineAt(pos);
|
|
207
|
+
let alreadyFolded = false;
|
|
208
|
+
foldedRanges(view.state).between(line.from, line.to, () => {
|
|
209
|
+
alreadyFolded = true;
|
|
210
|
+
return false;
|
|
211
|
+
});
|
|
212
|
+
if (alreadyFolded) return;
|
|
213
|
+
const range = foldable(view.state, line.from, line.to);
|
|
214
|
+
if (range) view.dispatch({ effects: foldEffect.of(range) });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Unfold any fold that starts on the line of the given position (no-op if not folded) */
|
|
218
|
+
export function unfoldAt(view: EditorView, pos: number) {
|
|
219
|
+
const line = view.state.doc.lineAt(pos);
|
|
220
|
+
const effects: ReturnType<typeof unfoldEffect.of>[] = [];
|
|
221
|
+
foldedRanges(view.state).between(line.from, line.to, (from, to) => {
|
|
222
|
+
if (from >= line.from && from <= line.to) effects.push(unfoldEffect.of({ from, to }));
|
|
223
|
+
});
|
|
224
|
+
if (effects.length) view.dispatch({ effects });
|
|
225
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Marker gutters replacing the CodeMirror 5 `setGutterMarker` / `clearGutter` API.
|
|
3
|
+
*
|
|
4
|
+
* Two logical marker types are supported: `gutterErrorBar` (syntax errors) and
|
|
5
|
+
* `gutterConstructWarning` (undefined variables in CONSTRUCT templates). Both are rendered
|
|
6
|
+
* in one physical gutter lane to keep gutter width minimal.
|
|
7
|
+
*/
|
|
8
|
+
import { EditorState, StateEffect, StateField, RangeSet, Extension } from "@codemirror/state";
|
|
9
|
+
import { EditorView, gutter, GutterMarker } from "@codemirror/view";
|
|
10
|
+
|
|
11
|
+
export type GutterName = "gutterErrorBar" | "gutterConstructWarning";
|
|
12
|
+
export const GUTTER_NAMES: GutterName[] = ["gutterErrorBar", "gutterConstructWarning"];
|
|
13
|
+
const RENDER_GUTTER_NAME: GutterName = "gutterErrorBar";
|
|
14
|
+
|
|
15
|
+
class ElementMarker extends GutterMarker {
|
|
16
|
+
constructor(readonly element: HTMLElement) {
|
|
17
|
+
super();
|
|
18
|
+
}
|
|
19
|
+
eq(other: ElementMarker) {
|
|
20
|
+
return other.element === this.element;
|
|
21
|
+
}
|
|
22
|
+
toDOM() {
|
|
23
|
+
// Return a fresh clone on every render. CodeMirror 6 owns (and may move/recreate) the node
|
|
24
|
+
// it gets from `toDOM`; handing out the same shared element across gutter re-renders detaches
|
|
25
|
+
// it from a previous gutter element, which later crashes with `nextSibling` of null.
|
|
26
|
+
const clone = this.element.cloneNode(true) as HTMLElement;
|
|
27
|
+
clone.onmouseover = this.element.onmouseover;
|
|
28
|
+
clone.onmouseout = this.element.onmouseout;
|
|
29
|
+
return clone;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface SetMarkerSpec {
|
|
34
|
+
gutter: GutterName;
|
|
35
|
+
/** 0-based line number */
|
|
36
|
+
line: number;
|
|
37
|
+
element: HTMLElement | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const setMarkerEffect = StateEffect.define<SetMarkerSpec>();
|
|
41
|
+
const clearGutterEffect = StateEffect.define<GutterName>();
|
|
42
|
+
|
|
43
|
+
function createMarkerField(name: GutterName): StateField<RangeSet<ElementMarker>> {
|
|
44
|
+
const field = StateField.define<RangeSet<ElementMarker>>({
|
|
45
|
+
create() {
|
|
46
|
+
return RangeSet.empty;
|
|
47
|
+
},
|
|
48
|
+
update(markers, tr) {
|
|
49
|
+
markers = markers.map(tr.changes);
|
|
50
|
+
for (const effect of tr.effects) {
|
|
51
|
+
if (effect.is(clearGutterEffect) && effect.value === name) {
|
|
52
|
+
markers = RangeSet.empty;
|
|
53
|
+
} else if (effect.is(setMarkerEffect) && effect.value.gutter === name) {
|
|
54
|
+
const { line, element } = effect.value;
|
|
55
|
+
if (line < 0 || line >= tr.state.doc.lines) continue;
|
|
56
|
+
const pos = tr.state.doc.line(line + 1).from;
|
|
57
|
+
markers = markers.update({ filter: (from) => from !== pos });
|
|
58
|
+
if (element) {
|
|
59
|
+
markers = markers.update({ add: [new ElementMarker(element).range(pos)] });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return markers;
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
return field;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function spacerElement() {
|
|
70
|
+
const el = document.createElement("span");
|
|
71
|
+
el.className = "cm-gutter-spacer";
|
|
72
|
+
el.textContent = "\u00a0";
|
|
73
|
+
return el;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function mergeMarkersWithErrorPriority(
|
|
77
|
+
errorMarkers: RangeSet<ElementMarker>,
|
|
78
|
+
warningMarkers: RangeSet<ElementMarker>,
|
|
79
|
+
): RangeSet<ElementMarker> {
|
|
80
|
+
const byPos = new Map<number, ElementMarker>();
|
|
81
|
+
|
|
82
|
+
const warningIter = warningMarkers.iter();
|
|
83
|
+
while (warningIter.value) {
|
|
84
|
+
byPos.set(warningIter.from, warningIter.value);
|
|
85
|
+
warningIter.next();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Errors overwrite warnings on the same line.
|
|
89
|
+
const errorIter = errorMarkers.iter();
|
|
90
|
+
while (errorIter.value) {
|
|
91
|
+
byPos.set(errorIter.from, errorIter.value);
|
|
92
|
+
errorIter.next();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const ranges: ReturnType<ElementMarker["range"]>[] = [];
|
|
96
|
+
const sortedPos = Array.from(byPos.keys()).sort((a, b) => a - b);
|
|
97
|
+
for (const pos of sortedPos) {
|
|
98
|
+
const marker = byPos.get(pos);
|
|
99
|
+
if (marker) ranges.push(marker.range(pos));
|
|
100
|
+
}
|
|
101
|
+
return RangeSet.of(ranges, true);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Fields are defined once so that they are shared between all editor instances.
|
|
105
|
+
const gutterFields = new Map<GutterName, StateField<RangeSet<ElementMarker>>>();
|
|
106
|
+
for (const name of GUTTER_NAMES) {
|
|
107
|
+
gutterFields.set(name, createMarkerField(name));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const gutterExtensions: Extension[] = [
|
|
111
|
+
gutterFields.get("gutterErrorBar")!,
|
|
112
|
+
gutterFields.get("gutterConstructWarning")!,
|
|
113
|
+
gutter({
|
|
114
|
+
class: `cm-gutter-${RENDER_GUTTER_NAME} CodeMirror-${RENDER_GUTTER_NAME}`,
|
|
115
|
+
markers: (view) =>
|
|
116
|
+
mergeMarkersWithErrorPriority(
|
|
117
|
+
view.state.field(gutterFields.get("gutterErrorBar")!),
|
|
118
|
+
view.state.field(gutterFields.get("gutterConstructWarning")!),
|
|
119
|
+
),
|
|
120
|
+
initialSpacer: () => new ElementMarker(spacerElement()),
|
|
121
|
+
}),
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
/** Extension enabling the yasqe marker gutters */
|
|
125
|
+
export function markerGutters(): Extension {
|
|
126
|
+
return gutterExtensions;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Set (or remove when `element` is null) the marker of `line` (0-based) in the given gutter */
|
|
130
|
+
export function setGutterMarker(view: EditorView, line: number, name: GutterName, element: HTMLElement | null) {
|
|
131
|
+
view.dispatch({ effects: setMarkerEffect.of({ gutter: name, line, element }) });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Remove all markers from the given gutter */
|
|
135
|
+
export function clearGutter(view: EditorView, name: GutterName) {
|
|
136
|
+
view.dispatch({ effects: clearGutterEffect.of(name) });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Returns the marker elements of the gutter, keyed by 0-based line number */
|
|
140
|
+
export function getGutterMarkers(state: EditorState, name: GutterName): Map<number, HTMLElement> {
|
|
141
|
+
const result = new Map<number, HTMLElement>();
|
|
142
|
+
const field = gutterFields.get(name);
|
|
143
|
+
if (!field) return result;
|
|
144
|
+
const set = state.field(field, false);
|
|
145
|
+
if (!set) return result;
|
|
146
|
+
const iter = set.iter();
|
|
147
|
+
while (iter.value) {
|
|
148
|
+
result.set(state.doc.lineAt(iter.from).number - 1, iter.value.element);
|
|
149
|
+
iter.next();
|
|
150
|
+
}
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keymap helpers for the CodeMirror 6 based editor.
|
|
3
|
+
*
|
|
4
|
+
* Yasqe historically exposed key bindings using CodeMirror 5 key names
|
|
5
|
+
* (e.g. "Shift-Ctrl-K", "Cmd-Enter", "Esc"). To stay backwards compatible we
|
|
6
|
+
* accept those names in the `extraKeys` config option and convert them to the
|
|
7
|
+
* CodeMirror 6 keymap format.
|
|
8
|
+
*/
|
|
9
|
+
import { Extension, Prec } from "@codemirror/state";
|
|
10
|
+
import { EditorView, KeyBinding, keymap } from "@codemirror/view";
|
|
11
|
+
import { defaultKeymap, historyKeymap, indentWithTab, indentLess, indentMore, insertTab } from "@codemirror/commands";
|
|
12
|
+
import { searchKeymap } from "@codemirror/search";
|
|
13
|
+
import { foldKeymap } from "@codemirror/language";
|
|
14
|
+
import { closeBracketsKeymap } from "@codemirror/autocomplete";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A key handler in the (CM5-compatible) `extraKeys` config. It receives the
|
|
18
|
+
* Yasqe instance. Returning `false` lets the event propagate to the next
|
|
19
|
+
* binding; anything else is considered "handled".
|
|
20
|
+
*/
|
|
21
|
+
export type ExtraKeyHandler<T = any> = (yasqe: T) => any;
|
|
22
|
+
export type ExtraKeys<T = any> = { [key: string]: ExtraKeyHandler<T> | string };
|
|
23
|
+
|
|
24
|
+
const MODIFIERS = new Set(["shift", "ctrl", "cmd", "alt", "mod", "meta"]);
|
|
25
|
+
|
|
26
|
+
const KEY_NAME_MAP: { [cm5: string]: string } = {
|
|
27
|
+
esc: "Escape",
|
|
28
|
+
space: "Space",
|
|
29
|
+
enter: "Enter",
|
|
30
|
+
tab: "Tab",
|
|
31
|
+
backspace: "Backspace",
|
|
32
|
+
delete: "Delete",
|
|
33
|
+
del: "Delete",
|
|
34
|
+
insert: "Insert",
|
|
35
|
+
home: "Home",
|
|
36
|
+
end: "End",
|
|
37
|
+
pageup: "PageUp",
|
|
38
|
+
pagedown: "PageDown",
|
|
39
|
+
left: "ArrowLeft",
|
|
40
|
+
right: "ArrowRight",
|
|
41
|
+
up: "ArrowUp",
|
|
42
|
+
down: "ArrowDown",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Convert a CodeMirror 5 style key name (e.g. "Shift-Ctrl-K") to a
|
|
47
|
+
* CodeMirror 6 key name ("Shift-Ctrl-k").
|
|
48
|
+
*
|
|
49
|
+
* CM6 uses `event.key` values, so single characters must be lower case
|
|
50
|
+
* (unless shift is pressed), and special keys use their DOM names.
|
|
51
|
+
*/
|
|
52
|
+
export function convertKeyName(cm5Name: string): string {
|
|
53
|
+
const parts = cm5Name.split(/-(?!$)/);
|
|
54
|
+
const modifiers: string[] = [];
|
|
55
|
+
let key = parts[parts.length - 1];
|
|
56
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
57
|
+
const mod = parts[i].toLowerCase();
|
|
58
|
+
if (mod === "meta") modifiers.push("Cmd");
|
|
59
|
+
else if (MODIFIERS.has(mod)) modifiers.push(parts[i][0].toUpperCase() + mod.slice(1));
|
|
60
|
+
}
|
|
61
|
+
const lowerKey = key.toLowerCase();
|
|
62
|
+
if (KEY_NAME_MAP[lowerKey]) {
|
|
63
|
+
key = KEY_NAME_MAP[lowerKey];
|
|
64
|
+
} else if (/^f\d{1,2}$/i.test(key)) {
|
|
65
|
+
key = key.toUpperCase();
|
|
66
|
+
} else if (key.length === 1) {
|
|
67
|
+
// Letters are lower case in CM6 (they match event.key). When shift is part of
|
|
68
|
+
// the binding CM6 also accepts the lower case form and derives the shifted variant.
|
|
69
|
+
key = key.toLowerCase();
|
|
70
|
+
}
|
|
71
|
+
return [...modifiers, key].join("-");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Build a keymap extension from a CM5-style `extraKeys` object.
|
|
76
|
+
*
|
|
77
|
+
* @param extraKeys CM5-style key -> handler map
|
|
78
|
+
* @param getYasqe returns the Yasqe instance (passed to the handler)
|
|
79
|
+
* @param commands named commands that string handlers may refer to
|
|
80
|
+
*/
|
|
81
|
+
export function extraKeysExtension<T>(
|
|
82
|
+
extraKeys: ExtraKeys<T> | undefined,
|
|
83
|
+
getYasqe: () => T,
|
|
84
|
+
commands: { [name: string]: (yasqe: T) => any } = {},
|
|
85
|
+
): Extension {
|
|
86
|
+
if (!extraKeys) return [];
|
|
87
|
+
const bindings: KeyBinding[] = [];
|
|
88
|
+
for (const cm5Key of Object.keys(extraKeys)) {
|
|
89
|
+
const handler = extraKeys[cm5Key];
|
|
90
|
+
const fn: ExtraKeyHandler<T> | undefined = typeof handler === "string" ? commands[handler] : handler;
|
|
91
|
+
if (!fn) continue;
|
|
92
|
+
bindings.push({
|
|
93
|
+
key: convertKeyName(cm5Key),
|
|
94
|
+
run: () => {
|
|
95
|
+
const result = fn(getYasqe());
|
|
96
|
+
return result !== false;
|
|
97
|
+
},
|
|
98
|
+
preventDefault: true,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
// Highest precedence so user defined keys win over the built-in ones.
|
|
102
|
+
return Prec.highest(keymap.of(bindings));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Tab handling that mimics CM5's `tabMode: "indent"` behaviour: Tab indents,
|
|
107
|
+
* Shift-Tab dedents. When `tabMode` is anything else, tab inserts a tab
|
|
108
|
+
* character (or spaces when `indentWithTabs` is false).
|
|
109
|
+
*/
|
|
110
|
+
export function tabKeymap(tabMode: string | undefined): Extension {
|
|
111
|
+
if (tabMode === "indent") {
|
|
112
|
+
return keymap.of([
|
|
113
|
+
{
|
|
114
|
+
key: "Tab",
|
|
115
|
+
run: (view: EditorView) => {
|
|
116
|
+
// Indent selection when something is selected, otherwise behave like indentMore.
|
|
117
|
+
return indentMore(view);
|
|
118
|
+
},
|
|
119
|
+
shift: indentLess,
|
|
120
|
+
},
|
|
121
|
+
]);
|
|
122
|
+
}
|
|
123
|
+
if (tabMode === "default") return keymap.of([indentWithTab]);
|
|
124
|
+
return keymap.of([{ key: "Tab", run: insertTab, shift: indentLess }]);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The default set of key bindings: standard editing, history, search, folding,
|
|
129
|
+
* bracket closing and autocompletion.
|
|
130
|
+
*/
|
|
131
|
+
export function defaultKeymaps(): Extension {
|
|
132
|
+
return keymap.of([...closeBracketsKeymap, ...defaultKeymap, ...searchKeymap, ...historyKeymap, ...foldKeymap]);
|
|
133
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CodeMirror 6 language definition for SPARQL 1.1, built on top of the
|
|
3
|
+
* legacy (stream based) LL(1) tokenizer in ../../grammar/tokenizer.ts.
|
|
4
|
+
*
|
|
5
|
+
* Every token style emitted by the tokenizer gets its own lezer Tag and a
|
|
6
|
+
* matching `cm-<style>` CSS class so the existing style sheets keep working.
|
|
7
|
+
*/
|
|
8
|
+
import { StreamLanguage, HighlightStyle, syntaxHighlighting, LanguageSupport } from "@codemirror/language";
|
|
9
|
+
import { Tag } from "@lezer/highlight";
|
|
10
|
+
import type { Extension } from "@codemirror/state";
|
|
11
|
+
import sparqlTokenizer, {
|
|
12
|
+
TOKEN_STYLES,
|
|
13
|
+
TokenStyle,
|
|
14
|
+
State as TokenizerState,
|
|
15
|
+
TokenizerConfig,
|
|
16
|
+
} from "../../grammar/tokenizer";
|
|
17
|
+
|
|
18
|
+
export type { TokenizerState };
|
|
19
|
+
|
|
20
|
+
export const sparqlTags: Record<TokenStyle, Tag> = TOKEN_STYLES.reduce(
|
|
21
|
+
(acc, style) => {
|
|
22
|
+
acc[style] = Tag.define();
|
|
23
|
+
return acc;
|
|
24
|
+
},
|
|
25
|
+
{} as Record<TokenStyle, Tag>,
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
export function createSparqlParser(config: TokenizerConfig = {}) {
|
|
29
|
+
const parser = sparqlTokenizer(config);
|
|
30
|
+
parser.tokenTable = sparqlTags;
|
|
31
|
+
return parser;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const sparqlLanguage = StreamLanguage.define<TokenizerState>(createSparqlParser());
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Highlight style mapping each SPARQL token tag to the classic CodeMirror 5 class name (`cm-keyword`, `cm-atom`, ...).
|
|
38
|
+
*/
|
|
39
|
+
export const sparqlHighlightStyle = HighlightStyle.define(
|
|
40
|
+
TOKEN_STYLES.map((style) => ({ tag: sparqlTags[style], class: `cm-${style}` })),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
export function sparql(): LanguageSupport {
|
|
44
|
+
return new LanguageSupport(sparqlLanguage, [syntaxHighlighting(sparqlHighlightStyle)]);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const sparqlExtensions = (): Extension => sparql();
|