@aardarch/aardink-web 0.6.0-rc2 → 0.6.0-rc3

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/README.md CHANGED
@@ -44,6 +44,18 @@ const savedAt = editor.getAlternativeVersionId(); // unsaved while it differs
44
44
  editor.dispose();
45
45
  ```
46
46
 
47
+ `setValue` is for opening a file: it clears the undo history. A change the user should be able to
48
+ undo, such as an assistant's rewrite, goes through `replaceValue` (the whole text) or Monaco's
49
+ `executeEdits` (ranges). Either is one undo step that keeps the history before it, and changes
50
+ only what differs, so carets and folds in unchanged text stay put:
51
+
52
+ ```ts
53
+ editor.replaceValue(rewritten); // undo() brings the old text back
54
+ editor.executeEdits('assistant', [
55
+ { range: { startLineNumber: 3, startColumn: 1, endLineNumber: 3, endColumn: 9 }, text: '<button/>' },
56
+ ]);
57
+ ```
58
+
47
59
  A language of your own is a Monarch grammar registered before use, optionally extending a built-in
48
60
  language's completions, diagnostics and folding, with Monaco's `defineTheme` for its colours:
49
61
 
@@ -52,14 +64,24 @@ import { defineTheme, registerLanguage } from '@aardarch/aardink-web';
52
64
 
53
65
  await registerLanguage(
54
66
  { id: 'toy', extends: 'xml', grammar: { tokenizer: { root: [[/\bshout\b/, 'keyword']] } } },
55
- { provideCompletionItems: (text, line, column) => [{ label: 'shout', kind: 'element' }] },
67
+ {
68
+ triggerCharacters: ['{'], // besides XML's
69
+ provideCompletionItems: (text, line, column) => [{ label: 'shout', kind: 'element' }],
70
+ },
56
71
  );
57
72
  await defineTheme('toy-dark', { base: 'vs-dark', inherit: true, rules: [{ token: 'keyword', foreground: 'ff00ff' }] });
58
73
  ```
59
74
 
