@oh-my-pi/pi-coding-agent 12.18.3 → 12.19.2
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/CHANGELOG.md +53 -0
- package/package.json +35 -27
- package/src/async/index.ts +1 -0
- package/src/async/job-manager.ts +341 -0
- package/src/cli/file-processor.ts +3 -3
- package/src/cli/list-models.ts +3 -17
- package/src/cli/stats-cli.ts +3 -22
- package/src/cli/web-search-cli.ts +8 -16
- package/src/commit/agentic/agent.ts +6 -9
- package/src/commit/agentic/index.ts +44 -50
- package/src/commit/agentic/state.ts +0 -9
- package/src/commit/agentic/tools/propose-commit.ts +1 -30
- package/src/commit/agentic/tools/schemas.ts +31 -0
- package/src/commit/agentic/tools/split-commit.ts +1 -30
- package/src/commit/agentic/validation.ts +1 -18
- package/src/commit/analysis/conventional.ts +3 -50
- package/src/commit/analysis/summary.ts +2 -13
- package/src/commit/changelog/detect.ts +4 -1
- package/src/commit/changelog/generate.ts +2 -25
- package/src/commit/changelog/index.ts +1 -2
- package/src/commit/cli.ts +4 -12
- package/src/commit/map-reduce/reduce-phase.ts +2 -43
- package/src/commit/pipeline.ts +7 -15
- package/src/commit/utils.ts +44 -0
- package/src/config/prompt-templates.ts +1 -81
- package/src/config/settings-schema.ts +20 -1
- package/src/config.ts +2 -3
- package/src/debug/index.ts +1 -6
- package/src/debug/system-info.ts +2 -6
- package/src/discovery/builtin.ts +5 -9
- package/src/discovery/helpers.ts +0 -26
- package/src/discovery/ssh.ts +1 -8
- package/src/exa/company.ts +8 -39
- package/src/exa/factory.ts +64 -0
- package/src/exa/index.ts +0 -16
- package/src/exa/linkedin.ts +8 -39
- package/src/exa/mcp-client.ts +0 -64
- package/src/exa/researcher.ts +17 -59
- package/src/exa/search.ts +30 -154
- package/src/extensibility/custom-tools/loader.ts +3 -41
- package/src/extensibility/extensions/loader.ts +2 -9
- package/src/extensibility/hooks/loader.ts +3 -20
- package/src/extensibility/hooks/runner.ts +3 -19
- package/src/extensibility/plugins/installer.ts +2 -1
- package/src/extensibility/plugins/loader.ts +29 -117
- package/src/extensibility/skills.ts +2 -89
- package/src/extensibility/slash-commands.ts +1 -63
- package/src/extensibility/utils.ts +38 -0
- package/src/index.ts +9 -25
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/jobs-protocol.ts +118 -0
- package/src/ipy/kernel.ts +2 -0
- package/src/lsp/config.ts +1 -5
- package/src/lsp/lspmux.ts +0 -17
- package/src/lsp/utils.ts +2 -24
- package/src/main.ts +16 -24
- package/src/mcp/client.ts +1 -46
- package/src/mcp/render.ts +8 -1
- package/src/mcp/tool-cache.ts +1 -5
- package/src/mcp/transports/http.ts +2 -7
- package/src/mcp/transports/stdio.ts +2 -7
- package/src/modes/components/bash-execution.ts +2 -16
- package/src/modes/components/extensions/inspector-panel.ts +8 -18
- package/src/modes/components/footer.ts +10 -50
- package/src/modes/components/model-selector.ts +2 -21
- package/src/modes/components/python-execution.ts +2 -16
- package/src/modes/components/settings-selector.ts +1 -10
- package/src/modes/components/status-line/segments.ts +8 -25
- package/src/modes/components/status-line.ts +14 -31
- package/src/modes/components/tool-execution.ts +8 -2
- package/src/modes/controllers/command-controller.ts +71 -30
- package/src/modes/controllers/event-controller.ts +34 -4
- package/src/modes/controllers/mcp-command-controller.ts +3 -34
- package/src/modes/controllers/selector-controller.ts +2 -2
- package/src/modes/controllers/ssh-command-controller.ts +3 -34
- package/src/modes/interactive-mode.ts +6 -2
- package/src/modes/rpc/rpc-client.ts +1 -5
- package/src/modes/shared.ts +73 -0
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +26 -2
- package/src/patch/hashline.ts +6 -286
- package/src/patch/index.ts +6 -57
- package/src/patch/normalize.ts +22 -65
- package/src/patch/shared.ts +16 -16
- package/src/prompts/system/custom-system-prompt.md +0 -10
- package/src/prompts/system/system-prompt.md +69 -89
- package/src/prompts/tools/async-result.md +5 -0
- package/src/prompts/tools/bash.md +5 -0
- package/src/prompts/tools/cancel-job.md +7 -0
- package/src/prompts/tools/hashline.md +0 -16
- package/src/prompts/tools/poll-jobs.md +7 -0
- package/src/prompts/tools/task.md +4 -0
- package/src/sdk.ts +70 -6
- package/src/session/agent-session.ts +43 -6
- package/src/session/agent-storage.ts +69 -278
- package/src/session/auth-storage.ts +14 -1430
- package/src/session/session-manager.ts +69 -5
- package/src/session/session-storage.ts +1 -5
- package/src/session/streaming-output.ts +637 -76
- package/src/slash-commands/builtin-registry.ts +8 -0
- package/src/ssh/connection-manager.ts +4 -12
- package/src/ssh/sshfs-mount.ts +3 -7
- package/src/ssh/utils.ts +8 -0
- package/src/system-prompt.ts +24 -90
- package/src/task/executor.ts +11 -1
- package/src/task/index.ts +258 -13
- package/src/task/parallel.ts +32 -0
- package/src/task/render.ts +15 -7
- package/src/task/types.ts +5 -0
- package/src/tools/ask.ts +4 -7
- package/src/tools/bash-interactive.ts +4 -5
- package/src/tools/bash.ts +125 -41
- package/src/tools/cancel-job.ts +93 -0
- package/src/tools/fetch.ts +7 -27
- package/src/tools/find.ts +3 -3
- package/src/tools/gemini-image.ts +15 -14
- package/src/tools/grep.ts +3 -3
- package/src/tools/index.ts +13 -29
- package/src/tools/json-tree.ts +12 -1
- package/src/tools/jtd-to-json-schema.ts +10 -74
- package/src/tools/jtd-to-typescript.ts +10 -72
- package/src/tools/jtd-utils.ts +102 -0
- package/src/tools/notebook.ts +4 -9
- package/src/tools/output-meta.ts +52 -26
- package/src/tools/path-utils.ts +13 -7
- package/src/tools/poll-jobs.ts +178 -0
- package/src/tools/python.ts +32 -35
- package/src/tools/read.ts +61 -82
- package/src/tools/render-utils.ts +8 -159
- package/src/tools/ssh.ts +7 -20
- package/src/tools/submit-result.ts +1 -1
- package/src/tools/tool-errors.ts +0 -30
- package/src/tools/tool-result.ts +1 -2
- package/src/tools/write.ts +8 -10
- package/src/tui/code-cell.ts +8 -3
- package/src/tui/status-line.ts +4 -4
- package/src/tui/types.ts +0 -1
- package/src/tui/utils.ts +1 -14
- package/src/utils/command-args.ts +76 -0
- package/src/utils/file-mentions.ts +15 -19
- package/src/utils/frontmatter.ts +5 -10
- package/src/utils/shell-snapshot.ts +0 -11
- package/src/utils/title-generator.ts +0 -12
- package/src/web/scrapers/artifacthub.ts +7 -16
- package/src/web/scrapers/arxiv.ts +3 -8
- package/src/web/scrapers/aur.ts +8 -22
- package/src/web/scrapers/biorxiv.ts +5 -14
- package/src/web/scrapers/bluesky.ts +13 -36
- package/src/web/scrapers/brew.ts +5 -10
- package/src/web/scrapers/cheatsh.ts +2 -12
- package/src/web/scrapers/chocolatey.ts +63 -26
- package/src/web/scrapers/choosealicense.ts +3 -18
- package/src/web/scrapers/cisa-kev.ts +4 -18
- package/src/web/scrapers/clojars.ts +6 -33
- package/src/web/scrapers/coingecko.ts +25 -33
- package/src/web/scrapers/crates-io.ts +7 -26
- package/src/web/scrapers/crossref.ts +4 -18
- package/src/web/scrapers/devto.ts +11 -41
- package/src/web/scrapers/discogs.ts +7 -10
- package/src/web/scrapers/discourse.ts +6 -31
- package/src/web/scrapers/dockerhub.ts +12 -35
- package/src/web/scrapers/fdroid.ts +8 -33
- package/src/web/scrapers/firefox-addons.ts +10 -34
- package/src/web/scrapers/flathub.ts +7 -24
- package/src/web/scrapers/github-gist.ts +2 -12
- package/src/web/scrapers/github.ts +9 -47
- package/src/web/scrapers/gitlab.ts +130 -185
- package/src/web/scrapers/go-pkg.ts +12 -22
- package/src/web/scrapers/hackage.ts +88 -43
- package/src/web/scrapers/hackernews.ts +25 -45
- package/src/web/scrapers/hex.ts +19 -36
- package/src/web/scrapers/huggingface.ts +26 -91
- package/src/web/scrapers/iacr.ts +3 -8
- package/src/web/scrapers/jetbrains-marketplace.ts +9 -20
- package/src/web/scrapers/lemmy.ts +5 -23
- package/src/web/scrapers/lobsters.ts +16 -28
- package/src/web/scrapers/mastodon.ts +24 -43
- package/src/web/scrapers/maven.ts +6 -21
- package/src/web/scrapers/mdn.ts +7 -11
- package/src/web/scrapers/metacpan.ts +9 -41
- package/src/web/scrapers/musicbrainz.ts +4 -28
- package/src/web/scrapers/npm.ts +8 -25
- package/src/web/scrapers/nuget.ts +14 -37
- package/src/web/scrapers/nvd.ts +6 -28
- package/src/web/scrapers/ollama.ts +7 -34
- package/src/web/scrapers/open-vsx.ts +5 -19
- package/src/web/scrapers/opencorporates.ts +30 -14
- package/src/web/scrapers/openlibrary.ts +49 -33
- package/src/web/scrapers/orcid.ts +4 -18
- package/src/web/scrapers/osv.ts +7 -24
- package/src/web/scrapers/packagist.ts +9 -24
- package/src/web/scrapers/pub-dev.ts +7 -50
- package/src/web/scrapers/pubmed.ts +54 -21
- package/src/web/scrapers/pypi.ts +8 -26
- package/src/web/scrapers/rawg.ts +11 -19
- package/src/web/scrapers/readthedocs.ts +4 -9
- package/src/web/scrapers/reddit.ts +5 -15
- package/src/web/scrapers/repology.ts +8 -20
- package/src/web/scrapers/rfc.ts +5 -14
- package/src/web/scrapers/rubygems.ts +6 -21
- package/src/web/scrapers/searchcode.ts +8 -36
- package/src/web/scrapers/sec-edgar.ts +4 -18
- package/src/web/scrapers/semantic-scholar.ts +15 -35
- package/src/web/scrapers/snapcraft.ts +5 -19
- package/src/web/scrapers/sourcegraph.ts +5 -43
- package/src/web/scrapers/spdx.ts +4 -18
- package/src/web/scrapers/spotify.ts +4 -23
- package/src/web/scrapers/stackoverflow.ts +8 -13
- package/src/web/scrapers/terraform.ts +9 -37
- package/src/web/scrapers/tldr.ts +3 -7
- package/src/web/scrapers/twitter.ts +3 -7
- package/src/web/scrapers/types.ts +105 -27
- package/src/web/scrapers/utils.ts +97 -103
- package/src/web/scrapers/vimeo.ts +7 -27
- package/src/web/scrapers/vscode-marketplace.ts +8 -17
- package/src/web/scrapers/w3c.ts +6 -14
- package/src/web/scrapers/wikidata.ts +5 -19
- package/src/web/scrapers/wikipedia.ts +2 -12
- package/src/web/scrapers/youtube.ts +5 -34
- package/src/web/search/index.ts +0 -9
- package/src/web/search/providers/anthropic.ts +3 -2
- package/src/web/search/providers/brave.ts +3 -18
- package/src/web/search/providers/exa.ts +1 -12
- package/src/web/search/providers/kimi.ts +5 -44
- package/src/web/search/providers/perplexity.ts +1 -12
- package/src/web/search/providers/synthetic.ts +3 -26
- package/src/web/search/providers/utils.ts +36 -0
- package/src/web/search/providers/zai.ts +9 -50
- package/src/web/search/types.ts +0 -28
- package/src/web/search/utils.ts +17 -0
- package/src/tools/output-utils.ts +0 -63
- package/src/tools/truncate.ts +0 -385
- package/src/web/search/auth.ts +0 -178
|
@@ -17,7 +17,7 @@ import { theme } from "../../modes/theme/theme";
|
|
|
17
17
|
import type { CompactionQueuedMessage, InteractiveModeContext } from "../../modes/types";
|
|
18
18
|
import { type CustomMessage, SKILL_PROMPT_MESSAGE_TYPE, type SkillPromptDetails } from "../../session/messages";
|
|
19
19
|
import type { SessionContext } from "../../session/session-manager";
|
|
20
|
-
import {
|
|
20
|
+
import { formatBytes, formatDuration } from "../../tools/render-utils";
|
|
21
21
|
|
|
22
22
|
type TextBlock = { type: "text"; text: string };
|
|
23
23
|
|
|
@@ -97,6 +97,30 @@ export class UiHelpers {
|
|
|
97
97
|
case "hookMessage":
|
|
98
98
|
case "custom": {
|
|
99
99
|
if (message.display) {
|
|
100
|
+
if (message.customType === "async-result") {
|
|
101
|
+
const details = (
|
|
102
|
+
message as CustomMessage<{
|
|
103
|
+
jobId?: string;
|
|
104
|
+
type?: "bash" | "task";
|
|
105
|
+
label?: string;
|
|
106
|
+
durationMs?: number;
|
|
107
|
+
}>
|
|
108
|
+
).details;
|
|
109
|
+
const jobId = details?.jobId ?? "unknown";
|
|
110
|
+
const typeLabel = details?.type ? `[${details.type}]` : "[job]";
|
|
111
|
+
const duration =
|
|
112
|
+
typeof details?.durationMs === "number" ? formatDuration(details.durationMs) : undefined;
|
|
113
|
+
const line = [
|
|
114
|
+
theme.fg("success", `${theme.status.success} Background job completed`),
|
|
115
|
+
theme.fg("dim", typeLabel),
|
|
116
|
+
theme.fg("accent", jobId),
|
|
117
|
+
duration ? theme.fg("dim", `(${duration})`) : undefined,
|
|
118
|
+
]
|
|
119
|
+
.filter(Boolean)
|
|
120
|
+
.join(" ");
|
|
121
|
+
this.ctx.chatContainer.addChild(new Text(line, 1, 0));
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
100
124
|
if (message.customType === SKILL_PROMPT_MESSAGE_TYPE) {
|
|
101
125
|
const component = new SkillMessageComponent(message as CustomMessage<SkillPromptDetails>);
|
|
102
126
|
component.setExpanded(this.ctx.toolOutputExpanded);
|
|
@@ -130,7 +154,7 @@ export class UiHelpers {
|
|
|
130
154
|
for (const file of message.files) {
|
|
131
155
|
let suffix: string;
|
|
132
156
|
if (file.skippedReason === "tooLarge") {
|
|
133
|
-
const size = typeof file.byteSize === "number" ?
|
|
157
|
+
const size = typeof file.byteSize === "number" ? formatBytes(file.byteSize) : "unknown size";
|
|
134
158
|
suffix = `(skipped: ${size})`;
|
|
135
159
|
} else {
|
|
136
160
|
suffix = file.image
|
package/src/patch/hashline.ts
CHANGED
|
@@ -21,159 +21,6 @@ export type HashlineEdit =
|
|
|
21
21
|
| { op: "append"; after?: LineTag; content: string[] }
|
|
22
22
|
| { op: "prepend"; before?: LineTag; content: string[] }
|
|
23
23
|
| { op: "insert"; after: LineTag; before: LineTag; content: string[] };
|
|
24
|
-
export type ReplaceTextEdit = { op: "replaceText"; old_text: string; new_text: string; all?: boolean };
|
|
25
|
-
export type EditSpec = HashlineEdit | ReplaceTextEdit;
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Compare two strings ignoring all whitespace differences.
|
|
29
|
-
*
|
|
30
|
-
* Returns true when the non-whitespace characters are identical — meaning
|
|
31
|
-
* the only differences are in spaces, tabs, or other whitespace.
|
|
32
|
-
*/
|
|
33
|
-
function equalsIgnoringWhitespace(a: string, b: string): boolean {
|
|
34
|
-
// Fast path: identical strings
|
|
35
|
-
if (a === b) return true;
|
|
36
|
-
// Compare with all whitespace removed
|
|
37
|
-
return a.replace(/\s+/g, "") === b.replace(/\s+/g, "");
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function stripAllWhitespace(s: string): string {
|
|
41
|
-
return s.replace(/\s+/g, "");
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function stripTrailingContinuationTokens(s: string): string {
|
|
45
|
-
// Heuristic: models often merge a continuation line into the prior line
|
|
46
|
-
// while also changing the trailing operator (e.g. `&&` → `||`).
|
|
47
|
-
// Strip common trailing continuation tokens so we can still detect merges.
|
|
48
|
-
return s.replace(/(?:&&|\|\||\?\?|\?|:|=|,|\+|-|\*|\/|\.|\()\s*$/u, "");
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function stripMergeOperatorChars(s: string): string {
|
|
52
|
-
// Used for merge detection when the model changes a logical operator like
|
|
53
|
-
// `||` → `??` while also merging adjacent lines.
|
|
54
|
-
return s.replace(/[|&?]/g, "");
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function leadingWhitespace(s: string): string {
|
|
58
|
-
const match = s.match(/^\s*/);
|
|
59
|
-
return match ? match[0] : "";
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function restoreLeadingIndent(templateLine: string, line: string): string {
|
|
63
|
-
if (line.length === 0) return line;
|
|
64
|
-
const templateIndent = leadingWhitespace(templateLine);
|
|
65
|
-
if (templateIndent.length === 0) return line;
|
|
66
|
-
const indent = leadingWhitespace(line);
|
|
67
|
-
if (indent.length > 0) return line;
|
|
68
|
-
return templateIndent + line;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function restoreIndentForPairedReplacement(oldLines: string[], newLines: string[]): string[] {
|
|
72
|
-
if (oldLines.length !== newLines.length) return newLines;
|
|
73
|
-
let changed = false;
|
|
74
|
-
const out = new Array<string>(newLines.length);
|
|
75
|
-
for (let i = 0; i < newLines.length; i++) {
|
|
76
|
-
const restored = restoreLeadingIndent(oldLines[i], newLines[i]);
|
|
77
|
-
out[i] = restored;
|
|
78
|
-
if (restored !== newLines[i]) changed = true;
|
|
79
|
-
}
|
|
80
|
-
return changed ? out : newLines;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Undo pure formatting rewrites where the model reflows a single logical line
|
|
85
|
-
* into multiple lines (or similar), but the token stream is identical.
|
|
86
|
-
*/
|
|
87
|
-
function restoreOldWrappedLines(oldLines: string[], newLines: string[]): string[] {
|
|
88
|
-
if (oldLines.length === 0 || newLines.length < 2) return newLines;
|
|
89
|
-
|
|
90
|
-
const canonToOld = new Map<string, { line: string; count: number }>();
|
|
91
|
-
for (const line of oldLines) {
|
|
92
|
-
const canon = stripAllWhitespace(line);
|
|
93
|
-
const bucket = canonToOld.get(canon);
|
|
94
|
-
if (bucket) bucket.count++;
|
|
95
|
-
else canonToOld.set(canon, { line, count: 1 });
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const candidates: { start: number; len: number; replacement: string; canon: string }[] = [];
|
|
99
|
-
for (let start = 0; start < newLines.length; start++) {
|
|
100
|
-
for (let len = 2; len <= 10 && start + len <= newLines.length; len++) {
|
|
101
|
-
const canonSpan = stripAllWhitespace(newLines.slice(start, start + len).join(""));
|
|
102
|
-
const old = canonToOld.get(canonSpan);
|
|
103
|
-
if (old && old.count === 1 && canonSpan.length >= 6) {
|
|
104
|
-
candidates.push({ start, len, replacement: old.line, canon: canonSpan });
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
if (candidates.length === 0) return newLines;
|
|
109
|
-
|
|
110
|
-
// Keep only spans whose canonical match is unique in the new output.
|
|
111
|
-
const canonCounts = new Map<string, number>();
|
|
112
|
-
for (const c of candidates) {
|
|
113
|
-
canonCounts.set(c.canon, (canonCounts.get(c.canon) ?? 0) + 1);
|
|
114
|
-
}
|
|
115
|
-
const uniqueCandidates = candidates.filter(c => (canonCounts.get(c.canon) ?? 0) === 1);
|
|
116
|
-
if (uniqueCandidates.length === 0) return newLines;
|
|
117
|
-
|
|
118
|
-
// Apply replacements back-to-front so indices remain stable.
|
|
119
|
-
uniqueCandidates.sort((a, b) => b.start - a.start);
|
|
120
|
-
const out = [...newLines];
|
|
121
|
-
for (const c of uniqueCandidates) {
|
|
122
|
-
out.splice(c.start, c.len, c.replacement);
|
|
123
|
-
}
|
|
124
|
-
return out;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function stripInsertAnchorEchoAfter(anchorLine: string, dstLines: string[]): string[] {
|
|
128
|
-
if (dstLines.length <= 1) return dstLines;
|
|
129
|
-
if (equalsIgnoringWhitespace(dstLines[0], anchorLine)) {
|
|
130
|
-
return dstLines.slice(1);
|
|
131
|
-
}
|
|
132
|
-
return dstLines;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function stripInsertAnchorEchoBefore(anchorLine: string, dstLines: string[]): string[] {
|
|
136
|
-
if (dstLines.length <= 1) return dstLines;
|
|
137
|
-
if (equalsIgnoringWhitespace(dstLines[dstLines.length - 1], anchorLine)) {
|
|
138
|
-
return dstLines.slice(0, -1);
|
|
139
|
-
}
|
|
140
|
-
return dstLines;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function stripInsertBoundaryEcho(afterLine: string, beforeLine: string, dstLines: string[]): string[] {
|
|
144
|
-
let out = dstLines;
|
|
145
|
-
if (out.length > 1 && equalsIgnoringWhitespace(out[0], afterLine)) {
|
|
146
|
-
out = out.slice(1);
|
|
147
|
-
}
|
|
148
|
-
if (out.length > 1 && equalsIgnoringWhitespace(out[out.length - 1], beforeLine)) {
|
|
149
|
-
out = out.slice(0, -1);
|
|
150
|
-
}
|
|
151
|
-
return out;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function stripRangeBoundaryEcho(fileLines: string[], startLine: number, endLine: number, dstLines: string[]): string[] {
|
|
155
|
-
// Only strip when the model replaced with multiple lines and grew the edit.
|
|
156
|
-
// This avoids turning a single-line replacement into a deletion.
|
|
157
|
-
const count = endLine - startLine + 1;
|
|
158
|
-
if (dstLines.length <= 1 || dstLines.length <= count) return dstLines;
|
|
159
|
-
|
|
160
|
-
let out = dstLines;
|
|
161
|
-
const beforeIdx = startLine - 2;
|
|
162
|
-
if (beforeIdx >= 0 && equalsIgnoringWhitespace(out[0], fileLines[beforeIdx])) {
|
|
163
|
-
out = out.slice(1);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const afterIdx = endLine;
|
|
167
|
-
if (
|
|
168
|
-
afterIdx < fileLines.length &&
|
|
169
|
-
out.length > 0 &&
|
|
170
|
-
equalsIgnoringWhitespace(out[out.length - 1], fileLines[afterIdx])
|
|
171
|
-
) {
|
|
172
|
-
out = out.slice(0, -1);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return out;
|
|
176
|
-
}
|
|
177
24
|
|
|
178
25
|
const NIBBLE_STR = "ZPMQVRWSNKTXJBYH";
|
|
179
26
|
|
|
@@ -594,38 +441,6 @@ export function applyHashlineEdits(
|
|
|
594
441
|
let firstChangedLine: number | undefined;
|
|
595
442
|
const noopEdits: Array<{ editIndex: number; loc: string; currentContent: string }> = [];
|
|
596
443
|
|
|
597
|
-
const autocorrect = Bun.env.PI_HL_AUTOCORRECT === "1";
|
|
598
|
-
|
|
599
|
-
function collectExplicitlyTouchedLines(): Set<number> {
|
|
600
|
-
const touched = new Set<number>();
|
|
601
|
-
for (const edit of edits) {
|
|
602
|
-
switch (edit.op) {
|
|
603
|
-
case "set":
|
|
604
|
-
touched.add(edit.tag.line);
|
|
605
|
-
break;
|
|
606
|
-
case "replace":
|
|
607
|
-
for (let ln = edit.first.line; ln <= edit.last.line; ln++) touched.add(ln);
|
|
608
|
-
break;
|
|
609
|
-
case "append":
|
|
610
|
-
if (edit.after) {
|
|
611
|
-
touched.add(edit.after.line);
|
|
612
|
-
}
|
|
613
|
-
break;
|
|
614
|
-
case "prepend":
|
|
615
|
-
if (edit.before) {
|
|
616
|
-
touched.add(edit.before.line);
|
|
617
|
-
}
|
|
618
|
-
break;
|
|
619
|
-
case "insert":
|
|
620
|
-
touched.add(edit.after.line);
|
|
621
|
-
touched.add(edit.before.line);
|
|
622
|
-
break;
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
return touched;
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
const explicitlyTouchedLines = collectExplicitlyTouchedLines();
|
|
629
444
|
// Pre-validate: collect all hash mismatches before mutating
|
|
630
445
|
const mismatches: HashMismatch[] = [];
|
|
631
446
|
function validateRef(ref: { line: number; hash: string }): boolean {
|
|
@@ -765,35 +580,8 @@ export function applyHashlineEdits(
|
|
|
765
580
|
for (const { edit, idx } of annotated) {
|
|
766
581
|
switch (edit.op) {
|
|
767
582
|
case "set": {
|
|
768
|
-
const merged = autocorrect ? maybeExpandSingleLineMerge(edit.tag.line, edit.content) : null;
|
|
769
|
-
if (merged) {
|
|
770
|
-
const origLines = originalFileLines.slice(
|
|
771
|
-
merged.startLine - 1,
|
|
772
|
-
merged.startLine - 1 + merged.deleteCount,
|
|
773
|
-
);
|
|
774
|
-
let nextLines = merged.newLines;
|
|
775
|
-
nextLines = restoreIndentForPairedReplacement([origLines[0] ?? ""], nextLines);
|
|
776
|
-
|
|
777
|
-
if (origLines.every((line, i) => line === nextLines[i])) {
|
|
778
|
-
noopEdits.push({
|
|
779
|
-
editIndex: idx,
|
|
780
|
-
loc: `${edit.tag.line}#${edit.tag.hash}`,
|
|
781
|
-
currentContent: origLines.join("\n"),
|
|
782
|
-
});
|
|
783
|
-
break;
|
|
784
|
-
}
|
|
785
|
-
fileLines.splice(merged.startLine - 1, merged.deleteCount, ...nextLines);
|
|
786
|
-
trackFirstChanged(merged.startLine);
|
|
787
|
-
break;
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
const count = 1;
|
|
791
583
|
const origLines = originalFileLines.slice(edit.tag.line - 1, edit.tag.line);
|
|
792
|
-
|
|
793
|
-
? stripRangeBoundaryEcho(originalFileLines, edit.tag.line, edit.tag.line, edit.content)
|
|
794
|
-
: edit.content;
|
|
795
|
-
stripped = autocorrect ? restoreOldWrappedLines(origLines, stripped) : stripped;
|
|
796
|
-
const newLines = autocorrect ? restoreIndentForPairedReplacement(origLines, stripped) : stripped;
|
|
584
|
+
const newLines = edit.content;
|
|
797
585
|
if (origLines.every((line, i) => line === newLines[i])) {
|
|
798
586
|
noopEdits.push({
|
|
799
587
|
editIndex: idx,
|
|
@@ -802,36 +590,19 @@ export function applyHashlineEdits(
|
|
|
802
590
|
});
|
|
803
591
|
break;
|
|
804
592
|
}
|
|
805
|
-
fileLines.splice(edit.tag.line - 1,
|
|
593
|
+
fileLines.splice(edit.tag.line - 1, 1, ...newLines);
|
|
806
594
|
trackFirstChanged(edit.tag.line);
|
|
807
595
|
break;
|
|
808
596
|
}
|
|
809
597
|
case "replace": {
|
|
810
598
|
const count = edit.last.line - edit.first.line + 1;
|
|
811
|
-
const
|
|
812
|
-
let stripped = autocorrect
|
|
813
|
-
? stripRangeBoundaryEcho(originalFileLines, edit.first.line, edit.last.line, edit.content)
|
|
814
|
-
: edit.content;
|
|
815
|
-
stripped = autocorrect ? restoreOldWrappedLines(origLines, stripped) : stripped;
|
|
816
|
-
const newLines = autocorrect ? restoreIndentForPairedReplacement(origLines, stripped) : stripped;
|
|
817
|
-
if (autocorrect && origLines.every((line, i) => line === newLines[i])) {
|
|
818
|
-
noopEdits.push({
|
|
819
|
-
editIndex: idx,
|
|
820
|
-
loc: `${edit.first.line}#${edit.first.hash}`,
|
|
821
|
-
currentContent: origLines.join("\n"),
|
|
822
|
-
});
|
|
823
|
-
break;
|
|
824
|
-
}
|
|
599
|
+
const newLines = edit.content;
|
|
825
600
|
fileLines.splice(edit.first.line - 1, count, ...newLines);
|
|
826
601
|
trackFirstChanged(edit.first.line);
|
|
827
602
|
break;
|
|
828
603
|
}
|
|
829
604
|
case "append": {
|
|
830
|
-
const inserted = edit.
|
|
831
|
-
? autocorrect
|
|
832
|
-
? stripInsertAnchorEchoAfter(originalFileLines[edit.after.line - 1], edit.content)
|
|
833
|
-
: edit.content
|
|
834
|
-
: edit.content;
|
|
605
|
+
const inserted = edit.content;
|
|
835
606
|
if (inserted.length === 0) {
|
|
836
607
|
noopEdits.push({
|
|
837
608
|
editIndex: idx,
|
|
@@ -855,11 +626,7 @@ export function applyHashlineEdits(
|
|
|
855
626
|
break;
|
|
856
627
|
}
|
|
857
628
|
case "prepend": {
|
|
858
|
-
const inserted = edit.
|
|
859
|
-
? autocorrect
|
|
860
|
-
? stripInsertAnchorEchoBefore(originalFileLines[edit.before.line - 1], edit.content)
|
|
861
|
-
: edit.content
|
|
862
|
-
: edit.content;
|
|
629
|
+
const inserted = edit.content;
|
|
863
630
|
if (inserted.length === 0) {
|
|
864
631
|
noopEdits.push({
|
|
865
632
|
editIndex: idx,
|
|
@@ -884,7 +651,7 @@ export function applyHashlineEdits(
|
|
|
884
651
|
case "insert": {
|
|
885
652
|
const afterLine = originalFileLines[edit.after.line - 1];
|
|
886
653
|
const beforeLine = originalFileLines[edit.before.line - 1];
|
|
887
|
-
const inserted =
|
|
654
|
+
const inserted = edit.content;
|
|
888
655
|
if (inserted.length === 0) {
|
|
889
656
|
noopEdits.push({
|
|
890
657
|
editIndex: idx,
|
|
@@ -911,51 +678,4 @@ export function applyHashlineEdits(
|
|
|
911
678
|
firstChangedLine = line;
|
|
912
679
|
}
|
|
913
680
|
}
|
|
914
|
-
|
|
915
|
-
function maybeExpandSingleLineMerge(
|
|
916
|
-
line: number,
|
|
917
|
-
content: string[],
|
|
918
|
-
): { startLine: number; deleteCount: number; newLines: string[] } | null {
|
|
919
|
-
if (content.length !== 1) return null;
|
|
920
|
-
if (line < 1 || line > fileLines.length) return null;
|
|
921
|
-
|
|
922
|
-
const newLine = content[0];
|
|
923
|
-
const newCanon = stripAllWhitespace(newLine);
|
|
924
|
-
const newCanonForMergeOps = stripMergeOperatorChars(newCanon);
|
|
925
|
-
if (newCanon.length === 0) return null;
|
|
926
|
-
|
|
927
|
-
const orig = fileLines[line - 1];
|
|
928
|
-
const origCanon = stripAllWhitespace(orig);
|
|
929
|
-
const origCanonForMatch = stripTrailingContinuationTokens(origCanon);
|
|
930
|
-
const origCanonForMergeOps = stripMergeOperatorChars(origCanon);
|
|
931
|
-
const origLooksLikeContinuation = origCanonForMatch.length < origCanon.length;
|
|
932
|
-
if (origCanon.length === 0) return null;
|
|
933
|
-
const nextIdx = line;
|
|
934
|
-
const prevIdx = line - 2;
|
|
935
|
-
// Case A: dst absorbed the next continuation line.
|
|
936
|
-
if (origLooksLikeContinuation && nextIdx < fileLines.length && !explicitlyTouchedLines.has(line + 1)) {
|
|
937
|
-
const next = fileLines[nextIdx];
|
|
938
|
-
const nextCanon = stripAllWhitespace(next);
|
|
939
|
-
const a = newCanon.indexOf(origCanonForMatch);
|
|
940
|
-
const b = newCanon.indexOf(nextCanon);
|
|
941
|
-
if (a !== -1 && b !== -1 && a < b && newCanon.length <= origCanon.length + nextCanon.length + 32) {
|
|
942
|
-
return { startLine: line, deleteCount: 2, newLines: [newLine] };
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
// Case B: dst absorbed the previous declaration/continuation line.
|
|
946
|
-
if (prevIdx >= 0 && !explicitlyTouchedLines.has(line - 1)) {
|
|
947
|
-
const prev = fileLines[prevIdx];
|
|
948
|
-
const prevCanon = stripAllWhitespace(prev);
|
|
949
|
-
const prevCanonForMatch = stripTrailingContinuationTokens(prevCanon);
|
|
950
|
-
const prevLooksLikeContinuation = prevCanonForMatch.length < prevCanon.length;
|
|
951
|
-
if (!prevLooksLikeContinuation) return null;
|
|
952
|
-
const a = newCanonForMergeOps.indexOf(stripMergeOperatorChars(prevCanonForMatch));
|
|
953
|
-
const b = newCanonForMergeOps.indexOf(origCanonForMergeOps);
|
|
954
|
-
if (a !== -1 && b !== -1 && a < b && newCanon.length <= prevCanon.length + origCanon.length + 32) {
|
|
955
|
-
return { startLine: line - 1, deleteCount: 2, newLines: [newLine] };
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
return null;
|
|
960
|
-
}
|
|
961
681
|
}
|
package/src/patch/index.ts
CHANGED
|
@@ -34,14 +34,7 @@ import { enforcePlanModeWrite, resolvePlanPath } from "../tools/plan-mode-guard"
|
|
|
34
34
|
import { applyPatch } from "./applicator";
|
|
35
35
|
import { generateDiffString, generateUnifiedDiffString, replaceText } from "./diff";
|
|
36
36
|
import { findMatch } from "./fuzzy";
|
|
37
|
-
import {
|
|
38
|
-
applyHashlineEdits,
|
|
39
|
-
computeLineHash,
|
|
40
|
-
type HashlineEdit,
|
|
41
|
-
type LineTag,
|
|
42
|
-
parseTag,
|
|
43
|
-
type ReplaceTextEdit,
|
|
44
|
-
} from "./hashline";
|
|
37
|
+
import { applyHashlineEdits, computeLineHash, type HashlineEdit, type LineTag, parseTag } from "./hashline";
|
|
45
38
|
import { detectLineEnding, normalizeToLF, restoreLineEndings, stripBom } from "./normalize";
|
|
46
39
|
import { buildNormativeUpdateInput } from "./normative";
|
|
47
40
|
import { type EditToolDetails, getLspBatchRequest } from "./shared";
|
|
@@ -139,8 +132,8 @@ export type PatchParams = Static<typeof patchEditSchema>;
|
|
|
139
132
|
/** Pattern matching hashline display format: `LINE#ID:CONTENT` */
|
|
140
133
|
const HASHLINE_PREFIX_RE = /^\s*(?:>>>|>>)?\s*\d+#[0-9a-zA-Z]{1,16}:/;
|
|
141
134
|
|
|
142
|
-
/** Pattern matching a unified-diff `+` prefix (but not `++`) */
|
|
143
|
-
const DIFF_PLUS_RE = /^[
|
|
135
|
+
/** Pattern matching a unified-diff added-line `+` prefix (but not `++`). Does NOT match `-` to avoid corrupting Markdown list items. */
|
|
136
|
+
const DIFF_PLUS_RE = /^[+](?![+])/;
|
|
144
137
|
|
|
145
138
|
/**
|
|
146
139
|
* Strip hashline display prefixes and diff `+` markers from replacement lines.
|
|
@@ -149,7 +142,7 @@ const DIFF_PLUS_RE = /^[+-](?![+-])/;
|
|
|
149
142
|
* replacement content, or include unified-diff `+` prefixes. Both corrupt the
|
|
150
143
|
* output file. This strips them heuristically before application.
|
|
151
144
|
*/
|
|
152
|
-
function stripNewLinePrefixes(lines: string[]): string[] {
|
|
145
|
+
export function stripNewLinePrefixes(lines: string[]): string[] {
|
|
153
146
|
// Detect whether the *majority* of non-empty lines carry a prefix —
|
|
154
147
|
// if only one line out of many has a match it's likely real content.
|
|
155
148
|
let hashPrefixCount = 0;
|
|
@@ -193,7 +186,7 @@ const hashlineTagFormat = (what: string) =>
|
|
|
193
186
|
description: `Tag identifying the ${what} in "LINE#ID" format`,
|
|
194
187
|
});
|
|
195
188
|
|
|
196
|
-
function hashlineParseContent(edit: string | string[] | null): string[] {
|
|
189
|
+
export function hashlineParseContent(edit: string | string[] | null): string[] {
|
|
197
190
|
if (edit === null) return [];
|
|
198
191
|
if (Array.isArray(edit)) return edit;
|
|
199
192
|
const lines = stripNewLinePrefixes(edit.split("\n"));
|
|
@@ -201,13 +194,6 @@ function hashlineParseContent(edit: string | string[] | null): string[] {
|
|
|
201
194
|
if (lines[lines.length - 1].trim() === "") return lines.slice(0, -1);
|
|
202
195
|
return lines;
|
|
203
196
|
}
|
|
204
|
-
|
|
205
|
-
function hashlineParseContentString(edit: string | string[] | null): string {
|
|
206
|
-
if (edit === null) return "";
|
|
207
|
-
if (Array.isArray(edit)) return edit.join("\n");
|
|
208
|
-
return edit;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
197
|
const hashlineTargetEditSchema = Type.Object(
|
|
212
198
|
{
|
|
213
199
|
op: Type.Literal("set"),
|
|
@@ -255,25 +241,12 @@ const hashlineInsertEditSchema = Type.Object(
|
|
|
255
241
|
{ additionalProperties: false },
|
|
256
242
|
);
|
|
257
243
|
|
|
258
|
-
const hashlineReplaceTextEditSchema = Type.Object(
|
|
259
|
-
{
|
|
260
|
-
op: Type.Literal("replaceText"),
|
|
261
|
-
old_text: Type.String({ description: "Text to find", minLength: 1 }),
|
|
262
|
-
new_text: hashlineReplaceContentFormat("Replacement"),
|
|
263
|
-
all: Type.Optional(Type.Boolean({ description: "Replace all occurrences" })),
|
|
264
|
-
},
|
|
265
|
-
{ additionalProperties: false },
|
|
266
|
-
);
|
|
267
|
-
|
|
268
|
-
const HL_REPLACE_ENABLED = Bun.env.PI_HL_REPLACETXT === "1";
|
|
269
|
-
|
|
270
244
|
const hashlineEditSpecSchema = Type.Union([
|
|
271
245
|
hashlineTargetEditSchema,
|
|
272
246
|
hashlineRangeEditSchema,
|
|
273
247
|
hashlineAppendEditSchema,
|
|
274
248
|
hashlinePrependEditSchema,
|
|
275
249
|
hashlineInsertEditSchema,
|
|
276
|
-
...(HL_REPLACE_ENABLED ? [hashlineReplaceTextEditSchema] : []),
|
|
277
250
|
]);
|
|
278
251
|
|
|
279
252
|
const hashlineEditSchema = Type.Object(
|
|
@@ -486,7 +459,7 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
486
459
|
case "patch":
|
|
487
460
|
return renderPromptTemplate(patchDescription);
|
|
488
461
|
case "hashline":
|
|
489
|
-
return renderPromptTemplate(hashlineDescription
|
|
462
|
+
return renderPromptTemplate(hashlineDescription);
|
|
490
463
|
default:
|
|
491
464
|
return renderPromptTemplate(replaceDescription);
|
|
492
465
|
}
|
|
@@ -581,7 +554,6 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
581
554
|
}
|
|
582
555
|
|
|
583
556
|
const anchorEdits: HashlineEdit[] = [];
|
|
584
|
-
const replaceEdits: ReplaceTextEdit[] = [];
|
|
585
557
|
for (const edit of edits) {
|
|
586
558
|
switch (edit.op) {
|
|
587
559
|
case "set": {
|
|
@@ -643,16 +615,6 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
643
615
|
}
|
|
644
616
|
break;
|
|
645
617
|
}
|
|
646
|
-
case "replaceText": {
|
|
647
|
-
const { old_text, new_text, all } = edit;
|
|
648
|
-
replaceEdits.push({
|
|
649
|
-
op: "replaceText",
|
|
650
|
-
old_text: old_text,
|
|
651
|
-
new_text: hashlineParseContentString(new_text),
|
|
652
|
-
all: all ?? false,
|
|
653
|
-
});
|
|
654
|
-
break;
|
|
655
|
-
}
|
|
656
618
|
default:
|
|
657
619
|
throw new Error(`Invalid edit operation: ${JSON.stringify(edit)}`);
|
|
658
620
|
}
|
|
@@ -668,19 +630,6 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
668
630
|
const anchorResult = applyHashlineEdits(normalizedContent, anchorEdits);
|
|
669
631
|
normalizedContent = anchorResult.content;
|
|
670
632
|
|
|
671
|
-
// Apply content-replace edits (substr-style fuzzy replace)
|
|
672
|
-
for (const r of replaceEdits) {
|
|
673
|
-
if (r.old_text.length === 0) {
|
|
674
|
-
throw new Error("old_text must not be empty.");
|
|
675
|
-
}
|
|
676
|
-
const rep = replaceText(normalizedContent, r.old_text, r.new_text, {
|
|
677
|
-
fuzzy: this.#allowFuzzy,
|
|
678
|
-
all: r.all ?? false,
|
|
679
|
-
threshold: this.#fuzzyThreshold,
|
|
680
|
-
});
|
|
681
|
-
normalizedContent = rep.content;
|
|
682
|
-
}
|
|
683
|
-
|
|
684
633
|
const result = {
|
|
685
634
|
content: normalizedContent,
|
|
686
635
|
firstChangedLine: anchorResult.firstChangedLine,
|
package/src/patch/normalize.ts
CHANGED
|
@@ -206,72 +206,29 @@ export function convertLeadingTabsToSpaces(text: string, spacesPerTab: number):
|
|
|
206
206
|
// Unicode Normalization
|
|
207
207
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
208
208
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
code === 0x2013 || // EN DASH
|
|
226
|
-
code === 0x2014 || // EM DASH
|
|
227
|
-
code === 0x2015 || // HORIZONTAL BAR
|
|
228
|
-
code === 0x2212 // MINUS SIGN
|
|
229
|
-
) {
|
|
230
|
-
return "-";
|
|
231
|
-
}
|
|
209
|
+
const UNICODE_REPLACEMENTS: [RegExp, string][] = [
|
|
210
|
+
// Various dash/hyphen code-points → ASCII '-'
|
|
211
|
+
[/[\u2010-\u2015\u2212]/g, "-"],
|
|
212
|
+
// Fancy single quotes → '
|
|
213
|
+
[/[\u2018-\u201B]/g, "'"],
|
|
214
|
+
// Fancy double quotes → "
|
|
215
|
+
[/[\u201C-\u201F]/g, '"'],
|
|
216
|
+
// Non-breaking space and other odd spaces → normal space
|
|
217
|
+
[/[\u00A0\u2002-\u200A\u202F\u205F\u3000]/g, " "],
|
|
218
|
+
// Not-equal sign → !=
|
|
219
|
+
[/\u2260/g, "!="],
|
|
220
|
+
// Vulgar fraction ½ → 1/2
|
|
221
|
+
[/\u00BD/g, "1/2"],
|
|
222
|
+
// Zero-width characters → remove
|
|
223
|
+
[/[\u200B-\u200D\uFEFF]/g, ""],
|
|
224
|
+
];
|
|
232
225
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
) {
|
|
240
|
-
return "'";
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// Fancy double quotes → "
|
|
244
|
-
if (
|
|
245
|
-
code === 0x201c || // LEFT DOUBLE QUOTATION MARK
|
|
246
|
-
code === 0x201d || // RIGHT DOUBLE QUOTATION MARK
|
|
247
|
-
code === 0x201e || // DOUBLE LOW-9 QUOTATION MARK
|
|
248
|
-
code === 0x201f // DOUBLE HIGH-REVERSED-9 QUOTATION MARK
|
|
249
|
-
) {
|
|
250
|
-
return '"';
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// Non-breaking space and other odd spaces → normal space
|
|
254
|
-
if (
|
|
255
|
-
code === 0x00a0 || // NO-BREAK SPACE
|
|
256
|
-
code === 0x2002 || // EN SPACE
|
|
257
|
-
code === 0x2003 || // EM SPACE
|
|
258
|
-
code === 0x2004 || // THREE-PER-EM SPACE
|
|
259
|
-
code === 0x2005 || // FOUR-PER-EM SPACE
|
|
260
|
-
code === 0x2006 || // SIX-PER-EM SPACE
|
|
261
|
-
code === 0x2007 || // FIGURE SPACE
|
|
262
|
-
code === 0x2008 || // PUNCTUATION SPACE
|
|
263
|
-
code === 0x2009 || // THIN SPACE
|
|
264
|
-
code === 0x200a || // HAIR SPACE
|
|
265
|
-
code === 0x202f || // NARROW NO-BREAK SPACE
|
|
266
|
-
code === 0x205f || // MEDIUM MATHEMATICAL SPACE
|
|
267
|
-
code === 0x3000 // IDEOGRAPHIC SPACE
|
|
268
|
-
) {
|
|
269
|
-
return " ";
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
return c;
|
|
273
|
-
})
|
|
274
|
-
.join("");
|
|
226
|
+
export function normalizeUnicode(s: string): string {
|
|
227
|
+
let result = s.trim();
|
|
228
|
+
for (const [pattern, replacement] of UNICODE_REPLACEMENTS) {
|
|
229
|
+
result = result.replace(pattern, replacement);
|
|
230
|
+
}
|
|
231
|
+
return result.normalize("NFC");
|
|
275
232
|
}
|
|
276
233
|
|
|
277
234
|
/**
|