@matdata/yasqe 5.20.2 → 6.0.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.
Files changed (76) hide show
  1. package/CHANGELOG.md +1 -1
  2. package/README.md +76 -59
  3. package/build/prefixes.json +2210 -2150
  4. package/build/ts/grammar/tokenizer.d.ts +9 -3
  5. package/build/ts/grammar/tokenizer.js +34 -6
  6. package/build/ts/grammar/tokenizer.js.map +1 -1
  7. package/build/ts/src/autocompleters/index.d.ts +4 -2
  8. package/build/ts/src/autocompleters/index.js +9 -33
  9. package/build/ts/src/autocompleters/index.js.map +1 -1
  10. package/build/ts/src/autocompleters/variables.js +17 -19
  11. package/build/ts/src/autocompleters/variables.js.map +1 -1
  12. package/build/ts/src/defaults.d.ts +17 -53
  13. package/build/ts/src/defaults.js +2 -5
  14. package/build/ts/src/defaults.js.map +1 -1
  15. package/build/ts/src/editor/autocompletion.d.ts +32 -0
  16. package/build/ts/src/editor/autocompletion.js +94 -0
  17. package/build/ts/src/editor/autocompletion.js.map +1 -0
  18. package/build/ts/src/editor/doc.d.ts +32 -0
  19. package/build/ts/src/editor/doc.js +121 -0
  20. package/build/ts/src/editor/doc.js.map +1 -0
  21. package/build/ts/src/editor/facade.d.ts +94 -0
  22. package/build/ts/src/editor/facade.js +392 -0
  23. package/build/ts/src/editor/facade.js.map +1 -0
  24. package/build/ts/src/editor/folding.d.ts +16 -0
  25. package/build/ts/src/editor/folding.js +190 -0
  26. package/build/ts/src/editor/folding.js.map +1 -0
  27. package/build/ts/src/editor/gutters.d.ts +8 -0
  28. package/build/ts/src/editor/gutters.js +113 -0
  29. package/build/ts/src/editor/gutters.js.map +1 -0
  30. package/build/ts/src/editor/keymap.d.ts +11 -0
  31. package/build/ts/src/editor/keymap.js +88 -0
  32. package/build/ts/src/editor/keymap.js.map +1 -0
  33. package/build/ts/src/editor/language.d.ts +11 -0
  34. package/build/ts/src/editor/language.js +19 -0
  35. package/build/ts/src/editor/language.js.map +1 -0
  36. package/build/ts/src/editor/tokenizerRunner.d.ts +37 -0
  37. package/build/ts/src/editor/tokenizerRunner.js +115 -0
  38. package/build/ts/src/editor/tokenizerRunner.js.map +1 -0
  39. package/build/ts/src/index.d.ts +49 -100
  40. package/build/ts/src/index.js +179 -100
  41. package/build/ts/src/index.js.map +1 -1
  42. package/build/ts/src/prefixes.json +2306 -2237
  43. package/build/ts/src/tooltip.js +9 -6
  44. package/build/ts/src/tooltip.js.map +1 -1
  45. package/build/yasqe.min.css +1 -1
  46. package/build/yasqe.min.css.map +3 -3
  47. package/build/yasqe.min.js +76 -91
  48. package/build/yasqe.min.js.map +4 -4
  49. package/grammar/tokenizer.ts +62 -8
  50. package/package.json +11 -6
  51. package/src/autocompleters/index.ts +24 -53
  52. package/src/autocompleters/show-hint.scss +17 -9
  53. package/src/autocompleters/variables.ts +20 -24
  54. package/src/defaults.ts +2 -6
  55. package/src/editor/autocompletion.ts +147 -0
  56. package/src/editor/doc.ts +151 -0
  57. package/src/editor/facade.ts +558 -0
  58. package/src/editor/folding.ts +225 -0
  59. package/src/editor/gutters.ts +152 -0
  60. package/src/editor/keymap.ts +133 -0
  61. package/src/editor/language.ts +47 -0
  62. package/src/editor/tokenizerRunner.ts +198 -0
  63. package/src/index.ts +193 -127
  64. package/src/prefixes.json +2210 -2150
  65. package/src/scss/buttons.scss +4 -2
  66. package/src/scss/codemirrorMods.scss +107 -11
  67. package/src/scss/yasqe.scss +4 -2
  68. package/src/tooltip.ts +16 -13
  69. package/build/ts/src/CodeMirror.d.ts +0 -30
  70. package/build/ts/src/CodeMirror.js +0 -20
  71. package/build/ts/src/CodeMirror.js.map +0 -1
  72. package/build/ts/src/prefixFold.d.ts +0 -8
  73. package/build/ts/src/prefixFold.js +0 -87
  74. package/build/ts/src/prefixFold.js.map +0 -1
  75. package/src/CodeMirror.ts +0 -54
  76. package/src/prefixFold.ts +0 -94
