@elabs-ai/components-editor 4.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.
- package/LICENSE +21 -0
- package/README.md +149 -0
- package/dist/chunk-LBC5VJBD.js +3748 -0
- package/dist/chunk-LBC5VJBD.js.map +1 -0
- package/dist/index.css +233 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.ts +199 -0
- package/dist/index.js +362 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/monaco-environment.d.ts +2 -0
- package/dist/lib/monaco-environment.js +44 -0
- package/dist/lib/monaco-environment.js.map +1 -0
- package/dist/markdown/frontmatter.d.ts +11 -0
- package/dist/markdown/frontmatter.js +33 -0
- package/dist/markdown/frontmatter.js.map +1 -0
- package/dist/markdown/index.css +233 -0
- package/dist/markdown/index.css.map +1 -0
- package/dist/markdown/index.d.ts +1486 -0
- package/dist/markdown/index.js +4882 -0
- package/dist/markdown/index.js.map +1 -0
- package/dist/markdown/parse.d.ts +25 -0
- package/dist/markdown/parse.js +14 -0
- package/dist/markdown/parse.js.map +1 -0
- package/dist/markdown-editor-DfBZibAn.d.ts +702 -0
- package/package.json +101 -0
- package/src/ai-objects/ai-objects-integration.test.tsx +128 -0
- package/src/ai-objects/ai-objects.stories.tsx +233 -0
- package/src/ai-objects/ai-objects.test.tsx +205 -0
- package/src/ai-objects/decision-card.tsx +178 -0
- package/src/ai-objects/directives.ts +136 -0
- package/src/ai-objects/entity.tsx +181 -0
- package/src/ai-objects/index.ts +34 -0
- package/src/ai-objects/knowledge-card.tsx +143 -0
- package/src/calc-block/calc-block.stories.tsx +297 -0
- package/src/calc-block/calc-block.test.tsx +164 -0
- package/src/calc-block/calc-block.tsx +398 -0
- package/src/calc-block/calc-editor-monaco.ts +214 -0
- package/src/calc-block/calc-editor-prose.ts +128 -0
- package/src/calc-block/calc-editor.css +80 -0
- package/src/calc-block/calc-editor.stories.tsx +272 -0
- package/src/calc-block/calc-editor.test.ts +228 -0
- package/src/calc-block/calc-editor.ts +270 -0
- package/src/calc-block/calc-inline.stories.tsx +44 -0
- package/src/calc-block/calc-inline.test.tsx +44 -0
- package/src/calc-block/calc-inline.tsx +75 -0
- package/src/calc-block/index.ts +20 -0
- package/src/calc-block/types.ts +164 -0
- package/src/code-editor/code-editor.stories.tsx +218 -0
- package/src/code-editor/code-editor.test.tsx +134 -0
- package/src/code-editor/code-editor.tsx +250 -0
- package/src/code-editor/index.ts +6 -0
- package/src/code-workspace/code-workspace.stories.tsx +51 -0
- package/src/code-workspace/code-workspace.test.tsx +146 -0
- package/src/code-workspace/code-workspace.tsx +253 -0
- package/src/code-workspace/index.ts +6 -0
- package/src/copy-button/copy-button.tsx +51 -0
- package/src/copy-button/index.ts +1 -0
- package/src/css.d.ts +3 -0
- package/src/diff-editor/diff-editor.stories.tsx +44 -0
- package/src/diff-editor/diff-editor.test.tsx +68 -0
- package/src/diff-editor/diff-editor.tsx +151 -0
- package/src/diff-editor/index.ts +1 -0
- package/src/editor-context-menu/editor-context-menu.tsx +120 -0
- package/src/editor-context-menu/index.ts +1 -0
- package/src/editor-toolbar/editor-toolbar.tsx +83 -0
- package/src/editor-toolbar/index.ts +1 -0
- package/src/index.ts +48 -0
- package/src/lib/editor-completions-monaco.test.ts +206 -0
- package/src/lib/editor-completions-monaco.ts +127 -0
- package/src/lib/editor-completions.test.ts +156 -0
- package/src/lib/editor-completions.ts +153 -0
- package/src/lib/editor-content-access-prose.test.ts +411 -0
- package/src/lib/editor-content-access-prose.ts +172 -0
- package/src/lib/editor-content-access.test.ts +182 -0
- package/src/lib/editor-content-access.ts +109 -0
- package/src/lib/languages.ts +32 -0
- package/src/lib/markdown/diff.test.ts +80 -0
- package/src/lib/markdown/diff.ts +194 -0
- package/src/lib/markdown/directives.ts +342 -0
- package/src/lib/markdown/frontmatter.ts +50 -0
- package/src/lib/markdown/markdown-scale.test.ts +65 -0
- package/src/lib/markdown/markdown-scale.ts +62 -0
- package/src/lib/markdown/merge.test.ts +64 -0
- package/src/lib/markdown/merge.ts +158 -0
- package/src/lib/markdown/slugify.test.ts +156 -0
- package/src/lib/markdown/slugify.ts +47 -0
- package/src/lib/monaco-environment.ts +103 -0
- package/src/lib/monaco-theme-bridge.ts +293 -0
- package/src/lib/use-data-theme.ts +61 -0
- package/src/markdown/frontmatter.ts +14 -0
- package/src/markdown/index.ts +323 -0
- package/src/markdown/parse.test.ts +60 -0
- package/src/markdown/parse.ts +38 -0
- package/src/markdown-academic/citations.test.ts +77 -0
- package/src/markdown-academic/citations.tsx +442 -0
- package/src/markdown-academic/footnotes.tsx +235 -0
- package/src/markdown-academic/index.ts +32 -0
- package/src/markdown-academic/math.tsx +163 -0
- package/src/markdown-academic/toc.tsx +88 -0
- package/src/markdown-editor/completions/completions-menu.tsx +96 -0
- package/src/markdown-editor/completions/completions-prose.test.ts +356 -0
- package/src/markdown-editor/completions/completions-prose.ts +313 -0
- package/src/markdown-editor/completions/completions-widget.tsx +75 -0
- package/src/markdown-editor/completions/index.ts +37 -0
- package/src/markdown-editor/directive-nodes.ts +151 -0
- package/src/markdown-editor/directive-views.test.tsx +189 -0
- package/src/markdown-editor/directive-views.tsx +795 -0
- package/src/markdown-editor/exit-keymap.test.ts +88 -0
- package/src/markdown-editor/exit-keymap.ts +87 -0
- package/src/markdown-editor/index.ts +6 -0
- package/src/markdown-editor/markdown-editor.css +250 -0
- package/src/markdown-editor/markdown-editor.directives.test.tsx +105 -0
- package/src/markdown-editor/markdown-editor.fill.test.ts +34 -0
- package/src/markdown-editor/markdown-editor.paste-embed.test.tsx +297 -0
- package/src/markdown-editor/markdown-editor.stories.tsx +560 -0
- package/src/markdown-editor/markdown-editor.strictmode.test.tsx +38 -0
- package/src/markdown-editor/markdown-editor.table.test.tsx +287 -0
- package/src/markdown-editor/markdown-editor.tsx +560 -0
- package/src/markdown-editor/milkdown-react/editor.tsx +33 -0
- package/src/markdown-editor/milkdown-react/index.ts +8 -0
- package/src/markdown-editor/milkdown-react/types.ts +27 -0
- package/src/markdown-editor/milkdown-react/use-editor.ts +27 -0
- package/src/markdown-editor/milkdown-react/use-get-editor.ts +56 -0
- package/src/markdown-editor/milkdown-react/use-instance.ts +23 -0
- package/src/markdown-editor/paste-embed.ts +355 -0
- package/src/markdown-editor/slash/brand-slash-commands.test.ts +194 -0
- package/src/markdown-editor/slash/brand-slash-commands.ts +324 -0
- package/src/markdown-editor/slash/brand-slash-plugin.test.ts +254 -0
- package/src/markdown-editor/slash/brand-slash-plugin.ts +385 -0
- package/src/markdown-editor/slash/index.ts +107 -0
- package/src/markdown-editor/slash/insert-directive.test.ts +263 -0
- package/src/markdown-editor/slash/insert-directive.ts +345 -0
- package/src/markdown-editor/slash/monaco-slash-menu.test.tsx +198 -0
- package/src/markdown-editor/slash/monaco-slash-menu.tsx +356 -0
- package/src/markdown-editor/slash/shortcut-monaco.ts +69 -0
- package/src/markdown-editor/slash/shortcut.test.ts +157 -0
- package/src/markdown-editor/slash/shortcut.ts +57 -0
- package/src/markdown-editor/slash/slash-menu.stories.tsx +139 -0
- package/src/markdown-editor/slash/slash-menu.tsx +115 -0
- package/src/markdown-editor/slash/slash-scroll.test.tsx +52 -0
- package/src/markdown-editor/slash/slash-widget.tsx +97 -0
- package/src/markdown-editor/slash/source-slash-trigger.test.ts +42 -0
- package/src/markdown-editor/slash/source-slash-trigger.ts +46 -0
- package/src/markdown-editor/table-view.tsx +249 -0
- package/src/markdown-iteration/directive.tsx +92 -0
- package/src/markdown-iteration/edit-context.ts +56 -0
- package/src/markdown-iteration/index.ts +24 -0
- package/src/markdown-iteration/iteration-block.test.tsx +138 -0
- package/src/markdown-iteration/iteration-builder-dialog.stories.tsx +293 -0
- package/src/markdown-iteration/iteration-builder-dialog.tsx +312 -0
- package/src/markdown-iteration/iteration-builder.test.ts +252 -0
- package/src/markdown-iteration/iteration-builder.ts +303 -0
- package/src/markdown-iteration/iteration.test.ts +56 -0
- package/src/markdown-iteration/iteration.tsx +333 -0
- package/src/markdown-iteration/template-dialog.stories.tsx +63 -0
- package/src/markdown-iteration/template-dialog.test.tsx +104 -0
- package/src/markdown-iteration/template-dialog.tsx +116 -0
- package/src/markdown-outline/document-outline.stories.tsx +76 -0
- package/src/markdown-outline/document-outline.tsx +83 -0
- package/src/markdown-outline/index.ts +2 -0
- package/src/markdown-outline/markdown-outline.test.ts +49 -0
- package/src/markdown-outline/markdown-outline.ts +60 -0
- package/src/markdown-preview/code-fence.tsx +193 -0
- package/src/markdown-preview/index.ts +8 -0
- package/src/markdown-preview/markdown-preview-academic.stories.tsx +203 -0
- package/src/markdown-preview/markdown-preview-academic.test.tsx +257 -0
- package/src/markdown-preview/markdown-preview-iteration-calc.test.tsx +71 -0
- package/src/markdown-preview/markdown-preview-iteration-nested.test.tsx +58 -0
- package/src/markdown-preview/markdown-preview-iteration.stories.tsx +187 -0
- package/src/markdown-preview/markdown-preview-iteration.test.tsx +90 -0
- package/src/markdown-preview/markdown-preview-linking.test.tsx +61 -0
- package/src/markdown-preview/markdown-preview-transclusion.test.tsx +33 -0
- package/src/markdown-preview/markdown-preview.stories.tsx +108 -0
- package/src/markdown-preview/markdown-preview.test.tsx +411 -0
- package/src/markdown-preview/markdown-preview.tsx +1541 -0
- package/src/markdown-toolbar/index.ts +8 -0
- package/src/markdown-toolbar/markdown-commands.test.ts +84 -0
- package/src/markdown-toolbar/markdown-commands.ts +104 -0
- package/src/markdown-toolbar/markdown-toolbar.stories.tsx +56 -0
- package/src/markdown-toolbar/markdown-toolbar.tsx +263 -0
- package/src/markdown-workspace/focus-writing.test.ts +52 -0
- package/src/markdown-workspace/focus-writing.ts +38 -0
- package/src/markdown-workspace/index.ts +6 -0
- package/src/markdown-workspace/markdown-workspace.stories.tsx +688 -0
- package/src/markdown-workspace/markdown-workspace.test.tsx +326 -0
- package/src/markdown-workspace/markdown-workspace.tsx +787 -0
- package/src/mermaid-diagram/index.ts +1 -0
- package/src/mermaid-diagram/mermaid-diagram.stories.tsx +42 -0
- package/src/mermaid-diagram/mermaid-diagram.test.tsx +86 -0
- package/src/mermaid-diagram/mermaid-diagram.tsx +318 -0
- package/src/mermaid-diagram/mermaid-viewer.test.tsx +89 -0
- package/src/mermaid-diagram/mermaid-viewer.tsx +367 -0
- package/src/mermaid-diagram/real-parse.test.ts +42 -0
- package/src/mermaid-diagram/remediate.test.ts +60 -0
- package/src/mermaid-diagram/remediate.ts +124 -0
- package/src/mermaid-workspace/index.ts +1 -0
- package/src/mermaid-workspace/mermaid-workspace.stories.tsx +28 -0
- package/src/mermaid-workspace/mermaid-workspace.tsx +71 -0
- package/src/metric-block/index.ts +1 -0
- package/src/metric-block/metric-block.stories.tsx +40 -0
- package/src/metric-block/metric-block.tsx +14 -0
- package/src/monaco-workers.d.ts +10 -0
- package/src/prose/index.ts +14 -0
- package/src/prose/prose.stories.tsx +44 -0
- package/src/prose/prose.test.tsx +44 -0
- package/src/prose/prose.tsx +21 -0
- package/src/timeline/index.ts +18 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the engine-neutral completion-provider helpers (#283) —
|
|
3
|
+
* `triggerQueryStart` / `resolveReplaceRange` (the pure range math both the
|
|
4
|
+
* Monaco and Milkdown adapters share) + `collectCompletions` (the provider
|
|
5
|
+
* fan-out that degrades safely on a throwing/rejecting `provide()`).
|
|
6
|
+
*/
|
|
7
|
+
import { describe, expect, it, vi } from "vitest";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
collectCompletions,
|
|
11
|
+
resolveReplaceRange,
|
|
12
|
+
triggerQueryStart,
|
|
13
|
+
type EditorCompletionItem,
|
|
14
|
+
type EditorCompletionProvider,
|
|
15
|
+
} from "./editor-completions";
|
|
16
|
+
|
|
17
|
+
describe("triggerQueryStart", () => {
|
|
18
|
+
it("resolves to the column right after the LAST trigger char before the caret", () => {
|
|
19
|
+
// "[[no" — caret after "no" (column 5). The `[` at index 1 (0-based) is the
|
|
20
|
+
// one whose query we want to replace, so the query start is column 3
|
|
21
|
+
// (right after the second `[`), preserving the typed "[[".
|
|
22
|
+
expect(triggerQueryStart("[[no", 5, ["["])).toBe(3);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("returns null when no trigger character precedes the caret", () => {
|
|
26
|
+
expect(triggerQueryStart("hello world", 6, ["["])).toBeNull();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("returns null when triggerCharacters is undefined or empty", () => {
|
|
30
|
+
expect(triggerQueryStart("[[no", 5, undefined)).toBeNull();
|
|
31
|
+
expect(triggerQueryStart("[[no", 5, [])).toBeNull();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("picks the LATEST of several distinct trigger characters", () => {
|
|
35
|
+
// "@user:na" with triggers "@" and ":" — the `:` (later) wins.
|
|
36
|
+
expect(triggerQueryStart("@user:na", 9, ["@", ":"])).toBe(7);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("handles the trigger character sitting at column 1", () => {
|
|
40
|
+
expect(triggerQueryStart("[abc", 5, ["["])).toBe(2);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("resolveReplaceRange", () => {
|
|
45
|
+
const position = { lineNumber: 3, column: 5 };
|
|
46
|
+
|
|
47
|
+
it("uses item.replaceFrom when present, ignoring trigger detection", () => {
|
|
48
|
+
const item: EditorCompletionItem = { label: "x", insertText: "y", replaceFrom: 2 };
|
|
49
|
+
const range = resolveReplaceRange(item, position, "[[no", ["["]);
|
|
50
|
+
expect(range).toEqual({
|
|
51
|
+
startLineNumber: 3,
|
|
52
|
+
startColumn: 2,
|
|
53
|
+
endLineNumber: 3,
|
|
54
|
+
endColumn: 5,
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("falls back to the trigger-query start when replaceFrom is absent", () => {
|
|
59
|
+
const item: EditorCompletionItem = { label: "x", insertText: "y" };
|
|
60
|
+
const range = resolveReplaceRange(item, position, "[[no", ["["]);
|
|
61
|
+
expect(range).toEqual({
|
|
62
|
+
startLineNumber: 3,
|
|
63
|
+
startColumn: 3,
|
|
64
|
+
endLineNumber: 3,
|
|
65
|
+
endColumn: 5,
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("falls back to a bare insert at the caret when neither is available", () => {
|
|
70
|
+
const item: EditorCompletionItem = { label: "x", insertText: "y" };
|
|
71
|
+
const range = resolveReplaceRange(item, position, "no trigger here", ["["]);
|
|
72
|
+
expect(range).toEqual({
|
|
73
|
+
startLineNumber: 3,
|
|
74
|
+
startColumn: 5,
|
|
75
|
+
endLineNumber: 3,
|
|
76
|
+
endColumn: 5,
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("clamps an out-of-range replaceFrom into [1, column] rather than producing a backwards edit", () => {
|
|
81
|
+
const item: EditorCompletionItem = { label: "x", insertText: "y", replaceFrom: 999 };
|
|
82
|
+
const range = resolveReplaceRange(item, position, "[[no", ["["]);
|
|
83
|
+
expect(range.startColumn).toBe(5);
|
|
84
|
+
const negative: EditorCompletionItem = { label: "x", insertText: "y", replaceFrom: -3 };
|
|
85
|
+
expect(resolveReplaceRange(negative, position, "[[no", ["["]).startColumn).toBe(1);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("collectCompletions", () => {
|
|
90
|
+
const ctx = { source: "[[no", line: 1, column: 5, lineText: "[[no" };
|
|
91
|
+
|
|
92
|
+
it("pairs each returned item with its provider", async () => {
|
|
93
|
+
const providerA: EditorCompletionProvider = {
|
|
94
|
+
id: "a",
|
|
95
|
+
provide: () => [{ label: "Alpha", insertText: "alpha" }],
|
|
96
|
+
};
|
|
97
|
+
const providerB: EditorCompletionProvider = {
|
|
98
|
+
id: "b",
|
|
99
|
+
provide: () => [{ label: "Beta", insertText: "beta" }],
|
|
100
|
+
};
|
|
101
|
+
const matches = await collectCompletions([providerA, providerB], ctx);
|
|
102
|
+
expect(matches).toEqual([
|
|
103
|
+
{ provider: providerA, item: { label: "Alpha", insertText: "alpha" } },
|
|
104
|
+
{ provider: providerB, item: { label: "Beta", insertText: "beta" } },
|
|
105
|
+
]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("awaits an async provide()", async () => {
|
|
109
|
+
const provider: EditorCompletionProvider = {
|
|
110
|
+
id: "async",
|
|
111
|
+
provide: async () => [{ label: "Async", insertText: "async" }],
|
|
112
|
+
};
|
|
113
|
+
const matches = await collectCompletions([provider], ctx);
|
|
114
|
+
expect(matches).toHaveLength(1);
|
|
115
|
+
expect(matches[0]?.item.label).toBe("Async");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("a throwing provide() contributes zero items without affecting others", async () => {
|
|
119
|
+
const bad: EditorCompletionProvider = {
|
|
120
|
+
id: "bad",
|
|
121
|
+
provide: () => {
|
|
122
|
+
throw new Error("boom");
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
const good: EditorCompletionProvider = {
|
|
126
|
+
id: "good",
|
|
127
|
+
provide: () => [{ label: "Good", insertText: "good" }],
|
|
128
|
+
};
|
|
129
|
+
const matches = await collectCompletions([bad, good], ctx);
|
|
130
|
+
expect(matches).toHaveLength(1);
|
|
131
|
+
expect(matches[0]?.item.label).toBe("Good");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("a rejecting async provide() contributes zero items without affecting others", async () => {
|
|
135
|
+
const bad: EditorCompletionProvider = {
|
|
136
|
+
id: "bad-async",
|
|
137
|
+
provide: async () => Promise.reject(new Error("nope")),
|
|
138
|
+
};
|
|
139
|
+
const good: EditorCompletionProvider = {
|
|
140
|
+
id: "good",
|
|
141
|
+
provide: () => [{ label: "Good", insertText: "good" }],
|
|
142
|
+
};
|
|
143
|
+
const matches = await collectCompletions([bad, good], ctx);
|
|
144
|
+
expect(matches).toHaveLength(1);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("passes the context through to provide()", async () => {
|
|
148
|
+
const provide = vi.fn().mockReturnValue([]);
|
|
149
|
+
await collectCompletions([{ id: "spy", provide }], ctx);
|
|
150
|
+
expect(provide).toHaveBeenCalledWith(ctx);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("an empty provider list resolves to an empty array", async () => {
|
|
154
|
+
expect(await collectCompletions([], ctx)).toEqual([]);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Engine-agnostic completion-provider contract (#283) — the `completions` prop
|
|
3
|
+
* on `MarkdownWorkspace` (and, via it, the WYSIWYG `MarkdownEditor`).
|
|
4
|
+
*
|
|
5
|
+
* ISOLATION INVARIANT (mirrors `editor-content-access.ts`): this file imports
|
|
6
|
+
* NOTHING engine-specific — no `monaco-editor`, no `@milkdown`. It is the
|
|
7
|
+
* dependency-light leaf both engine adapters (`editor-completions-monaco.ts` for
|
|
8
|
+
* Monaco, `markdown-editor/completions/` for Milkdown/ProseMirror) build on, so a
|
|
9
|
+
* consumer can import the TYPES with zero engine imports (#283 acceptance: "zero
|
|
10
|
+
* `monaco-editor` imports in app code").
|
|
11
|
+
*
|
|
12
|
+
* D5 / the calc `complete` hook precedent: the library only REGISTERS the
|
|
13
|
+
* provider and RENDERS its suggestions — candidate list, filtering/ranking, and
|
|
14
|
+
* insert text are entirely consumer-owned. Nothing here fetches anything; a
|
|
15
|
+
* throwing/rejecting `provide()` degrades to "no suggestions" (never blanks or
|
|
16
|
+
* throws), the same degrade-safely contract as `resolveResults`/`CalcBlock`.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** One completion candidate a provider offers at the caret. */
|
|
20
|
+
export interface EditorCompletionItem {
|
|
21
|
+
/** Text shown in the suggestion list. */
|
|
22
|
+
label: string;
|
|
23
|
+
/** Text inserted when the item is chosen. */
|
|
24
|
+
insertText: string;
|
|
25
|
+
/** Secondary detail shown muted beside the label. */
|
|
26
|
+
detail?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Start column (1-based, Monaco convention) of the already-typed token to
|
|
29
|
+
* replace. Defaults to the start of the "trigger query" — the run of text
|
|
30
|
+
* typed since the provider's trigger character (see {@link triggerQueryStart}).
|
|
31
|
+
*/
|
|
32
|
+
replaceFrom?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** What a provider knows at the caret when it is asked for completions. */
|
|
36
|
+
export interface EditorCompletionContext {
|
|
37
|
+
/** The full document text. */
|
|
38
|
+
source: string;
|
|
39
|
+
/** 1-based line number. */
|
|
40
|
+
line: number;
|
|
41
|
+
/** 1-based column. */
|
|
42
|
+
column: number;
|
|
43
|
+
/** The full text of the current line. */
|
|
44
|
+
lineText: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A declarative completion source. Registered ONCE per `MarkdownWorkspace` (or
|
|
49
|
+
* `MarkdownEditor`) tree via the `completions` prop — the library owns the
|
|
50
|
+
* engine registration lifecycle (Monaco's `registerCompletionItemProvider` is
|
|
51
|
+
* global-per-language, refcounted here; see `editor-completions-monaco.ts`).
|
|
52
|
+
*/
|
|
53
|
+
export interface EditorCompletionProvider {
|
|
54
|
+
/** Stable id — surfaced for consumer bookkeeping (not currently rendered). */
|
|
55
|
+
id: string;
|
|
56
|
+
/** Characters that (re)open the suggestion list, e.g. `["["]`. */
|
|
57
|
+
triggerCharacters?: string[];
|
|
58
|
+
/** Return the candidates for the caret described by `ctx`. May be async. */
|
|
59
|
+
provide(ctx: EditorCompletionContext): EditorCompletionItem[] | Promise<EditorCompletionItem[]>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Find the 1-based column right after the LAST occurrence, before `column`, of
|
|
64
|
+
* any of `triggerCharacters` in `lineText` — the start of the "trigger query"
|
|
65
|
+
* (the text typed since the trigger character). `null` when no trigger
|
|
66
|
+
* character precedes the caret on this line (falls back to inserting at the
|
|
67
|
+
* bare caret — see {@link resolveReplaceRange}).
|
|
68
|
+
*
|
|
69
|
+
* Deliberately the LAST occurrence, not the first: `[[note` (triggerCharacters
|
|
70
|
+
* `["["]`) resolves the query start to right after the SECOND `[`, so the
|
|
71
|
+
* inserted text replaces only `note`, preserving the `[[` the user typed.
|
|
72
|
+
*/
|
|
73
|
+
export function triggerQueryStart(
|
|
74
|
+
lineText: string,
|
|
75
|
+
column: number,
|
|
76
|
+
triggerCharacters: string[] | undefined,
|
|
77
|
+
): number | null {
|
|
78
|
+
if (!triggerCharacters || triggerCharacters.length === 0) return null;
|
|
79
|
+
const before = lineText.slice(0, Math.max(0, column - 1));
|
|
80
|
+
let bestIndex = -1;
|
|
81
|
+
for (const ch of triggerCharacters) {
|
|
82
|
+
if (!ch) continue;
|
|
83
|
+
const idx = before.lastIndexOf(ch);
|
|
84
|
+
if (idx > bestIndex) bestIndex = idx;
|
|
85
|
+
}
|
|
86
|
+
if (bestIndex === -1) return null;
|
|
87
|
+
return bestIndex + 2; // 0-based char index → 1-based column, then step past it
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** A 1-based, Monaco-`IRange`-shaped span (structural — no monaco import). */
|
|
91
|
+
export interface CompletionReplaceRange {
|
|
92
|
+
startLineNumber: number;
|
|
93
|
+
startColumn: number;
|
|
94
|
+
endLineNumber: number;
|
|
95
|
+
endColumn: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Resolve the range an item's `insertText` replaces: `item.replaceFrom` when
|
|
100
|
+
* given, else the trigger-query start (see {@link triggerQueryStart}), else the
|
|
101
|
+
* bare caret (a pure insert, nothing replaced). Always ends at the caret, on
|
|
102
|
+
* the caret's line — a completion never spans multiple lines.
|
|
103
|
+
*/
|
|
104
|
+
export function resolveReplaceRange(
|
|
105
|
+
item: EditorCompletionItem,
|
|
106
|
+
position: { lineNumber: number; column: number },
|
|
107
|
+
lineText: string,
|
|
108
|
+
triggerCharacters: string[] | undefined,
|
|
109
|
+
): CompletionReplaceRange {
|
|
110
|
+
const start =
|
|
111
|
+
item.replaceFrom ??
|
|
112
|
+
triggerQueryStart(lineText, position.column, triggerCharacters) ??
|
|
113
|
+
position.column;
|
|
114
|
+
return {
|
|
115
|
+
startLineNumber: position.lineNumber,
|
|
116
|
+
// Clamp into [1, column] — a bad/stale `replaceFrom` degrades to "insert at
|
|
117
|
+
// caret" rather than producing a backwards/out-of-range edit.
|
|
118
|
+
startColumn: Math.min(Math.max(1, start), position.column),
|
|
119
|
+
endLineNumber: position.lineNumber,
|
|
120
|
+
endColumn: position.column,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** One resolved completion item, paired with the provider that produced it. */
|
|
125
|
+
export interface CompletionMatch {
|
|
126
|
+
provider: EditorCompletionProvider;
|
|
127
|
+
item: EditorCompletionItem;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Call every provider's `provide(ctx)`, collect the results, and pair each item
|
|
132
|
+
* back with its provider (so a caller can resolve a per-provider replace range
|
|
133
|
+
* via that provider's `triggerCharacters`). Never throws: a provider whose
|
|
134
|
+
* `provide()` throws or whose returned promise rejects contributes zero items
|
|
135
|
+
* (mirrors `resolveResults`'s calc-hook degrade-safely contract) — one bad
|
|
136
|
+
* provider never blanks another's suggestions.
|
|
137
|
+
*/
|
|
138
|
+
export async function collectCompletions(
|
|
139
|
+
providers: EditorCompletionProvider[],
|
|
140
|
+
ctx: EditorCompletionContext,
|
|
141
|
+
): Promise<CompletionMatch[]> {
|
|
142
|
+
const perProvider = await Promise.all(
|
|
143
|
+
providers.map(async (provider): Promise<CompletionMatch[]> => {
|
|
144
|
+
try {
|
|
145
|
+
const items = (await provider.provide(ctx)) ?? [];
|
|
146
|
+
return items.map((item) => ({ provider, item }));
|
|
147
|
+
} catch {
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
}),
|
|
151
|
+
);
|
|
152
|
+
return perProvider.flat();
|
|
153
|
+
}
|