pim-agent 0.0.1 → 0.10.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 +54 -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 +177 -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 +36 -0
- package/packages/core/src/extensions/system-prompt/prompt.ts +232 -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 +132 -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 +77 -0
- package/packages/core/src/session/AgentRuntime.ts +85 -0
- package/packages/core/src/session/EventLog.ts +156 -0
- package/packages/core/src/session/MessageText.ts +21 -0
- package/packages/core/src/session/ReadCursors.ts +94 -0
- package/packages/core/src/session/SearchIndex.ts +752 -0
- package/packages/core/src/session/SearchTokens.ts +97 -0
- package/packages/core/src/session/SessionCache.ts +68 -0
- package/packages/core/src/session/SessionDigest.ts +161 -0
- package/packages/core/src/session/SessionHost.ts +892 -0
- package/packages/core/src/session/SessionLease.ts +279 -0
- package/packages/core/src/session/SessionMeta.ts +267 -0
- package/packages/core/src/session/SessionName.ts +18 -0
- package/packages/core/src/session/SessionRegistry.ts +244 -0
- package/packages/core/src/session/WriteMark.ts +51 -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/DiffPatch.ts +70 -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 +121 -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 +530 -0
- package/packages/core/src/shared/GitMonitor.ts +221 -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 +83 -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 +50 -0
- package/packages/core/src/shared/RateLimiter.ts +50 -0
- package/packages/core/src/shared/Renderer.ts +371 -0
- package/packages/core/src/shared/RepoDiff.ts +741 -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/Surface.ts +2 -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 +209 -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/DiffExpand.ts +214 -0
- package/packages/core/src/view/DiffLayout.ts +69 -0
- package/packages/core/src/view/DiffPairs.ts +47 -0
- package/packages/core/src/view/MarkdownPainter.ts +314 -0
- package/packages/core/src/view/MovePath.ts +70 -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 +56 -0
- package/packages/daemon/src/index.ts +37 -0
- package/packages/protocol/src/Command.ts +219 -0
- package/packages/protocol/src/Diff.ts +15 -0
- package/packages/protocol/src/ImageRoute.ts +23 -0
- package/packages/protocol/src/ServerEvent.ts +406 -0
- package/packages/server/src/AttachmentEndpoint.ts +184 -0
- package/packages/server/src/ClientConnection.ts +277 -0
- package/packages/server/src/ImageEndpoint.ts +36 -0
- package/packages/server/src/ProbeClient.ts +374 -0
- package/packages/server/src/Reloader.ts +93 -0
- package/packages/server/src/SendFileTool.ts +88 -0
- package/packages/server/src/SessionCatalogue.ts +456 -0
- package/packages/server/src/SessionProjection.ts +194 -0
- package/packages/server/src/SessionStream.ts +627 -0
- package/packages/server/src/StaticClient.ts +178 -0
- package/packages/server/src/WebOptions.ts +72 -0
- package/packages/server/src/WsGateway.ts +798 -0
- package/packages/server/src/probe.ts +200 -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 +253 -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 +75 -0
- package/packages/tui/src/extensions/file-picker/index.ts +156 -0
- package/packages/tui/src/extensions/footer/index.ts +93 -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 +253 -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-300-normal-B-iV2FbL.woff2 +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-300-normal-kM0OTZCv.woff +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-CA0aSzPu.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-B9be-i9d.css +1 -0
- package/packages/web/dist/client/assets/index-CpinM5NK.js +45 -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,66 @@
|
|
|
1
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
2
|
+
import { type Static, Type } from "typebox";
|
|
3
|
+
|
|
4
|
+
export const GLOB_HEAD_LIMIT_MAX = 1000;
|
|
5
|
+
|
|
6
|
+
export const GLOB_PATH_FORMATS = ["relative", "absolute"] as const;
|
|
7
|
+
|
|
8
|
+
export const globSchema = Type.Object({
|
|
9
|
+
pattern: Type.String({
|
|
10
|
+
description:
|
|
11
|
+
"Glob pattern relative to path (eg. **/*.ts). Brace expansion spans sibling dirs (eg. {src,docs}/**/*.ts). A bare directory (eg. src/tools) expands to everything under it.",
|
|
12
|
+
}),
|
|
13
|
+
path: Type.Optional(
|
|
14
|
+
Type.String({
|
|
15
|
+
description:
|
|
16
|
+
"Absolute or relative path to directory (resolved against cwd). Defaults to cwd.",
|
|
17
|
+
})
|
|
18
|
+
),
|
|
19
|
+
exclude: Type.Optional(
|
|
20
|
+
Type.Array(Type.String(), {
|
|
21
|
+
description:
|
|
22
|
+
"Glob patterns to exclude, relative to path (eg. **/*.test.ts or dist/**).",
|
|
23
|
+
})
|
|
24
|
+
),
|
|
25
|
+
includeDotfiles: Type.Optional(
|
|
26
|
+
Type.Boolean({
|
|
27
|
+
description:
|
|
28
|
+
"Include dot-prefixed files and directories such as .env or .github. Defaults to false.",
|
|
29
|
+
})
|
|
30
|
+
),
|
|
31
|
+
includeIgnored: Type.Optional(
|
|
32
|
+
Type.Boolean({
|
|
33
|
+
description:
|
|
34
|
+
"Include gitignored and normally ignored paths such as node_modules. Defaults to false.",
|
|
35
|
+
})
|
|
36
|
+
),
|
|
37
|
+
pathFormat: Type.Optional(
|
|
38
|
+
StringEnum(GLOB_PATH_FORMATS, {
|
|
39
|
+
description:
|
|
40
|
+
"`relative` (default): output paths relative to cwd when possible. `absolute`: output absolute paths.",
|
|
41
|
+
})
|
|
42
|
+
),
|
|
43
|
+
headLimit: Type.Optional(
|
|
44
|
+
Type.Integer({
|
|
45
|
+
minimum: 1,
|
|
46
|
+
maximum: GLOB_HEAD_LIMIT_MAX,
|
|
47
|
+
description: `Maximum returned entries. Defaults to ${GLOB_HEAD_LIMIT_MAX}.`,
|
|
48
|
+
})
|
|
49
|
+
),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export type GlobInput = Static<typeof globSchema>;
|
|
53
|
+
export type GlobPathFormat = (typeof GLOB_PATH_FORMATS)[number];
|
|
54
|
+
|
|
55
|
+
export type GlobDetails = {
|
|
56
|
+
readonly absolutePath: string;
|
|
57
|
+
readonly pattern: string;
|
|
58
|
+
readonly exclude: readonly string[] | undefined;
|
|
59
|
+
readonly includeDotfiles: boolean;
|
|
60
|
+
readonly includeIgnored: boolean;
|
|
61
|
+
readonly pathFormat: GlobPathFormat;
|
|
62
|
+
readonly fileCount: number;
|
|
63
|
+
readonly totalItems: number;
|
|
64
|
+
readonly visibleItems: number;
|
|
65
|
+
readonly truncated: boolean;
|
|
66
|
+
};
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { FileScanner, type FileScanOptions } from "../../shared/FileScanner";
|
|
2
|
+
import { FsErrors } from "../../shared/FsErrors";
|
|
3
|
+
import { Lines } from "../../shared/Lines";
|
|
4
|
+
import { Paths } from "../../shared/Paths";
|
|
5
|
+
import { Pool } from "../../shared/Pool";
|
|
6
|
+
|
|
7
|
+
const MATCH_CONCURRENCY = 16;
|
|
8
|
+
|
|
9
|
+
export type GrepLine = {
|
|
10
|
+
readonly lineNumber: number;
|
|
11
|
+
readonly text: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type GrepLineRange = {
|
|
15
|
+
readonly startLineNumber: number;
|
|
16
|
+
readonly endLineNumber: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type GrepMatch = {
|
|
20
|
+
readonly filePath: string;
|
|
21
|
+
readonly mtime: number;
|
|
22
|
+
readonly lines: readonly GrepLine[];
|
|
23
|
+
readonly ranges: readonly GrepLineRange[];
|
|
24
|
+
readonly fileLines: readonly string[];
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type GrepMatcher = {
|
|
28
|
+
readonly regex: RegExp;
|
|
29
|
+
readonly matchAcrossLines: boolean;
|
|
30
|
+
/** Raw-byte needle for the literal fast path; undefined leaves the regex path unchanged. */
|
|
31
|
+
readonly literal: Buffer | undefined;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type GrepScanOptions = FileScanOptions & {
|
|
35
|
+
readonly retainFileLines?: boolean;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// ASCII-only: these stand for themselves in both a default-flag regex and raw UTF-8 bytes.
|
|
39
|
+
const PURE_LITERAL = /^[A-Za-z0-9_ \-/]+$/;
|
|
40
|
+
|
|
41
|
+
function literalNeedle(
|
|
42
|
+
pattern: string,
|
|
43
|
+
caseInsensitive: boolean,
|
|
44
|
+
matchAcrossLines: boolean
|
|
45
|
+
): Buffer | undefined {
|
|
46
|
+
if (caseInsensitive || matchAcrossLines || !PURE_LITERAL.test(pattern)) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
return Buffer.from(pattern, "utf8");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function buildMatcher(options: {
|
|
53
|
+
readonly pattern: string;
|
|
54
|
+
readonly caseInsensitive: boolean;
|
|
55
|
+
readonly matchAcrossLines: boolean;
|
|
56
|
+
}): GrepMatcher {
|
|
57
|
+
const flags = `${options.matchAcrossLines ? "s" : ""}${
|
|
58
|
+
options.caseInsensitive ? "i" : ""
|
|
59
|
+
}`;
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
return {
|
|
63
|
+
regex: new RegExp(options.pattern, flags),
|
|
64
|
+
matchAcrossLines: options.matchAcrossLines,
|
|
65
|
+
literal: literalNeedle(
|
|
66
|
+
options.pattern,
|
|
67
|
+
options.caseInsensitive,
|
|
68
|
+
options.matchAcrossLines
|
|
69
|
+
),
|
|
70
|
+
};
|
|
71
|
+
} catch (error) {
|
|
72
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
73
|
+
throw new Error(
|
|
74
|
+
`Invalid regular expression /${options.pattern}/${flags}: ${message}. Escape regex metacharacters for a literal search or simplify the pattern.`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function findMatches(
|
|
80
|
+
path: string,
|
|
81
|
+
glob: string | undefined,
|
|
82
|
+
matcher: GrepMatcher,
|
|
83
|
+
options: GrepScanOptions
|
|
84
|
+
): Promise<readonly GrepMatch[]> {
|
|
85
|
+
const metadata = await FsErrors.statOrThrow(path);
|
|
86
|
+
const files = metadata.isFile()
|
|
87
|
+
? [path]
|
|
88
|
+
: (await FileScanner.scan(path, glob ?? "**/*", options)).toSorted(
|
|
89
|
+
Paths.compare
|
|
90
|
+
);
|
|
91
|
+
const scanned = await Pool.mapPooled(files, MATCH_CONCURRENCY, (filePath) =>
|
|
92
|
+
matchFile(filePath, matcher, options.retainFileLines ?? true)
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
return scanned.filter((match) => match !== undefined);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// An entry may not be a readable regular file: an unreadable one is a non-match, never a failure.
|
|
99
|
+
async function matchFile(
|
|
100
|
+
filePath: string,
|
|
101
|
+
matcher: GrepMatcher,
|
|
102
|
+
retainFileLines: boolean
|
|
103
|
+
): Promise<GrepMatch | undefined> {
|
|
104
|
+
const file = Bun.file(filePath);
|
|
105
|
+
|
|
106
|
+
let text: string;
|
|
107
|
+
try {
|
|
108
|
+
if (await Lines.isBinary(file)) {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (matcher.literal !== undefined) {
|
|
113
|
+
// Literal fast path: an ASCII needle's raw-byte hit/miss matches the decoded result.
|
|
114
|
+
const bytes = Buffer.from(await file.arrayBuffer());
|
|
115
|
+
if (bytes.indexOf(matcher.literal) < 0) {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
text = bytes.toString("utf8");
|
|
119
|
+
} else {
|
|
120
|
+
text = await file.text();
|
|
121
|
+
}
|
|
122
|
+
} catch {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const content = Lines.normalize(text);
|
|
127
|
+
const fileLines = Lines.splitNormalized(content);
|
|
128
|
+
const ranges = matcher.matchAcrossLines
|
|
129
|
+
? regexRanges(content, matcher.regex)
|
|
130
|
+
: matchLineByLine(fileLines, matcher.regex);
|
|
131
|
+
|
|
132
|
+
if (ranges.length === 0) {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
filePath,
|
|
138
|
+
mtime: file.lastModified,
|
|
139
|
+
lines: linesForRanges(fileLines, ranges),
|
|
140
|
+
ranges,
|
|
141
|
+
fileLines: retainFileLines ? fileLines : [],
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function matchLineByLine(
|
|
146
|
+
lines: readonly string[],
|
|
147
|
+
regex: RegExp
|
|
148
|
+
): readonly GrepLineRange[] {
|
|
149
|
+
const ranges: GrepLineRange[] = [];
|
|
150
|
+
|
|
151
|
+
for (const [index, line] of lines.entries()) {
|
|
152
|
+
if (regex.test(line)) {
|
|
153
|
+
const lineNumber = index + 1;
|
|
154
|
+
ranges.push({ startLineNumber: lineNumber, endLineNumber: lineNumber });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return ranges;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function regexRanges(content: string, regex: RegExp): readonly GrepLineRange[] {
|
|
162
|
+
const globalRegex = new RegExp(regex.source, addFlag(regex.flags, "g"));
|
|
163
|
+
const ranges: GrepLineRange[] = [];
|
|
164
|
+
const cursor: LineCursor = { offset: 0, line: 1 };
|
|
165
|
+
|
|
166
|
+
while (true) {
|
|
167
|
+
const match = globalRegex.exec(content);
|
|
168
|
+
|
|
169
|
+
if (match === null) {
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
ranges.push(
|
|
174
|
+
lineRangeForOffsets(
|
|
175
|
+
content,
|
|
176
|
+
match.index,
|
|
177
|
+
match.index + match[0].length,
|
|
178
|
+
cursor
|
|
179
|
+
)
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
if (match[0].length === 0) {
|
|
183
|
+
globalRegex.lastIndex += 1;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return ranges;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function addFlag(flags: string, flag: string): string {
|
|
191
|
+
return flags.includes(flag) ? flags : `${flags}${flag}`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
type LineCursor = {
|
|
195
|
+
offset: number;
|
|
196
|
+
line: number;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
function lineRangeForOffsets(
|
|
200
|
+
content: string,
|
|
201
|
+
startOffset: number,
|
|
202
|
+
endOffset: number,
|
|
203
|
+
cursor: LineCursor
|
|
204
|
+
): GrepLineRange {
|
|
205
|
+
return {
|
|
206
|
+
startLineNumber: lineNumberForOffset(content, startOffset, cursor),
|
|
207
|
+
endLineNumber: lineNumberForOffset(
|
|
208
|
+
content,
|
|
209
|
+
Math.max(startOffset, endOffset - 1),
|
|
210
|
+
cursor
|
|
211
|
+
),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function lineNumberForOffset(
|
|
216
|
+
content: string,
|
|
217
|
+
offset: number,
|
|
218
|
+
cursor: LineCursor
|
|
219
|
+
): number {
|
|
220
|
+
const target = Math.min(offset, content.length);
|
|
221
|
+
|
|
222
|
+
for (let index = cursor.offset; index < target; index += 1) {
|
|
223
|
+
if (content[index] === "\n") {
|
|
224
|
+
cursor.line += 1;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
cursor.offset = target;
|
|
229
|
+
|
|
230
|
+
return cursor.line;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function linesForRanges(
|
|
234
|
+
fileLines: readonly string[],
|
|
235
|
+
ranges: readonly GrepLineRange[]
|
|
236
|
+
): readonly GrepLine[] {
|
|
237
|
+
const seen = new Set<number>();
|
|
238
|
+
const lines: GrepLine[] = [];
|
|
239
|
+
|
|
240
|
+
for (const range of ranges) {
|
|
241
|
+
for (
|
|
242
|
+
let lineNumber = range.startLineNumber;
|
|
243
|
+
lineNumber <= range.endLineNumber;
|
|
244
|
+
lineNumber += 1
|
|
245
|
+
) {
|
|
246
|
+
if (seen.has(lineNumber)) {
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
seen.add(lineNumber);
|
|
251
|
+
lines.push({ lineNumber, text: fileLines[lineNumber - 1] ?? "" });
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return lines;
|
|
256
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Paths } from "../../shared/Paths";
|
|
3
|
+
import { Renderer } from "../../shared/Renderer";
|
|
4
|
+
import { Tools } from "../../shared/Tools";
|
|
5
|
+
import { buildMatcher, findMatches } from "./grep";
|
|
6
|
+
import { buildView, type GrepViewDetails, renderMatches } from "./render";
|
|
7
|
+
import {
|
|
8
|
+
GREP_HEAD_LIMIT_MAX,
|
|
9
|
+
type GrepInput,
|
|
10
|
+
type GrepOutputMode,
|
|
11
|
+
type GrepPathFormat,
|
|
12
|
+
grepSchema,
|
|
13
|
+
} from "./schema";
|
|
14
|
+
|
|
15
|
+
const DEFAULT_OUTPUT_MODE: GrepOutputMode = "files_with_matches";
|
|
16
|
+
const DEFAULT_PATH_FORMAT: GrepPathFormat = "relative";
|
|
17
|
+
|
|
18
|
+
export default function (pi: ExtensionAPI): void {
|
|
19
|
+
Tools.register(pi, {
|
|
20
|
+
name: "grep",
|
|
21
|
+
label: "grep",
|
|
22
|
+
description:
|
|
23
|
+
"Search UTF-8 text files with a JavaScript regex. " +
|
|
24
|
+
"Directory scans skip binary files, gitignored paths, and dotfiles unless requested; direct file paths are always searched. " +
|
|
25
|
+
"Use grep to search file contents instead of bash with grep, rg, ag, find -exec, or similar.",
|
|
26
|
+
parameters: grepSchema,
|
|
27
|
+
renderShell: "self",
|
|
28
|
+
effect: { kind: "readOnly" },
|
|
29
|
+
executionMode: "parallel",
|
|
30
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
31
|
+
const {
|
|
32
|
+
pattern,
|
|
33
|
+
path,
|
|
34
|
+
glob,
|
|
35
|
+
exclude,
|
|
36
|
+
outputMode,
|
|
37
|
+
matchAcrossLines,
|
|
38
|
+
context,
|
|
39
|
+
includeDotfiles,
|
|
40
|
+
includeIgnored,
|
|
41
|
+
pathFormat,
|
|
42
|
+
caseInsensitive,
|
|
43
|
+
headLimit,
|
|
44
|
+
} = params as GrepInput;
|
|
45
|
+
|
|
46
|
+
if (signal?.aborted) {
|
|
47
|
+
throw new Error("Grep aborted before execution.");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const resolvedPathFormat = pathFormat ?? DEFAULT_PATH_FORMAT;
|
|
51
|
+
const resolvedContext = context ?? 0;
|
|
52
|
+
const resolvedOutputMode = outputMode ?? DEFAULT_OUTPUT_MODE;
|
|
53
|
+
const limit = Math.min(
|
|
54
|
+
headLimit ?? GREP_HEAD_LIMIT_MAX,
|
|
55
|
+
GREP_HEAD_LIMIT_MAX
|
|
56
|
+
);
|
|
57
|
+
const matcher = buildMatcher({
|
|
58
|
+
pattern,
|
|
59
|
+
caseInsensitive: caseInsensitive ?? false,
|
|
60
|
+
matchAcrossLines: matchAcrossLines ?? false,
|
|
61
|
+
});
|
|
62
|
+
const absolutePath = Paths.resolve(path ?? ".", ctx.cwd);
|
|
63
|
+
const matches = await findMatches(absolutePath, glob, matcher, {
|
|
64
|
+
exclude,
|
|
65
|
+
includeDotfiles: includeDotfiles ?? false,
|
|
66
|
+
includeIgnored: includeIgnored ?? false,
|
|
67
|
+
retainFileLines:
|
|
68
|
+
resolvedOutputMode === "content" && resolvedContext > 0,
|
|
69
|
+
});
|
|
70
|
+
const outcome = renderMatches(matches, resolvedOutputMode, limit, {
|
|
71
|
+
cwd: ctx.cwd,
|
|
72
|
+
pathFormat: resolvedPathFormat,
|
|
73
|
+
context: resolvedContext,
|
|
74
|
+
});
|
|
75
|
+
const content: Array<{ type: "text"; text: string }> = [
|
|
76
|
+
{ type: "text", text: outcome.body },
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
if (outcome.truncated) {
|
|
80
|
+
content.push({
|
|
81
|
+
type: "text",
|
|
82
|
+
text: `[grep tool: showing ${outcome.visibleItems} of ${outcome.totalItems} ${outcome.itemNoun}; narrow the pattern, scope to a specific path, or use a glob filter to reduce results.]`,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
content,
|
|
88
|
+
details: {
|
|
89
|
+
absolutePath,
|
|
90
|
+
outputMode: resolvedOutputMode,
|
|
91
|
+
exclude,
|
|
92
|
+
matchAcrossLines: matchAcrossLines ?? false,
|
|
93
|
+
context: resolvedContext,
|
|
94
|
+
includeDotfiles: includeDotfiles ?? false,
|
|
95
|
+
includeIgnored: includeIgnored ?? false,
|
|
96
|
+
pathFormat: resolvedPathFormat,
|
|
97
|
+
fileCount: outcome.fileCount,
|
|
98
|
+
totalMatches: outcome.totalMatches,
|
|
99
|
+
totalItems: outcome.totalItems,
|
|
100
|
+
visibleItems: outcome.visibleItems,
|
|
101
|
+
truncated: outcome.truncated,
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
toViewModel({ args, result, cwd }) {
|
|
106
|
+
return buildView({
|
|
107
|
+
args: (args ?? {}) as Partial<GrepInput>,
|
|
108
|
+
body: Renderer.firstText(result),
|
|
109
|
+
details: result?.details as GrepViewDetails | undefined,
|
|
110
|
+
cwd,
|
|
111
|
+
});
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { Format } from "../../shared/Format";
|
|
2
|
+
import { OutputBudget } from "../../shared/OutputBudget";
|
|
3
|
+
import { Paths } from "../../shared/Paths";
|
|
4
|
+
import {
|
|
5
|
+
NO_MATCHES,
|
|
6
|
+
type SearchList,
|
|
7
|
+
SearchRender,
|
|
8
|
+
} from "../../shared/SearchRender";
|
|
9
|
+
import type { ToolView, ViewBlock } from "../../view/ViewBlock";
|
|
10
|
+
import type { GrepLineRange, GrepMatch } from "./grep";
|
|
11
|
+
import type { GrepInput, GrepOutputMode, GrepPathFormat } from "./schema";
|
|
12
|
+
|
|
13
|
+
export type RenderOutcome = SearchList & {
|
|
14
|
+
readonly fileCount: number;
|
|
15
|
+
readonly totalMatches: number;
|
|
16
|
+
readonly itemNoun: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type RenderOptions = {
|
|
20
|
+
readonly cwd: string;
|
|
21
|
+
readonly pathFormat: GrepPathFormat;
|
|
22
|
+
readonly context: number;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const renderers: Record<
|
|
26
|
+
GrepOutputMode,
|
|
27
|
+
{
|
|
28
|
+
readonly itemNoun: string;
|
|
29
|
+
readonly toLines: (
|
|
30
|
+
matches: readonly GrepMatch[],
|
|
31
|
+
options: RenderOptions
|
|
32
|
+
) => readonly string[];
|
|
33
|
+
}
|
|
34
|
+
> = {
|
|
35
|
+
files_with_matches: { itemNoun: "files", toLines: renderFiles },
|
|
36
|
+
content: { itemNoun: "lines", toLines: renderContent },
|
|
37
|
+
count: { itemNoun: "files", toLines: renderCounts },
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export function renderMatches(
|
|
41
|
+
matches: readonly GrepMatch[],
|
|
42
|
+
outputMode: GrepOutputMode,
|
|
43
|
+
headLimit: number,
|
|
44
|
+
options: RenderOptions
|
|
45
|
+
): RenderOutcome {
|
|
46
|
+
const totalMatches = matches.reduce(
|
|
47
|
+
(sum, match) => sum + matchCount(match),
|
|
48
|
+
0
|
|
49
|
+
);
|
|
50
|
+
const { itemNoun, toLines } = renderers[outputMode];
|
|
51
|
+
const lines = toLines(matches, options);
|
|
52
|
+
const list = SearchRender.capList(lines, headLimit);
|
|
53
|
+
const empty = list.totalItems === 0;
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
...list,
|
|
57
|
+
fileCount: empty ? 0 : matches.length,
|
|
58
|
+
totalMatches: empty ? 0 : totalMatches,
|
|
59
|
+
itemNoun,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type TitleOptions = {
|
|
64
|
+
readonly pattern: string | undefined;
|
|
65
|
+
readonly path: string | undefined;
|
|
66
|
+
readonly glob: string | undefined;
|
|
67
|
+
readonly cwd: string;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export function formatTitle(options: TitleOptions): string {
|
|
71
|
+
return SearchRender.subjectTitle({
|
|
72
|
+
subject: formatPattern(options.pattern),
|
|
73
|
+
path: options.path,
|
|
74
|
+
suffix: options.glob,
|
|
75
|
+
cwd: options.cwd,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type GrepViewDetails = {
|
|
80
|
+
readonly outputMode?: GrepOutputMode;
|
|
81
|
+
readonly fileCount?: number;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export type GrepViewInput = {
|
|
85
|
+
/** Partially streamed while the call is in flight; every field is optional. */
|
|
86
|
+
readonly args: Partial<GrepInput>;
|
|
87
|
+
/** The result body the tool already rendered for the model; empty in flight. */
|
|
88
|
+
readonly body: string;
|
|
89
|
+
readonly details?: GrepViewDetails;
|
|
90
|
+
readonly cwd: string;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export function buildView({
|
|
94
|
+
args,
|
|
95
|
+
body,
|
|
96
|
+
details,
|
|
97
|
+
cwd,
|
|
98
|
+
}: GrepViewInput): ToolView {
|
|
99
|
+
const fileCount = details?.fileCount;
|
|
100
|
+
return {
|
|
101
|
+
label: "Grep",
|
|
102
|
+
icon: "search",
|
|
103
|
+
title: [
|
|
104
|
+
{
|
|
105
|
+
kind: "text",
|
|
106
|
+
text: formatTitle({
|
|
107
|
+
pattern: args.pattern,
|
|
108
|
+
path: args.path,
|
|
109
|
+
glob: args.glob,
|
|
110
|
+
cwd,
|
|
111
|
+
}),
|
|
112
|
+
},
|
|
113
|
+
...(fileCount === undefined
|
|
114
|
+
? []
|
|
115
|
+
: ([
|
|
116
|
+
{
|
|
117
|
+
kind: "text",
|
|
118
|
+
tone: "muted",
|
|
119
|
+
text: Format.count(fileCount, "file"),
|
|
120
|
+
},
|
|
121
|
+
] as const)),
|
|
122
|
+
],
|
|
123
|
+
body: bodyBlocks(body, details?.outputMode),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function bodyBlocks(
|
|
128
|
+
body: string,
|
|
129
|
+
outputMode: GrepOutputMode | undefined
|
|
130
|
+
): readonly ViewBlock[] {
|
|
131
|
+
if (
|
|
132
|
+
outputMode === "files_with_matches" &&
|
|
133
|
+
body !== "" &&
|
|
134
|
+
body !== NO_MATCHES
|
|
135
|
+
) {
|
|
136
|
+
return body.split("\n").map((path) => ({ kind: "file", path }) as const);
|
|
137
|
+
}
|
|
138
|
+
return [{ kind: "text", text: body }];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function formatPattern(pattern: string | undefined): string {
|
|
142
|
+
return pattern === undefined ? "..." : `/${pattern}/`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function renderFiles(
|
|
146
|
+
matches: readonly GrepMatch[],
|
|
147
|
+
options: RenderOptions
|
|
148
|
+
): readonly string[] {
|
|
149
|
+
return byRecency(matches).map((match) =>
|
|
150
|
+
SearchRender.formatPath(match.filePath, options)
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function renderContent(
|
|
155
|
+
matches: readonly GrepMatch[],
|
|
156
|
+
options: RenderOptions
|
|
157
|
+
): readonly string[] {
|
|
158
|
+
if (options.context > 0) {
|
|
159
|
+
return byRecency(matches).flatMap((match) =>
|
|
160
|
+
renderContextContent(match, options)
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return byRecency(matches).flatMap((match) =>
|
|
165
|
+
match.lines.map(
|
|
166
|
+
(line) =>
|
|
167
|
+
`${SearchRender.formatPath(match.filePath, options)}:${line.lineNumber}:${OutputBudget.truncateLine(line.text)}`
|
|
168
|
+
)
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function renderContextContent(
|
|
173
|
+
match: GrepMatch,
|
|
174
|
+
options: RenderOptions
|
|
175
|
+
): readonly string[] {
|
|
176
|
+
const path = SearchRender.formatPath(match.filePath, options);
|
|
177
|
+
const blocks = contextBlocks(
|
|
178
|
+
match.ranges,
|
|
179
|
+
match.fileLines.length,
|
|
180
|
+
options.context
|
|
181
|
+
);
|
|
182
|
+
const lines: string[] = [];
|
|
183
|
+
|
|
184
|
+
for (const [blockIndex, block] of blocks.entries()) {
|
|
185
|
+
if (blockIndex > 0) {
|
|
186
|
+
lines.push("--");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
for (
|
|
190
|
+
let lineNumber = block.startLineNumber;
|
|
191
|
+
lineNumber <= block.endLineNumber;
|
|
192
|
+
lineNumber += 1
|
|
193
|
+
) {
|
|
194
|
+
const marker = isMatchLine(match.ranges, lineNumber) ? ">" : " ";
|
|
195
|
+
const text = OutputBudget.truncateLine(
|
|
196
|
+
match.fileLines[lineNumber - 1] ?? ""
|
|
197
|
+
);
|
|
198
|
+
lines.push(`${marker} ${path}:${lineNumber}:${text}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return lines;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function renderCounts(
|
|
206
|
+
matches: readonly GrepMatch[],
|
|
207
|
+
options: RenderOptions
|
|
208
|
+
): readonly string[] {
|
|
209
|
+
return [...matches]
|
|
210
|
+
.sort(
|
|
211
|
+
(left, right) =>
|
|
212
|
+
matchCount(right) - matchCount(left) ||
|
|
213
|
+
right.mtime - left.mtime ||
|
|
214
|
+
Paths.compare(left.filePath, right.filePath)
|
|
215
|
+
)
|
|
216
|
+
.map(
|
|
217
|
+
(match) =>
|
|
218
|
+
`${SearchRender.formatPath(match.filePath, options)}:${matchCount(match)}`
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function contextBlocks(
|
|
223
|
+
ranges: readonly GrepLineRange[],
|
|
224
|
+
lineCount: number,
|
|
225
|
+
context: number
|
|
226
|
+
): readonly GrepLineRange[] {
|
|
227
|
+
const blocks: GrepLineRange[] = [];
|
|
228
|
+
|
|
229
|
+
for (const range of ranges) {
|
|
230
|
+
const expanded = {
|
|
231
|
+
startLineNumber: Math.max(1, range.startLineNumber - context),
|
|
232
|
+
endLineNumber: Math.min(lineCount, range.endLineNumber + context),
|
|
233
|
+
};
|
|
234
|
+
const previous = blocks.at(-1);
|
|
235
|
+
|
|
236
|
+
if (
|
|
237
|
+
previous !== undefined &&
|
|
238
|
+
expanded.startLineNumber <= previous.endLineNumber + 1
|
|
239
|
+
) {
|
|
240
|
+
blocks[blocks.length - 1] = {
|
|
241
|
+
startLineNumber: previous.startLineNumber,
|
|
242
|
+
endLineNumber: Math.max(previous.endLineNumber, expanded.endLineNumber),
|
|
243
|
+
};
|
|
244
|
+
} else {
|
|
245
|
+
blocks.push(expanded);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return blocks;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function matchCount(match: GrepMatch): number {
|
|
253
|
+
return match.ranges.length;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function isMatchLine(
|
|
257
|
+
ranges: readonly GrepLineRange[],
|
|
258
|
+
lineNumber: number
|
|
259
|
+
): boolean {
|
|
260
|
+
return ranges.some(
|
|
261
|
+
(range) =>
|
|
262
|
+
lineNumber >= range.startLineNumber && lineNumber <= range.endLineNumber
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function byRecency(matches: readonly GrepMatch[]): readonly GrepMatch[] {
|
|
267
|
+
return [...matches].sort(
|
|
268
|
+
(left, right) =>
|
|
269
|
+
right.mtime - left.mtime || Paths.compare(left.filePath, right.filePath)
|
|
270
|
+
);
|
|
271
|
+
}
|