@@ -1,6 +1,6 @@
1
- import CodeMirror from "codemirror";
1
+ import type { StringStream, StreamParser, IndentContext } from "@codemirror/language";
2
2
  export interface State {
3
- tokenize: (stream: CodeMirror.StringStream, state: State) => string;
3
+ tokenize: (stream: StringStream, state: State) => string;
4
4
  inLiteral: "SINGLE" | "DOUBLE" | undefined;
5
5
  errorStartPos: number | undefined;
6
6
  errorEndPos: number | undefined;
@@ -52,7 +52,54 @@ export interface Token {
52
52
  start: number;
53
53
  }
54
54
  import * as grammar from "./_tokenizer-table.js";
55
- export default function (config: CodeMirror.EditorConfiguration): CodeMirror.Mode<State> {
55
+
56
+ export interface TokenizerConfig {
57
+ indentUnit?: number;
58
+ }
59
+
60
+ /**
61
+ * All token style names that the tokenizer can emit (the space-separated
62
+ * parts of the `style` strings). Exported so that the CodeMirror 6 language
63
+ * definition can build a token table and a highlight style for them.
64
+ */
65
+ export const TOKEN_STYLES = [
66
+ "ws",
67
+ "comment",
68
+ "variable-3",
69
+ "atom",
70
+ "meta",
71
+ "number",
72
+ "string",
73
+ "string-2",
74
+ "punc",
75
+ "keyword",
76
+ "error",
77
+ "bracket",
78
+ "bracket-level-0",
79
+ "bracket-level-1",
80
+ "bracket-level-2",
81
+ "bracket-level-3",
82
+ "bracket-paren",
83
+ "bracket-mismatch",
84
+ ] as const;
85
+ export type TokenStyle = (typeof TOKEN_STYLES)[number];
86
+
87
+ export function copyState(s: State): State {
88
+ return {
89
+ ...s,
90
+ possibleCurrent: s.possibleCurrent.slice(),
91
+ possibleNext: s.possibleNext.slice(),
92
+ stack: s.stack.slice(),
93
+ variables: { ...s.variables },
94
+ prefixes: { ...s.prefixes },
95
+ constructVariables: { ...s.constructVariables },
96
+ whereVariables: { ...s.whereVariables },
97
+ pendingToken: s.pendingToken ? { ...s.pendingToken } : undefined,
98
+ bracketStack: s.bracketStack.map((b) => ({ ...b })),
99
+ };
100
+ }
101
+
102
+ export default function (config: TokenizerConfig = {}): StreamParser<State> {
56
103
  const ll1_table = grammar.table;
57
104
 
58
105
  const IRI_REF = '<[^<>"`|{}^\\\x00-\x20]*>';
@@ -324,7 +371,7 @@ export default function (config: CodeMirror.EditorConfiguration): CodeMirror.Mod
324
371
  return possibles;
325
372
  }
326
373
 
327
- function tokenBase(stream: CodeMirror.StringStream, state: State) {
374
+ function tokenBase(stream: StringStream, state: State): string {
328
375
  // If we have a pending token from a previous split, consume it from stream and return style
329
376
  if (state.pendingToken) {
330
377
  const token = state.pendingToken;
@@ -764,7 +811,7 @@ export default function (config: CodeMirror.EditorConfiguration): CodeMirror.Mod
764
811
  // "*[;,?[or([verbPath,verbSimple]),objectList]]": 1,
765
812
  };
766
813
 
767
- function indent(state: State, textAfter: string) {
814
+ function indent(state: State, textAfter: string, indentUnit: number) {
768
815
  //just avoid we don't indent multi-line literals
769
816
  if (state.inLiteral) return 0;
770
817
  if (
@@ -800,12 +847,21 @@ export default function (config: CodeMirror.EditorConfiguration): CodeMirror.Mod
800
847
  n += dn;
801
848
  }
802
849
  }
803
- return n * (config.indentUnit ?? 2);
850
+ return n * indentUnit;
804
851
  }
805
852
  }
806
853
 
807
854
  return {
855
+ name: "sparql11",
808
856
  token: tokenBase,
857
+ copyState,
858
+ indent: (state: State, textAfter: string, context: IndentContext) =>
859
+ indent(state, textAfter, context.unit ?? config.indentUnit ?? 2),
860
+ languageData: {
861
+ commentTokens: { line: "#" },
862
+ indentOnInput: /^\s*[\}\]\)]$/,
863
+ closeBrackets: { brackets: ["(", "[", "{", "'", '"'] },
864
+ },
809
865
  startState: function (): State {
810
866
  return {
811
867
  tokenize: tokenBase,
@@ -839,7 +895,5 @@ export default function (config: CodeMirror.EditorConfiguration): CodeMirror.Mod
839
895
  bracketLevel: 0,
840
896
  };
841
897
  },
842
- indent: indent,
843
- electricChars: "}])",
844
898
  };