75
+ Completion items take Monaco's `filterText`, `sortText` and `range` too, and
76
+ `inheritCompletions: false` in the definition leaves out the extended language's completions
77
+ (XML's are Android-flavoured) while keeping its diagnostics and folding. A hover can carry an
78
+ `example`, shown as code.
79
+
60
80
  The editor fills its container, so give the container a size. `createEditor` is async because
61
81
  the first call loads the WebAssembly module (about 13 MB, 4.7 MB gzipped); every editor on the
62
- page shares that one module. See [`index.d.ts`](./index.d.ts) for the full API, and the
82
+ page shares that one module. If that load fails, the call rejects and the next call loads again
83
+ (though Chrome keeps a failed module download until the page is reloaded). See
84
+ [`index.d.ts`](./index.d.ts) for the full API, and the
63
85
  [integration guide](https://github.com/aardarch/aardink/blob/main/docs/WEB_INTEGRATION.md) for how
64
86
  Monaco's options and calls map onto it.
65
87
 
package/index.d.ts CHANGED
@@ -72,14 +72,49 @@ export interface AardinkContentChange {
72
72
  versionId: number;
73
73
  isUndoing: boolean;
74
74
  isRedoing: boolean;
75
- /** The whole text was replaced (`setValue`). */
75
+ /** The whole text was replaced and the undo history cleared (`setValue`); `false` for `replaceValue` and `executeEdits`. */
76
76
  isFlush: boolean;
77
77
  }
78
78
 
79
+ /**
80
+ * Monaco's `IIdentifiedSingleEditOperation`, for `executeEdits`: replaces `range` with `text`.
81
+ * `null` or `''` deletes the range; an empty range inserts. A position past the end of its line is
82
+ * the line's end, and one past the last line the end of the text, as Monaco validates them.
83
+ */
84
+ export interface AardinkEditOperation {
85
+ range: AardinkRange;
86
+ text: string | null;
87
+ /** Accepted for Monaco's shape, and ignored. */
88
+ forceMoveMarkers?: boolean;
89
+ }
90
+
79
91
  export interface AardinkEditor {
80
92
  getValue(): string;
81
- /** Replaces the whole text and clears undo history. Fires the change listeners. */
93
+ /**
94
+ * Replaces the whole text and clears undo history, as when a file is opened. Fires the change
95
+ * listeners with `isFlush: true`. For a change the user should be able to undo, use
96
+ * `replaceValue` or `executeEdits`.
97
+ */
82
98
  setValue(text: string): void;
99
+ /**
100
+ * Replaces the whole text as one undo step, keeping the history before it: for a rewrite the user
101
+ * should be able to undo, such as an assistant's. Only what differs changes (lines compared, then
102
+ * characters), so carets, folds and diagnostics in unchanged text stay where they are. The change
103
+ * listeners get it with `isFlush: false` and a new `versionId`; `undo()` brings the old text back,
104
+ * and `getAlternativeVersionId()` its value then. Works in a read-only editor, as `setValue` does.
105
+ * Returns whether the text changed.
106
+ */
107
+ replaceValue(text: string): boolean;
108
+ /**
109
+ * Monaco's `executeEdits`: applies `edits` (which must not overlap) as one undo step, keeping the
110
+ * history, each reduced to the characters it really changes so carets and folds elsewhere stay
111
+ * put. Then selects `endCursorState` when given (positions in the text after the edits, as in
112
+ * Monaco), else carries the selections through the edits;
113
+ * a cursor-state computer function is not called. `source` is accepted for Monaco's shape.
114
+ * Returns `false`, changing nothing, in a read-only editor, as Monaco does; throws for edits that
115
+ * overlap. Reported to the change listeners as for `replaceValue`.
116
+ */
117
+ executeEdits(source: string | null | undefined, edits: AardinkEditOperation[], endCursorState?: AardinkSelection[] | ((...args: unknown[]) => unknown)): boolean;
83
118
  updateOptions(options: Partial<AardinkOptions>): void;
84
119
  /** Full text after every change, at most once per frame, and what the change was. Returns an unsubscribe function. */
85
120
  onDidChangeContent(listener: (text: string, change: AardinkContentChange) => void): () => void;
@@ -92,11 +127,24 @@ export interface AardinkEditor {
92
127
  setDiagnostics(diagnostics: AardinkDiagnostic[] | null): void;
93
128
  /**
94
129
  * The language's own diagnostics each time they are collected: 500 ms after the editor appears,
95
- * and after each pause in typing. Not called while a list from `setDiagnostics` is shown.
130
+ * after each pause in typing, and on `revalidate()`. Not called while a list from
131
+ * `setDiagnostics` is shown.
96
132
  * Returns an unsubscribe function.
97
133
  */
98
134
  onDidChangeDiagnostics(listener: (diagnostics: AardinkDiagnostic[]) => void): () => void;
135
+ /**
136
+ * Collects the language's own diagnostics again now, without waiting for an edit: for when what
137
+ * your `provideDiagnostics` depends on outside the text has changed (another file, a setting).
138
+ * They reach `onDidChangeDiagnostics` as usual. Does nothing while a list from `setDiagnostics`
139
+ * is shown.
140
+ */
141
+ revalidate(): void;
99
142
  revealPosition(line: number, column: number): void;
143
+ /**
144
+ * Opens the find panel and moves the keyboard focus to its find field, from wherever on the
145
+ * page it was. Enter goes to the next match, Shift+Enter to the previous one, and Escape closes
146
+ * the panel and gives the focus back to the text.
147
+ */
100
148
  showFind(): void;
101
149
  undo(): boolean;
102
150
  redo(): boolean;
@@ -133,6 +181,13 @@ export interface AardinkLanguageDefinition {
133
181
  grammar: object;
134
182
  /** A built-in or registered language whose completions, diagnostics, hover and folding this one takes too, e.g. `'xml'`. */
135
183
  extends?: string;
184
+ /**
185
+ * With `extends`: `false` leaves out that language's completions (and, when the providers give
186
+ * `triggerCharacters`, its trigger characters), keeping its diagnostics, hover, folding and the
187
+ * rest. `true` by default. To leave them out for one request only, answer with
188
+ * `{ suggestions, exclusive: true }`.
189
+ */
190
+ inheritCompletions?: boolean;
136
191
  displayName?: string;
137
192
  extensions?: string[];
138
193
  /** As in Monaco's language configuration: what Ctrl/Cmd+/ comments with. */
@@ -148,25 +203,82 @@ export declare const CompletionItemInsertTextRule: {
148
203
  readonly InsertAsSnippet: 4;
149
204
  };
150
205
 
206
+ /** Monaco's `IRange`: 1-based lines and columns, `endColumn` exclusive. */
207
+ export interface AardinkRange {
208
+ startLineNumber: number;
209
+ startColumn: number;
210
+ endLineNumber: number;
211
+ endColumn: number;
212
+ }
213
+
151
214
  export interface AardinkCompletionItem {
152
215
  label: string;
153
216
  /** What accepting it types; the label when absent. A snippet with `InsertAsSnippet`. */
154
217
  insertText?: string;
155
218
  /** `CompletionItemInsertTextRule` flags, as in Monaco. */
156
219
  insertTextRules?: number;
157
- kind?: 'element' | 'attribute' | 'value' | 'snippet' | 'module' | 'property';
220
+ /**
221
+ * The icon. `'function'` is another name for `'transform'`, and `'color'` for `'colorRef'`, as
222
+ * Monaco calls them; any other name shows as `'value'`.
223
+ */
224
+ kind?: 'element' | 'attribute' | 'value' | 'snippet' | 'module' | 'property' | 'transform' | 'function' | 'colorRef' | 'color';
225
+ /** Shown beside the label when there is no `documentation`. */
158
226
  detail?: string;
159
- documentation?: string;
227
+ /** Shown beside the label; Monaco's `{ value }` is shown as plain text. */
228
+ documentation?: string | { value: string };
229
+ /**
230
+ * What the text typed so far is matched against, as in Monaco: its characters in order, ignoring
231
+ * case, the first at the start of `filterText` or of a word in it. Typed so far is the text from
232
+ * the start of what the item replaces to the caret. Without it the item is not filtered: your
233
+ * provider is asked again on each letter typed, and offers what fits.
234
+ */
235
+ filterText?: string;
236
+ /** When any item has one, your items are ordered by it (by `label` where it is missing), as in Monaco. */
237
+ sortText?: string;
238
+ /**
239
+ * The text accepting the item replaces, which must contain the caret, as in Monaco. Of
240
+ * `{ insert, replace }`, `replace` is used. Without it the item replaces the word before the
241
+ * caret, back to the first of `< > { } ( ) [ ] " ' = , ; . @ | :` or whitespace.
242
+ */
243
+ range?: AardinkRange | { insert: AardinkRange; replace: AardinkRange };
244
+ }
245
+
246
+ /** Monaco's `CompletionList`, plus `exclusive`: `true` leaves out the extended language's completions for this request. */
247
+ export interface AardinkCompletionList {
248
+ suggestions: AardinkCompletionItem[];
249
+ exclusive?: boolean;
250
+ /** Accepted for Monaco's shape, and ignored: your provider is asked again on each letter typed anyway. */
251
+ incomplete?: boolean;
252
+ }
253
+
254
+ export interface AardinkHover {
255
+ title?: string;
256
+ /** Plain text. */
257
+ contents: string;
258
+ /** Shown as code below `contents`. */
259
+ example?: string;
160
260
  }
161
261
 
162
262
  /**
163
263
  * What a registered language knows beyond its grammar. Each gets the whole text and, where it
164
264
  * asks about a place, a 1-based line and column; each may answer at once or with a Promise. With
165
- * `extends`, the built-in language's answers come after these.
265
+ * `extends`, the built-in language's answers come after these, less any completion with the same
266
+ * `kind` and `label` as one of yours (see `inheritCompletions` to leave them all out).
166
267
  */
167
268
  export interface AardinkLanguageProviders {
168
- provideCompletionItems?(text: string, line: number, column: number): AardinkCompletionItem[] | null | Promise<AardinkCompletionItem[] | null>;
169
- provideHover?(text: string, line: number, column: number): { title?: string; contents: string } | null | Promise<{ title?: string; contents: string } | null>;
269
+ /**
270
+ * Characters that open the completion list when typed, as on Monaco's completion provider, e.g.
271
+ * `['{', '|', '@', '$']`. With `extends` they add to that language's (`'xml'`: `< / space : " =`),
272
+ * or replace them with `inheritCompletions: false`; without, that language's apply. Letters and
273
+ * digits always ask again while the list is open.
274
+ */
275
+ triggerCharacters?: string[];
276
+ provideCompletionItems?(
277
+ text: string,
278
+ line: number,
279
+ column: number,
280
+ ): AardinkCompletionItem[] | AardinkCompletionList | null | Promise<AardinkCompletionItem[] | AardinkCompletionList | null>;
281
+ provideHover?(text: string, line: number, column: number): AardinkHover | null | Promise<AardinkHover | null>;
170
282
  provideDiagnostics?(text: string): AardinkDiagnostic[] | Promise<AardinkDiagnostic[]>;
171
283
  }
172
284
 
@@ -205,7 +317,12 @@ export function createEditor(
205
317
  /** For debugging a grammar: the tokens of `text` in `languageId`, per line, as Monaco's `tokenize` gives them. */
206
318
  export function tokenize(text: string, languageId: string): Promise<{ offset: number; type: string }[][]>;
207
319
 
208
- /** Starts loading the WebAssembly module without mounting anything. */
320
+ /**
321
+ * Starts loading the WebAssembly module without mounting anything. Every call shares one load.
322
+ * When it fails (a network error, say), this and every call waiting on it reject, and the next
323
+ * call of any function here starts a new load. A browser may still hold on to a module whose
324
+ * download failed until the page is reloaded (Chrome does), so that new load can fail the same way.
325
+ */
209
326
  export function preloadAardink(): Promise<void>;
210
327
 
211
328
  /** The Aardink version this package was built from. */
package/index.js CHANGED
@@ -17,7 +17,7 @@
17
17
  // Adapts the Kotlin module's integer-handle exports (aardinkCreate, aardinkGetValue, ...) into
18
18
  // the Monaco-shaped object described in index.d.ts.
19
19
 
20
- export const version = '0.6.0-rc2';
20
+ export const version = '0.6.0-rc3';
21
21
 
22
22
  /** Monaco's flags for a completion's `insertText`. */
23
23
  export const CompletionItemInsertTextRule = Object.freeze({ None: 0, KeepWhitespace: 1, InsertAsSnippet: 4 });
@@ -25,15 +25,22 @@ export const CompletionItemInsertTextRule = Object.freeze({ None: 0, KeepWhitesp
25
25
  let kotlinModule;
26
26
 
27
27
  function load() {
28
- // Memoised: every editor on the page shares one WebAssembly instance.
29
- kotlinModule ??= import('./kotlin/aardink-web.mjs').then((k) => {
30
- // Compose looks for resources relative to the page, not this package. A literal
31
- // new URL(..., import.meta.url) is also what makes a bundler (Vite) emit the font file.
32
- k.aardinkSetBundledFontUrl(
33
- new URL('./kotlin/composeResources/com.aardarch.aardink.web.res/font/jetbrains_mono_regular.ttf', import.meta.url).href,
34
- );
35
- return k;
36
- });
28
+ // Memoised: every editor on the page shares one WebAssembly instance. A failed load is
29
+ // forgotten, so the next call tries again rather than repeating one network blip for the
30
+ // page's life.
31
+ kotlinModule ??= import('./kotlin/aardink-web.mjs')
32
+ .then((k) => {
33
+ // Compose looks for resources relative to the page, not this package. A literal
34
+ // new URL(..., import.meta.url) is also what makes a bundler (Vite) emit the font file.
35
+ k.aardinkSetBundledFontUrl(
36
+ new URL('./kotlin/composeResources/com.aardarch.aardink.web.res/font/jetbrains_mono_regular.ttf', import.meta.url).href,
37
+ );
38
+ return k;
39
+ })
40
+ .catch((error) => {
41
+ kotlinModule = undefined;
42
+ throw error;
43
+ });
37
44
  return kotlinModule;
38
45
  }
39
46
 
@@ -57,6 +64,25 @@ function withRange(s) {
57
64
  };
58
65
  }
59
66
 
67
+ /** The fields of a Monaco `ISelection` the Kotlin side reads. */
68
+ function toKotlinSelection(s) {
69
+ return {
70
+ selectionStartLineNumber: s.selectionStartLineNumber,
71
+ selectionStartColumn: s.selectionStartColumn,
72
+ positionLineNumber: s.positionLineNumber,
73
+ positionColumn: s.positionColumn,
74
+ };
75
+ }
76
+
77
+ /** The fields of a Monaco edit operation the Kotlin side reads; a Monaco `Range` is a class, so copy them. */
78
+ function toKotlinEdit(edit) {
79
+ const r = edit.range;
80
+ return {
81
+ range: { startLineNumber: r.startLineNumber, startColumn: r.startColumn, endLineNumber: r.endLineNumber, endColumn: r.endColumn },
82
+ text: edit.text ?? null,
83
+ };
84
+ }
85
+
60
86
  /** For debugging a grammar: the tokens of `text` in `languageId`, per line, as Monaco's tokenize gives them. */
61
87
  export async function tokenize(text, languageId) {
62
88
  const k = await load();
@@ -98,6 +124,9 @@ export async function registerLanguage(definition, providers = {}) {
98
124
  extends: definition.extends,
99
125
  displayName: definition.displayName,
100
126
  extensions: definition.extensions,
127
+ inheritCompletions: definition.inheritCompletions,
128
+ // On the providers, as on Monaco's completion provider; the Kotlin side reads it with the definition.
129
+ triggerCharacters: providers.triggerCharacters,
101
130
  grammar,
102
131
  }),
103
132
  (text, line, column) => answer(providers.provideCompletionItems, text, line, column),
@@ -182,6 +211,15 @@ export async function createEditor(container, onChange, options = {}) {
182
211
  return {
183
212
  getValue: () => k.aardinkGetValue(id),
184
213
  setValue: (text) => k.aardinkSetValue(id, text),
214
+ replaceValue: (text) => k.aardinkReplaceValue(id, text),
215
+ executeEdits(source, edits, endCursorState) {
216
+ // `source` only names the edit in Monaco; a cursor-state computer function is not called.
217
+ const selections = Array.isArray(endCursorState) && endCursorState.length ? endCursorState.map(toKotlinSelection) : null;
218
+ const result = k.aardinkExecuteEdits(id, JSON.stringify(edits.map(toKotlinEdit)), JSON.stringify(selections));
219
+ if (result === 'read-only') return false;
220
+ if (result) throw new Error(`Aardink: ${result}`);
221
+ return true;
222
+ },
185
223
  updateOptions: (patch) => k.aardinkUpdateOptions(id, JSON.stringify(toKotlinOptions(patch))),
186
224
  onDidChangeContent(listener) {
187
225
  contentListeners.add(listener);
@@ -196,21 +234,13 @@ export async function createEditor(container, onChange, options = {}) {
196
234
  diagnosticsListeners.add(listener);
197
235
  return () => diagnosticsListeners.delete(listener);
198
236
  },
237
+ revalidate: () => k.aardinkRevalidate(id),
199
238
  revealPosition: (line, column) => k.aardinkRevealPosition(id, line, column),
200
239
  setBaseline: (text) => k.aardinkSetBaseline(id, text ?? ''),
201
240
  format: () => new Promise((resolve) => k.aardinkFormat(id, resolve)),
202
241
  focus: () => k.aardinkFocus(id),
203
242
  getSelections: () => JSON.parse(k.aardinkGetSelections(id)).map(withRange),
204
- setSelections: (selections) =>
205
- k.aardinkSetSelections(
206
- id,
207
- JSON.stringify(selections.map((s) => ({
208
- selectionStartLineNumber: s.selectionStartLineNumber,
209
- selectionStartColumn: s.selectionStartColumn,
210
- positionLineNumber: s.positionLineNumber,
211
- positionColumn: s.positionColumn,
212
- }))),
213
- ),
243
+ setSelections: (selections) => k.aardinkSetSelections(id, JSON.stringify(selections.map(toKotlinSelection))),
214
244
  canUndo: () => k.aardinkCanUndo(id),
215
245
  canRedo: () => k.aardinkCanRedo(id),
216
246
  pushUndoStop: () => k.aardinkPushUndoStop(id),
@@ -561,6 +561,23 @@ const js_code = {
561
561
  'androidx.compose.material3.internal.set_$external_fun' : (_this, p0, p1) => _this.set(p0, p1),
562
562
  'androidx.compose.material3.internal.get_$external_fun' : (_this, p0) => _this.get(p0),
563
563
  'com.aardarch.aardink.platform.readClipboardText' : () => navigator.clipboard.readText(),
564
+ 'com.aardarch.aardink.web.followSize' : (element) => {
565
+ let last = element.clientWidth + 'x' + element.clientHeight;
566
+ let frame = 0;
567
+ const observer = new ResizeObserver(() => {
568
+ const size = element.clientWidth + 'x' + element.clientHeight;
569
+ if (size === last) return;
570
+ last = size;
571
+ cancelAnimationFrame(frame);
572
+ frame = requestAnimationFrame(() => window.dispatchEvent(new Event('resize')));
573
+ });
574
+ observer.observe(element);
575
+ return () => {
576
+ cancelAnimationFrame(frame);
577
+ observer.disconnect();
578
+ };
579
+ },
580
+ 'com.aardarch.aardink.web.callStop' : (stop) => stop(),
564
581
  'com.aardarch.aardink.sampleweb.__callJsClosure_((String)->Unit)' : (f, p0) => f(p0),
565
582
  'com.aardarch.aardink.sampleweb.__callJsClosure_((Int,Int)->Unit)' : (f, p0, p1) => f(p0, p1),
566
583
  'com.aardarch.aardink.sampleweb.__callJsClosure_((String,Int,Int)->Js)' : (f, p0, p1, p2) => f(p0, p1, p2),
@@ -81,11 +81,14 @@ export const {
81
81
  aardinkCreate,
82
82
  aardinkGetValue,
83
83
  aardinkSetValue,
84
+ aardinkReplaceValue,
85
+ aardinkExecuteEdits,
84
86
  aardinkUpdateOptions,
85
87
  aardinkOnChange,
86
88
  aardinkOnCursorChange,
87
89
  aardinkSetDiagnostics,
88
90
  aardinkOnDiagnosticsChange,
91
+ aardinkRevalidate,
89
92
  aardinkRegisterLanguage,
90
93
  aardinkRegisterTheme,
91
94
  aardinkOnContentChange,
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aardarch/aardink-web",
3
- "version": "0.6.0-rc2",
3
+ "version": "0.6.0-rc3",
4
4
  "description": "The Aardink code editor for the browser: Compose Multiplatform on WebAssembly behind a Monaco-shaped API.",
5
5
  "keywords": [
6
6
  "code-editor",