@kabmo-labs/kabmo 0.8.0 → 0.9.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/README.md +7 -0
- package/dist/client/index.html +1 -1
- package/dist/client/index.js +314 -314
- package/dist/client/static/js/{index-qwpvd9fv.js → index-bpptfkz2.js} +922 -892
- package/dist/client/static/js/index-k315p1e8.css +1 -0
- package/package.json +9 -1
- package/src/client/components/common/code-surface/documents.test.ts +120 -0
- package/src/client/components/common/code-surface/documents.ts +201 -0
- package/src/client/components/common/code-surface/extensions.test.ts +110 -0
- package/src/client/components/common/code-surface/extensions.ts +162 -0
- package/src/client/components/common/code-surface/handle.ts +102 -0
- package/src/client/components/common/code-surface/index.tsx +253 -314
- package/src/client/components/common/code-surface/keymap.test.ts +133 -0
- package/src/client/components/common/code-surface/keymap.ts +267 -0
- package/src/client/components/common/code-surface/marks.ts +74 -0
- package/src/client/components/common/code-surface/shiki.ts +234 -0
- package/src/client/components/common/code-surface/sync.test.ts +185 -0
- package/src/client/components/common/code-surface/sync.ts +58 -0
- package/src/client/components/common/code-surface/theme.test.ts +202 -0
- package/src/client/components/common/code-surface/theme.ts +111 -0
- package/src/client/components/common/editor-view-buttons/index.test.tsx +148 -0
- package/src/client/components/common/editor-view-buttons/index.tsx +109 -0
- package/src/client/components/common/shell-update-button/Modal.tsx +163 -0
- package/src/client/components/common/shell-update-button/index.test.tsx +251 -0
- package/src/client/components/common/shell-update-button/index.tsx +125 -0
- package/src/client/components/common/toolbar-row/index.test.tsx +166 -0
- package/src/client/components/common/toolbar-row/index.tsx +189 -0
- package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +41 -78
- package/src/client/components/layout/desktop-layout/index.tsx +47 -67
- package/src/client/components/layout/desktop-layout/resizer/index.tsx +6 -3
- package/src/client/components/layout/session-sidebar/Footer.tsx +8 -15
- package/src/client/components/mobile/LayoutWrapper.tsx +1 -0
- package/src/client/components/mobile/mobile-right-sidebar/FullDiff.tsx +3 -0
- package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +63 -38
- package/src/client/components/mobile/mobile-right-sidebar/OutlineButton.test.tsx +137 -0
- package/src/client/components/mobile/mobile-right-sidebar/OutlineButton.tsx +14 -3
- package/src/client/components/mobile/mobile-session-sidebar/Footer.tsx +9 -21
- package/src/client/components/settings/Modal.tsx +8 -0
- package/src/client/components/settings/categories/AppearanceSettings.tsx +3 -0
- package/src/client/components/settings/categories/appearance-settings/EditorThemeSection.test.tsx +167 -0
- package/src/client/components/settings/categories/appearance-settings/EditorThemeSection.tsx +184 -0
- package/src/client/components/settings/categories/project-settings/WorktreeSettings.tsx +8 -2
- package/src/client/components/workspace/chat-timeline/EmptyWorkspacePrompt.tsx +16 -8
- package/src/client/components/workspace/chat-timeline/SystemNotice.tsx +9 -4
- package/src/client/components/workspace/chat-timeline/TaskResultContent.tsx +9 -7
- package/src/client/components/workspace/chat-timeline/target-picker/WorkspacePicker.test.tsx +132 -0
- package/src/client/components/workspace/chat-timeline/target-picker/WorkspacePicker.tsx +120 -0
- package/src/client/components/workspace/chat-timeline/target-picker/WorktreePicker.test.tsx +319 -0
- package/src/client/components/workspace/chat-timeline/target-picker/WorktreePicker.tsx +285 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/index.test.ts +2 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/hashline-patch/index.tsx +34 -29
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/AstEdit.tsx +11 -9
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Bash.tsx +10 -7
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Edit.tsx +25 -8
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Eval.tsx +8 -11
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches.test.ts +114 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches.tsx +111 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Read.tsx +7 -7
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/SearchTool.tsx +9 -59
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/ArtifactBody.tsx +2 -7
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.test.ts +27 -28
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.tsx +39 -26
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/ExcerptCode.tsx +8 -8
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/HighlightedCode.tsx +35 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/JsonCodeBlock.tsx +4 -4
- package/src/client/components/workspace/diff-panel/MergeDiffView.tsx +179 -0
- package/src/client/components/workspace/diff-panel/ThemedMergeDiffView.tsx +24 -0
- package/src/client/components/workspace/diff-panel/Toolbar.tsx +224 -186
- package/src/client/components/workspace/diff-panel/ToolbarSegments.tsx +98 -0
- package/src/client/components/workspace/diff-panel/index.tsx +76 -145
- package/src/client/components/workspace/diff-panel/merge.ts +315 -0
- package/src/client/components/workspace/editor/Body.tsx +4 -7
- package/src/client/components/workspace/editor/CommandPalette.tsx +2 -2
- package/src/client/components/workspace/editor/FileDocument.tsx +58 -63
- package/src/client/components/workspace/editor/FilePane.tsx +4 -7
- package/src/client/components/workspace/editor/FindWidget.tsx +7 -2
- package/src/client/components/workspace/editor/Toolbar.tsx +220 -123
- package/src/client/components/workspace/editor/index.tsx +7 -9
- package/src/client/components/workspace/file-explorer/OutlineSection.test.tsx +18 -0
- package/src/client/components/workspace/file-explorer/OutlineSection.tsx +10 -3
- package/src/client/components/workspace/search-panel/Body.tsx +1 -1
- package/src/client/components/workspace/worktree-modal/index.test.tsx +118 -0
- package/src/client/hooks/chat/omp/reconnect.test.ts +338 -0
- package/src/client/hooks/chat/omp/reconnect.ts +187 -0
- package/src/client/hooks/chat/omp/run-liveness.test.ts +71 -0
- package/src/client/hooks/chat/omp/run-liveness.ts +65 -0
- package/src/client/hooks/chat/omp/session-list.activity.test.ts +114 -0
- package/src/client/hooks/chat/omp/session-list.test-util.ts +164 -0
- package/src/client/hooks/chat/omp/session-list.test.ts +30 -150
- package/src/client/hooks/chat/omp/session-list.tsx +69 -9
- package/src/client/hooks/chat/timeline/generating.ts +19 -2
- package/src/client/hooks/chat/timeline/index.ts +19 -24
- package/src/client/hooks/chat/timeline/lost-run.test.ts +329 -0
- package/src/client/hooks/chat/timeline/lost-run.ts +177 -0
- package/src/client/hooks/chat/timeline/session-load.test-util.ts +205 -0
- package/src/client/hooks/chat/timeline/session-load.test.ts +61 -162
- package/src/client/hooks/chat/timeline/session-load.ts +69 -37
- package/src/client/hooks/chat/timeline/session-switch.test.ts +65 -0
- package/src/client/hooks/chat/timeline/session-switch.ts +63 -0
- package/src/client/hooks/code/deferred-highlight.test.ts +182 -0
- package/src/client/hooks/code/deferred-highlight.ts +129 -0
- package/src/client/hooks/editor/find-state.ts +1 -1
- package/src/client/hooks/editor/theme.test.ts +206 -0
- package/src/client/hooks/editor/theme.ts +82 -0
- package/src/client/hooks/editor/use-editor-drop.test.ts +101 -0
- package/src/client/hooks/editor/use-editor-drop.ts +67 -14
- package/src/client/hooks/editor/use-editor-find.ts +7 -7
- package/src/client/hooks/editor/use-outline-view.test.ts +182 -0
- package/src/client/hooks/editor/use-outline-view.ts +94 -7
- package/src/client/hooks/editor/use-scroll-restore.test.ts +55 -2
- package/src/client/hooks/editor/use-scroll-restore.ts +124 -4
- package/src/client/hooks/editor/use-search-reveal.ts +1 -1
- package/src/client/hooks/ui/settings-modal.ts +56 -0
- package/src/client/hooks/ui/updates/shell-download.test.ts +2 -0
- package/src/client/hooks/ui/updates/shell-download.ts +55 -14
- package/src/client/hooks/ui/updates/shell-offer.test.ts +26 -0
- package/src/client/hooks/ui/updates/shell-update.test.ts +140 -0
- package/src/client/hooks/ui/updates/shell-update.ts +119 -0
- package/src/client/hooks/workspace/diff-file.ts +29 -10
- package/src/client/hooks/workspace/diff-git-actions.ts +147 -0
- package/src/client/hooks/workspace/editor-view.ts +61 -0
- package/src/client/hooks/workspace/file-tabs.test.ts +62 -5
- package/src/client/hooks/workspace/file-tabs.ts +24 -11
- package/src/client/hooks/workspace/navbar-session-actions.ts +72 -0
- package/src/client/hooks/workspace/worktree/index.ts +20 -5
- package/src/client/lib/desktop/bridge.ts +45 -0
- package/src/client/lib/search-reveal.ts +1 -1
- package/src/client/main.tsx +5 -1
- package/src/client/tailwind.css +37 -8
- package/src/server/lib/auth/guard.test.ts +28 -0
- package/src/server/lib/auth/guard.ts +10 -0
- package/src/server/lib/fs/worktree/create.ts +2 -3
- package/src/server/lib/fs/worktree/list.ts +13 -0
- package/src/server/lib/fs/worktree/remove.ts +3 -3
- package/src/server/lib/lifecycle/claim-port.test.ts +156 -0
- package/src/server/lib/lifecycle/port-guard.test.ts +20 -164
- package/src/server/lib/lifecycle/port-guard.ts +39 -7
- package/src/server/lib/lifecycle/probe-health.test.ts +166 -0
- package/src/server/lib/lifecycle/probe.test.ts +77 -164
- package/src/server/lib/lifecycle/probe.ts +121 -13
- package/src/server/lib/omp/rpc/frame-fold-signals.test.ts +112 -0
- package/src/server/lib/omp/rpc/frame-fold.ts +10 -5
- package/src/server/lib/omp/session/sidebar-activity.server.test.ts +162 -0
- package/src/server/lib/omp/session/sidebar-activity.server.ts +70 -0
- package/src/server/lib/omp/session/telemetry/compute.ts +13 -1
- package/src/server/lib/omp/session/telemetry.test.ts +33 -0
- package/src/server/lib/realtime/signals.server.ts +10 -0
- package/src/server/lib/realtime/topics/catalog.server.ts +4 -1
- package/src/server/lib/realtime/topics.server.ts +16 -0
- package/src/server/lib/updates/shell-assets.ts +94 -3
- package/src/server/lib/updates/shell-cache.ts +105 -0
- package/src/server/lib/updates/shell-pages.test.ts +33 -2
- package/src/server/lib/updates/shell-pages.ts +17 -5
- package/src/server/lib/updates/shell-release-desktop.test.ts +166 -0
- package/src/server/lib/updates/shell-release.test.ts +10 -27
- package/src/server/lib/updates/shell-release.ts +107 -111
- package/src/server/routes/fs/git-merge.test.ts +11 -1
- package/src/server/routes/fs/read.test.ts +24 -7
- package/src/server/routes/fs/scope.test.ts +15 -10
- package/src/server/routes/fs/scope.ts +3 -14
- package/src/server/routes/updates/android.ts +6 -1
- package/src/server/routes/updates/desktop.ts +29 -7
- package/src/server/routes/updates/ios.ts +5 -1
- package/src/server/routes/worktrees/list.ts +11 -1
- package/src/server/routes/worktrees/routes.test.ts +17 -0
- package/src/shared/lib/chat/timeline/omp-callbacks.test.ts +36 -0
- package/src/shared/lib/chat/timeline/omp-callbacks.ts +18 -0
- package/src/shared/lib/chat/timeline/run-signals.test.ts +2 -0
- package/src/shared/lib/code/editor/comment-syntax.test.ts +37 -0
- package/src/shared/lib/code/editor/comment-syntax.ts +55 -0
- package/src/shared/lib/code/editor/find-replace.test.ts +134 -0
- package/src/shared/lib/code/editor/{find.ts → find-replace.ts} +23 -107
- package/src/shared/lib/code/editor/{find.test.ts → find-scan.test.ts} +19 -73
- package/src/shared/lib/code/editor/find-scan.ts +177 -0
- package/src/shared/lib/code/editor/keymap.ts +3 -3
- package/src/shared/lib/code/editor/lines.test.ts +7 -55
- package/src/shared/lib/code/editor/lines.ts +8 -48
- package/src/shared/lib/code/editor/marks.test.ts +4 -5
- package/src/shared/lib/code/editor/marks.ts +13 -25
- package/src/shared/lib/code/editor/theme-loader.ts +97 -0
- package/src/shared/lib/code/editor/themes.generated.ts +156 -0
- package/src/shared/lib/code/editor/themes.test.ts +165 -0
- package/src/shared/lib/code/editor/themes.ts +165 -0
- package/src/shared/lib/code/highlight-memo.test.ts +103 -0
- package/src/shared/lib/code/highlight-memo.ts +149 -0
- package/src/shared/lib/code/highlighter-warmup.ts +112 -0
- package/src/shared/lib/code/highlighter.ts +3 -1
- package/src/shared/lib/code/idle.ts +37 -0
- package/src/shared/lib/code/outline/rows.test.ts +96 -0
- package/src/shared/lib/code/outline/rows.ts +94 -0
- package/src/shared/lib/code/syntax-highlight.test.ts +1 -79
- package/src/shared/lib/code/syntax-highlight.ts +2 -3
- package/src/shared/lib/fs/file-kind-classify.test.ts +25 -11
- package/src/shared/lib/fs/file-kind.ts +47 -13
- package/src/shared/lib/fs/file-url.test.ts +52 -0
- package/src/shared/lib/fs/file-url.ts +37 -0
- package/src/shared/lib/fs/patch-sides.test.ts +114 -0
- package/src/shared/lib/fs/patch-sides.ts +139 -0
- package/src/shared/lib/realtime/protocol.ts +16 -0
- package/src/shared/lib/ui/toolbar-overflow.test.ts +83 -0
- package/src/shared/lib/ui/toolbar-overflow.ts +86 -0
- package/src/shared/lib/updates/shell-arch.ts +54 -0
- package/src/shared/lib/updates/shell-ua.ts +4 -4
- package/src/shared/lib/workspace/editor-view-mode.test.ts +70 -0
- package/src/shared/lib/workspace/editor-view-mode.ts +48 -0
- package/src/shared/lib/workspace/panel-widths.test.ts +30 -0
- package/src/shared/lib/workspace/panel-widths.ts +25 -5
- package/src/shared/lib/workspace/session-state/cache.ts +159 -0
- package/src/shared/lib/workspace/session-state/harness.test-util.ts +55 -0
- package/src/shared/lib/workspace/session-state/merge.ts +100 -0
- package/src/shared/lib/workspace/session-state/store-load.test.ts +206 -0
- package/src/shared/lib/workspace/session-state/store.test.ts +17 -139
- package/src/shared/lib/workspace/session-state/store.ts +92 -130
- package/src/shared/lib/workspace/workspace-panel-widths.ts +124 -0
- package/src/shared/types/realtime.ts +29 -0
- package/src/shared/types/settings/state.ts +10 -0
- package/src/test-support/loopback.ts +183 -0
- package/dist/client/static/js/index-gtbpewr7.css +0 -1
- package/src/client/components/common/code-editor/handler.test.ts +0 -274
- package/src/client/components/common/code-editor/handler.ts +0 -175
- package/src/client/components/common/code-editor/index.tsx +0 -314
- package/src/client/components/common/code-surface/measure.ts +0 -148
- package/src/client/components/workspace/chat-timeline/target-picker/index.test.tsx +0 -227
- package/src/client/components/workspace/chat-timeline/target-picker/index.tsx +0 -260
- package/src/client/components/workspace/diff-panel/SplitView.tsx +0 -209
- package/src/client/components/workspace/diff-panel/UnifiedView.tsx +0 -150
- package/src/client/hooks/editor/scroll-container.test.ts +0 -102
- package/src/client/hooks/editor/scroll-container.ts +0 -26
- package/src/client/hooks/editor/use-code-window.ts +0 -100
- package/src/client/hooks/editor/use-editor-history.test.ts +0 -216
- package/src/client/hooks/editor/use-editor-history.ts +0 -134
- package/src/client/hooks/editor/use-occurrences.test.ts +0 -236
- package/src/client/hooks/editor/use-occurrences.ts +0 -98
- package/src/shared/lib/code/editor/commands.test.ts +0 -155
- package/src/shared/lib/code/editor/commands.ts +0 -260
- package/src/shared/lib/code/editor/edits.test.ts +0 -100
- package/src/shared/lib/code/editor/edits.ts +0 -122
- package/src/shared/lib/code/editor/history.test.ts +0 -118
- package/src/shared/lib/code/editor/history.ts +0 -101
- package/src/shared/lib/code/editor/layers.test.ts +0 -77
- package/src/shared/lib/code/editor/layers.ts +0 -67
- package/src/shared/lib/code/editor/line-edits.test.ts +0 -131
- package/src/shared/lib/code/editor/line-edits.ts +0 -159
- package/src/shared/lib/code/editor/occurrences.test.ts +0 -117
- package/src/shared/lib/code/editor/occurrences.ts +0 -111
- package/src/shared/lib/code/editor/reset-css.test.ts +0 -35
- package/src/shared/lib/code/editor/reset-css.ts +0 -32
- package/src/shared/lib/code/editor/text-commands.test.ts +0 -178
- package/src/shared/lib/code/editor/text-commands.ts +0 -154
- package/src/shared/lib/code/lazy-window.test.ts +0 -161
- package/src/shared/lib/code/lazy-window.ts +0 -156
- package/src/shared/lib/code/windowed-highlight.ts +0 -114
- package/src/shared/lib/fs/diff-parser.test.ts +0 -116
- package/src/shared/lib/fs/diff-parser.ts +0 -213
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Find scanning over one document buffer.
|
|
8
|
+
*
|
|
9
|
+
* The SCAN is CodeMirror's: `RegExpCursor` walks the document and hands back the
|
|
10
|
+
* same ordered match list the surface paints, so the matcher and the editor
|
|
11
|
+
* cannot disagree about where a match is. That replaced a hand-written
|
|
12
|
+
* `RegExp.exec` loop, which had to re-implement the two things a cursor gets
|
|
13
|
+
* right for free — never returning a zero-width match (nothing to highlight,
|
|
14
|
+
* nothing to replace, and stepping past it is what keeps the scan terminating)
|
|
15
|
+
* and starting the next search after the previous match rather than at its own
|
|
16
|
+
* index.
|
|
17
|
+
*
|
|
18
|
+
* Everything around the scan is still Kabmo's, deliberately: the options shape,
|
|
19
|
+
* the cap and the `truncated`/`invalid` distinction are the find widget's
|
|
20
|
+
* contract. Replacement expansion lives in `find-replace.ts`, because a caller
|
|
21
|
+
* that only paints matches should not carry the template grammar.
|
|
22
|
+
*
|
|
23
|
+
* A regex that does not compile is a DIFFERENT answer from "no matches", which
|
|
24
|
+
* is why `invalid` exists: a user typing `(` must be told their pattern is
|
|
25
|
+
* broken, not that the file does not contain it.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { Text } from '@codemirror/state';
|
|
29
|
+
import { RegExpCursor } from '@codemirror/search';
|
|
30
|
+
|
|
31
|
+
import { lineIndexAt, lineStartOffset, type TextRange } from '@/shared/lib/code/editor/lines';
|
|
32
|
+
|
|
33
|
+
export { lineIndexAt, lineStartOffset };
|
|
34
|
+
|
|
35
|
+
export interface FindOptions {
|
|
36
|
+
matchCase: boolean;
|
|
37
|
+
wholeWord: boolean;
|
|
38
|
+
isRegex: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Half-open `[start, end)` offset range in the document. */
|
|
42
|
+
export type FindMatch = TextRange;
|
|
43
|
+
|
|
44
|
+
export interface FindResult {
|
|
45
|
+
matches: FindMatch[];
|
|
46
|
+
/** The document holds more matches than the cap; the list stops at it. */
|
|
47
|
+
truncated: boolean;
|
|
48
|
+
/** A regex query that does not compile — a different answer from "no matches". */
|
|
49
|
+
invalid: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Ceiling on collected matches. A one-character query over a large file matches
|
|
54
|
+
* tens of thousands of times and every match is a decoration, so past this the
|
|
55
|
+
* list stops and the widget reports `N+`.
|
|
56
|
+
*/
|
|
57
|
+
export const MAX_FIND_MATCHES = 5_000;
|
|
58
|
+
|
|
59
|
+
export const DEFAULT_FIND_OPTIONS: FindOptions = {
|
|
60
|
+
matchCase: false,
|
|
61
|
+
wholeWord: false,
|
|
62
|
+
isRegex: false,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The flags `RegExpCursor` compiles with, stated once.
|
|
67
|
+
*
|
|
68
|
+
* This is not a preference: the cursor builds its own `RegExp` from the pattern
|
|
69
|
+
* SOURCE with `gm` (plus `u` where the engine has unicode property escapes), and
|
|
70
|
+
* a write path that compiled the same source with different flags would answer
|
|
71
|
+
* differently from the list on screen. Measured before this was aligned:
|
|
72
|
+
*
|
|
73
|
+
* - `^foo` / `foo$` — the cursor's `m` makes the anchor per LINE, so the widget
|
|
74
|
+
* counted 2 matches on a three-line file while Replace All rewrote only the
|
|
75
|
+
* first (no `m` = start/end of the whole document).
|
|
76
|
+
* - `\p{L}\d` — matched three times in the scan and zero in the replacement,
|
|
77
|
+
* because a property escape is only a property escape with `u`; without it
|
|
78
|
+
* `\p` is a literal `p`, so Replace All and Replace One did nothing at all.
|
|
79
|
+
* - `\-`, `\A` — invalid escapes under `u`, so the scan called the pattern
|
|
80
|
+
* broken while Replace All still edited the file with the non-unicode reading.
|
|
81
|
+
*
|
|
82
|
+
* So the flags live here, `buildFindRegex` is the only compiler, and the scan,
|
|
83
|
+
* the replace preview and both write paths read it.
|
|
84
|
+
*/
|
|
85
|
+
const BASE_FLAGS = 'gm' + ('unicode' in RegExp.prototype ? 'u' : '');
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The pattern a query compiles to, or null when the query is empty or does not
|
|
89
|
+
* compile. A literal query is escaped here rather than at the call site so the
|
|
90
|
+
* match scan and the replacement splice can never disagree about what the
|
|
91
|
+
* pattern was.
|
|
92
|
+
*
|
|
93
|
+
* Whole-word is a `\b`-anchored non-capturing group rather than a bare wrap so
|
|
94
|
+
* an alternation (`foo|bar`) keeps its own precedence.
|
|
95
|
+
*/
|
|
96
|
+
export function buildFindRegex(query: string, options: FindOptions): RegExp | null {
|
|
97
|
+
if (!query) return null;
|
|
98
|
+
const source = options.isRegex ? query : query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
99
|
+
const wrapped = options.wholeWord ? `\\b(?:${source})\\b` : source;
|
|
100
|
+
try {
|
|
101
|
+
return new RegExp(wrapped, options.matchCase ? BASE_FLAGS : `${BASE_FLAGS}i`);
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The last string converted to a `Text`, kept for one call.
|
|
109
|
+
*
|
|
110
|
+
* A cursor needs a `Text`, and `Text.of(text.split('\n'))` builds a rope over
|
|
111
|
+
* the whole file. The find widget asks for matches on every keystroke in its
|
|
112
|
+
* query and on every edit, always for the same document, so keeping the last
|
|
113
|
+
* conversion is the difference between one split per render and one per call.
|
|
114
|
+
*/
|
|
115
|
+
let lastTextSource: string | null = null;
|
|
116
|
+
let lastText: Text | null = null;
|
|
117
|
+
|
|
118
|
+
function asText(source: string): Text {
|
|
119
|
+
if (lastTextSource === source && lastText) return lastText;
|
|
120
|
+
lastText = Text.of(source.split('\n'));
|
|
121
|
+
lastTextSource = source;
|
|
122
|
+
return lastText;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Every match of `query` in `text`, in document order.
|
|
127
|
+
*
|
|
128
|
+
* `RegExpCursor` searches LINE by line for a pattern that cannot span one, and
|
|
129
|
+
* falls back to a whole-document cursor when the pattern mentions `\n`, `\r`,
|
|
130
|
+
* `\s`, `\W`, `\D`, `\b`-free character classes or `[^`. Both return document
|
|
131
|
+
* offsets, so the caller never learns which one ran.
|
|
132
|
+
*/
|
|
133
|
+
export function findMatches(text: string, query: string, options: FindOptions): FindResult {
|
|
134
|
+
if (!query) return { matches: [], truncated: false, invalid: false };
|
|
135
|
+
const pattern = buildFindRegex(query, options);
|
|
136
|
+
if (!pattern) return { matches: [], truncated: false, invalid: true };
|
|
137
|
+
|
|
138
|
+
const matches: FindMatch[] = [];
|
|
139
|
+
let cursor: RegExpCursor;
|
|
140
|
+
try {
|
|
141
|
+
cursor = new RegExpCursor(asText(text), pattern.source, { ignoreCase: !options.matchCase });
|
|
142
|
+
} catch {
|
|
143
|
+
// A pattern `new RegExp` accepts but the cursor's own construction refuses
|
|
144
|
+
// (an unsupported flag combination) is still a broken pattern to the user.
|
|
145
|
+
return { matches: [], truncated: false, invalid: true };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
while (matches.length < MAX_FIND_MATCHES && !cursor.next().done) {
|
|
149
|
+
const { from, to } = cursor.value;
|
|
150
|
+
if (from === to) continue;
|
|
151
|
+
matches.push({ start: from, end: to });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return { matches, truncated: matches.length >= MAX_FIND_MATCHES, invalid: false };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* The match to call "current" for a caret/anchor at `offset`: the first one
|
|
159
|
+
* that ends after it, so a caret sitting inside a match still counts as being
|
|
160
|
+
* on that match. Wraps to the first match when the anchor is past the last one,
|
|
161
|
+
* and reports -1 for an empty list.
|
|
162
|
+
*/
|
|
163
|
+
export function matchIndexAtOrAfter(matches: readonly FindMatch[], offset: number): number {
|
|
164
|
+
if (matches.length === 0) return -1;
|
|
165
|
+
for (let index = 0; index < matches.length; index++) {
|
|
166
|
+
if (matches[index].end > offset) return index;
|
|
167
|
+
}
|
|
168
|
+
return 0;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Step one match forward (`1`) or back (`-1`), wrapping at either end. */
|
|
172
|
+
export function stepMatchIndex(index: number, count: number, direction: 1 | -1): number {
|
|
173
|
+
if (count <= 0) return -1;
|
|
174
|
+
if (index < 0) return direction === 1 ? 0 : count - 1;
|
|
175
|
+
return (index + direction + count) % count;
|
|
176
|
+
}
|
|
177
|
+
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
*
|
|
9
9
|
* One table is the whole truth about an editor shortcut: what it does, which
|
|
10
10
|
* chords reach it on each platform, and what the palette calls it. The
|
|
11
|
-
*
|
|
12
|
-
* palette all read this table, so a shortcut cannot be listed in the
|
|
13
|
-
* and unbound in the editor.
|
|
11
|
+
* generated CodeMirror keymap, the find bar's buttons, the tooltips and the
|
|
12
|
+
* command palette all read this table, so a shortcut cannot be listed in the
|
|
13
|
+
* palette and unbound in the editor.
|
|
14
14
|
*
|
|
15
15
|
* VS Code's defaults are followed wherever this editor has an equivalent. Where
|
|
16
16
|
* it has none — folding, go-to-definition, the `⌘K` prefix chords — the command
|
|
@@ -4,21 +4,17 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Line arithmetic, which
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* Line arithmetic, which the highlighter's per-line walk and the find widget's
|
|
8
|
+
* "jump to line N" both read.
|
|
9
|
+
*
|
|
10
|
+
* The interesting cases are the two the arithmetic gets wrong by accident: the
|
|
11
|
+
* empty line a trailing newline opens, and an offset past the end of the
|
|
12
|
+
* document.
|
|
11
13
|
*/
|
|
12
14
|
|
|
13
15
|
import { describe, expect, test } from 'bun:test';
|
|
14
16
|
|
|
15
|
-
import {
|
|
16
|
-
countLines,
|
|
17
|
-
lineIndexAt,
|
|
18
|
-
lineRangeAt,
|
|
19
|
-
lineRangesInSelection,
|
|
20
|
-
lineStartOffset,
|
|
21
|
-
} from '@/shared/lib/code/editor/lines';
|
|
17
|
+
import { lineIndexAt, lineStartOffset } from '@/shared/lib/code/editor/lines';
|
|
22
18
|
|
|
23
19
|
describe('lineIndexAt and lineStartOffset', () => {
|
|
24
20
|
test('are inverses for every line start', () => {
|
|
@@ -34,48 +30,4 @@ describe('lineIndexAt and lineStartOffset', () => {
|
|
|
34
30
|
expect(lineIndexAt('one\ntwo', 999)).toBe(1);
|
|
35
31
|
expect(lineStartOffset('one\ntwo', 99)).toBe(7);
|
|
36
32
|
});
|
|
37
|
-
|
|
38
|
-
test('count a trailing newline as opening one more, empty line', () => {
|
|
39
|
-
expect(countLines('one\ntwo')).toBe(2);
|
|
40
|
-
expect(countLines('one\ntwo\n')).toBe(3);
|
|
41
|
-
expect(countLines('')).toBe(1);
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
describe('lineRangeAt', () => {
|
|
46
|
-
test('includes the trailing newline, so the range can delete or move the line', () => {
|
|
47
|
-
expect(lineRangeAt('one\ntwo\nthree', 5)).toEqual({ start: 4, end: 8 });
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test('the last line has no newline to include', () => {
|
|
51
|
-
expect(lineRangeAt('one\ntwo', 5)).toEqual({ start: 4, end: 7 });
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
test('a caret on the empty line a trailing newline opens yields an empty range there', () => {
|
|
55
|
-
// `a\n` has an empty second line at offset 2; the honest reading of that
|
|
56
|
-
// position is "no characters of its own", not the previous line.
|
|
57
|
-
expect(lineRangeAt('a\n', 2)).toEqual({ start: 2, end: 2 });
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
describe('lineRangesInSelection', () => {
|
|
62
|
-
test('covers every line the selection touches', () => {
|
|
63
|
-
const text = 'one\ntwo\nthree';
|
|
64
|
-
expect(lineRangesInSelection(text, { start: 4, end: 7 })).toEqual([{ start: 4, end: 8 }]);
|
|
65
|
-
expect(lineRangesInSelection(text, { start: 0, end: 7 })).toEqual([
|
|
66
|
-
{ start: 0, end: 4 },
|
|
67
|
-
{ start: 4, end: 8 },
|
|
68
|
-
]);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test('a selection ending exactly at a line start does not touch that line', () => {
|
|
72
|
-
// The rule VS Code applies, and the one that keeps a whole-line selection
|
|
73
|
-
// from splitting off an empty trailing entry.
|
|
74
|
-
const text = 'one\ntwo';
|
|
75
|
-
expect(lineRangesInSelection(text, { start: 0, end: 4 })).toEqual([{ start: 0, end: 4 }]);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
test('an empty selection still names the line the caret is on', () => {
|
|
79
|
-
expect(lineRangesInSelection('one\ntwo', { start: 5, end: 5 })).toEqual([{ start: 4, end: 7 }]);
|
|
80
|
-
});
|
|
81
33
|
});
|
|
@@ -4,13 +4,15 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Line arithmetic over a buffer
|
|
7
|
+
* Line arithmetic over a buffer: which line an offset is on, and where that
|
|
8
|
+
* line begins.
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* Two consumers, and they must agree — the highlighter's per-line walk and the
|
|
11
|
+
* find widget's "jump to line N" both turn a line number into an offset. The
|
|
12
|
+
* editor's own line COMMANDS no longer come through here: every one of them
|
|
13
|
+
* (delete, move, duplicate, comment, indent) is a CodeMirror command now, and
|
|
14
|
+
* `state.doc.line*` is what they read. `lineRangeAt`/`lineRangesInSelection`
|
|
15
|
+
* went with them.
|
|
14
16
|
*/
|
|
15
17
|
|
|
16
18
|
/** Half-open `[start, end)` offset range in a document. */
|
|
@@ -39,45 +41,3 @@ export function lineStartOffset(text: string, lineIndex: number): number {
|
|
|
39
41
|
}
|
|
40
42
|
return offset;
|
|
41
43
|
}
|
|
42
|
-
|
|
43
|
-
/** How many lines `text` holds; a trailing newline opens one more (empty) line. */
|
|
44
|
-
export function countLines(text: string): number {
|
|
45
|
-
let count = 1;
|
|
46
|
-
for (let index = text.indexOf('\n'); index !== -1; index = text.indexOf('\n', index + 1)) count++;
|
|
47
|
-
return count;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* The whole line containing `offset`, INCLUDING its trailing newline.
|
|
52
|
-
*
|
|
53
|
-
* Including the newline is what makes the range directly usable by the line
|
|
54
|
-
* commands: deleting it removes the line rather than blanking it, and moving it
|
|
55
|
-
* carries the line's own separator with it. A caret on the empty line a
|
|
56
|
-
* trailing newline opens (`"a\n"` at offset 2) yields an empty range there,
|
|
57
|
-
* which the commands treat as "the newline above" — the honest reading of a
|
|
58
|
-
* position that has no characters of its own.
|
|
59
|
-
*/
|
|
60
|
-
export function lineRangeAt(text: string, offset: number): TextRange {
|
|
61
|
-
const lineStart = lineStartOffset(text, lineIndexAt(text, offset));
|
|
62
|
-
const lineEnd = text.indexOf('\n', lineStart);
|
|
63
|
-
if (lineEnd === -1) return { start: lineStart, end: text.length };
|
|
64
|
-
return { start: lineStart, end: lineEnd + 1 };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* The ranges of every line a selection touches, in document order.
|
|
69
|
-
*
|
|
70
|
-
* A selection ending exactly at a line start does not touch that line — the
|
|
71
|
-
* same rule VS Code applies, and the one that keeps ⌘⇧L on a whole-line
|
|
72
|
-
* selection from splitting off an empty trailing entry.
|
|
73
|
-
*/
|
|
74
|
-
export function lineRangesInSelection(text: string, selection: TextRange): TextRange[] {
|
|
75
|
-
const startLine = lineIndexAt(text, selection.start);
|
|
76
|
-
const lastOffset = selection.end > selection.start && text[selection.end - 1] === '\n' ? selection.end - 1 : selection.end;
|
|
77
|
-
const endLine = lineIndexAt(text, lastOffset);
|
|
78
|
-
const ranges: TextRange[] = [];
|
|
79
|
-
for (let line = startLine; line <= endLine; line++) {
|
|
80
|
-
ranges.push(lineRangeAt(text, lineStartOffset(text, line)));
|
|
81
|
-
}
|
|
82
|
-
return ranges;
|
|
83
|
-
}
|
|
@@ -57,12 +57,11 @@ describe('tokenMarkup', () => {
|
|
|
57
57
|
expect(tokenMarkup('<', 0, [{ start: 0, end: 1 }], -1)).toContain('<');
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
-
test('the current match
|
|
61
|
-
// The caret
|
|
62
|
-
//
|
|
60
|
+
test('the current match is painted differently from the others', () => {
|
|
61
|
+
// The caret is aimed at exactly one match, so the reader has to be able to
|
|
62
|
+
// see which — and the others must not claim that mark.
|
|
63
63
|
expect(tokenMarkup('a', 0, [{ start: 0, end: 1 }], 0)).toContain('data-find-current');
|
|
64
|
-
expect(tokenMarkup('a', 0, [{ start: 0, end: 1 }], -1
|
|
65
|
-
expect(tokenMarkup('a', 0, [{ start: 0, end: 1 }], -1, 0)).not.toContain('data-find-current');
|
|
64
|
+
expect(tokenMarkup('a', 0, [{ start: 0, end: 1 }], -1)).not.toContain('data-find-current');
|
|
66
65
|
});
|
|
67
66
|
|
|
68
67
|
test('a plain match is neither current nor an occurrence', () => {
|
|
@@ -12,11 +12,16 @@
|
|
|
12
12
|
* match routinely spans several tokens (`const alpha` is three) and a token
|
|
13
13
|
* routinely holds several matches.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
15
|
+
* This is the markup path for the surfaces that render highlighted HTML (the
|
|
16
|
+
* chat's excerpt cards, the search panel's rows, the wiki reader). The EDITOR no
|
|
17
|
+
* longer uses it: CodeMirror draws its own decorations from a state field
|
|
18
|
+
* (`code-surface/marks.ts`), which is what stopped every find result from
|
|
19
|
+
* forcing a re-tokenize.
|
|
20
|
+
*
|
|
21
|
+
* `<mark>` is an INLINE element with a background and no padding or border, so
|
|
22
|
+
* it paints without moving the glyphs it sits between — anything that changes
|
|
23
|
+
* the box (a block display, a border, an outline with an offset) shifts the text
|
|
24
|
+
* and breaks the alignment a caller's own layout depends on.
|
|
20
25
|
*
|
|
21
26
|
* Kept out of `syntax-highlight.ts` so the tokenizer module stays under the
|
|
22
27
|
* repository's line ceiling and because this is the find feature's concern
|
|
@@ -24,7 +29,7 @@
|
|
|
24
29
|
*/
|
|
25
30
|
|
|
26
31
|
import { escapeCode } from '@/shared/lib/code/highlighter';
|
|
27
|
-
import type { FindMatch } from '@/shared/lib/code/editor/find';
|
|
32
|
+
import type { FindMatch } from '@/shared/lib/code/editor/find-scan';
|
|
28
33
|
|
|
29
34
|
/**
|
|
30
35
|
* How `highlightLines` paints its marks. Declared here rather than in the
|
|
@@ -41,32 +46,16 @@ export interface HighlightLinesOptions {
|
|
|
41
46
|
* Match ranges (absolute offsets in `code`) to wrap in `<mark class="find-match">`.
|
|
42
47
|
* The find widget's highlights ride on the same markup the tokens do, so a
|
|
43
48
|
* match cannot drift from the text it belongs to — and an inline `<mark>`
|
|
44
|
-
* paints a background without moving the glyphs
|
|
45
|
-
* transparent `<textarea>` above aligned with this layer.
|
|
49
|
+
* paints a background without moving the glyphs it sits between.
|
|
46
50
|
*/
|
|
47
51
|
marks?: readonly FindMatch[];
|
|
48
52
|
/** Index into `marks` of the current match, marked `data-find-current`. */
|
|
49
53
|
currentMark?: number;
|
|
50
|
-
/**
|
|
51
|
-
* Marks from this index on are extra ⌘D SELECTIONS rather than find matches,
|
|
52
|
-
* and are painted as such. The caller appends them after the matches, so one
|
|
53
|
-
* index is all the split needs.
|
|
54
|
-
*/
|
|
55
|
-
occurrenceFrom?: number;
|
|
56
54
|
}
|
|
57
55
|
|
|
58
56
|
export const MARK_OPEN = '<mark class="find-match">';
|
|
59
57
|
/** The current match is the one the caret/next-step is aimed at; the CSS paints it darker. */
|
|
60
58
|
export const MARK_CURRENT_OPEN = '<mark class="find-match" data-find-current="">';
|
|
61
|
-
/**
|
|
62
|
-
* An extra ⌘D range. It is not a find match — it is a second SELECTION, and it
|
|
63
|
-
* has to read as one: the caret can only be in the primary range, so painting
|
|
64
|
-
* them alike would leave the user unable to tell which one their keystrokes go
|
|
65
|
-
* to. The two differ only in colour; the geometry is shared, which is what
|
|
66
|
-
* keeps the layers aligned.
|
|
67
|
-
*/
|
|
68
|
-
export const MARK_OCCURRENCE_OPEN = '<mark class="find-match" data-occurrence="">';
|
|
69
|
-
|
|
70
59
|
/**
|
|
71
60
|
* `content` with every intersecting mark spliced in, HTML-escaped.
|
|
72
61
|
*
|
|
@@ -79,7 +68,6 @@ export function tokenMarkup(
|
|
|
79
68
|
tokenStart: number,
|
|
80
69
|
marks: readonly FindMatch[] | undefined,
|
|
81
70
|
currentMark: number,
|
|
82
|
-
occurrenceFrom = -1,
|
|
83
71
|
): string {
|
|
84
72
|
const tokenEnd = tokenStart + content.length;
|
|
85
73
|
if (!marks || marks.length === 0) return escapeCode(content);
|
|
@@ -98,7 +86,7 @@ export function tokenMarkup(
|
|
|
98
86
|
const to = Math.min(mark.end, tokenEnd) - tokenStart;
|
|
99
87
|
if (to <= cursor) continue;
|
|
100
88
|
html += escapeCode(content.slice(cursor, from));
|
|
101
|
-
const open = index === currentMark ? MARK_CURRENT_OPEN :
|
|
89
|
+
const open = index === currentMark ? MARK_CURRENT_OPEN : MARK_OPEN;
|
|
102
90
|
html += open + escapeCode(content.slice(from, to)) + '</mark>';
|
|
103
91
|
cursor = to;
|
|
104
92
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Registering an editor theme with the shared Shiki highlighter, on demand.
|
|
8
|
+
*
|
|
9
|
+
* Shiki throws for a theme it has not been given, and `@shikijs/themes` ships 65
|
|
10
|
+
* of them (1.3 MB of TextMate JSON). So a theme is loaded the first time it is
|
|
11
|
+
* SELECTED — the picker's grid draws from the generated colour rows and fetches
|
|
12
|
+
* nothing — and a surface that asks for one before it lands keeps the plain
|
|
13
|
+
* fallback and re-runs on `onEditorThemeReady`.
|
|
14
|
+
*
|
|
15
|
+
* This lives beside the tokenizer rather than in a client hook because the
|
|
16
|
+
* decoration plugin is what needs the notification, and it must not import a
|
|
17
|
+
* Preact module to get it.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { getHighlighterSync } from '@/shared/lib/code/highlighter';
|
|
21
|
+
import {
|
|
22
|
+
EDITOR_AUTO_THEME_ID,
|
|
23
|
+
EDITOR_THEME_LOADERS,
|
|
24
|
+
} from '@/shared/lib/code/editor/themes';
|
|
25
|
+
|
|
26
|
+
/** Ids registered with the highlighter. */
|
|
27
|
+
const loaded = new Set<string>();
|
|
28
|
+
/** In-flight loads, so two surfaces selecting one theme share a fetch. */
|
|
29
|
+
const pending = new Map<string, Promise<void>>();
|
|
30
|
+
const listeners = new Set<(id: string) => void>();
|
|
31
|
+
|
|
32
|
+
function notifyReady(id: string): void {
|
|
33
|
+
for (const listener of [...listeners]) {
|
|
34
|
+
try {
|
|
35
|
+
listener(id);
|
|
36
|
+
} catch {
|
|
37
|
+
// A listener must never break the others.
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Fires when a theme module lands, so a pending tokenize can retry. */
|
|
43
|
+
export function onEditorThemeReady(listener: (id: string) => void): () => void {
|
|
44
|
+
listeners.add(listener);
|
|
45
|
+
return () => {
|
|
46
|
+
listeners.delete(listener);
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Whether Shiki can tokenize with `id` synchronously right now. */
|
|
51
|
+
export function isEditorThemeReady(id: string): boolean {
|
|
52
|
+
if (id === EDITOR_AUTO_THEME_ID) return true;
|
|
53
|
+
return loaded.has(id);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Register `id`'s theme with the shared highlighter.
|
|
58
|
+
*
|
|
59
|
+
* Resolves immediately for `auto`, for an id this build does not ship, and for
|
|
60
|
+
* a highlighter that has not booted yet (the caller keeps the fallback and is
|
|
61
|
+
* retried by `onSyntaxReady`). Never rejects: a theme that fails to load leaves
|
|
62
|
+
* the surface on the app palette, which is a wrong palette rather than a blank
|
|
63
|
+
* editor.
|
|
64
|
+
*/
|
|
65
|
+
export function loadEditorTheme(id: string): Promise<void> {
|
|
66
|
+
if (id === EDITOR_AUTO_THEME_ID || loaded.has(id)) return Promise.resolve();
|
|
67
|
+
const loader = EDITOR_THEME_LOADERS[id];
|
|
68
|
+
if (!loader) return Promise.resolve();
|
|
69
|
+
const inFlight = pending.get(id);
|
|
70
|
+
if (inFlight) return inFlight;
|
|
71
|
+
|
|
72
|
+
const highlighter = getHighlighterSync();
|
|
73
|
+
if (!highlighter) return Promise.resolve();
|
|
74
|
+
|
|
75
|
+
const load = highlighter
|
|
76
|
+
.loadTheme(loader as never)
|
|
77
|
+
.then(() => {
|
|
78
|
+
loaded.add(id);
|
|
79
|
+
notifyReady(id);
|
|
80
|
+
})
|
|
81
|
+
.catch(() => {
|
|
82
|
+
// Left out of `loaded` so a later attempt can still succeed.
|
|
83
|
+
})
|
|
84
|
+
.finally(() => {
|
|
85
|
+
pending.delete(id);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
pending.set(id, load);
|
|
89
|
+
return load;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Test seam: forget every registration (a highlighter restart). */
|
|
93
|
+
export function resetEditorThemeLoader(): void {
|
|
94
|
+
loaded.clear();
|
|
95
|
+
pending.clear();
|
|
96
|
+
listeners.clear();
|
|
97
|
+
}
|