845
899
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@matdata/yasqe",
3
- "description": "Yet Another SPARQL Query Editor",
4
- "version": "5.20.2",
3
+ "description": "MatGUI query editor: a modern SPARQL query editor (formerly Yasgui/YASQE).",
4
+ "version": "6.0.0",
5
5
  "main": "build/yasqe.min.js",
6
6
  "types": "build/ts/src/index.d.ts",
7
7
  "license": "MIT",
@@ -17,22 +17,27 @@
17
17
  "Semantic Web",
18
18
  "Linked Data"
19
19
  ],
20
- "bugs": "https://github.com/Matdata-eu/Yasgui/issues/",
20
+ "bugs": "https://github.com/Matdata-eu/Matgui/issues/",
21
21
  "repository": {
22
22
  "type": "git",
23
- "url": "https://github.com/Matdata-eu/Yasgui.git",
23
+ "url": "https://github.com/Matdata-eu/Matgui.git",
24
24
  "directory": "packages/yasqe"
25
25
  },
26
26
  "dependencies": {
27
+ "@codemirror/autocomplete": "^6.20.3",
28
+ "@codemirror/commands": "^6.11.0",
29
+ "@codemirror/language": "^6.12.4",
30
+ "@codemirror/search": "^6.7.2",
31
+ "@codemirror/state": "^6.7.2",
32
+ "@codemirror/view": "^6.43.10",
33
+ "@lezer/highlight": "^1.2.3",
27
34
  "@matdata/yasgui-utils": "^4.6.1",
28
- "codemirror": "^5.51.0",
29
35
  "leaflet": "^1.9.4",
30
36
  "lodash-es": "^4.17.15",
31
37
  "query-string": "^6.10.1",
32
38
  "sparql-formatter": "^1.0.2"
33
39
  },
34
40
  "devDependencies": {
35
- "@types/codemirror": "0.0.100",
36
41
  "@types/leaflet": "^1.9.21",
37
42
  "@types/lodash-es": "^4.17.3",
38
43
  "@types/node": "^22.5.4"
@@ -1,8 +1,7 @@
1
- import { default as Yasqe, Token, Hint, Position, Config, HintFn, HintConfig } from "../";
1
+ import { default as Yasqe, Token, Hint, Position, Config, HintFn, HintList } from "../";
2
2
  import Trie from "../trie";
3
3
  import { EventEmitter } from "events";
4
4
  import { take } from "lodash-es";
5
- import CodeMirror from "codemirror";
6
5
  import "./show-hint.scss";
7
6
  export interface CompleterConfig {
8
7
  onInitialize?: (this: CompleterConfig, yasqe: Yasqe) => void; //allows for e.g. registering event listeners in yasqe, like the prefix autocompleter does
@@ -173,39 +172,39 @@ export class Completer extends EventEmitter {
173
172
  });
174
173
  return Promise.resolve([]);
175
174
  }
