@oh-my-pi/pi-coding-agent 15.10.9 → 15.10.11
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 +117 -0
- package/dist/cli.js +23087 -0
- package/dist/tokenizers.linux-x64-gnu-xcjh3jwk.node +0 -0
- package/dist/types/async/job-manager.d.ts +18 -0
- package/dist/types/cli/args.d.ts +1 -1
- package/dist/types/cli/dry-balance-cli.d.ts +1 -1
- package/dist/types/cli/gallery-cli.d.ts +1 -1
- package/dist/types/cli/gallery-fixtures/types.d.ts +1 -1
- package/dist/types/cli/usage-cli.d.ts +72 -0
- package/dist/types/commands/launch.d.ts +1 -1
- package/dist/types/commands/read.d.ts +1 -1
- package/dist/types/commands/usage.d.ts +25 -0
- package/dist/types/config/append-only-context-mode.d.ts +2 -1
- package/dist/types/config/model-discovery.d.ts +55 -0
- package/dist/types/config/model-registry.d.ts +20 -219
- package/dist/types/config/model-resolver.d.ts +16 -10
- package/dist/types/config/model-roles.d.ts +28 -0
- package/dist/types/config/models-config-schema.d.ts +523 -42
- package/dist/types/config/models-config.d.ts +385 -0
- package/dist/types/config/settings-schema.d.ts +12 -16
- package/dist/types/config/settings.d.ts +1 -1
- package/dist/types/debug/log-viewer.d.ts +1 -1
- package/dist/types/debug/raw-sse.d.ts +1 -1
- package/dist/types/debug/terminal-info.d.ts +0 -1
- package/dist/types/eval/backend.d.ts +0 -2
- package/dist/types/eval/idle-timeout.d.ts +0 -4
- package/dist/types/eval/js/shared/rewrite-imports.d.ts +6 -6
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/extensibility/extensions/types.d.ts +3 -3
- package/dist/types/hindsight/mental-models.d.ts +17 -8
- package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
- package/dist/types/internal-urls/types.d.ts +1 -1
- package/dist/types/lsp/edits.d.ts +9 -0
- package/dist/types/lsp/index.d.ts +2 -2
- package/dist/types/lsp/types.d.ts +2 -0
- package/dist/types/lsp/utils.d.ts +3 -0
- package/dist/types/mcp/json-rpc.d.ts +5 -0
- package/dist/types/mnemopi/state.d.ts +11 -1
- package/dist/types/modes/components/agent-dashboard.d.ts +1 -1
- package/dist/types/modes/components/assistant-message.d.ts +3 -1
- package/dist/types/modes/components/bash-execution.d.ts +1 -1
- package/dist/types/modes/components/copy-selector.d.ts +1 -1
- package/dist/types/modes/components/dynamic-border.d.ts +1 -1
- package/dist/types/modes/components/extensions/extension-dashboard.d.ts +1 -1
- package/dist/types/modes/components/extensions/extension-list.d.ts +1 -1
- package/dist/types/modes/components/extensions/inspector-panel.d.ts +1 -1
- package/dist/types/modes/components/footer.d.ts +1 -1
- package/dist/types/modes/components/hook-editor.d.ts +5 -0
- package/dist/types/modes/components/hook-input.d.ts +4 -0
- package/dist/types/modes/components/hook-selector.d.ts +1 -1
- package/dist/types/modes/components/model-selector.d.ts +1 -1
- package/dist/types/modes/components/plan-review-overlay.d.ts +1 -1
- package/dist/types/modes/components/session-observer-overlay.d.ts +1 -1
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/components/status-line/component.d.ts +1 -1
- package/dist/types/modes/components/tiny-title-download-progress.d.ts +1 -1
- package/dist/types/modes/components/transcript-container.d.ts +31 -26
- package/dist/types/modes/components/tree-selector.d.ts +1 -1
- package/dist/types/modes/components/user-message-selector.d.ts +1 -1
- package/dist/types/modes/components/user-message.d.ts +2 -1
- package/dist/types/modes/components/visual-truncate.d.ts +1 -1
- package/dist/types/modes/components/welcome.d.ts +19 -3
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +1 -1
- package/dist/types/modes/controllers/streaming-reveal.d.ts +1 -1
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +1 -1
- package/dist/types/modes/setup-wizard/scenes/types.d.ts +1 -1
- package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +1 -1
- package/dist/types/modes/setup-wizard/wizard-overlay.d.ts +1 -1
- package/dist/types/modes/types.d.ts +2 -1
- package/dist/types/session/agent-session.d.ts +1 -1
- package/dist/types/session/auth-broker-config.d.ts +4 -0
- package/dist/types/session/session-manager.d.ts +1 -1
- package/dist/types/slash-commands/helpers/stats-dashboard.d.ts +13 -0
- package/dist/types/ssh/connection-manager.d.ts +8 -0
- package/dist/types/task/discovery.d.ts +1 -2
- package/dist/types/task/parallel.d.ts +2 -2
- package/dist/types/task/worktree.d.ts +2 -0
- package/dist/types/tiny/title-client.d.ts +1 -1
- package/dist/types/tools/ask.d.ts +4 -0
- package/dist/types/tools/conflict-detect.d.ts +16 -0
- package/dist/types/tools/github-cache.d.ts +7 -0
- package/dist/types/tools/sqlite-reader.d.ts +3 -0
- package/dist/types/tools/todo.d.ts +2 -0
- package/dist/types/tui/output-block.d.ts +3 -3
- package/dist/types/utils/changelog.d.ts +8 -0
- package/dist/types/web/scrapers/readthedocs.d.ts +3 -0
- package/dist/types/web/scrapers/types.d.ts +12 -0
- package/dist/types/web/search/providers/codex.d.ts +1 -1
- package/dist/types/web/search/providers/gemini.d.ts +1 -1
- package/examples/extensions/tools.ts +5 -4
- package/package.json +14 -11
- package/scripts/build-binary.ts +18 -23
- package/scripts/bundle-dist.ts +81 -0
- package/scripts/{dev-launch → omp} +1 -1
- package/scripts/{dev-launch-preload.ts → omp.ts} +1 -1
- package/src/async/job-manager.ts +57 -3
- package/src/autoresearch/dashboard.ts +1 -1
- package/src/autoresearch/prompt-setup.md +6 -6
- package/src/autoresearch/prompt.md +6 -6
- package/src/capability/fs.ts +10 -0
- package/src/cli/args.ts +1 -1
- package/src/cli/auth-gateway-cli.ts +1 -3
- package/src/cli/dry-balance-cli.ts +1 -1
- package/src/cli/gallery-cli.ts +1 -1
- package/src/cli/gallery-fixtures/fs.ts +1 -1
- package/src/cli/gallery-fixtures/types.ts +5 -1
- package/src/cli/list-models.ts +7 -12
- package/src/cli/usage-cli.ts +603 -0
- package/src/cli-commands.ts +1 -0
- package/src/cli.ts +69 -5
- package/src/commands/complete.ts +1 -1
- package/src/commands/launch.ts +1 -1
- package/src/commands/read.ts +6 -3
- package/src/commands/usage.ts +35 -0
- package/src/commit/agentic/agent.ts +1 -1
- package/src/commit/model-selection.ts +1 -1
- package/src/config/append-only-context-mode.ts +6 -12
- package/src/config/model-discovery.ts +554 -0
- package/src/config/model-registry.ts +308 -1025
- package/src/config/model-resolver.ts +113 -156
- package/src/config/model-roles.ts +74 -0
- package/src/config/models-config-schema.ts +57 -8
- package/src/config/models-config.ts +129 -0
- package/src/config/settings-schema.ts +18 -14
- package/src/config/settings.ts +37 -1
- package/src/dap/client.ts +124 -37
- package/src/dap/session.ts +259 -158
- package/src/debug/log-viewer.ts +1 -1
- package/src/debug/raw-sse.ts +1 -1
- package/src/debug/terminal-info.ts +0 -3
- package/src/edit/diff.ts +95 -18
- package/src/edit/hashline/block-resolver.ts +20 -1
- package/src/edit/hashline/diff.ts +36 -1
- package/src/edit/hashline/execute.ts +8 -2
- package/src/edit/index.ts +16 -1
- package/src/edit/modes/patch.ts +52 -0
- package/src/edit/modes/replace.ts +56 -22
- package/src/edit/notebook.ts +22 -2
- package/src/edit/renderer.ts +36 -10
- package/src/eval/__tests__/completion-bridge.test.ts +1 -1
- package/src/eval/backend.ts +0 -2
- package/src/eval/completion-bridge.ts +2 -1
- package/src/eval/idle-timeout.ts +2 -9
- package/src/eval/js/context-manager.ts +6 -8
- package/src/eval/js/executor.ts +6 -2
- package/src/eval/js/index.ts +0 -2
- package/src/eval/js/shared/helpers.ts +5 -6
- package/src/eval/js/shared/local-module-loader.ts +1 -1
- package/src/eval/js/shared/prelude.txt +62 -1
- package/src/eval/js/shared/rewrite-imports.ts +49 -23
- package/src/eval/js/shared/runtime.ts +1 -1
- package/src/eval/py/index.ts +0 -2
- package/src/eval/py/kernel.ts +19 -0
- package/src/eval/py/runner.py +107 -3
- package/src/exec/bash-executor.ts +3 -1
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +3 -1
- package/src/extensibility/extensions/types.ts +3 -2
- package/src/extensibility/plugins/legacy-pi-compat.ts +20 -3
- package/src/hindsight/mental-models.ts +59 -12
- package/src/hindsight/state.ts +6 -1
- package/src/internal-urls/artifact-protocol.ts +11 -2
- package/src/internal-urls/docs-index.generated.ts +10 -10
- package/src/internal-urls/issue-pr-protocol.ts +12 -5
- package/src/internal-urls/router.ts +1 -1
- package/src/internal-urls/types.ts +1 -1
- package/src/lib/xai-http.ts +1 -1
- package/src/lsp/client.ts +118 -38
- package/src/lsp/clients/biome-client.ts +101 -39
- package/src/lsp/edits.ts +143 -95
- package/src/lsp/index.ts +31 -22
- package/src/lsp/render.ts +1 -1
- package/src/lsp/types.ts +2 -0
- package/src/lsp/utils.ts +28 -10
- package/src/main.ts +165 -17
- package/src/mcp/json-rpc.ts +35 -5
- package/src/mcp/transports/stdio.ts +7 -1
- package/src/memories/index.ts +2 -1
- package/src/mnemopi/backend.ts +25 -3
- package/src/mnemopi/state.ts +38 -2
- package/src/modes/components/agent-dashboard.ts +10 -7
- package/src/modes/components/assistant-message.ts +19 -13
- package/src/modes/components/bash-execution.ts +1 -1
- package/src/modes/components/copy-selector.ts +1 -1
- package/src/modes/components/diff.ts +13 -2
- package/src/modes/components/dynamic-border.ts +12 -3
- package/src/modes/components/extensions/extension-dashboard.ts +8 -5
- package/src/modes/components/extensions/extension-list.ts +1 -1
- package/src/modes/components/extensions/inspector-panel.ts +1 -1
- package/src/modes/components/footer.ts +1 -1
- package/src/modes/components/history-search.ts +1 -1
- package/src/modes/components/hook-editor.ts +8 -0
- package/src/modes/components/hook-input.ts +8 -0
- package/src/modes/components/hook-selector.ts +2 -2
- package/src/modes/components/model-selector.ts +66 -54
- package/src/modes/components/plan-review-overlay.ts +1 -1
- package/src/modes/components/session-observer-overlay.ts +2 -2
- package/src/modes/components/session-selector.ts +1 -1
- package/src/modes/components/settings-selector.ts +5 -1
- package/src/modes/components/status-line/component.ts +1 -1
- package/src/modes/components/tiny-title-download-progress.ts +1 -1
- package/src/modes/components/transcript-container.ts +373 -141
- package/src/modes/components/tree-selector.ts +3 -3
- package/src/modes/components/user-message-selector.ts +1 -1
- package/src/modes/components/user-message.ts +17 -5
- package/src/modes/components/visual-truncate.ts +1 -1
- package/src/modes/components/welcome.ts +108 -26
- package/src/modes/controllers/command-controller.ts +10 -3
- package/src/modes/controllers/event-controller.ts +73 -49
- package/src/modes/controllers/input-controller.ts +5 -5
- package/src/modes/controllers/mcp-command-controller.ts +1 -1
- package/src/modes/controllers/selector-controller.ts +1 -5
- package/src/modes/controllers/streaming-reveal.ts +85 -18
- package/src/modes/interactive-mode.ts +5 -19
- package/src/modes/setup-wizard/scenes/glyph.ts +1 -1
- package/src/modes/setup-wizard/scenes/providers.ts +1 -1
- package/src/modes/setup-wizard/scenes/sign-in.ts +1 -1
- package/src/modes/setup-wizard/scenes/theme.ts +1 -1
- package/src/modes/setup-wizard/scenes/types.ts +1 -1
- package/src/modes/setup-wizard/scenes/web-search.ts +1 -1
- package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
- package/src/modes/types.ts +2 -1
- package/src/prompts/agents/explore.md +2 -2
- package/src/prompts/agents/librarian.md +1 -2
- package/src/prompts/agents/oracle.md +1 -1
- package/src/prompts/agents/plan.md +5 -5
- package/src/prompts/agents/task.md +5 -5
- package/src/prompts/ci-green-request.md +5 -7
- package/src/prompts/goals/goal-budget-limit.md +2 -2
- package/src/prompts/goals/goal-continuation.md +4 -4
- package/src/prompts/goals/goal-mode-active.md +1 -1
- package/src/prompts/memories/read-path.md +1 -1
- package/src/prompts/memories/stage_one_system.md +2 -2
- package/src/prompts/review-custom-request.md +1 -1
- package/src/prompts/system/agent-creation-architect.md +2 -2
- package/src/prompts/system/auto-continue.md +1 -1
- package/src/prompts/system/background-tan-dispatch.md +1 -1
- package/src/prompts/system/btw-user.md +2 -2
- package/src/prompts/system/commit-message-system.md +13 -1
- package/src/prompts/system/custom-system-prompt.md +1 -1
- package/src/prompts/system/eager-todo.md +2 -2
- package/src/prompts/system/irc-incoming.md +1 -1
- package/src/prompts/system/manual-continue.md +1 -1
- package/src/prompts/system/omfg-user.md +3 -4
- package/src/prompts/system/orchestrate-notice.md +9 -9
- package/src/prompts/system/plan-mode-active.md +4 -4
- package/src/prompts/system/plan-mode-subagent.md +4 -5
- package/src/prompts/system/plan-mode-tool-decision-reminder.md +1 -1
- package/src/prompts/system/project-prompt.md +2 -2
- package/src/prompts/system/subagent-system-prompt.md +4 -4
- package/src/prompts/system/system-prompt.md +15 -26
- package/src/prompts/system/title-system.md +2 -2
- package/src/prompts/system/ttsr-tool-reminder.md +1 -1
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/ast-edit.md +1 -1
- package/src/prompts/tools/ast-grep.md +2 -2
- package/src/prompts/tools/bash.md +8 -10
- package/src/prompts/tools/browser.md +7 -7
- package/src/prompts/tools/debug.md +1 -1
- package/src/prompts/tools/eval.md +3 -3
- package/src/prompts/tools/find.md +0 -1
- package/src/prompts/tools/github.md +8 -7
- package/src/prompts/tools/goal.md +1 -1
- package/src/prompts/tools/image-gen.md +1 -1
- package/src/prompts/tools/inspect-image-system.md +1 -1
- package/src/prompts/tools/irc.md +15 -15
- package/src/prompts/tools/lsp.md +2 -2
- package/src/prompts/tools/patch.md +2 -2
- package/src/prompts/tools/read.md +3 -4
- package/src/prompts/tools/recall.md +1 -1
- package/src/prompts/tools/reflect.md +1 -1
- package/src/prompts/tools/render-mermaid.md +2 -2
- package/src/prompts/tools/replace.md +4 -10
- package/src/prompts/tools/rewind.md +2 -2
- package/src/prompts/tools/search-tool-bm25.md +1 -9
- package/src/prompts/tools/search.md +0 -1
- package/src/prompts/tools/ssh.md +0 -4
- package/src/prompts/tools/task.md +2 -3
- package/src/prompts/tools/todo.md +6 -2
- package/src/sdk.ts +23 -10
- package/src/session/agent-session.ts +44 -10
- package/src/session/auth-broker-config.ts +30 -1
- package/src/session/session-manager.ts +2 -2
- package/src/session/streaming-output.ts +23 -2
- package/src/slash-commands/builtin-registry.ts +20 -0
- package/src/slash-commands/helpers/stats-dashboard.ts +85 -0
- package/src/ssh/connection-manager.ts +27 -0
- package/src/task/commands.ts +2 -1
- package/src/task/discovery.ts +17 -24
- package/src/task/executor.ts +61 -53
- package/src/task/index.ts +137 -60
- package/src/task/parallel.ts +3 -3
- package/src/task/render.ts +2 -2
- package/src/task/worktree.ts +64 -56
- package/src/thinking.ts +2 -1
- package/src/tiny/title-client.ts +32 -14
- package/src/tools/archive-reader.ts +30 -2
- package/src/tools/ask.ts +104 -21
- package/src/tools/ast-edit.ts +25 -5
- package/src/tools/auto-generated-guard.ts +20 -3
- package/src/tools/bash-interactive.ts +27 -7
- package/src/tools/bash.ts +54 -13
- package/src/tools/browser/launch.ts +11 -2
- package/src/tools/browser/readable.ts +19 -2
- package/src/tools/browser/registry.ts +4 -1
- package/src/tools/browser/render.ts +2 -2
- package/src/tools/browser/tab-supervisor.ts +55 -16
- package/src/tools/conflict-detect.ts +50 -4
- package/src/tools/debug.ts +1 -1
- package/src/tools/eval-render.ts +5 -5
- package/src/tools/eval.ts +0 -2
- package/src/tools/fetch.ts +33 -10
- package/src/tools/gh-cache-invalidation.ts +63 -8
- package/src/tools/gh-renderer.ts +1 -1
- package/src/tools/gh.ts +172 -29
- package/src/tools/github-cache.ts +70 -6
- package/src/tools/image-gen.ts +3 -9
- package/src/tools/irc.ts +5 -1
- package/src/tools/job.ts +1 -1
- package/src/tools/read.ts +202 -61
- package/src/tools/render-utils.ts +3 -3
- package/src/tools/resolve.ts +1 -1
- package/src/tools/search.ts +92 -29
- package/src/tools/sqlite-reader.ts +17 -5
- package/src/tools/ssh.ts +8 -8
- package/src/tools/todo.ts +51 -12
- package/src/tools/write.ts +118 -18
- package/src/tui/output-block.ts +4 -4
- package/src/utils/changelog.ts +27 -1
- package/src/utils/file-mentions.ts +2 -1
- package/src/web/scrapers/arxiv.ts +1 -1
- package/src/web/scrapers/go-pkg.ts +1 -1
- package/src/web/scrapers/iacr.ts +1 -1
- package/src/web/scrapers/readthedocs.ts +1 -1
- package/src/web/scrapers/twitter.ts +2 -1
- package/src/web/scrapers/types.ts +87 -8
- package/src/web/scrapers/wikipedia.ts +1 -1
- package/src/web/scrapers/youtube.ts +6 -1
- package/src/web/search/index.ts +1 -1
- package/src/web/search/providers/anthropic.ts +8 -2
- package/src/web/search/providers/codex.ts +2 -1
- package/src/web/search/providers/gemini.ts +2 -3
- package/src/web/search/render.ts +8 -6
- package/dist/types/config/model-equivalence.d.ts +0 -24
- package/dist/types/config/model-id-affixes.d.ts +0 -12
- package/dist/types/config/model-provider-priority.d.ts +0 -1
- package/dist/types/exec/idle-timeout-watchdog.d.ts +0 -18
- package/src/config/model-equivalence.ts +0 -875
- package/src/config/model-id-affixes.ts +0 -81
- package/src/config/model-provider-priority.ts +0 -56
- package/src/exec/idle-timeout-watchdog.ts +0 -126
package/src/edit/diff.ts
CHANGED
|
@@ -55,13 +55,10 @@ function formatNumberedDiffLine(prefix: "+" | "-" | " ", lineNum: number, conten
|
|
|
55
55
|
return `${prefix}${lineNum}|${content}`;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
type DiffSource = "old" | "new";
|
|
59
|
-
|
|
60
58
|
interface ParsedNumberedDiffRow {
|
|
61
59
|
prefix: "+" | "-" | " ";
|
|
62
60
|
lineNumber: number;
|
|
63
61
|
content: string;
|
|
64
|
-
source: DiffSource;
|
|
65
62
|
}
|
|
66
63
|
|
|
67
64
|
function parseNumberedDiffRow(row: string): ParsedNumberedDiffRow | undefined {
|
|
@@ -70,18 +67,56 @@ function parseNumberedDiffRow(row: string): ParsedNumberedDiffRow | undefined {
|
|
|
70
67
|
const prefix = match[1] as "+" | "-" | " ";
|
|
71
68
|
const lineNumber = Number.parseInt(match[2], 10);
|
|
72
69
|
if (!Number.isFinite(lineNumber)) return undefined;
|
|
73
|
-
return {
|
|
74
|
-
prefix,
|
|
75
|
-
lineNumber,
|
|
76
|
-
content: match[3] ?? "",
|
|
77
|
-
source: prefix === "+" ? "new" : "old",
|
|
78
|
-
};
|
|
70
|
+
return { prefix, lineNumber, content: match[3] ?? "" };
|
|
79
71
|
}
|
|
80
72
|
|
|
81
73
|
function isDiffChangeRow(row: string | undefined): boolean {
|
|
82
74
|
return row !== undefined && (row.startsWith("+") || row.startsWith("-"));
|
|
83
75
|
}
|
|
84
76
|
|
|
77
|
+
/** Blank row separating non-contiguous regions of a numbered diff. */
|
|
78
|
+
const DIFF_GAP_ROW = "";
|
|
79
|
+
|
|
80
|
+
/** Old-file line number of a source-visible row (`-` or context); `+`/gap/other rows yield undefined. */
|
|
81
|
+
function parseSourceRowLineNumber(row: string): number | undefined {
|
|
82
|
+
const parsed = parseNumberedDiffRow(row);
|
|
83
|
+
return parsed === undefined || parsed.prefix === "+" ? undefined : parsed.lineNumber;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Drop gap rows that no longer separate anything. Context rows are inserted
|
|
88
|
+
* one at a time, each adding its own gap rows from a snapshot of the diff, so
|
|
89
|
+
* the raw result can contain adjacent gap rows, gap rows whose neighbors
|
|
90
|
+
* became contiguous after a later insert filled the hole, and gap rows at the
|
|
91
|
+
* diff edges. The sweep keeps a gap row only when it sits between two
|
|
92
|
+
* source-numbered rows (old-file coordinates — the same numbering the
|
|
93
|
+
* insertion gap test uses) that are actually non-contiguous, and never keeps
|
|
94
|
+
* two in a row.
|
|
95
|
+
*/
|
|
96
|
+
function normalizeDiffGapRows(rows: string[]): void {
|
|
97
|
+
const kept: string[] = [];
|
|
98
|
+
for (let i = 0; i < rows.length; i++) {
|
|
99
|
+
const row = rows[i];
|
|
100
|
+
if (row !== DIFF_GAP_ROW) {
|
|
101
|
+
kept.push(row);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (kept.length === 0 || kept[kept.length - 1] === DIFF_GAP_ROW) continue;
|
|
105
|
+
let before: number | undefined;
|
|
106
|
+
for (let j = kept.length - 1; j >= 0 && before === undefined; j--) {
|
|
107
|
+
before = parseSourceRowLineNumber(kept[j]);
|
|
108
|
+
}
|
|
109
|
+
let after: number | undefined;
|
|
110
|
+
for (let j = i + 1; j < rows.length && after === undefined; j++) {
|
|
111
|
+
if (rows[j] === DIFF_GAP_ROW) continue;
|
|
112
|
+
after = parseSourceRowLineNumber(rows[j]);
|
|
113
|
+
}
|
|
114
|
+
if (before === undefined || after === undefined || after <= before + 1) continue;
|
|
115
|
+
kept.push(row);
|
|
116
|
+
}
|
|
117
|
+
if (kept.length !== rows.length) rows.splice(0, rows.length, ...kept);
|
|
118
|
+
}
|
|
119
|
+
|
|
85
120
|
function adjustedContextInsertIndex(rows: readonly string[], index: number): number {
|
|
86
121
|
let start = index;
|
|
87
122
|
while (start > 0 && isDiffChangeRow(rows[start - 1])) start--;
|
|
@@ -92,7 +127,6 @@ function adjustedContextInsertIndex(rows: readonly string[], index: number): num
|
|
|
92
127
|
|
|
93
128
|
function insertBracketContextRows(
|
|
94
129
|
rows: string[],
|
|
95
|
-
source: DiffSource,
|
|
96
130
|
contextLines: ReadonlyMap<number, string>,
|
|
97
131
|
seenRows: Set<string>,
|
|
98
132
|
): void {
|
|
@@ -106,7 +140,7 @@ function insertBracketContextRows(
|
|
|
106
140
|
let nextSourceLine: number | undefined;
|
|
107
141
|
for (let i = 0; i < rows.length; i++) {
|
|
108
142
|
const parsed = parseNumberedDiffRow(rows[i]);
|
|
109
|
-
if (!parsed || parsed.
|
|
143
|
+
if (!parsed || parsed.prefix === "+") continue;
|
|
110
144
|
if (parsed.lineNumber < lineNumber) {
|
|
111
145
|
previousSourceLine = parsed.lineNumber;
|
|
112
146
|
continue;
|
|
@@ -117,16 +151,26 @@ function insertBracketContextRows(
|
|
|
117
151
|
}
|
|
118
152
|
|
|
119
153
|
const chunk: string[] = [];
|
|
120
|
-
if (previousSourceLine !== undefined && lineNumber > previousSourceLine + 1) chunk.push(
|
|
154
|
+
if (previousSourceLine !== undefined && lineNumber > previousSourceLine + 1) chunk.push(DIFF_GAP_ROW);
|
|
121
155
|
chunk.push(row);
|
|
122
|
-
if (nextSourceLine !== undefined && nextSourceLine > lineNumber + 1) chunk.push(
|
|
156
|
+
if (nextSourceLine !== undefined && nextSourceLine > lineNumber + 1) chunk.push(DIFF_GAP_ROW);
|
|
123
157
|
|
|
124
158
|
const adjustedIndex = adjustedContextInsertIndex(rows, insertIndex);
|
|
125
159
|
rows.splice(adjustedIndex, 0, ...chunk);
|
|
126
|
-
|
|
160
|
+
seenRows.add(row);
|
|
127
161
|
}
|
|
128
162
|
}
|
|
129
163
|
|
|
164
|
+
/**
|
|
165
|
+
* Insert off-window block-boundary rows (enclosing header, matching closing
|
|
166
|
+
* bracket, …) into a numbered diff. Context rows carry pre-edit line numbers —
|
|
167
|
+
* the renumbering contract of `buildCompactDiffPreview` — so boundary lines
|
|
168
|
+
* discovered in the new file are translated back to their pre-edit numbers
|
|
169
|
+
* and merged with the old-file pass before a single insertion sweep. Without
|
|
170
|
+
* the translation, a context line sitting in a net-offset region would be
|
|
171
|
+
* re-inserted under its post-edit number: duplicated, out of order, and
|
|
172
|
+
* renumbered incorrectly by the preview.
|
|
173
|
+
*/
|
|
130
174
|
function addMatchingBracketContextRows(
|
|
131
175
|
rows: string[],
|
|
132
176
|
oldLines: readonly string[],
|
|
@@ -136,16 +180,49 @@ function addMatchingBracketContextRows(
|
|
|
136
180
|
const oldVisible: number[] = [];
|
|
137
181
|
const newVisible: number[] = [];
|
|
138
182
|
const seenRows = new Set(rows);
|
|
183
|
+
// Change positions in new-file coordinates, used to translate an unchanged
|
|
184
|
+
// new-file line number back to its pre-edit equivalent.
|
|
185
|
+
const changes: { newPos: number; delta: 1 | -1 }[] = [];
|
|
186
|
+
let offset = 0;
|
|
139
187
|
|
|
140
188
|
for (const row of rows) {
|
|
141
189
|
const parsed = parseNumberedDiffRow(row);
|
|
142
190
|
if (!parsed) continue;
|
|
143
|
-
|
|
144
|
-
|
|
191
|
+
switch (parsed.prefix) {
|
|
192
|
+
case "-":
|
|
193
|
+
oldVisible.push(parsed.lineNumber);
|
|
194
|
+
changes.push({ newPos: parsed.lineNumber + offset, delta: -1 });
|
|
195
|
+
offset--;
|
|
196
|
+
break;
|
|
197
|
+
case "+":
|
|
198
|
+
newVisible.push(parsed.lineNumber);
|
|
199
|
+
changes.push({ newPos: parsed.lineNumber, delta: 1 });
|
|
200
|
+
offset++;
|
|
201
|
+
break;
|
|
202
|
+
default:
|
|
203
|
+
// Context rows are visible in BOTH files: pre-edit number as
|
|
204
|
+
// written, post-edit number shifted by the net change so far.
|
|
205
|
+
oldVisible.push(parsed.lineNumber);
|
|
206
|
+
newVisible.push(parsed.lineNumber + offset);
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
145
209
|
}
|
|
146
210
|
|
|
147
|
-
|
|
148
|
-
|
|
211
|
+
const toOldLineNumber = (newLineNumber: number): number => {
|
|
212
|
+
let shift = 0;
|
|
213
|
+
for (const change of changes) {
|
|
214
|
+
if (change.newPos <= newLineNumber) shift += change.delta;
|
|
215
|
+
}
|
|
216
|
+
return newLineNumber - shift;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const contextRows = findBlockContextLines(oldLines, oldVisible, source);
|
|
220
|
+
for (const [lineNumber, text] of findBlockContextLines(newLines, newVisible, source)) {
|
|
221
|
+
const oldLineNumber = toOldLineNumber(lineNumber);
|
|
222
|
+
if (!contextRows.has(oldLineNumber)) contextRows.set(oldLineNumber, text);
|
|
223
|
+
}
|
|
224
|
+
insertBracketContextRows(rows, contextRows, seenRows);
|
|
225
|
+
normalizeDiffGapRows(rows);
|
|
149
226
|
}
|
|
150
227
|
|
|
151
228
|
/**
|
|
@@ -8,7 +8,26 @@
|
|
|
8
8
|
import type { BlockResolver } from "@oh-my-pi/hashline";
|
|
9
9
|
import { blockRangeAt } from "@oh-my-pi/pi-natives";
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* `blockRangeAt` runs a full synchronous tree-sitter parse of `text` per
|
|
13
|
+
* call, and streaming previews re-resolve the same (text, line) every
|
|
14
|
+
* streamed chunk. Memoize by content: identical text + line always yields the
|
|
15
|
+
* same span. FIFO-bounded; hashing the text is orders of magnitude cheaper
|
|
16
|
+
* than re-parsing it.
|
|
17
|
+
*/
|
|
18
|
+
const resolutionCache = new Map<string, { start: number; end: number } | null>();
|
|
19
|
+
const RESOLUTION_CACHE_MAX = 512;
|
|
20
|
+
|
|
11
21
|
export const nativeBlockResolver: BlockResolver = ({ path, text, line }) => {
|
|
22
|
+
const key = `${Bun.hash(text).toString(36)}:${text.length}:${line}:${path}`;
|
|
23
|
+
const cached = resolutionCache.get(key);
|
|
24
|
+
if (cached !== undefined) return cached;
|
|
12
25
|
const range = blockRangeAt({ code: text, path, line });
|
|
13
|
-
|
|
26
|
+
const result = range ? { start: range.startLine, end: range.endLine } : null;
|
|
27
|
+
if (resolutionCache.size >= RESOLUTION_CACHE_MAX) {
|
|
28
|
+
const oldest = resolutionCache.keys().next().value;
|
|
29
|
+
if (oldest !== undefined) resolutionCache.delete(oldest);
|
|
30
|
+
}
|
|
31
|
+
resolutionCache.set(key, result);
|
|
32
|
+
return result;
|
|
14
33
|
};
|
|
@@ -57,6 +57,39 @@ async function readSectionText(absolutePath: string, sectionPath: string): Promi
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Streaming previews recompute on every streamed chunk; re-reading the target
|
|
62
|
+
* file from disk each tick dominates the cost on large files. Cache the raw
|
|
63
|
+
* section text keyed by mtime+size so any on-disk change invalidates
|
|
64
|
+
* naturally. Used by the streaming path only — the args-complete pass always
|
|
65
|
+
* reads fresh.
|
|
66
|
+
*/
|
|
67
|
+
const streamingTextCache = new Map<string, { mtimeMs: number; size: number; rawContent: string }>();
|
|
68
|
+
const STREAMING_TEXT_CACHE_MAX = 8;
|
|
69
|
+
|
|
70
|
+
async function readSectionTextCached(absolutePath: string, sectionPath: string): Promise<string> {
|
|
71
|
+
let stamp: { mtimeMs: number; size: number } | undefined;
|
|
72
|
+
try {
|
|
73
|
+
const stat = await Bun.file(absolutePath).stat();
|
|
74
|
+
stamp = { mtimeMs: stat.mtimeMs, size: stat.size };
|
|
75
|
+
} catch {
|
|
76
|
+
stamp = undefined;
|
|
77
|
+
}
|
|
78
|
+
if (stamp) {
|
|
79
|
+
const cached = streamingTextCache.get(absolutePath);
|
|
80
|
+
if (cached && cached.mtimeMs === stamp.mtimeMs && cached.size === stamp.size) return cached.rawContent;
|
|
81
|
+
}
|
|
82
|
+
const rawContent = await readSectionText(absolutePath, sectionPath);
|
|
83
|
+
if (stamp) {
|
|
84
|
+
if (streamingTextCache.size >= STREAMING_TEXT_CACHE_MAX && !streamingTextCache.has(absolutePath)) {
|
|
85
|
+
const oldest = streamingTextCache.keys().next().value;
|
|
86
|
+
if (oldest !== undefined) streamingTextCache.delete(oldest);
|
|
87
|
+
}
|
|
88
|
+
streamingTextCache.set(absolutePath, { mtimeMs: stamp.mtimeMs, size: stamp.size, rawContent });
|
|
89
|
+
}
|
|
90
|
+
return rawContent;
|
|
91
|
+
}
|
|
92
|
+
|
|
60
93
|
function hasAnchorScopedEdit(edits: readonly Edit[]): boolean {
|
|
61
94
|
return edits.some(edit => {
|
|
62
95
|
if (edit.kind === "delete") return true;
|
|
@@ -220,7 +253,9 @@ export async function computeHashlineSectionDiff(
|
|
|
220
253
|
): Promise<{ diff: string; firstChangedLine: number | undefined } | { error: string }> {
|
|
221
254
|
try {
|
|
222
255
|
const absolutePath = resolveToCwd(section.path, cwd);
|
|
223
|
-
const rawContent =
|
|
256
|
+
const rawContent = options.streaming
|
|
257
|
+
? await readSectionTextCached(absolutePath, section.path)
|
|
258
|
+
: await readSectionText(absolutePath, section.path);
|
|
224
259
|
const { text: content } = stripBom(rawContent);
|
|
225
260
|
const normalized = normalizeToLF(content);
|
|
226
261
|
// Streaming favors a stable, monotonic preview over an exact unified
|
|
@@ -78,11 +78,17 @@ interface RenderedSection {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
function formatBlockResolution(resolution: BlockResolution): string {
|
|
81
|
-
const op =
|
|
81
|
+
const op =
|
|
82
|
+
resolution.op === "delete"
|
|
83
|
+
? "delete block"
|
|
84
|
+
: resolution.op === "insert_after"
|
|
85
|
+
? "insert after block"
|
|
86
|
+
: "replace block";
|
|
82
87
|
const lines = resolution.end - resolution.start + 1;
|
|
83
88
|
const span =
|
|
84
89
|
resolution.start === resolution.end ? `line ${resolution.start}` : `lines ${resolution.start}-${resolution.end}`;
|
|
85
|
-
|
|
90
|
+
const suffix = resolution.op === "insert_after" ? `; body lands after line ${resolution.end}` : "";
|
|
91
|
+
return `${op} ${resolution.anchorLine} → resolved ${span} (${lines} line${lines === 1 ? "" : "s"})${suffix}`;
|
|
86
92
|
}
|
|
87
93
|
|
|
88
94
|
function renderSection(result: PatchSectionResult, diagnostics: FileDiagnosticsResult | undefined): RenderedSection {
|
package/src/edit/index.ts
CHANGED
|
@@ -238,8 +238,23 @@ async function executeSinglePathEntries(
|
|
|
238
238
|
if (text) contentTexts.push(text);
|
|
239
239
|
} catch (err) {
|
|
240
240
|
const errorText = err instanceof Error ? err.message : String(err);
|
|
241
|
-
contentTexts.push(`Error editing ${path}: ${errorText}`);
|
|
241
|
+
contentTexts.push(`Error editing ${path} (entry ${i + 1} of ${runs.length}): ${errorText}`);
|
|
242
|
+
if (i > 0) {
|
|
243
|
+
contentTexts.push(i === 1 ? `Entry 1 was already applied.` : `Entries 1-${i} were already applied.`);
|
|
244
|
+
}
|
|
245
|
+
if (i + 1 < runs.length) {
|
|
246
|
+
contentTexts.push(
|
|
247
|
+
(i + 2 === runs.length
|
|
248
|
+
? `Entry ${runs.length} was NOT applied`
|
|
249
|
+
: `Entries ${i + 2}-${runs.length} were NOT applied`) +
|
|
250
|
+
`; re-read the file and re-issue only the failed and unapplied entries.`,
|
|
251
|
+
);
|
|
252
|
+
}
|
|
242
253
|
errorCount++;
|
|
254
|
+
// Stop at the first failure: later entries were authored against
|
|
255
|
+
// line numbers/content that assumed this entry succeeded, and
|
|
256
|
+
// applying them after a failure compounds the damage.
|
|
257
|
+
break;
|
|
243
258
|
}
|
|
244
259
|
|
|
245
260
|
if (!isLast && onUpdate) {
|
package/src/edit/modes/patch.ts
CHANGED
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
countLeadingWhitespace,
|
|
41
41
|
detectLineEnding,
|
|
42
42
|
getLeadingWhitespace,
|
|
43
|
+
normalizeForFuzzy,
|
|
43
44
|
normalizeToLF,
|
|
44
45
|
restoreLineEndings,
|
|
45
46
|
stripBom,
|
|
@@ -1007,6 +1008,41 @@ async function readExistingPatchFile(fileSystem: FileSystem, absolutePath: strin
|
|
|
1007
1008
|
}
|
|
1008
1009
|
}
|
|
1009
1010
|
|
|
1011
|
+
/**
|
|
1012
|
+
* A prefix/substring strategy matched pattern lines that cover only part of
|
|
1013
|
+
* the corresponding file lines; replacing whole lines would silently drop the
|
|
1014
|
+
* uncovered text the model never saw. Allow the replacement only when every
|
|
1015
|
+
* discarded piece (normalized) survives somewhere in the hunk's new lines.
|
|
1016
|
+
*/
|
|
1017
|
+
function assertPartialMatchPreservesDiscardedText(
|
|
1018
|
+
path: string,
|
|
1019
|
+
pattern: string[],
|
|
1020
|
+
matchedLines: string[],
|
|
1021
|
+
newLines: string[],
|
|
1022
|
+
matchStartIndex: number,
|
|
1023
|
+
): void {
|
|
1024
|
+
let newLinesNorm: string | undefined;
|
|
1025
|
+
for (let j = 0; j < pattern.length; j++) {
|
|
1026
|
+
const lineNorm = normalizeForFuzzy(matchedLines[j]);
|
|
1027
|
+
const patternNorm = normalizeForFuzzy(pattern[j]);
|
|
1028
|
+
if (lineNorm === patternNorm) continue;
|
|
1029
|
+
const at = lineNorm.indexOf(patternNorm);
|
|
1030
|
+
if (at === -1) continue;
|
|
1031
|
+
const discardedParts = [lineNorm.slice(0, at).trim(), lineNorm.slice(at + patternNorm.length).trim()];
|
|
1032
|
+
for (const part of discardedParts) {
|
|
1033
|
+
if (part.length === 0) continue;
|
|
1034
|
+
newLinesNorm ??= newLines.map(normalizeForFuzzy).join("\n");
|
|
1035
|
+
if (!newLinesNorm.includes(part)) {
|
|
1036
|
+
throw new ApplyPatchError(
|
|
1037
|
+
`Refusing partial-line match in ${path} at line ${matchStartIndex + j + 1}: ` +
|
|
1038
|
+
`the file line also contains ${JSON.stringify(part)}, which the replacement would silently drop. ` +
|
|
1039
|
+
`Provide the complete line in the hunk.`,
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1010
1046
|
/**
|
|
1011
1047
|
* Compute replacements needed to transform originalLines using the diff hunks.
|
|
1012
1048
|
*/
|
|
@@ -1253,6 +1289,18 @@ function computeReplacements(
|
|
|
1253
1289
|
if (searchResult.strategy === "fuzzy-dominant") {
|
|
1254
1290
|
const similarity = Math.round(searchResult.confidence * 100);
|
|
1255
1291
|
warnings.push(`Dominant fuzzy match selected in ${path} near line ${found + 1} (${similarity}% similar).`);
|
|
1292
|
+
} else if (
|
|
1293
|
+
searchResult.strategy === "comment-prefix" ||
|
|
1294
|
+
searchResult.strategy === "prefix" ||
|
|
1295
|
+
searchResult.strategy === "substring" ||
|
|
1296
|
+
searchResult.strategy === "fuzzy" ||
|
|
1297
|
+
searchResult.strategy === "character"
|
|
1298
|
+
) {
|
|
1299
|
+
const similarity = Math.round(searchResult.confidence * 100);
|
|
1300
|
+
warnings.push(
|
|
1301
|
+
`Inexact match in ${path} near line ${found + 1}: matched via ${searchResult.strategy} strategy ` +
|
|
1302
|
+
`(${similarity}% similar). Re-read the file if the result is not what you intended.`,
|
|
1303
|
+
);
|
|
1256
1304
|
}
|
|
1257
1305
|
|
|
1258
1306
|
// Reject if match is ambiguous (prefix/substring matching found multiple matches)
|
|
@@ -1305,6 +1353,10 @@ function computeReplacements(
|
|
|
1305
1353
|
continue;
|
|
1306
1354
|
}
|
|
1307
1355
|
|
|
1356
|
+
if (searchResult.strategy === "prefix" || searchResult.strategy === "substring") {
|
|
1357
|
+
assertPartialMatchPreservesDiscardedText(path, pattern, actualMatchedLines, newSlice, found);
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1308
1360
|
const adjustedNewLines = adjustLinesIndentation(pattern, actualMatchedLines, newSlice);
|
|
1309
1361
|
replacements.push({ startIndex: found, oldLen: pattern.length, newLines: adjustedNewLines });
|
|
1310
1362
|
lineIndex = found + pattern.length;
|
|
@@ -525,29 +525,45 @@ function matchesAt(lines: string[], pattern: string[], i: number, compare: (a: s
|
|
|
525
525
|
return true;
|
|
526
526
|
}
|
|
527
527
|
|
|
528
|
-
/**
|
|
529
|
-
|
|
528
|
+
/**
|
|
529
|
+
* Compute average similarity score for pre-normalized pattern lines at
|
|
530
|
+
* position `i` of pre-normalized file lines.
|
|
531
|
+
*
|
|
532
|
+
* `minScore` is a bail threshold: when even perfect similarity on the
|
|
533
|
+
* remaining lines cannot lift the average to `minScore`, returns the partial
|
|
534
|
+
* average early (always ≤ the true score). The length-difference lower bound
|
|
535
|
+
* on Levenshtein distance is used to skip the DP entirely for line pairs the
|
|
536
|
+
* bail test already rules out.
|
|
537
|
+
*/
|
|
538
|
+
function fuzzyScoreAt(linesNorm: string[], patternNorm: string[], i: number, minScore = 0): number {
|
|
539
|
+
const count = patternNorm.length;
|
|
530
540
|
let totalScore = 0;
|
|
531
|
-
for (let j = 0; j <
|
|
532
|
-
const lineNorm =
|
|
533
|
-
const
|
|
534
|
-
|
|
541
|
+
for (let j = 0; j < count; j++) {
|
|
542
|
+
const lineNorm = linesNorm[i + j];
|
|
543
|
+
const patNorm = patternNorm[j];
|
|
544
|
+
if (lineNorm === patNorm) {
|
|
545
|
+
totalScore += 1;
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
const remaining = count - j - 1;
|
|
549
|
+
const maxLen = Math.max(lineNorm.length, patNorm.length);
|
|
550
|
+
// similarity ≤ 1 − |lenA−lenB|/maxLen: test the bound before the DP.
|
|
551
|
+
const upperBound = 1 - Math.abs(lineNorm.length - patNorm.length) / maxLen;
|
|
552
|
+
if ((totalScore + upperBound + remaining) / count < minScore) return totalScore / count;
|
|
553
|
+
if (upperBound > 0) totalScore += similarity(lineNorm, patNorm);
|
|
554
|
+
if ((totalScore + remaining) / count < minScore) return totalScore / count;
|
|
535
555
|
}
|
|
536
|
-
return totalScore /
|
|
556
|
+
return totalScore / count;
|
|
537
557
|
}
|
|
538
558
|
|
|
539
|
-
/** Check if line starts with pattern
|
|
540
|
-
function
|
|
541
|
-
const lineNorm = normalizeForFuzzy(line);
|
|
542
|
-
const patternNorm = normalizeForFuzzy(pattern);
|
|
559
|
+
/** Check if pre-normalized line starts with pre-normalized pattern */
|
|
560
|
+
function normStartsWith(lineNorm: string, patternNorm: string): boolean {
|
|
543
561
|
if (patternNorm.length === 0) return lineNorm.length === 0;
|
|
544
562
|
return lineNorm.startsWith(patternNorm);
|
|
545
563
|
}
|
|
546
564
|
|
|
547
|
-
/** Check if line contains pattern as significant substring */
|
|
548
|
-
function
|
|
549
|
-
const lineNorm = normalizeForFuzzy(line);
|
|
550
|
-
const patternNorm = normalizeForFuzzy(pattern);
|
|
565
|
+
/** Check if pre-normalized line contains pre-normalized pattern as significant substring */
|
|
566
|
+
function normIncludes(lineNorm: string, patternNorm: string): boolean {
|
|
551
567
|
if (patternNorm.length === 0) return lineNorm.length === 0;
|
|
552
568
|
if (patternNorm.length < PARTIAL_MATCH_MIN_LENGTH) return false;
|
|
553
569
|
if (!lineNorm.includes(patternNorm)) return false;
|
|
@@ -613,6 +629,13 @@ export function seekSequence(
|
|
|
613
629
|
const searchStart = eof && lines.length >= pattern.length ? lines.length - pattern.length : start;
|
|
614
630
|
const maxStart = lines.length - pattern.length;
|
|
615
631
|
|
|
632
|
+
// Fuzzy and partial passes compare normalizeForFuzzy forms; normalize the
|
|
633
|
+
// file and pattern once per call instead of once per candidate position.
|
|
634
|
+
let linesNormCache: string[] | undefined;
|
|
635
|
+
let patternNormCache: string[] | undefined;
|
|
636
|
+
const getLinesNorm = () => (linesNormCache ??= lines.map(normalizeForFuzzy));
|
|
637
|
+
const getPatternNorm = () => (patternNormCache ??= pattern.map(normalizeForFuzzy));
|
|
638
|
+
|
|
616
639
|
const runExactPasses = (from: number, to: number): SequenceSearchResult | undefined => {
|
|
617
640
|
const comparisonPasses: Array<{
|
|
618
641
|
compare: (a: string, b: string) => boolean;
|
|
@@ -646,17 +669,19 @@ export function seekSequence(
|
|
|
646
669
|
return undefined;
|
|
647
670
|
}
|
|
648
671
|
|
|
672
|
+
const linesNorm = getLinesNorm();
|
|
673
|
+
const patternNorm = getPatternNorm();
|
|
649
674
|
const partialPasses: Array<{
|
|
650
|
-
compare: (
|
|
675
|
+
compare: (lineNorm: string, patternLineNorm: string) => boolean;
|
|
651
676
|
confidence: number;
|
|
652
677
|
strategy: SequenceMatchStrategy;
|
|
653
678
|
}> = [
|
|
654
|
-
{ compare:
|
|
655
|
-
{ compare:
|
|
679
|
+
{ compare: normStartsWith, confidence: 0.965, strategy: "prefix" },
|
|
680
|
+
{ compare: normIncludes, confidence: 0.94, strategy: "substring" },
|
|
656
681
|
];
|
|
657
682
|
|
|
658
683
|
for (const pass of partialPasses) {
|
|
659
|
-
const matches = collectIndexedMatches(from, to, i => matchesAt(
|
|
684
|
+
const matches = collectIndexedMatches(from, to, i => matchesAt(linesNorm, patternNorm, i, pass.compare));
|
|
660
685
|
const result = toAmbiguousMatchResult(matches, pass.confidence, pass.strategy);
|
|
661
686
|
if (result) {
|
|
662
687
|
return result;
|
|
@@ -692,9 +717,14 @@ export function seekSequence(
|
|
|
692
717
|
matchIndices: [],
|
|
693
718
|
};
|
|
694
719
|
|
|
720
|
+
const fuzzyLinesNorm = getLinesNorm();
|
|
721
|
+
const fuzzyPatternNorm = getPatternNorm();
|
|
722
|
+
// Positions scoring below this can neither become a fuzzy match nor affect
|
|
723
|
+
// the dominant-fuzzy gap test; let fuzzyScoreAt bail early on them.
|
|
724
|
+
const fuzzyBail = SEQUENCE_FUZZY_THRESHOLD - DOMINANT_FUZZY_DELTA;
|
|
695
725
|
const scoreFuzzyRange = (from: number, to: number): void => {
|
|
696
726
|
for (let i = from; i <= to; i++) {
|
|
697
|
-
const score = fuzzyScoreAt(
|
|
727
|
+
const score = fuzzyScoreAt(fuzzyLinesNorm, fuzzyPatternNorm, i, fuzzyBail);
|
|
698
728
|
if (score >= SEQUENCE_FUZZY_THRESHOLD) {
|
|
699
729
|
if (fuzzyMatches.firstMatch === undefined) {
|
|
700
730
|
fuzzyMatches.firstMatch = i;
|
|
@@ -787,12 +817,16 @@ export function findClosestSequenceMatch(
|
|
|
787
817
|
const eof = options?.eof ?? false;
|
|
788
818
|
const maxStart = lines.length - pattern.length;
|
|
789
819
|
const searchStart = eof && lines.length >= pattern.length ? maxStart : start;
|
|
820
|
+
const linesNorm = lines.map(normalizeForFuzzy);
|
|
821
|
+
const patternNorm = pattern.map(normalizeForFuzzy);
|
|
790
822
|
|
|
791
823
|
let bestIndex: number | undefined;
|
|
792
824
|
let bestScore = 0;
|
|
793
825
|
|
|
826
|
+
// Passing the running best as the bail threshold is exact: a bailed
|
|
827
|
+
// position returns a value strictly below it, so it can never win.
|
|
794
828
|
for (let i = searchStart; i <= maxStart; i++) {
|
|
795
|
-
const score = fuzzyScoreAt(
|
|
829
|
+
const score = fuzzyScoreAt(linesNorm, patternNorm, i, bestScore);
|
|
796
830
|
if (score > bestScore) {
|
|
797
831
|
bestScore = score;
|
|
798
832
|
bestIndex = i;
|
|
@@ -801,7 +835,7 @@ export function findClosestSequenceMatch(
|
|
|
801
835
|
|
|
802
836
|
if (eof && searchStart > start) {
|
|
803
837
|
for (let i = start; i < searchStart; i++) {
|
|
804
|
-
const score = fuzzyScoreAt(
|
|
838
|
+
const score = fuzzyScoreAt(linesNorm, patternNorm, i, bestScore);
|
|
805
839
|
if (score > bestScore) {
|
|
806
840
|
bestScore = score;
|
|
807
841
|
bestIndex = i;
|
package/src/edit/notebook.ts
CHANGED
|
@@ -21,6 +21,26 @@ export interface NotebookDocument {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
const CELL_MARKER_RE = /^# %% \[(code|markdown|raw)\](?: cell:(\d+))?$/;
|
|
24
|
+
/**
|
|
25
|
+
* Cell source lines that would themselves parse as (possibly already-escaped)
|
|
26
|
+
* cell markers gain one extra `%` on render and lose it on parse, so a
|
|
27
|
+
* notebook that *contains* the literal text `# %% [markdown] cell:3` survives
|
|
28
|
+
* the editable-text round trip instead of being split into extra cells.
|
|
29
|
+
*/
|
|
30
|
+
const ESCAPABLE_MARKER_RE = /^# %%+ \[(?:code|markdown|raw)\](?: cell:\d+)?$/;
|
|
31
|
+
const ESCAPED_MARKER_RE = /^# %%%+ \[(?:code|markdown|raw)\](?: cell:\d+)?$/;
|
|
32
|
+
|
|
33
|
+
function escapeMarkerLikeSourceLines(source: string): string {
|
|
34
|
+
if (!source.includes("# %%")) return source;
|
|
35
|
+
return source
|
|
36
|
+
.split("\n")
|
|
37
|
+
.map(line => (ESCAPABLE_MARKER_RE.test(line) ? line.replace("# %", "# %%") : line))
|
|
38
|
+
.join("\n");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function unescapeMarkerLikeLine(line: string): string {
|
|
42
|
+
return ESCAPED_MARKER_RE.test(line) ? line.replace("# %%", "# %") : line;
|
|
43
|
+
}
|
|
24
44
|
|
|
25
45
|
export function isNotebookPath(filePath: string): boolean {
|
|
26
46
|
return path.extname(filePath).toLowerCase() === ".ipynb";
|
|
@@ -100,7 +120,7 @@ export async function readNotebookDocument(absolutePath: string, displayPath: st
|
|
|
100
120
|
export function notebookToEditableText(notebook: NotebookDocument): string {
|
|
101
121
|
return notebook.cells
|
|
102
122
|
.map((cell, index) => {
|
|
103
|
-
const source = sourceToText(cell.source);
|
|
123
|
+
const source = escapeMarkerLikeSourceLines(sourceToText(cell.source));
|
|
104
124
|
return source.length > 0
|
|
105
125
|
? `# %% [${cell.cell_type}] cell:${index}\n${source}`
|
|
106
126
|
: `# %% [${cell.cell_type}] cell:${index}`;
|
|
@@ -156,7 +176,7 @@ function parseNotebookEditableText(text: string, displayPath: string): ParsedVir
|
|
|
156
176
|
`Invalid notebook editable representation for ${displayPath}: expected first line to be "# %% [code] cell:0", "# %% [markdown] cell:0", or "# %% [raw] cell:0".`,
|
|
157
177
|
);
|
|
158
178
|
}
|
|
159
|
-
current.lines.push(line);
|
|
179
|
+
current.lines.push(unescapeMarkerLikeLine(line));
|
|
160
180
|
}
|
|
161
181
|
flush();
|
|
162
182
|
return cells;
|