pim-agent 0.0.1 → 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/LICENSE +21 -0
- package/README.md +293 -7
- package/bin/pim.ts +151 -0
- package/package.json +82 -4
- package/packages/core/src/attachments/AttachmentStore.ts +99 -0
- package/packages/core/src/attachments/Attachments.ts +69 -0
- package/packages/core/src/extensions/CoreExtensions.ts +51 -0
- package/packages/core/src/extensions/apply-patch/coordinator.ts +53 -0
- package/packages/core/src/extensions/apply-patch/executor.ts +550 -0
- package/packages/core/src/extensions/apply-patch/index.ts +69 -0
- package/packages/core/src/extensions/apply-patch/matcher.ts +47 -0
- package/packages/core/src/extensions/apply-patch/model.ts +39 -0
- package/packages/core/src/extensions/apply-patch/parser.ts +379 -0
- package/packages/core/src/extensions/apply-patch/render.ts +216 -0
- package/packages/core/src/extensions/apply-patch/schema.ts +37 -0
- package/packages/core/src/extensions/apply-patch/types.ts +30 -0
- package/packages/core/src/extensions/bash/capture.ts +86 -0
- package/packages/core/src/extensions/bash/format.ts +110 -0
- package/packages/core/src/extensions/bash/image.ts +31 -0
- package/packages/core/src/extensions/bash/index.ts +88 -0
- package/packages/core/src/extensions/bash/render.ts +31 -0
- package/packages/core/src/extensions/bash/run.ts +221 -0
- package/packages/core/src/extensions/bash/schema.ts +64 -0
- package/packages/core/src/extensions/edit/edit.ts +366 -0
- package/packages/core/src/extensions/edit/index.ts +36 -0
- package/packages/core/src/extensions/edit/render.ts +16 -0
- package/packages/core/src/extensions/edit/schema.ts +37 -0
- package/packages/core/src/extensions/glob/glob.ts +34 -0
- package/packages/core/src/extensions/glob/index.ts +83 -0
- package/packages/core/src/extensions/glob/render.ts +72 -0
- package/packages/core/src/extensions/glob/schema.ts +66 -0
- package/packages/core/src/extensions/grep/grep.ts +256 -0
- package/packages/core/src/extensions/grep/index.ts +114 -0
- package/packages/core/src/extensions/grep/render.ts +271 -0
- package/packages/core/src/extensions/grep/schema.ts +92 -0
- package/packages/core/src/extensions/read/ImageMemory.ts +42 -0
- package/packages/core/src/extensions/read/image.ts +57 -0
- package/packages/core/src/extensions/read/index.ts +88 -0
- package/packages/core/src/extensions/read/read.ts +191 -0
- package/packages/core/src/extensions/read/render.ts +58 -0
- package/packages/core/src/extensions/read/schema.ts +49 -0
- package/packages/core/src/extensions/subagent/index.ts +33 -0
- package/packages/core/src/extensions/subagent/render.ts +127 -0
- package/packages/core/src/extensions/subagent/schema.ts +9 -0
- package/packages/core/src/extensions/subagent/subagent.ts +419 -0
- package/packages/core/src/extensions/system-prompt/index.ts +28 -0
- package/packages/core/src/extensions/system-prompt/prompt.ts +228 -0
- package/packages/core/src/extensions/todo/index.ts +123 -0
- package/packages/core/src/extensions/todo/render.ts +156 -0
- package/packages/core/src/extensions/todo/schema.ts +24 -0
- package/packages/core/src/extensions/todo/todo.ts +185 -0
- package/packages/core/src/extensions/web-fetch/JinaReaderClient.ts +187 -0
- package/packages/core/src/extensions/web-fetch/WebViewFetchClient.ts +183 -0
- package/packages/core/src/extensions/web-fetch/WebViewMarkdownSnapshot.ts +512 -0
- package/packages/core/src/extensions/web-fetch/fetch.ts +265 -0
- package/packages/core/src/extensions/web-fetch/image.ts +165 -0
- package/packages/core/src/extensions/web-fetch/index.ts +83 -0
- package/packages/core/src/extensions/web-fetch/render.ts +100 -0
- package/packages/core/src/extensions/web-fetch/schema.ts +46 -0
- package/packages/core/src/extensions/web-search/ExaMcpClient.ts +251 -0
- package/packages/core/src/extensions/web-search/SearchBreaker.ts +147 -0
- package/packages/core/src/extensions/web-search/SearchChain.ts +77 -0
- package/packages/core/src/extensions/web-search/index.ts +90 -0
- package/packages/core/src/extensions/web-search/providers/DuckDuckGoProvider.ts +195 -0
- package/packages/core/src/extensions/web-search/providers/ExaProvider.ts +54 -0
- package/packages/core/src/extensions/web-search/providers/FirecrawlProvider.ts +144 -0
- package/packages/core/src/extensions/web-search/providers/SearchProvider.ts +118 -0
- package/packages/core/src/extensions/web-search/render.ts +49 -0
- package/packages/core/src/extensions/web-search/schema.ts +29 -0
- package/packages/core/src/extensions/web-search/search.ts +23 -0
- package/packages/core/src/extensions/write/index.ts +58 -0
- package/packages/core/src/extensions/write/render.ts +16 -0
- package/packages/core/src/extensions/write/schema.ts +14 -0
- package/packages/core/src/extensions/write/write.ts +87 -0
- package/packages/core/src/picker/FilePickerSuggestionEngine.ts +14 -0
- package/packages/core/src/picker/InProcessFilePickerSuggestionEngine.ts +52 -0
- package/packages/core/src/picker/PickerItem.ts +6 -0
- package/packages/core/src/picker/PickerService.ts +110 -0
- package/packages/core/src/picker/RemoteFilePickerSuggestionEngine.ts +56 -0
- package/packages/core/src/picker/WorkerFilePickerSuggestionEngine.ts +268 -0
- package/packages/core/src/picker/catalog.ts +220 -0
- package/packages/core/src/picker/commandRanker.ts +17 -0
- package/packages/core/src/picker/filePickerWorker.ts +66 -0
- package/packages/core/src/picker/filePickerWorkerMessages.ts +39 -0
- package/packages/core/src/picker/ranker.ts +245 -0
- package/packages/core/src/picker/token.ts +70 -0
- package/packages/core/src/session/AgentRuntime.ts +85 -0
- package/packages/core/src/session/EventLog.ts +213 -0
- package/packages/core/src/session/MessageText.ts +21 -0
- package/packages/core/src/session/ReadCursors.ts +94 -0
- package/packages/core/src/session/SessionCache.ts +68 -0
- package/packages/core/src/session/SessionHost.ts +876 -0
- package/packages/core/src/session/SessionLease.ts +279 -0
- package/packages/core/src/session/SessionRegistry.ts +169 -0
- package/packages/core/src/session/WriteMark.ts +37 -0
- package/packages/core/src/shared/Cli.ts +25 -0
- package/packages/core/src/shared/DaemonUnit.ts +13 -0
- package/packages/core/src/shared/DiffLines.ts +304 -0
- package/packages/core/src/shared/DiffRenderer.ts +283 -0
- package/packages/core/src/shared/DiffView.ts +29 -0
- package/packages/core/src/shared/Directories.ts +61 -0
- package/packages/core/src/shared/EditMatcher.ts +767 -0
- package/packages/core/src/shared/Errors.ts +9 -0
- package/packages/core/src/shared/ExtensionToggles.ts +130 -0
- package/packages/core/src/shared/FileEnumerator.ts +361 -0
- package/packages/core/src/shared/FileScanner.ts +57 -0
- package/packages/core/src/shared/FileWatch.ts +107 -0
- package/packages/core/src/shared/Format.ts +74 -0
- package/packages/core/src/shared/Fs.ts +91 -0
- package/packages/core/src/shared/FsErrors.ts +67 -0
- package/packages/core/src/shared/FuzzyMatcher.ts +71 -0
- package/packages/core/src/shared/Git.ts +89 -0
- package/packages/core/src/shared/GlobExclusions.ts +13 -0
- package/packages/core/src/shared/Http.ts +14 -0
- package/packages/core/src/shared/ImageMime.ts +36 -0
- package/packages/core/src/shared/Images.ts +357 -0
- package/packages/core/src/shared/Json.ts +19 -0
- package/packages/core/src/shared/Languages.ts +95 -0
- package/packages/core/src/shared/Levenshtein.ts +30 -0
- package/packages/core/src/shared/Lines.ts +79 -0
- package/packages/core/src/shared/McpClient.ts +444 -0
- package/packages/core/src/shared/OutputBudget.ts +84 -0
- package/packages/core/src/shared/PatchSummary.ts +40 -0
- package/packages/core/src/shared/Paths.ts +80 -0
- package/packages/core/src/shared/PimSettings.ts +128 -0
- package/packages/core/src/shared/PimVersion.ts +93 -0
- package/packages/core/src/shared/Pool.ts +26 -0
- package/packages/core/src/shared/Proc.ts +30 -0
- package/packages/core/src/shared/RateLimiter.ts +50 -0
- package/packages/core/src/shared/Renderer.ts +371 -0
- package/packages/core/src/shared/SafePath.ts +17 -0
- package/packages/core/src/shared/SearchRender.ts +82 -0
- package/packages/core/src/shared/SendFile.ts +23 -0
- package/packages/core/src/shared/SpillCache.ts +78 -0
- package/packages/core/src/shared/SubagentLogs.ts +102 -0
- package/packages/core/src/shared/Supervisor.ts +460 -0
- package/packages/core/src/shared/Sweeper.ts +32 -0
- package/packages/core/src/shared/Tools.ts +801 -0
- package/packages/core/src/shared/Updater.ts +193 -0
- package/packages/core/src/view/AnsiPainter.ts +227 -0
- package/packages/core/src/view/BodyRenderer.ts +102 -0
- package/packages/core/src/view/DiffBlocks.ts +52 -0
- package/packages/core/src/view/DiffLayout.ts +69 -0
- package/packages/core/src/view/MarkdownPainter.ts +314 -0
- package/packages/core/src/view/Painting.ts +182 -0
- package/packages/core/src/view/ViewBlock.ts +124 -0
- package/packages/daemon/src/Daemon.ts +63 -0
- package/packages/daemon/src/DaemonInstall.ts +115 -0
- package/packages/daemon/src/Surfaces.ts +66 -0
- package/packages/daemon/src/TelegramSurface.ts +30 -0
- package/packages/daemon/src/WebSurface.ts +57 -0
- package/packages/daemon/src/index.ts +37 -0
- package/packages/protocol/src/Command.ts +92 -0
- package/packages/protocol/src/ImageRoute.ts +23 -0
- package/packages/protocol/src/Protocol.ts +6 -0
- package/packages/protocol/src/ServerEvent.ts +251 -0
- package/packages/server/src/AttachmentEndpoint.ts +184 -0
- package/packages/server/src/ClientConnection.ts +265 -0
- package/packages/server/src/ImageEndpoint.ts +36 -0
- package/packages/server/src/ProbeClient.ts +312 -0
- package/packages/server/src/Reloader.ts +93 -0
- package/packages/server/src/SendFileTool.ts +88 -0
- package/packages/server/src/SessionCatalogue.ts +202 -0
- package/packages/server/src/SessionProjection.ts +194 -0
- package/packages/server/src/SessionStream.ts +601 -0
- package/packages/server/src/StaticClient.ts +95 -0
- package/packages/server/src/WebOptions.ts +72 -0
- package/packages/server/src/WsGateway.ts +563 -0
- package/packages/server/src/probe.ts +146 -0
- package/packages/telegram/src/Bot.ts +229 -0
- package/packages/telegram/src/Commands.ts +768 -0
- package/packages/telegram/src/Config.ts +148 -0
- package/packages/telegram/src/Markdown.ts +235 -0
- package/packages/telegram/src/Message.ts +152 -0
- package/packages/telegram/src/Renderer.ts +456 -0
- package/packages/telegram/src/SendFileSchema.ts +18 -0
- package/packages/telegram/src/SendFileTool.ts +65 -0
- package/packages/telegram/src/Session.ts +252 -0
- package/packages/telegram/src/SessionRegistry.ts +126 -0
- package/packages/telegram/src/TaskScheduler.ts +296 -0
- package/packages/telegram/src/TaskSchema.ts +88 -0
- package/packages/telegram/src/TaskStore.ts +67 -0
- package/packages/telegram/src/TaskTool.ts +208 -0
- package/packages/telegram/src/TelegramHtml.ts +288 -0
- package/packages/telegram/src/TypingIndicator.ts +43 -0
- package/packages/telegram/src/UpdateConfirm.ts +61 -0
- package/packages/tui/src/extensions/_init/index.ts +144 -0
- package/packages/tui/src/extensions/command-picker/index.ts +43 -0
- package/packages/tui/src/extensions/file-picker/index.ts +156 -0
- package/packages/tui/src/extensions/footer/index.ts +117 -0
- package/packages/tui/src/extensions/footer/powerline.ts +87 -0
- package/packages/tui/src/extensions/footer/segments.ts +202 -0
- package/packages/tui/src/extensions/pim/index.ts +89 -0
- package/packages/tui/src/extensions/session-lease/index.ts +248 -0
- package/packages/tui/src/extensions/tps/index.ts +118 -0
- package/packages/tui/src/extensions/working-indicator/index.ts +75 -0
- package/packages/tui/src/extensions/wrapProvider.ts +18 -0
- package/packages/tui/src/themes/pim-dark.json +84 -0
- package/packages/tui/src/themes/pim-light.json +84 -0
- package/packages/tui/src/themes/themeCliArgs.ts +20 -0
- package/packages/web/dist/client/apple-touch-icon.png +0 -0
- package/packages/web/dist/client/assets/bash-LmRjUKlR.js +1 -0
- package/packages/web/dist/client/assets/c-DBVbJFpC.js +1 -0
- package/packages/web/dist/client/assets/clojure-CR7dvLJ4.js +1 -0
- package/packages/web/dist/client/assets/cmake-CgP6gB4W.js +1 -0
- package/packages/web/dist/client/assets/commit-mono-latin-300-italic-B48RJMeK.woff +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-300-italic-DBq5bJCE.woff2 +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-400-italic-BXinMwCi.woff +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-400-italic-DjSHLl2N.woff2 +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-400-normal-s0S3qwFW.woff +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-400-normal-wzhe4RuD.woff2 +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-600-normal-BuQDXT0M.woff +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-600-normal-SwgSWHaV.woff2 +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-700-normal-1-GU0IUE.woff +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-700-normal-DU8mrtj2.woff2 +0 -0
- package/packages/web/dist/client/assets/core-CGdQx9la.js +3 -0
- package/packages/web/dist/client/assets/cpp-N2d8M7Nc.js +1 -0
- package/packages/web/dist/client/assets/csharp-yYt0Ikks.js +1 -0
- package/packages/web/dist/client/assets/css-D8M8GYaw.js +1 -0
- package/packages/web/dist/client/assets/diff-BhdZizse.js +1 -0
- package/packages/web/dist/client/assets/dockerfile-C0zufGsU.js +1 -0
- package/packages/web/dist/client/assets/elixir-DXIHlGsR.js +1 -0
- package/packages/web/dist/client/assets/erlang-BjYI8dhz.js +1 -0
- package/packages/web/dist/client/assets/go-CYstVVJV.js +1 -0
- package/packages/web/dist/client/assets/graphql-BHHhacaR.js +1 -0
- package/packages/web/dist/client/assets/haskell-DUkhXIda.js +1 -0
- package/packages/web/dist/client/assets/index-BMNwleWO.js +40 -0
- package/packages/web/dist/client/assets/index-CqjB2RPr.css +1 -0
- package/packages/web/dist/client/assets/ini-B1CroMsf.js +1 -0
- package/packages/web/dist/client/assets/java-Dde-Wnb4.js +1 -0
- package/packages/web/dist/client/assets/javascript-8AmHuH5O.js +1 -0
- package/packages/web/dist/client/assets/json-PmyKWHmm.js +1 -0
- package/packages/web/dist/client/assets/kotlin-DgSK8bu-.js +2 -0
- package/packages/web/dist/client/assets/less-Bp0mgmpG.js +1 -0
- package/packages/web/dist/client/assets/lua-1dXYaL-H.js +1 -0
- package/packages/web/dist/client/assets/makefile-BX-o3EIz.js +1 -0
- package/packages/web/dist/client/assets/markdown-D6FrnlqJ.js +1 -0
- package/packages/web/dist/client/assets/ocaml-B-P1Uyq8.js +1 -0
- package/packages/web/dist/client/assets/perl-CvqMZYuf.js +1 -0
- package/packages/web/dist/client/assets/php-DuZl5e-L.js +2 -0
- package/packages/web/dist/client/assets/powershell-BUe16zE4.js +1 -0
- package/packages/web/dist/client/assets/protobuf-DGOW1qT8.js +1 -0
- package/packages/web/dist/client/assets/python-gHmu6VPG.js +1 -0
- package/packages/web/dist/client/assets/r-DNWPCnjT.js +1 -0
- package/packages/web/dist/client/assets/ruby-BGpZ3p4_.js +1 -0
- package/packages/web/dist/client/assets/rust-BIKPVoi6.js +1 -0
- package/packages/web/dist/client/assets/scala-C9lP4FWY.js +1 -0
- package/packages/web/dist/client/assets/scss-gOyOKiXd.js +1 -0
- package/packages/web/dist/client/assets/sql-DWRKyvJ-.js +1 -0
- package/packages/web/dist/client/assets/swift-BTn9__hL.js +1 -0
- package/packages/web/dist/client/assets/typescript-CxkPLmSc.js +1 -0
- package/packages/web/dist/client/assets/vim-Dhhcn_CA.js +1 -0
- package/packages/web/dist/client/assets/xml-COl5yFSC.js +1 -0
- package/packages/web/dist/client/assets/yaml-DjOhhVLG.js +1 -0
- package/packages/web/dist/client/favicon.svg +1 -0
- package/packages/web/dist/client/icon-192.png +0 -0
- package/packages/web/dist/client/icon-512.png +0 -0
- package/packages/web/dist/client/icon-maskable.png +0 -0
- package/packages/web/dist/client/index.html +24 -0
- package/packages/web/dist/client/manifest.webmanifest +21 -0
- package/packages/web/dist/client/wordmark.svg +1 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { readdir, stat } from "node:fs/promises";
|
|
2
|
+
import { isAbsolute, join, parse, resolve, sep } from "node:path";
|
|
3
|
+
import { FileEnumerator } from "../shared/FileEnumerator";
|
|
4
|
+
import { Paths } from "../shared/Paths";
|
|
5
|
+
|
|
6
|
+
export type FileCandidate = {
|
|
7
|
+
readonly insertPath: string;
|
|
8
|
+
readonly displayPath: string;
|
|
9
|
+
readonly matchHaystack: string;
|
|
10
|
+
readonly isDirectory: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type AbsoluteCatalog = {
|
|
14
|
+
readonly candidates: readonly FileCandidate[];
|
|
15
|
+
readonly residualQuery: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type GitSpawnResult = {
|
|
19
|
+
readonly exitCode: number;
|
|
20
|
+
readonly stdout: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type GitSpawner = (
|
|
24
|
+
args: readonly string[],
|
|
25
|
+
options: { readonly cwd: string }
|
|
26
|
+
) => Promise<GitSpawnResult>;
|
|
27
|
+
|
|
28
|
+
export type LoadRelativeOptions = {
|
|
29
|
+
readonly root: string;
|
|
30
|
+
readonly gitSpawner?: GitSpawner;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type LoadAbsoluteOptions = {
|
|
34
|
+
readonly query: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const defaultGitSpawner: GitSpawner = async (args, options) => {
|
|
38
|
+
try {
|
|
39
|
+
const child = Bun.spawn(["git", ...args], {
|
|
40
|
+
cwd: options.cwd,
|
|
41
|
+
stdout: "pipe",
|
|
42
|
+
stderr: "ignore",
|
|
43
|
+
stdin: "ignore",
|
|
44
|
+
});
|
|
45
|
+
const stdout = await new Response(child.stdout).text();
|
|
46
|
+
const exitCode = await child.exited;
|
|
47
|
+
return { exitCode, stdout };
|
|
48
|
+
} catch {
|
|
49
|
+
return { exitCode: -1, stdout: "" };
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const inflightRelative = new Map<string, Promise<readonly FileCandidate[]>>();
|
|
54
|
+
const COLLATOR = new Intl.Collator(undefined);
|
|
55
|
+
|
|
56
|
+
export function loadRelative(
|
|
57
|
+
options: LoadRelativeOptions
|
|
58
|
+
): Promise<readonly FileCandidate[]> {
|
|
59
|
+
const root = resolve(options.root);
|
|
60
|
+
const existing = inflightRelative.get(root);
|
|
61
|
+
if (existing !== undefined) {
|
|
62
|
+
return existing;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const promise = runLoadRelative(root, options).finally(() => {
|
|
66
|
+
inflightRelative.delete(root);
|
|
67
|
+
});
|
|
68
|
+
inflightRelative.set(root, promise);
|
|
69
|
+
return promise;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function loadAbsolute(
|
|
73
|
+
options: LoadAbsoluteOptions
|
|
74
|
+
): Promise<AbsoluteCatalog> {
|
|
75
|
+
const expanded = Paths.expandHome(options.query);
|
|
76
|
+
const normalized = isAbsolute(expanded) ? resolve(expanded) : expanded;
|
|
77
|
+
const anchor = await findAnchor(normalized);
|
|
78
|
+
const residualSlice = normalized.slice(anchor.length);
|
|
79
|
+
const residualQuery =
|
|
80
|
+
residualSlice.startsWith(sep) || residualSlice.startsWith("/")
|
|
81
|
+
? residualSlice.slice(1)
|
|
82
|
+
: residualSlice;
|
|
83
|
+
|
|
84
|
+
let entries: { readonly name: string; readonly isDirectory: boolean }[];
|
|
85
|
+
try {
|
|
86
|
+
const dirents = await readdir(anchor, { withFileTypes: true });
|
|
87
|
+
entries = dirents.map((dirent) => ({
|
|
88
|
+
name: dirent.name,
|
|
89
|
+
isDirectory: dirent.isDirectory(),
|
|
90
|
+
}));
|
|
91
|
+
} catch {
|
|
92
|
+
return { candidates: [], residualQuery };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const includeDot = residualQuery.startsWith(".");
|
|
96
|
+
const filtered = includeDot
|
|
97
|
+
? entries
|
|
98
|
+
: entries.filter((entry) => !entry.name.startsWith("."));
|
|
99
|
+
|
|
100
|
+
filtered.sort((a, b) => {
|
|
101
|
+
if (a.isDirectory !== b.isDirectory) {
|
|
102
|
+
return a.isDirectory ? -1 : 1;
|
|
103
|
+
}
|
|
104
|
+
return a.name.localeCompare(b.name, undefined, { sensitivity: "base" });
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const candidates: FileCandidate[] = filtered.map((entry) => {
|
|
108
|
+
const fullPath = join(anchor, entry.name);
|
|
109
|
+
return {
|
|
110
|
+
insertPath: fullPath,
|
|
111
|
+
displayPath: fullPath,
|
|
112
|
+
matchHaystack: entry.name,
|
|
113
|
+
isDirectory: entry.isDirectory,
|
|
114
|
+
};
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return { candidates, residualQuery };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function runLoadRelative(
|
|
121
|
+
root: string,
|
|
122
|
+
options: LoadRelativeOptions
|
|
123
|
+
): Promise<readonly FileCandidate[]> {
|
|
124
|
+
const spawner = options.gitSpawner ?? defaultGitSpawner;
|
|
125
|
+
|
|
126
|
+
const fastPath = await tryGitListFiles(root, spawner);
|
|
127
|
+
if (fastPath !== undefined) {
|
|
128
|
+
return finalizeRelative(fastPath);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const fallback = await scanWithGlob(root);
|
|
132
|
+
return finalizeRelative(fallback);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function tryGitListFiles(
|
|
136
|
+
root: string,
|
|
137
|
+
spawner: GitSpawner
|
|
138
|
+
): Promise<readonly string[] | undefined> {
|
|
139
|
+
const result = await spawner(
|
|
140
|
+
["ls-files", "--cached", "--others", "--exclude-standard"],
|
|
141
|
+
{ cwd: root }
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
if (result.exitCode !== 0) {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return result.stdout.split("\n").filter((line) => line.length > 0);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function scanWithGlob(root: string): Promise<readonly string[]> {
|
|
152
|
+
return FileEnumerator.enumerate(root, {
|
|
153
|
+
includeDotfiles: false,
|
|
154
|
+
includeIgnored: false,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function finalizeRelative(paths: readonly string[]): readonly FileCandidate[] {
|
|
159
|
+
const normalized = paths.map(Paths.toForwardSlashes);
|
|
160
|
+
|
|
161
|
+
// git ls-files only emits files; recover directories from their prefixes.
|
|
162
|
+
const directories = new Set<string>();
|
|
163
|
+
for (const path of normalized) {
|
|
164
|
+
for (
|
|
165
|
+
let slash = path.indexOf("/");
|
|
166
|
+
slash !== -1;
|
|
167
|
+
slash = path.indexOf("/", slash + 1)
|
|
168
|
+
) {
|
|
169
|
+
directories.add(path.slice(0, slash));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const candidates = [
|
|
174
|
+
...[...directories].map((path) => toRelativeCandidate(path, true)),
|
|
175
|
+
...normalized.map((path) => toRelativeCandidate(path, false)),
|
|
176
|
+
];
|
|
177
|
+
candidates.sort((a, b) => COLLATOR.compare(a.insertPath, b.insertPath));
|
|
178
|
+
|
|
179
|
+
return candidates;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function toRelativeCandidate(
|
|
183
|
+
path: string,
|
|
184
|
+
isDirectory: boolean
|
|
185
|
+
): FileCandidate {
|
|
186
|
+
return {
|
|
187
|
+
insertPath: path,
|
|
188
|
+
displayPath: path,
|
|
189
|
+
matchHaystack: path,
|
|
190
|
+
isDirectory,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function findAnchor(absolutePath: string): Promise<string> {
|
|
195
|
+
if (!isAbsolute(absolutePath)) {
|
|
196
|
+
return parse(process.cwd()).root;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const filesystemRoot = parse(absolutePath).root;
|
|
200
|
+
let current = absolutePath;
|
|
201
|
+
|
|
202
|
+
while (true) {
|
|
203
|
+
try {
|
|
204
|
+
const metadata = await stat(current);
|
|
205
|
+
if (metadata.isDirectory()) {
|
|
206
|
+
return current;
|
|
207
|
+
}
|
|
208
|
+
} catch {}
|
|
209
|
+
|
|
210
|
+
if (current === filesystemRoot) {
|
|
211
|
+
return filesystemRoot;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const parent = resolve(current, "..");
|
|
215
|
+
if (parent === current) {
|
|
216
|
+
return filesystemRoot;
|
|
217
|
+
}
|
|
218
|
+
current = parent;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { PickerItem } from "./PickerItem";
|
|
2
|
+
import { FuzzyMatcher, type FuzzyCandidate } from "../shared/FuzzyMatcher";
|
|
3
|
+
|
|
4
|
+
export function rankCommands(
|
|
5
|
+
query: string,
|
|
6
|
+
items: readonly PickerItem[],
|
|
7
|
+
options: { readonly limit?: number } = {}
|
|
8
|
+
): PickerItem[] {
|
|
9
|
+
const candidates: FuzzyCandidate<PickerItem>[] = items.map((item) => ({
|
|
10
|
+
item,
|
|
11
|
+
haystacks: [item.label, item.description ?? ""],
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
const hits = FuzzyMatcher.rank(query, candidates, options);
|
|
15
|
+
|
|
16
|
+
return hits.map((hit) => hit.item);
|
|
17
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Errors } from "../shared/Errors";
|
|
2
|
+
import type { FileCandidate } from "./catalog";
|
|
3
|
+
import { loadRelative } from "./catalog";
|
|
4
|
+
import type {
|
|
5
|
+
FilePickerWorkerRequest,
|
|
6
|
+
FilePickerWorkerResponse,
|
|
7
|
+
} from "./filePickerWorkerMessages";
|
|
8
|
+
import { rank } from "./ranker";
|
|
9
|
+
|
|
10
|
+
let cachedRelative: readonly FileCandidate[] | undefined;
|
|
11
|
+
|
|
12
|
+
const refreshRelative = async (id: number, root: string): Promise<void> => {
|
|
13
|
+
try {
|
|
14
|
+
cachedRelative = await loadRelative({ root });
|
|
15
|
+
self.postMessage({
|
|
16
|
+
id,
|
|
17
|
+
type: "refreshRelative",
|
|
18
|
+
ok: true,
|
|
19
|
+
} satisfies FilePickerWorkerResponse);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if (cachedRelative === undefined) {
|
|
22
|
+
cachedRelative = [];
|
|
23
|
+
}
|
|
24
|
+
self.postMessage({
|
|
25
|
+
id,
|
|
26
|
+
type: "refreshRelative",
|
|
27
|
+
ok: false,
|
|
28
|
+
error: Errors.describe(error),
|
|
29
|
+
} satisfies FilePickerWorkerResponse);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const rankSuggestions = async (
|
|
34
|
+
id: number,
|
|
35
|
+
query: string,
|
|
36
|
+
limit: number | undefined
|
|
37
|
+
): Promise<void> => {
|
|
38
|
+
try {
|
|
39
|
+
const items = await rank(query, { cachedRelative, limit });
|
|
40
|
+
self.postMessage({
|
|
41
|
+
id,
|
|
42
|
+
type: "rank",
|
|
43
|
+
ok: true,
|
|
44
|
+
items,
|
|
45
|
+
} satisfies FilePickerWorkerResponse);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
self.postMessage({
|
|
48
|
+
id,
|
|
49
|
+
type: "rank",
|
|
50
|
+
ok: false,
|
|
51
|
+
error: Errors.describe(error),
|
|
52
|
+
} satisfies FilePickerWorkerResponse);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
self.onmessage = (event: MessageEvent<FilePickerWorkerRequest>): void => {
|
|
57
|
+
const message = event.data;
|
|
58
|
+
switch (message.type) {
|
|
59
|
+
case "refreshRelative":
|
|
60
|
+
void refreshRelative(message.id, message.root);
|
|
61
|
+
break;
|
|
62
|
+
case "rank":
|
|
63
|
+
void rankSuggestions(message.id, message.query, message.limit);
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { PickerItem } from "./PickerItem";
|
|
2
|
+
|
|
3
|
+
export type FilePickerWorkerRequest =
|
|
4
|
+
| {
|
|
5
|
+
readonly id: number;
|
|
6
|
+
readonly type: "refreshRelative";
|
|
7
|
+
readonly root: string;
|
|
8
|
+
}
|
|
9
|
+
| {
|
|
10
|
+
readonly id: number;
|
|
11
|
+
readonly type: "rank";
|
|
12
|
+
readonly query: string;
|
|
13
|
+
readonly limit?: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type FilePickerWorkerResponse =
|
|
17
|
+
| {
|
|
18
|
+
readonly id: number;
|
|
19
|
+
readonly type: "refreshRelative";
|
|
20
|
+
readonly ok: true;
|
|
21
|
+
}
|
|
22
|
+
| {
|
|
23
|
+
readonly id: number;
|
|
24
|
+
readonly type: "refreshRelative";
|
|
25
|
+
readonly ok: false;
|
|
26
|
+
readonly error: string;
|
|
27
|
+
}
|
|
28
|
+
| {
|
|
29
|
+
readonly id: number;
|
|
30
|
+
readonly type: "rank";
|
|
31
|
+
readonly ok: true;
|
|
32
|
+
readonly items: readonly PickerItem[] | undefined;
|
|
33
|
+
}
|
|
34
|
+
| {
|
|
35
|
+
readonly id: number;
|
|
36
|
+
readonly type: "rank";
|
|
37
|
+
readonly ok: false;
|
|
38
|
+
readonly error: string;
|
|
39
|
+
};
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import type { PickerItem } from "./PickerItem";
|
|
3
|
+
import {
|
|
4
|
+
FuzzyMatcher,
|
|
5
|
+
type FuzzyCandidate,
|
|
6
|
+
type FuzzyIndex,
|
|
7
|
+
} from "../shared/FuzzyMatcher";
|
|
8
|
+
import { type FileCandidate, loadAbsolute } from "./catalog";
|
|
9
|
+
|
|
10
|
+
export type FileRankOptions = {
|
|
11
|
+
readonly cachedRelative: readonly FileCandidate[] | undefined;
|
|
12
|
+
readonly limit?: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const isAbsoluteQuery = (query: string): boolean =>
|
|
16
|
+
query.startsWith("/") || query.startsWith("~");
|
|
17
|
+
const GLOBAL_FUZZY_QUERY_MIN_LENGTH = 3;
|
|
18
|
+
|
|
19
|
+
const cachedIndexes = new WeakMap<
|
|
20
|
+
readonly FileCandidate[],
|
|
21
|
+
RelativeRankingIndex
|
|
22
|
+
>();
|
|
23
|
+
|
|
24
|
+
const indexFor = (
|
|
25
|
+
candidates: readonly FileCandidate[]
|
|
26
|
+
): RelativeRankingIndex => {
|
|
27
|
+
const cached = cachedIndexes.get(candidates);
|
|
28
|
+
if (cached !== undefined) {
|
|
29
|
+
return cached;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const index = new RelativeRankingIndex(candidates);
|
|
33
|
+
cachedIndexes.set(candidates, index);
|
|
34
|
+
return index;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type LoweredCandidate = {
|
|
38
|
+
readonly candidate: FileCandidate;
|
|
39
|
+
readonly haystackLower: string;
|
|
40
|
+
readonly nameStart: number;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
class RelativeRankingIndex {
|
|
44
|
+
private readonly childrenByDirectory = new Map<string, FileCandidate[]>();
|
|
45
|
+
private readonly loweredSource: readonly LoweredCandidate[];
|
|
46
|
+
|
|
47
|
+
private readonly scopedIndexes = new Map<string, FuzzyIndex<FileCandidate>>();
|
|
48
|
+
private globalIndex: FuzzyIndex<FileCandidate> | undefined;
|
|
49
|
+
|
|
50
|
+
public constructor(private readonly source: readonly FileCandidate[]) {
|
|
51
|
+
const lowered: LoweredCandidate[] = [];
|
|
52
|
+
|
|
53
|
+
for (const candidate of source) {
|
|
54
|
+
const slash = candidate.insertPath.lastIndexOf("/");
|
|
55
|
+
const directory =
|
|
56
|
+
slash === -1 ? "" : candidate.insertPath.slice(0, slash);
|
|
57
|
+
const children = this.childrenByDirectory.get(directory) ?? [];
|
|
58
|
+
children.push(candidate);
|
|
59
|
+
this.childrenByDirectory.set(directory, children);
|
|
60
|
+
|
|
61
|
+
lowered.push({
|
|
62
|
+
candidate,
|
|
63
|
+
haystackLower: candidate.matchHaystack.toLocaleLowerCase(),
|
|
64
|
+
nameStart: slash + 1,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
this.loweredSource = lowered;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
public rank(query: string, limit: number | undefined): PickerItem[] {
|
|
72
|
+
const scoped = this.scopedCandidates(query);
|
|
73
|
+
if (scoped !== undefined) {
|
|
74
|
+
return rankCandidates(
|
|
75
|
+
scoped.candidates,
|
|
76
|
+
scoped.residualQuery,
|
|
77
|
+
limit,
|
|
78
|
+
() => this.indexForScope(scoped.directory, scoped.candidates)
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const literalHits = this.literalRank(query, limit);
|
|
83
|
+
if (literalHits !== undefined) {
|
|
84
|
+
return literalHits;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return rankCandidates(this.source, query, limit, () =>
|
|
88
|
+
this.indexForGlobal()
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private scopedCandidates(query: string):
|
|
93
|
+
| {
|
|
94
|
+
readonly directory: string;
|
|
95
|
+
readonly residualQuery: string;
|
|
96
|
+
readonly candidates: readonly FileCandidate[];
|
|
97
|
+
}
|
|
98
|
+
| undefined {
|
|
99
|
+
const slash = query.lastIndexOf("/");
|
|
100
|
+
if (slash === -1) {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const directory = query.slice(0, slash);
|
|
105
|
+
const candidates = this.childrenByDirectory.get(directory);
|
|
106
|
+
if (candidates === undefined) {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
directory,
|
|
112
|
+
residualQuery: query.slice(slash + 1),
|
|
113
|
+
candidates,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private indexForScope(
|
|
118
|
+
directory: string,
|
|
119
|
+
candidates: readonly FileCandidate[]
|
|
120
|
+
): FuzzyIndex<FileCandidate> {
|
|
121
|
+
const cached = this.scopedIndexes.get(directory);
|
|
122
|
+
if (cached !== undefined) {
|
|
123
|
+
return cached;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const index = prepareIndex(candidates, (candidate) =>
|
|
127
|
+
basename(candidate.insertPath)
|
|
128
|
+
);
|
|
129
|
+
this.scopedIndexes.set(directory, index);
|
|
130
|
+
return index;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private indexForGlobal(): FuzzyIndex<FileCandidate> {
|
|
134
|
+
this.globalIndex ??= prepareIndex(
|
|
135
|
+
this.source,
|
|
136
|
+
(candidate) => candidate.matchHaystack
|
|
137
|
+
);
|
|
138
|
+
return this.globalIndex;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private literalRank(
|
|
142
|
+
query: string,
|
|
143
|
+
limit: number | undefined
|
|
144
|
+
): PickerItem[] | undefined {
|
|
145
|
+
const needle = query.trim().toLocaleLowerCase();
|
|
146
|
+
if (needle.length === 0 || query.includes("/")) {
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const prefixHits: FileCandidate[] = [];
|
|
151
|
+
const substringHits: FileCandidate[] = [];
|
|
152
|
+
const limitSize = limit ?? Infinity;
|
|
153
|
+
|
|
154
|
+
for (const { candidate, haystackLower, nameStart } of this.loweredSource) {
|
|
155
|
+
if (
|
|
156
|
+
haystackLower.startsWith(needle) ||
|
|
157
|
+
haystackLower.startsWith(needle, nameStart)
|
|
158
|
+
) {
|
|
159
|
+
prefixHits.push(candidate);
|
|
160
|
+
} else if (
|
|
161
|
+
substringHits.length < limitSize &&
|
|
162
|
+
haystackLower.includes(needle)
|
|
163
|
+
) {
|
|
164
|
+
substringHits.push(candidate);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Only prefix hits may end the scan early: a late prefix must not lose to an earlier substring.
|
|
168
|
+
if (prefixHits.length >= limitSize) {
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const hitCount = prefixHits.length + substringHits.length;
|
|
174
|
+
if (hitCount === 0) {
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (
|
|
179
|
+
prefixHits.length === 0 &&
|
|
180
|
+
needle.length >= GLOBAL_FUZZY_QUERY_MIN_LENGTH &&
|
|
181
|
+
hitCount < limitSize
|
|
182
|
+
) {
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return [...prefixHits, ...substringHits]
|
|
187
|
+
.slice(0, limit)
|
|
188
|
+
.map((candidate) => toItem(candidate));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export async function rank(
|
|
193
|
+
query: string,
|
|
194
|
+
options: FileRankOptions
|
|
195
|
+
): Promise<PickerItem[] | undefined> {
|
|
196
|
+
if (isAbsoluteQuery(query)) {
|
|
197
|
+
const { candidates, residualQuery } = await loadAbsolute({ query });
|
|
198
|
+
return rankCandidates(candidates, residualQuery, options.limit);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (options.cachedRelative === undefined) {
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return indexFor(options.cachedRelative).rank(query, options.limit);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const rankCandidates = (
|
|
209
|
+
candidates: readonly FileCandidate[],
|
|
210
|
+
query: string,
|
|
211
|
+
limit: number | undefined,
|
|
212
|
+
index: () => FuzzyIndex<FileCandidate> = () => prepareIndex(candidates)
|
|
213
|
+
): PickerItem[] => {
|
|
214
|
+
if (query.trim().length === 0) {
|
|
215
|
+
return candidates.slice(0, limit).map((candidate) => toItem(candidate));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const hits = index().find(query, { limit });
|
|
219
|
+
return hits.map((hit) => toItem(hit.item));
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const prepareIndex = (
|
|
223
|
+
candidates: readonly FileCandidate[],
|
|
224
|
+
haystack: (candidate: FileCandidate) => string = (candidate) =>
|
|
225
|
+
candidate.matchHaystack
|
|
226
|
+
): FuzzyIndex<FileCandidate> => {
|
|
227
|
+
const fuzzy: FuzzyCandidate<FileCandidate>[] = candidates.map(
|
|
228
|
+
(candidate) => ({
|
|
229
|
+
item: candidate,
|
|
230
|
+
haystacks: [haystack(candidate)],
|
|
231
|
+
})
|
|
232
|
+
);
|
|
233
|
+
return FuzzyMatcher.prepare(fuzzy);
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const toItem = (candidate: FileCandidate): PickerItem => {
|
|
237
|
+
const suffix = candidate.isDirectory ? "/" : "";
|
|
238
|
+
const value = `${candidate.insertPath}${suffix}`;
|
|
239
|
+
const label = `${basename(candidate.insertPath)}${suffix}`;
|
|
240
|
+
return {
|
|
241
|
+
value,
|
|
242
|
+
label,
|
|
243
|
+
description: `${candidate.displayPath}${suffix}`,
|
|
244
|
+
};
|
|
245
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/** Which picker the caret is sitting in, if any. */
|
|
2
|
+
export type PickerToken = {
|
|
3
|
+
readonly kind: "file" | "command";
|
|
4
|
+
readonly query: string;
|
|
5
|
+
/** Offset of the sigil itself, so a completion replaces it too. */
|
|
6
|
+
readonly start: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const AT_PREFIX = /(?:^|\s)@(\S*)$/;
|
|
10
|
+
export const SLASH_PREFIX = /^\/(\S*)$/;
|
|
11
|
+
|
|
12
|
+
/** Offset of the sigil in an `AT_PREFIX` match, which also swallows the whitespace before it. */
|
|
13
|
+
export function sigilOffset(match: RegExpMatchArray): number {
|
|
14
|
+
const matched = match[0] ?? "";
|
|
15
|
+
return (match.index ?? 0) + (matched.startsWith("@") ? 0 : 1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function isDirectoryItem(item: { readonly label: string }): boolean {
|
|
19
|
+
return item.label.endsWith("/");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function activeToken(
|
|
23
|
+
text: string,
|
|
24
|
+
caret: number
|
|
25
|
+
): PickerToken | undefined {
|
|
26
|
+
const lineStart = text.lastIndexOf("\n", Math.max(0, caret - 1)) + 1;
|
|
27
|
+
const beforeCaret = text.slice(lineStart, caret);
|
|
28
|
+
|
|
29
|
+
const slash = SLASH_PREFIX.exec(beforeCaret);
|
|
30
|
+
if (slash) {
|
|
31
|
+
return { kind: "command", query: slash[1] ?? "", start: lineStart };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const at = AT_PREFIX.exec(beforeCaret);
|
|
35
|
+
if (!at) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
kind: "file",
|
|
40
|
+
query: at[1] ?? "",
|
|
41
|
+
start: lineStart + sigilOffset(at),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function tokenKey(token: PickerToken | undefined): string {
|
|
46
|
+
return token === undefined ? "" : `${token.kind}\u0000${token.query}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type Completion = {
|
|
50
|
+
readonly text: string;
|
|
51
|
+
readonly caret: number;
|
|
52
|
+
/** A directory keeps the picker open so the next segment can be drilled into. */
|
|
53
|
+
readonly keepOpen: boolean;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export function applyCompletion(
|
|
57
|
+
text: string,
|
|
58
|
+
caret: number,
|
|
59
|
+
token: PickerToken,
|
|
60
|
+
item: { readonly value: string; readonly label: string }
|
|
61
|
+
): Completion {
|
|
62
|
+
const inserted = token.kind === "file" ? `@${item.value}` : `${item.value} `;
|
|
63
|
+
const head = text.slice(0, token.start);
|
|
64
|
+
const tail = text.slice(caret);
|
|
65
|
+
return {
|
|
66
|
+
text: `${head}${inserted}${tail}`,
|
|
67
|
+
caret: head.length + inserted.length,
|
|
68
|
+
keepOpen: token.kind === "file" && isDirectoryItem(item),
|
|
69
|
+
};
|
|
70
|
+
}
|