176
- public autocomplete(fromAutoShow: boolean) {
175
+ /**
176
+ * Returns whether the cursor is currently at a position this completer can complete.
177
+ * Also takes care of showing/hiding the "press ctrl-space" notification.
178
+ */
179
+ public isValidCompletionPosition(): boolean {
180
+ return this.isValidPosition();
181
+ }
182
+
183
+ /**
184
+ * Returns a hint function for the current cursor position, or `false` when this completer should not
185
+ * (or cannot) show completions right now. The caller (Yasqe) is responsible for opening the completion popup.
186
+ */
187
+ public autocomplete(fromAutoShow: boolean): HintFn | false {
177
188
  //this part goes before the autoshow check, as we _would_ like notification showing to indicate a user can press ctrl-space
178
189
  if (!this.isValidPosition()) return false;
179
- const previousCompletionItem = this.yasqe.state.completionActive;
180
-
181
- // Showhint by defaults takes the autocomplete start position (the location of the cursor at the time of starting the autocompletion).
182
- const cursor = this.yasqe.getDoc().getCursor();
183
- if (
184
- // When the cursor goes before current completionItem (e.g. using arrow keys), it would close the autocompletions.
185
- // We want the autocompletion to be active at whatever point we are in the token, so let's modify this start pos with the start pos of the token
186
- previousCompletionItem &&
187
- cursor.sticky && // Is undefined at the end of the token, otherwise it is set as either "before" or "after" (The movement of the cursor)
188
- cursor.ch !== previousCompletionItem.startPos.ch
189
- ) {
190
- this.yasqe.state.completionActive.startPos = cursor;
191
- } else if (previousCompletionItem && !cursor.sticky && cursor.ch < previousCompletionItem.startPos.ch) {
192
- // A similar thing happens when pressing backspace, CodeMirror will close this autocomplete when 'startLen' changes downward
193
- cursor.sticky = previousCompletionItem.startPos.sticky;
194
- this.yasqe.state.completionActive.startPos.ch = cursor.ch;
195
- this.yasqe.state.completionActive.startLen--;
196
- }
197
190
  if (
198
191
  fromAutoShow && // from autoShow, i.e. this gets called each time the editor content changes
199
- (!this.config.autoShow || this.yasqe.state.completionActive) // Don't show and don't create a new instance when its already active
192
+ (!this.config.autoShow || this.yasqe.isAutocompletionActive()) // Don't show and don't create a new instance when its already active
200
193
  ) {
201
194
  return false;
202
195
  }
196
+ return this.getHintFn();
197
+ }
203
198
 
199
+ /**
200
+ * Hint function that computes the hints for the token at the current cursor position
201
+ */
202
+ public getHintFn(): HintFn {
204
203
  const getHints: HintFn = () => {
205
- return this.getHints(this.yasqe.getCompleteToken()).then((list) => {
204
+ return this.getHints(this.yasqe.getCompleteToken()).then((list): HintList => {
206
205
  const cur = this.yasqe.getDoc().getCursor();
207
206
  const token: AutocompletionToken = this.yasqe.getCompleteToken();
208
- const hintResult = {
207
+ return {
209
208
  list: list,
210
209
  from: <Position>{
211
210
  line: cur.line,
@@ -216,37 +215,9 @@ export class Completer extends EventEmitter {
216
215
  ch: token.end,
217
216
  },
218
217
  };
219
- CodeMirror.on(hintResult, "shown", () => {
220
- this.yasqe.emit("autocompletionShown", (this.yasqe as any).state.completionActive.widget);
221
- });
222
- CodeMirror.on(hintResult, "close", () => {
223
- this.yasqe.emit("autocompletionClose");
224
- });
225
- return hintResult;
226
218
  });
227
219
  };
228
-
229
- getHints.async = false; //in their code, async means using a callback
230
- //we always return a promise, which should be properly handled regardless of this val
231
- var hintConfig: HintConfig = {
232
- closeCharacters: /[\s>"]/,
233
- completeSingle: false,
234
- hint: getHints,
235
- container: this.yasqe.rootEl,
236
- // Override these actions back to use their default function
237
- // Otherwise these would navigate to the start/end of the suggestion list, while this can also be accomplished with PgUp and PgDn
238
- extraKeys: {
239
- Home: (yasqe, event) => {
240
- yasqe.getDoc().setCursor({ ch: 0, line: event.data.from.line });
241
- },
242
- End: (yasqe, event) => {
243
- yasqe.getDoc().setCursor({ ch: yasqe.getLine(event.data.to.line).length, line: event.data.to.line });
244
- },
245
- },
246
- ...this.yasqe.config.hintConfig,
247
- };
248
- this.yasqe.showHint(hintConfig);
249
- return true;
220
+ return getHints;
250
221
  }
251
222
  }
252
223
 
@@ -1,13 +1,11 @@
1
- //This is a copy of the codemirror show-hint css
2
- //Including this ourselves, as we don't want this css to be auto-scoped to yasqe
1
+ // Styling for the autocompletion tooltip (CodeMirror 6 `@codemirror/autocomplete`).
2
+ // The tooltip gets the legacy `CodeMirror-hints` class (see editor/autocompletion.ts) so that
3
+ // existing consumers / tests keep working.
4
+ // Including this ourselves, as we don't want this css to be auto-scoped to yasqe
5
+ .cm-tooltip.CodeMirror-hints,
3
6
  .CodeMirror-hints {
4
- position: absolute;
5
7
  z-index: 10;
6
8
  overflow: hidden;
7
- list-style: none;
8
-
9
- margin: 0;
10
- padding: 2px;
11
9
 
12
10
  -webkit-box-shadow: 2px 3px 5px rgba(0, 0, 0, 0.2);
13
11
  -moz-box-shadow: 2px 3px 5px rgba(0, 0, 0, 0.2);
@@ -18,10 +16,18 @@
18
16
  background: white;
19
17
  font-size: 90%;
20
18
  font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
21
- max-height: 20em;
22
- overflow-y: auto;
19
+
20
+ & > ul {
21
+ list-style: none;
22
+ margin: 0;
23
+ padding: 2px;
24
+ max-height: 20em;
25
+ overflow-y: auto;
26
+ font-family: inherit;
27
+ }
23
28
  }
24
29
 
30
+ .CodeMirror-hints > ul > li,
25
31
  .CodeMirror-hint {
26
32
  margin: 0;
27
33
  padding: 0 4px;
@@ -29,8 +35,10 @@
29
35
  white-space: pre;
30
36
  color: black;
31
37
  cursor: pointer;
38
+ line-height: 1.4;
32
39
  }
33
40
 
41
+ .CodeMirror-hints > ul > li[aria-selected],
34
42
  li.CodeMirror-hint-active {
35
43
  background: #08f;
36
44
  color: white;
@@ -1,4 +1,5 @@
1
1
  import * as autocompleter from "./";
2
+ import { runMode } from "../editor/tokenizerRunner";
2
3
 
3
4
  var conf: autocompleter.CompleterConfig = {
4
5
  name: "variables",
@@ -16,33 +17,28 @@ var conf: autocompleter.CompleterConfig = {
16
17
  if (!token || token.string.length == 0) return []; //nothing to autocomplete
17
18
  const distinctVars: { [varname: string]: any } = {};
18
19
  const vars: string[] = [];
19
- //do this outside of codemirror. I expect jquery to be faster here (just finding dom elements with classnames)
20
- //and: this'll still work when the query is incorrect (i.e., when simply typing '?')
21
- const atoms = yasqe.getWrapperElement().querySelectorAll(".cm-atom");
22
- for (let i = 0; i < atoms.length; i++) {
23
- const atom = atoms[i];
24
- var variable = atom.innerHTML;
25
- if (variable[0] === "?" || variable[0] === "$") {
26
- //ok, lets check if the next element in the div is an atom as well. In that case, they belong together (may happen sometimes when query is not syntactically valid)
27
- // var nextElClass = nextEl.attr("class");
28
- const nextEl: HTMLElement = <any>atom.nextSibling;
29
- if (nextEl && nextEl.className && nextEl.className.indexOf("cm-atom") >= 0) {
30
- variable += nextEl.innerText;
31
- }
32
- if (distinctVars[variable]) continue; //already in list
33
- //skip single questionmarks
34
- if (variable.length <= 1) continue;
20
+ // Tokenize the whole query (independent of what is rendered in the viewport), collecting all
21
+ // variable tokens. This still works when the query is syntactically incorrect (e.g. when simply typing '?').
22
+ const atoms: string[] = [];
23
+ runMode(yasqe.getValue(), (stringVal: string, style: string | null) => {
24
+ if (style === "atom" && (stringVal[0] === "?" || stringVal[0] === "$")) {
25
+ atoms.push(stringVal);
26
+ }
27
+ });
28
+ for (const variable of atoms) {
29
+ if (distinctVars[variable]) continue; //already in list
30
+ //skip single questionmarks
31
+ if (variable.length <= 1) continue;
35
32
 
36
- //it should match our token ofcourse
37
- if (variable.indexOf(token.string) !== 0) continue;
33
+ //it should match our token ofcourse
34
+ if (variable.indexOf(token.string) !== 0) continue;
38
35
 
39
- //skip exact matches
40
- if (variable === token.string) continue;
36
+ //skip exact matches
37
+ if (variable === token.string) continue;
41
38
 
42
- //store in map so we have a unique list
43
- distinctVars[variable] = true;
44
- vars.push(variable);
45
- }
39
+ //store in map so we have a unique list
40
+ distinctVars[variable] = true;
41
+ vars.push(variable);
46
42
  }
47
43
  return vars.sort();
48
44
  },
package/src/defaults.ts CHANGED
@@ -6,8 +6,6 @@
6
6
  */
7
7
  import { default as Yasqe, Config, PlainRequestConfig } from "./";
8
8
  import * as queryString from "query-string";
9
- import CodeMirror from "codemirror";
10
- //need to pass Yasqe object as argument, as the imported version might not have inherited all (e.g. `fold`) props of Codemirror yet
11
9
  export default function get() {
12
10
  const prefixCcApi = null; // null = use bundled prefix data (packages/yasqe/src/prefixes.json, refreshed each build)
13
11
  const config: Omit<Config, "requestConfig"> = {
@@ -23,11 +21,9 @@ SELECT * WHERE {
23
21
  tabMode: "indent",
24
22
  lineNumbers: true,
25
23
  lineWrapping: true,
26
- foldGutter: {
27
- rangeFinder: new (<any>CodeMirror).fold.combine((<any>CodeMirror).fold.brace, (<any>CodeMirror).fold.prefix),
28
- },
24
+ foldGutter: true, // Folding of braces and prefix blocks is handled by the built-in SPARQL fold service
29
25
  collapsePrefixesOnLoad: false,
30
- gutters: ["gutterErrorBar", "gutterConstructWarning", "CodeMirror-linenumbers", "CodeMirror-foldgutter"],
26
+ gutters: ["gutterErrorBar", "gutterConstructWarning"],
31
27
  matchBrackets: true,
32
28
  fixedGutter: true,
33
29
  syntaxErrorCheck: true,
@@ -0,0 +1,147 @@
1
+ /**
2
+ * CodeMirror 6 autocompletion bridge.
3
+ *
4
+ * YASQE's autocompleters (see ../autocompleters) were written against the CodeMirror 5
5
+ * `show-hint` addon: a "hint function" returns a list of `Hint`s together with the range
6
+ * that should be replaced. This module adapts that model onto `@codemirror/autocomplete`:
7
+ *
8
+ * - A single `CompletionSource` is registered. Whenever it is queried it asks the host
9
+ * (the `Yasqe` facade) for the currently active hint function and converts its result
10
+ * into a `CompletionResult`.
11
+ * - `Hint.from` / `Hint.to` are honoured per completion by using a custom `apply`.
12
+ * - Open/close transitions are observed through `completionStatus` and reported to the host
13
+ * so that the legacy `autocompletionShown` / `autocompletionClose` events keep working.
14
+ */
15
+ import { EditorState, Extension, Text } from "@codemirror/state";
16
+ import { EditorView, ViewUpdate } from "@codemirror/view";
17
+ import {
18
+ autocompletion,
19
+ Completion,
20
+ CompletionContext,
21
+ CompletionResult,
22
+ completionStatus,
23
+ completionKeymap,
24
+ startCompletion,
25
+ closeCompletion,
26
+ } from "@codemirror/autocomplete";
27
+ import { keymap } from "@codemirror/view";
28
+ import { Prec } from "@codemirror/state";
29
+ import type { Position } from "./tokenizerRunner";
30
+ import { posToOffset, offsetToPos } from "./doc";
31
+
32
+ export interface Hint {
33
+ text: string;
34
+ displayText?: string;
35
+ className?: string;
36
+ /** Kept for API compatibility; custom rendering is not supported by the CM6 bridge. */
37
+ render?: (el: HTMLElement, self: Hint, data: any) => void;
38
+ from?: Position;
39
+ to?: Position;
40
+ }
41
+ export interface HintList {
42
+ list: Hint[];
43
+ from: Position;
44
+ to: Position;
45
+ }
46
+ export type HintFn = { async?: boolean } & (() => Promise<HintList | undefined> | HintList | undefined);
47
+
48
+ export interface AutocompletionHost {
49
+ /** Returns the hint function that should be used for the current completion session (if any) */
50
+ getHintFn: () => HintFn | undefined;
51
+ onShown?: (tooltipDom: HTMLElement | null) => void;
52
+ onClose?: () => void;
53
+ }
54
+
55
+ export { posToOffset, offsetToPos };
56
+
57
+ function hintToCompletion(hint: Hint): Completion {
58
+ return {
59
+ label: hint.displayText || hint.text,
60
+ type: hint.className,
61
+ apply: (view: EditorView, _completion: Completion, from: number, to: number) => {
62
+ const doc = view.state.doc;
63
+ const replaceFrom = hint.from ? posToOffset(doc, hint.from) : from;
64
+ const replaceTo = hint.to ? posToOffset(doc, hint.to) : to;
65
+ const start = Math.min(replaceFrom, replaceTo);
66
+ const end = Math.max(replaceFrom, replaceTo);
67
+ view.dispatch({
68
+ changes: { from: start, to: end, insert: hint.text },
69
+ selection: { anchor: start + hint.text.length },
70
+ userEvent: "input.complete",
71
+ });
72
+ },
73
+ };
74
+ }
75
+
76
+ export function hintListToCompletionResult(hintList: HintList, doc: Text): CompletionResult | null {
77
+ if (!hintList || !hintList.list || hintList.list.length === 0) return null;
78
+ const from = posToOffset(doc, hintList.from);
79
+ const to = posToOffset(doc, hintList.to);
80
+ return {
81
+ from: Math.min(from, to),
82
+ to: Math.max(from, to),
83
+ options: hintList.list.map(hintToCompletion),
84
+ // Completers already filtered and ordered their suggestions
85
+ filter: false,
86
+ };
87
+ }
88
+
89
+ function createCompletionSource(host: AutocompletionHost) {
90
+ return async (context: CompletionContext): Promise<CompletionResult | null> => {
91
+ const hintFn = host.getHintFn();
92
+ if (!hintFn) return null;
93
+ const result = await hintFn();
94
+ if (!result || context.aborted) return null;
95
+ return hintListToCompletionResult(result, context.state.doc);
96
+ };
97
+ }
98
+
99
+ /**
100
+ * Autocompletion extension. `host.getHintFn()` is consulted every time CodeMirror queries for completions.
101
+ */
102
+ export function sparqlAutocompletion(host: AutocompletionHost): Extension {
103
+ const statusListener = EditorView.updateListener.of((update: ViewUpdate) => {
104
+ const before = completionStatus(update.startState);
105
+ const after = completionStatus(update.state);
106
+ if (before === null && after !== null) {
107
+ host.onShown?.(update.view.dom.querySelector<HTMLElement>(".cm-tooltip-autocomplete"));
108
+ } else if (before !== null && after === null) {
109
+ host.onClose?.();
110
+ }
111
+ });
112
+ return [
113
+ autocompletion({
114
+ override: [createCompletionSource(host)],
115
+ // YASQE decides itself when to open the popup (see Yasqe.autocomplete)
116
+ activateOnTyping: false,
117
+ closeOnBlur: true,
118
+ // We register the navigation keys ourselves (below) so that `Ctrl-Space` is exclusively handled by
119
+ // YASQE's `extraKeys` (which builds the hint function before opening the popup).
120
+ defaultKeymap: false,
121
+ icons: false,
122
+ // CM5 rendered the complete hint list; CM6 pages by 100 items by default. Render everything so that
123
+ // consumers that inspect the popup (and the existing tests) see the full list.
124
+ maxRenderedOptions: 100000,
125
+ // CM5 accepted a completion immediately after the popup opened; CM6 ignores Enter/arrows during the first
126
+ // 75ms (and lets the key fall through, inserting a newline). Keep the CM5 behaviour.
127
+ interactionDelay: 0,
128
+ // Keep the CM5 class names around so existing styling / tests keep working
129
+ tooltipClass: () => "CodeMirror-hints",
130
+ optionClass: (completion) => (completion.type ? completion.type : ""),
131
+ }),
132
+ Prec.highest(keymap.of(completionKeymap.filter((binding) => binding.key !== "Ctrl-Space"))),
133
+ statusListener,
134
+ ];
135
+ }
136
+
137
+ export function isCompletionActive(state: EditorState): boolean {
138
+ return completionStatus(state) !== null;
139
+ }
140
+
141
+ export function openCompletion(view: EditorView): boolean {
142
+ return startCompletion(view);
143
+ }
144
+
145
+ export function hideCompletion(view: EditorView): boolean {
146
+ return closeCompletion(view);
147
+ }