berry-agent 0.1.0-alpha.1
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 +96 -0
- package/dist/.api-emit.stamp +1 -0
- package/dist/.build-meta.json +6 -0
- package/dist/agent/events.js +1 -0
- package/dist/agent/index.js +2 -0
- package/dist/agent/loop.js +139 -0
- package/dist/agent/queue.js +119 -0
- package/dist/agent/stream.js +62 -0
- package/dist/agent/tools-batch.js +221 -0
- package/dist/agent/types.js +8 -0
- package/dist/api/berry-agent-llm.d.ts +18 -0
- package/dist/api/berry-agent.d.ts +9 -0
- package/dist/api/surface.json +1632 -0
- package/dist/api/tsconfig.paths.json +12 -0
- package/dist/api/typebox-compile.d.ts +2 -0
- package/dist/api/typebox-value.d.ts +2 -0
- package/dist/api/typebox.d.ts +2 -0
- package/dist/browser/cdp.js +256 -0
- package/dist/browser/codes.js +31 -0
- package/dist/browser/discover.js +107 -0
- package/dist/browser/engine.js +169 -0
- package/dist/browser/index.js +17 -0
- package/dist/browser/install.js +342 -0
- package/dist/browser/page.js +321 -0
- package/dist/browser/service.js +158 -0
- package/dist/browser/tools.js +198 -0
- package/dist/browser/types.js +81 -0
- package/dist/channels/ask-queue.js +82 -0
- package/dist/channels/commands.js +129 -0
- package/dist/channels/engine/cell.js +143 -0
- package/dist/channels/engine/diff.js +143 -0
- package/dist/channels/engine/engine.js +416 -0
- package/dist/channels/engine/index.js +8 -0
- package/dist/channels/engine/input-keys.js +86 -0
- package/dist/channels/engine/input.js +596 -0
- package/dist/channels/engine/memory-io.js +71 -0
- package/dist/channels/engine/process-io.js +38 -0
- package/dist/channels/engine/types.js +17 -0
- package/dist/channels/engine/width.js +122 -0
- package/dist/channels/index.js +15 -0
- package/dist/channels/registry.js +56 -0
- package/dist/channels/sdk/admit.js +29 -0
- package/dist/channels/sdk/backend.js +102 -0
- package/dist/channels/sdk/cursor.js +56 -0
- package/dist/channels/sdk/index.js +20 -0
- package/dist/channels/sdk/jsonl.js +208 -0
- package/dist/channels/sdk/protocol.js +24 -0
- package/dist/channels/sdk/schema.js +91 -0
- package/dist/channels/sdk/wire-core.js +446 -0
- package/dist/channels/service.js +169 -0
- package/dist/channels/tui/autocomplete/autocomplete.js +42 -0
- package/dist/channels/tui/autocomplete/file-mentions.js +106 -0
- package/dist/channels/tui/autocomplete/popup.js +148 -0
- package/dist/channels/tui/autocomplete/provider.js +7 -0
- package/dist/channels/tui/autocomplete/token.js +68 -0
- package/dist/channels/tui/backend/ansi-rows.js +211 -0
- package/dist/channels/tui/backend/main-screen.js +207 -0
- package/dist/channels/tui/backend/osc.js +91 -0
- package/dist/channels/tui/backend/transcript.js +230 -0
- package/dist/channels/tui/backend/tui-backend.js +915 -0
- package/dist/channels/tui/editor/editor-model.js +543 -0
- package/dist/channels/tui/editor/editor-view.js +140 -0
- package/dist/channels/tui/editor/editor.js +275 -0
- package/dist/channels/tui/editor/undo-stack.js +35 -0
- package/dist/channels/tui/editor/visual-lines.js +126 -0
- package/dist/channels/tui/editor/word-nav.js +106 -0
- package/dist/channels/tui/history/history-viewer.js +403 -0
- package/dist/channels/tui/index.js +3 -0
- package/dist/channels/tui/layout.js +167 -0
- package/dist/channels/tui/markdown/blocks.js +111 -0
- package/dist/channels/tui/markdown/inline.js +75 -0
- package/dist/channels/tui/markdown/markdown.js +182 -0
- package/dist/channels/tui/memory/memory-viewer.js +540 -0
- package/dist/channels/tui/overlay/alt-screen.js +105 -0
- package/dist/channels/tui/overlay/overlay.js +57 -0
- package/dist/channels/tui/overlay/select-confirm.js +156 -0
- package/dist/channels/tui/panels/todo-panel.js +54 -0
- package/dist/channels/tui/panels/tool-progress-panel.js +93 -0
- package/dist/channels/tui/scroll/scroll-view.js +254 -0
- package/dist/channels/tui/status/status-line.js +76 -0
- package/dist/channels/tui/text.js +38 -0
- package/dist/channels/tui/theme.js +41 -0
- package/dist/channels/types.js +11 -0
- package/dist/channels/ui-core.js +209 -0
- package/dist/checkpoint/capture.js +45 -0
- package/dist/checkpoint/codes.js +33 -0
- package/dist/checkpoint/command.js +82 -0
- package/dist/checkpoint/gate.js +48 -0
- package/dist/checkpoint/index.js +20 -0
- package/dist/checkpoint/restore.js +172 -0
- package/dist/checkpoint/store.js +247 -0
- package/dist/checkpoint/types.js +4 -0
- package/dist/checkpoint/walk.js +146 -0
- package/dist/compaction/ccr-tools.js +112 -0
- package/dist/compaction/ccr.js +84 -0
- package/dist/compaction/codes.js +21 -0
- package/dist/compaction/index.js +18 -0
- package/dist/compaction/policy.js +200 -0
- package/dist/compaction/service.js +422 -0
- package/dist/compaction/slots.js +121 -0
- package/dist/compaction/types.js +10 -0
- package/dist/context/codes.js +40 -0
- package/dist/context/events.js +160 -0
- package/dist/context/index.js +13 -0
- package/dist/context/logger.js +136 -0
- package/dist/context/scope.js +182 -0
- package/dist/context/workspace.js +104 -0
- package/dist/contracts/agent-events.d.ts +73 -0
- package/dist/contracts/agent-events.js +16 -0
- package/dist/contracts/api.d.ts +249 -0
- package/dist/contracts/api.js +299 -0
- package/dist/contracts/approval.d.ts +37 -0
- package/dist/contracts/approval.js +9 -0
- package/dist/contracts/env-ref.d.ts +41 -0
- package/dist/contracts/env-ref.js +32 -0
- package/dist/contracts/errors.d.ts +56 -0
- package/dist/contracts/errors.js +266 -0
- package/dist/contracts/events.d.ts +70 -0
- package/dist/contracts/events.js +329 -0
- package/dist/contracts/index.d.ts +32 -0
- package/dist/contracts/index.js +26 -0
- package/dist/contracts/llm.d.ts +275 -0
- package/dist/contracts/llm.js +12 -0
- package/dist/contracts/messages.d.ts +63 -0
- package/dist/contracts/messages.js +65 -0
- package/dist/contracts/redact.d.ts +46 -0
- package/dist/contracts/redact.js +223 -0
- package/dist/contracts/tools.d.ts +197 -0
- package/dist/contracts/tools.js +33 -0
- package/dist/contracts/types.d.ts +302 -0
- package/dist/contracts/types.js +50 -0
- package/dist/contracts/ui.d.ts +122 -0
- package/dist/contracts/ui.js +15 -0
- package/dist/conversation/agent-service.js +61 -0
- package/dist/conversation/approval-wiring.js +82 -0
- package/dist/conversation/backoff.js +38 -0
- package/dist/conversation/codes.js +47 -0
- package/dist/conversation/control-tools.js +102 -0
- package/dist/conversation/control.js +210 -0
- package/dist/conversation/driver.js +922 -0
- package/dist/conversation/index.js +12 -0
- package/dist/conversation/model-visible.js +169 -0
- package/dist/conversation/open-tools.js +126 -0
- package/dist/conversation/reseed.js +132 -0
- package/dist/conversation/sessions.js +215 -0
- package/dist/conversation/todo.js +203 -0
- package/dist/conversation/types.js +35 -0
- package/dist/conversation/wiring.js +204 -0
- package/dist/credentials/codes.js +59 -0
- package/dist/credentials/commands.js +186 -0
- package/dist/credentials/env-ref.js +34 -0
- package/dist/credentials/index.js +28 -0
- package/dist/credentials/migration.js +18 -0
- package/dist/credentials/oauth.js +266 -0
- package/dist/credentials/refresh.js +152 -0
- package/dist/credentials/secrets.js +118 -0
- package/dist/credentials/types.js +30 -0
- package/dist/exec/bash.js +279 -0
- package/dist/exec/codes.js +37 -0
- package/dist/exec/env.js +99 -0
- package/dist/exec/environment.js +39 -0
- package/dist/exec/git-guard.js +623 -0
- package/dist/exec/index.js +24 -0
- package/dist/exec/registry.js +120 -0
- package/dist/exec/spawn.js +281 -0
- package/dist/exec/tail.js +77 -0
- package/dist/exec/types.js +5 -0
- package/dist/goal/codes.js +33 -0
- package/dist/goal/command.js +175 -0
- package/dist/goal/fold.js +121 -0
- package/dist/goal/gates.js +122 -0
- package/dist/goal/index.js +22 -0
- package/dist/goal/migration.js +48 -0
- package/dist/goal/service.js +501 -0
- package/dist/goal/todo-tool.js +174 -0
- package/dist/goal/types.js +1 -0
- package/dist/goal/update-tool.js +46 -0
- package/dist/host/approval-cmd.js +276 -0
- package/dist/host/assembly.js +1146 -0
- package/dist/host/boot-failures.js +104 -0
- package/dist/host/budget-advisory.js +74 -0
- package/dist/host/budget-broadcast.js +50 -0
- package/dist/host/builtins.js +54 -0
- package/dist/host/cli.js +544 -0
- package/dist/host/codes.js +120 -0
- package/dist/host/config-schema.js +195 -0
- package/dist/host/conversation-stack.js +654 -0
- package/dist/host/core-plugins.js +1685 -0
- package/dist/host/credentials-cmd.js +42 -0
- package/dist/host/disclosure.js +30 -0
- package/dist/host/dispatch.js +105 -0
- package/dist/host/doors-cmd.js +154 -0
- package/dist/host/dump-config.js +44 -0
- package/dist/host/hook-dispatch-guard.js +27 -0
- package/dist/host/import-gate.js +153 -0
- package/dist/host/index.js +54 -0
- package/dist/host/issue-session.js +241 -0
- package/dist/host/loader.js +372 -0
- package/dist/host/main.js +161 -0
- package/dist/host/manifest.js +381 -0
- package/dist/host/mcp-entry.js +53 -0
- package/dist/host/plugin-boot.js +952 -0
- package/dist/host/plugin-context.js +672 -0
- package/dist/host/plugin-install.js +480 -0
- package/dist/host/plugin-reload.js +124 -0
- package/dist/host/plugin-store.js +462 -0
- package/dist/host/plugin-tools.js +359 -0
- package/dist/host/plugin-uninstall.js +257 -0
- package/dist/host/plugins-cmd.js +302 -0
- package/dist/host/plugins-command.js +130 -0
- package/dist/host/plugins-config.js +195 -0
- package/dist/host/prompt-sections.js +133 -0
- package/dist/host/run-entry.js +544 -0
- package/dist/host/runtime.js +209 -0
- package/dist/host/scheduler-tick.js +294 -0
- package/dist/host/serve-daemon.js +501 -0
- package/dist/host/serve-entry.js +279 -0
- package/dist/host/session-anchor.js +41 -0
- package/dist/host/sessions-cmd.js +219 -0
- package/dist/host/sessions-face.js +47 -0
- package/dist/host/settings-store.js +112 -0
- package/dist/host/signals.js +69 -0
- package/dist/host/single-instance.js +105 -0
- package/dist/host/subagent-factory.js +291 -0
- package/dist/host/testkit/harness.js +283 -0
- package/dist/host/testkit/index.js +14 -0
- package/dist/host/testkit/matrix.js +302 -0
- package/dist/host/tool-policy-store.js +177 -0
- package/dist/host/triggers.js +212 -0
- package/dist/host/tui-entry.js +216 -0
- package/dist/host/webui-bridge.js +165 -0
- package/dist/issue/codes.js +38 -0
- package/dist/issue/filter.js +146 -0
- package/dist/issue/github.js +116 -0
- package/dist/issue/index.js +17 -0
- package/dist/issue/mount.js +70 -0
- package/dist/issue/poll.js +58 -0
- package/dist/issue/service.js +341 -0
- package/dist/issue/tools.js +70 -0
- package/dist/issue/types.js +34 -0
- package/dist/issue/webhook.js +126 -0
- package/dist/llm/codes.js +77 -0
- package/dist/llm/complete.js +196 -0
- package/dist/llm/events.js +15 -0
- package/dist/llm/index.js +24 -0
- package/dist/llm/inflight.js +65 -0
- package/dist/llm/model-id.js +58 -0
- package/dist/llm/provider-face.d.ts +32 -0
- package/dist/llm/provider-face.js +34 -0
- package/dist/llm/recovery.js +146 -0
- package/dist/llm/runtime.js +46 -0
- package/dist/llm/stream-fn.js +147 -0
- package/dist/lsp/codes.js +31 -0
- package/dist/lsp/connection.js +147 -0
- package/dist/lsp/frame.js +110 -0
- package/dist/lsp/index.js +16 -0
- package/dist/lsp/inject.js +58 -0
- package/dist/lsp/instance.js +264 -0
- package/dist/lsp/service.js +522 -0
- package/dist/lsp/tools.js +53 -0
- package/dist/lsp/types.js +201 -0
- package/dist/mcp/bridge.js +222 -0
- package/dist/mcp/codes.js +21 -0
- package/dist/mcp/index.js +19 -0
- package/dist/mcp/jsonrpc.js +230 -0
- package/dist/mcp/service.js +125 -0
- package/dist/mcp/tools.js +145 -0
- package/dist/mcp/types.js +108 -0
- package/dist/memory/cite.js +131 -0
- package/dist/memory/codes.js +50 -0
- package/dist/memory/command.js +71 -0
- package/dist/memory/consolidate.js +219 -0
- package/dist/memory/cycle.js +109 -0
- package/dist/memory/dao.js +878 -0
- package/dist/memory/diff.js +238 -0
- package/dist/memory/extract.js +181 -0
- package/dist/memory/fts.js +58 -0
- package/dist/memory/index.js +42 -0
- package/dist/memory/inject.js +354 -0
- package/dist/memory/merge.js +178 -0
- package/dist/memory/migration.js +132 -0
- package/dist/memory/pollution.js +59 -0
- package/dist/memory/port.js +385 -0
- package/dist/memory/review.js +201 -0
- package/dist/memory/scan.js +89 -0
- package/dist/memory/tools.js +508 -0
- package/dist/memory/types.js +140 -0
- package/dist/obs/codes.js +38 -0
- package/dist/obs/db.js +115 -0
- package/dist/obs/index.js +20 -0
- package/dist/obs/rollup.js +61 -0
- package/dist/obs/service.js +363 -0
- package/dist/obs/session-tools.js +282 -0
- package/dist/obs/session-view.js +225 -0
- package/dist/obs/tool.js +97 -0
- package/dist/obs/types.js +1 -0
- package/dist/persist/audit.js +63 -0
- package/dist/persist/aux.js +39 -0
- package/dist/persist/codes.js +35 -0
- package/dist/persist/index.js +30 -0
- package/dist/persist/load-history.js +50 -0
- package/dist/persist/migrations.js +37 -0
- package/dist/persist/paths.js +108 -0
- package/dist/persist/persistence.js +229 -0
- package/dist/persist/schema.js +99 -0
- package/dist/persist/secret-box.js +95 -0
- package/dist/persist/store.js +706 -0
- package/dist/persist/write-behind.js +238 -0
- package/dist/safety/approval.js +171 -0
- package/dist/safety/bwrap.js +123 -0
- package/dist/safety/codes.js +57 -0
- package/dist/safety/danger.js +505 -0
- package/dist/safety/gate.js +226 -0
- package/dist/safety/index.js +24 -0
- package/dist/safety/presets.js +73 -0
- package/dist/safety/roots.js +139 -0
- package/dist/safety/sandbox.js +214 -0
- package/dist/safety/seatbelt.js +88 -0
- package/dist/safety/sensitive.js +34 -0
- package/dist/safety/tool-policy.js +203 -0
- package/dist/safety/types.js +13 -0
- package/dist/scheduler/codes.js +54 -0
- package/dist/scheduler/cron-backend.js +135 -0
- package/dist/scheduler/engine.js +280 -0
- package/dist/scheduler/gates.js +60 -0
- package/dist/scheduler/index.js +25 -0
- package/dist/scheduler/migration.js +22 -0
- package/dist/scheduler/runner.js +171 -0
- package/dist/scheduler/schedule.js +193 -0
- package/dist/scheduler/service.js +283 -0
- package/dist/scheduler/tick.js +139 -0
- package/dist/scheduler/types.js +1 -0
- package/dist/sdk/http.js +865 -0
- package/dist/sdk/index.js +22 -0
- package/dist/sdk/mcp.js +289 -0
- package/dist/sdk/plugin-route-registry.js +126 -0
- package/dist/sdk/plugin-routes.js +149 -0
- package/dist/sdk/security.js +116 -0
- package/dist/sdk/types.js +32 -0
- package/dist/session/budget.js +159 -0
- package/dist/session/codes.js +40 -0
- package/dist/session/derive.js +180 -0
- package/dist/session/event-data.js +1 -0
- package/dist/session/fork.js +72 -0
- package/dist/session/import-gates.js +148 -0
- package/dist/session/index.js +18 -0
- package/dist/session/recover.js +103 -0
- package/dist/session/session.js +279 -0
- package/dist/session/snapshot.js +107 -0
- package/dist/skills/agents.js +242 -0
- package/dist/skills/codes.js +51 -0
- package/dist/skills/discovery.js +266 -0
- package/dist/skills/frontmatter.js +215 -0
- package/dist/skills/index.js +28 -0
- package/dist/skills/load.js +118 -0
- package/dist/skills/manage.js +201 -0
- package/dist/skills/registry.js +138 -0
- package/dist/skills/render.js +124 -0
- package/dist/skills/sections.js +171 -0
- package/dist/skills/types.js +21 -0
- package/dist/subagent/codes.js +50 -0
- package/dist/subagent/declarative.js +55 -0
- package/dist/subagent/index.js +19 -0
- package/dist/subagent/notify.js +51 -0
- package/dist/subagent/provide.js +7 -0
- package/dist/subagent/registry.js +149 -0
- package/dist/subagent/service.js +338 -0
- package/dist/subagent/surface.js +44 -0
- package/dist/subagent/tool.js +153 -0
- package/dist/subagent/types.js +25 -0
- package/dist/tools/apply-patch.js +164 -0
- package/dist/tools/codes.js +109 -0
- package/dist/tools/fs.js +501 -0
- package/dist/tools/index.js +17 -0
- package/dist/tools/observed.js +99 -0
- package/dist/tools/pipeline.js +176 -0
- package/dist/tools/protected-read.js +54 -0
- package/dist/tools/registry.js +250 -0
- package/dist/tools/search.js +451 -0
- package/dist/tools/worktree.js +327 -0
- package/dist/web/codes.js +37 -0
- package/dist/web/gate.js +43 -0
- package/dist/web/hygiene.js +195 -0
- package/dist/web/index.js +16 -0
- package/dist/web/service.js +207 -0
- package/dist/web/ssrf-guard.js +44 -0
- package/dist/web/tool.js +76 -0
- package/dist/web/types.js +10 -0
- package/dist/webui/assets/index-B-t2O8gh.js +22 -0
- package/dist/webui/assets/index-DOlgSZ9Q.css +2 -0
- package/dist/webui/index.html +14 -0
- package/dist/webui/index.js +14 -0
- package/dist/webui/server.js +549 -0
- package/dist/webui/types.js +38 -0
- package/examples/README.md +26 -0
- package/examples/minimal-code-plugin/entry.js +43 -0
- package/examples/minimal-code-plugin/package.json +12 -0
- package/examples/pure-skill-pack/package.json +14 -0
- package/examples/pure-skill-pack/skills/markdown-table/SKILL.md +32 -0
- package/package.json +86 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { EditorModel } from './editor-model.js';
|
|
2
|
+
import { EditorView } from './editor-view.js';
|
|
3
|
+
/* ---------------- 键位表(pi 平移——见类注差异注记) ---------------- */
|
|
4
|
+
const SUBMIT = [{ key: 'enter' }];
|
|
5
|
+
const NEW_LINE = [
|
|
6
|
+
{ key: 'enter', shift: true },
|
|
7
|
+
{ key: 'j', ctrl: true },
|
|
8
|
+
];
|
|
9
|
+
const UNDO = [
|
|
10
|
+
{ key: '-', ctrl: true }, // kitty 轨规范形
|
|
11
|
+
{ key: '_', ctrl: true }, // legacy 轨 0x1f 解码形
|
|
12
|
+
];
|
|
13
|
+
const MOVE_LEFT = [{ key: 'left' }, { key: 'b', ctrl: true }];
|
|
14
|
+
const MOVE_RIGHT = [{ key: 'right' }, { key: 'f', ctrl: true }];
|
|
15
|
+
const MOVE_WORD_LEFT = [
|
|
16
|
+
{ key: 'left', alt: true },
|
|
17
|
+
{ key: 'left', ctrl: true },
|
|
18
|
+
{ key: 'b', alt: true },
|
|
19
|
+
];
|
|
20
|
+
const MOVE_WORD_RIGHT = [
|
|
21
|
+
{ key: 'right', alt: true },
|
|
22
|
+
{ key: 'right', ctrl: true },
|
|
23
|
+
{ key: 'f', alt: true },
|
|
24
|
+
];
|
|
25
|
+
const LINE_START = [{ key: 'home' }, { key: 'a', ctrl: true }];
|
|
26
|
+
const LINE_END = [{ key: 'end' }, { key: 'e', ctrl: true }];
|
|
27
|
+
const JUMP_FORWARD = [{ key: ']', ctrl: true }];
|
|
28
|
+
const JUMP_BACKWARD = [{ key: ']', ctrl: true, alt: true }];
|
|
29
|
+
const PAGE_UP = [{ key: 'pageup' }];
|
|
30
|
+
const PAGE_DOWN = [{ key: 'pagedown' }];
|
|
31
|
+
const DELETE_BACKWARD = [{ key: 'backspace' }];
|
|
32
|
+
const DELETE_FORWARD = [{ key: 'delete' }, { key: 'd', ctrl: true }];
|
|
33
|
+
const DELETE_WORD_BACKWARD = [
|
|
34
|
+
{ key: 'w', ctrl: true },
|
|
35
|
+
{ key: 'backspace', alt: true },
|
|
36
|
+
];
|
|
37
|
+
const DELETE_WORD_FORWARD = [
|
|
38
|
+
{ key: 'd', alt: true },
|
|
39
|
+
{ key: 'delete', alt: true },
|
|
40
|
+
];
|
|
41
|
+
const DELETE_TO_LINE_START = [{ key: 'u', ctrl: true }];
|
|
42
|
+
const DELETE_TO_LINE_END = [{ key: 'k', ctrl: true }];
|
|
43
|
+
const HISTORY_PREV = [{ key: 'up' }];
|
|
44
|
+
const HISTORY_NEXT = [{ key: 'down' }];
|
|
45
|
+
/** 键位匹配(单字符键名大小写不敏感归一——shift 产大写形) */
|
|
46
|
+
function matches(e, b) {
|
|
47
|
+
if (e.meta)
|
|
48
|
+
return false;
|
|
49
|
+
if (e.ctrl !== !!b.ctrl || e.alt !== !!b.alt || e.shift !== !!b.shift)
|
|
50
|
+
return false;
|
|
51
|
+
if (e.key === b.key)
|
|
52
|
+
return true;
|
|
53
|
+
return e.key.length === 1 && b.key.length === 1 && e.key.toLowerCase() === b.key;
|
|
54
|
+
}
|
|
55
|
+
function matchesAny(e, bindings) {
|
|
56
|
+
return bindings.some((b) => matches(e, b));
|
|
57
|
+
}
|
|
58
|
+
/** 可打印字符键判(jump 待靶态的靶字符判据——无修饰或仅 shift) */
|
|
59
|
+
function isPlainCharKey(e) {
|
|
60
|
+
return e.key.length === 1 && !e.ctrl && !e.alt && !e.meta;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 多行输入组件:状态归模型、呈现归视图、本件只做事件分发与装配面。
|
|
64
|
+
*/
|
|
65
|
+
export class Editor {
|
|
66
|
+
model;
|
|
67
|
+
view;
|
|
68
|
+
/** jump 待靶方向(null = 常态) */
|
|
69
|
+
jumpPending = null;
|
|
70
|
+
onSubmit;
|
|
71
|
+
pageSize;
|
|
72
|
+
constructor(options = {}) {
|
|
73
|
+
this.onSubmit = options.onSubmit;
|
|
74
|
+
this.pageSize = Math.max(1, options.maxVisibleLines ?? 8);
|
|
75
|
+
this.model = new EditorModel();
|
|
76
|
+
this.model.onChange = (text) => options.onChange?.(text);
|
|
77
|
+
this.view = new EditorView(this.model, { maxVisibleLines: options.maxVisibleLines });
|
|
78
|
+
}
|
|
79
|
+
/* ---------------- 装配面便捷委托 ---------------- */
|
|
80
|
+
/** 聚焦态切换(事件路由器裁决) */
|
|
81
|
+
setFocused(focused) {
|
|
82
|
+
this.view.setFocused(focused);
|
|
83
|
+
}
|
|
84
|
+
getText() {
|
|
85
|
+
return this.model.getText();
|
|
86
|
+
}
|
|
87
|
+
setText(text) {
|
|
88
|
+
this.model.setText(text);
|
|
89
|
+
}
|
|
90
|
+
/* ---------------- 渲染委托(两段协商直通视图) ---------------- */
|
|
91
|
+
measure(width) {
|
|
92
|
+
return this.view.measure(width);
|
|
93
|
+
}
|
|
94
|
+
render(buffer, region) {
|
|
95
|
+
this.view.render(buffer, region);
|
|
96
|
+
}
|
|
97
|
+
/* ---------------- 输入事件分发 ---------------- */
|
|
98
|
+
/** 四路分发:返回是否消费(false = 归上层,如 ctrl+c 的 abort 路) */
|
|
99
|
+
handleEvent(event) {
|
|
100
|
+
switch (event.kind) {
|
|
101
|
+
case 'key':
|
|
102
|
+
return this.handleKey(event);
|
|
103
|
+
case 'text': {
|
|
104
|
+
// jump 待靶态:首字符为跳转靶(kitty / legacy 可打印均走 text 事件)
|
|
105
|
+
if (this.jumpPending !== null && event.text.length > 0) {
|
|
106
|
+
this.model.jumpToChar([...event.text][0], this.jumpPending);
|
|
107
|
+
this.jumpPending = null;
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
this.model.insertText(event.text);
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
case 'ime':
|
|
114
|
+
if (event.committed) {
|
|
115
|
+
this.model.setPreedit(null);
|
|
116
|
+
this.model.insertText(event.text);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
this.model.setPreedit(event.text);
|
|
120
|
+
}
|
|
121
|
+
return true;
|
|
122
|
+
case 'paste':
|
|
123
|
+
// 整段入正文(规范条款——无标记机制,多行粘贴直接呈现)
|
|
124
|
+
this.jumpPending = null;
|
|
125
|
+
this.model.insertText(event.text);
|
|
126
|
+
return true;
|
|
127
|
+
case 'mouse':
|
|
128
|
+
return false; // 主屏零鼠标(07 屏模型注)——到达即吞不消费(副屏选区路不经本件)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/** 键事件路(release 相不动作;ctrl+c 显式透传) */
|
|
132
|
+
handleKey(e) {
|
|
133
|
+
if (e.phase === 'release')
|
|
134
|
+
return false; // 释放相归上层(不消费)
|
|
135
|
+
// jump 待靶态:escape 取消、可打印字符为靶(key 路防御位——text 路为主)
|
|
136
|
+
if (this.jumpPending !== null) {
|
|
137
|
+
if (matchesAny(e, [{ key: 'escape' }])) {
|
|
138
|
+
this.jumpPending = null;
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
if (isPlainCharKey(e)) {
|
|
142
|
+
this.model.jumpToChar(e.key, this.jumpPending);
|
|
143
|
+
this.jumpPending = null;
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (e.ctrl && !e.alt && !e.shift && !e.meta && e.key === 'c')
|
|
148
|
+
return false; // ctrl+c 透传上层 abort
|
|
149
|
+
if (matchesAny(e, SUBMIT))
|
|
150
|
+
return this.handleSubmit();
|
|
151
|
+
if (matchesAny(e, NEW_LINE)) {
|
|
152
|
+
this.jumpPending = null;
|
|
153
|
+
this.model.addNewLine();
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
if (matchesAny(e, UNDO)) {
|
|
157
|
+
this.jumpPending = null;
|
|
158
|
+
this.model.undo();
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
if (matchesAny(e, MOVE_LEFT)) {
|
|
162
|
+
this.jumpPending = null;
|
|
163
|
+
this.model.moveLeft();
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
if (matchesAny(e, MOVE_RIGHT)) {
|
|
167
|
+
this.jumpPending = null;
|
|
168
|
+
this.model.moveRight();
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
if (matchesAny(e, MOVE_WORD_LEFT)) {
|
|
172
|
+
this.jumpPending = null;
|
|
173
|
+
this.model.moveWordBackward();
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
if (matchesAny(e, MOVE_WORD_RIGHT)) {
|
|
177
|
+
this.jumpPending = null;
|
|
178
|
+
this.model.moveWordForward();
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
if (matchesAny(e, LINE_START)) {
|
|
182
|
+
this.jumpPending = null;
|
|
183
|
+
this.model.moveHome();
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
if (matchesAny(e, LINE_END)) {
|
|
187
|
+
this.jumpPending = null;
|
|
188
|
+
this.model.moveEnd();
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
if (matchesAny(e, JUMP_FORWARD)) {
|
|
192
|
+
this.jumpPending = 'forward';
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
if (matchesAny(e, JUMP_BACKWARD)) {
|
|
196
|
+
this.jumpPending = 'backward';
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
if (matchesAny(e, PAGE_UP)) {
|
|
200
|
+
this.jumpPending = null;
|
|
201
|
+
this.model.pageUp(this.pageSize);
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
if (matchesAny(e, PAGE_DOWN)) {
|
|
205
|
+
this.jumpPending = null;
|
|
206
|
+
this.model.pageDown(this.pageSize);
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
if (matchesAny(e, DELETE_BACKWARD)) {
|
|
210
|
+
this.jumpPending = null;
|
|
211
|
+
this.model.backspace();
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
if (matchesAny(e, DELETE_FORWARD)) {
|
|
215
|
+
this.jumpPending = null;
|
|
216
|
+
this.model.deleteForward();
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
if (matchesAny(e, DELETE_WORD_BACKWARD)) {
|
|
220
|
+
this.jumpPending = null;
|
|
221
|
+
this.model.deleteWordBackward();
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
if (matchesAny(e, DELETE_WORD_FORWARD)) {
|
|
225
|
+
this.jumpPending = null;
|
|
226
|
+
this.model.deleteWordForward();
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
if (matchesAny(e, DELETE_TO_LINE_START)) {
|
|
230
|
+
this.jumpPending = null;
|
|
231
|
+
this.model.deleteToLineStart();
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
if (matchesAny(e, DELETE_TO_LINE_END)) {
|
|
235
|
+
this.jumpPending = null;
|
|
236
|
+
this.model.deleteToLineEnd();
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
if (matchesAny(e, HISTORY_PREV)) {
|
|
240
|
+
this.jumpPending = null;
|
|
241
|
+
// 首视觉行 +(空框 / 已在浏览态 / 逻辑行首)→ 历史回溯;否则视觉行上移
|
|
242
|
+
const cursor = this.model.getCursor();
|
|
243
|
+
if (this.model.isOnFirstVisualLine() &&
|
|
244
|
+
(this.model.isEmpty() || this.model.isBrowsingHistory() || cursor.col === 0)) {
|
|
245
|
+
this.model.navigateHistory(-1);
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
this.model.moveUp();
|
|
249
|
+
}
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
if (matchesAny(e, HISTORY_NEXT)) {
|
|
253
|
+
this.jumpPending = null;
|
|
254
|
+
// 末视觉行 → 历史回新(含回草稿);否则视觉行下移
|
|
255
|
+
if (this.model.isOnLastVisualLine()) {
|
|
256
|
+
this.model.navigateHistory(1);
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
this.model.moveDown();
|
|
260
|
+
}
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
return false; // 未绑定键归上层(escape / f 键 / tab…)
|
|
264
|
+
}
|
|
265
|
+
/** 提交:模型全清取文、非空才入册 + 回调 */
|
|
266
|
+
handleSubmit() {
|
|
267
|
+
this.jumpPending = null;
|
|
268
|
+
const text = this.model.submit();
|
|
269
|
+
if (text !== '') {
|
|
270
|
+
this.model.addToHistory(text);
|
|
271
|
+
this.onSubmit?.(text);
|
|
272
|
+
}
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 泛型 undo 栈(多行输入件 Editor 件族基件——07 §4.1 引擎节件 6(组件与呈现装配件)
|
|
3
|
+
* 「undo 上限收 v1」条款的载体)。
|
|
4
|
+
*
|
|
5
|
+
* clone-on-push:入栈即深拷贝快照(structuredClone),弹出直付(已脱钩
|
|
6
|
+
* 无需再拷);上限超量丢最旧(上限值码面定 100——与输入历史帽同值,
|
|
7
|
+
* 规范只钉「有上限」不收数字)。
|
|
8
|
+
*/
|
|
9
|
+
/** 快照类型约束:可 structuredClone 的纯数据形 */
|
|
10
|
+
// (泛型约束不写死——structuredClone 接受任意可克隆值,调用面以类型保证)
|
|
11
|
+
/** undo 栈:深拷贝快照栈 + 上帽丢最旧 */
|
|
12
|
+
export class UndoStack {
|
|
13
|
+
limit;
|
|
14
|
+
stack = [];
|
|
15
|
+
constructor(limit = 100) {
|
|
16
|
+
this.limit = limit;
|
|
17
|
+
}
|
|
18
|
+
/** 深拷贝入栈(超帽丢最旧——最近编辑永远可撤) */
|
|
19
|
+
push(state) {
|
|
20
|
+
this.stack.push(structuredClone(state));
|
|
21
|
+
if (this.stack.length > this.limit)
|
|
22
|
+
this.stack.shift();
|
|
23
|
+
}
|
|
24
|
+
/** 弹出最近快照(空栈 undefined) */
|
|
25
|
+
pop() {
|
|
26
|
+
return this.stack.pop();
|
|
27
|
+
}
|
|
28
|
+
/** 清空(提交后弃撤回域) */
|
|
29
|
+
clear() {
|
|
30
|
+
this.stack.length = 0;
|
|
31
|
+
}
|
|
32
|
+
get length() {
|
|
33
|
+
return this.stack.length;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 视觉行映射纯函数(多行输入件 Editor 件族基件):逻辑行 → 折行后视觉行
|
|
3
|
+
* 的映射单源(垂直光标移动 / 视口滚动 / 渲染三消费方共用)。
|
|
4
|
+
*
|
|
5
|
+
* 本仓化关键差异(07 引擎节件 6(组件与呈现装配件)「字素光标算术——CJK 双宽对齐」条款):
|
|
6
|
+
* - 折行按**显示列宽**(字素 graphemeWidth 累计、整字下移不产半字——与引擎
|
|
7
|
+
* width 件 wrapText 同规则),段记录 UTF-16 下标面(startCol/length 供
|
|
8
|
+
* 字符串切片)+ 显示宽面(width 供视觉列算术)双坐标;
|
|
9
|
+
* - 垂直移动的视觉列 = **显示列**(光标前字素列宽和),非 UTF-16 码元差
|
|
10
|
+
* (pi 以码元差近似,CJK 双宽下 sticky 列漂移——本仓按规范双宽对齐)。
|
|
11
|
+
*/
|
|
12
|
+
import { graphemeWidth, splitGraphemes } from '../../engine/index.js';
|
|
13
|
+
/**
|
|
14
|
+
* 单逻辑行按显示宽硬折(字素累宽超限整字下移——空行占一段零宽)。
|
|
15
|
+
* 返回段的 UTF-16 与显示宽双坐标。
|
|
16
|
+
*/
|
|
17
|
+
function foldLine(line, lineNo, width) {
|
|
18
|
+
if (width <= 0)
|
|
19
|
+
return [{ line: lineNo, startCol: 0, length: line.length, width: 0 }];
|
|
20
|
+
if (line === '')
|
|
21
|
+
return [{ line: lineNo, startCol: 0, length: 0, width: 0 }];
|
|
22
|
+
const segments = [];
|
|
23
|
+
let start = 0; // 当前段 UTF-16 起点
|
|
24
|
+
let usedCols = 0; // 当前段已占显示宽
|
|
25
|
+
let segLen = 0; // 当前段 UTF-16 长
|
|
26
|
+
for (const g of splitGraphemes(line)) {
|
|
27
|
+
const w = graphemeWidth(g);
|
|
28
|
+
if (usedCols + w > width) {
|
|
29
|
+
segments.push({ line: lineNo, startCol: start, length: segLen, width: usedCols });
|
|
30
|
+
start += segLen;
|
|
31
|
+
segLen = g.length;
|
|
32
|
+
usedCols = w;
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
segLen += g.length;
|
|
36
|
+
usedCols += w;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
segments.push({ line: lineNo, startCol: start, length: segLen, width: usedCols });
|
|
40
|
+
return segments;
|
|
41
|
+
}
|
|
42
|
+
/** 全文档视觉行映射(逻辑行逐行硬折串接) */
|
|
43
|
+
export function buildVisualLineMap(lines, width) {
|
|
44
|
+
const out = [];
|
|
45
|
+
for (let i = 0; i < lines.length; i++)
|
|
46
|
+
out.push(...foldLine(lines[i] ?? '', i, width));
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* 定位逻辑位置所在视觉行下标(col 为 UTF-16)。
|
|
51
|
+
* 逻辑行末段的末位(col = 行长)属末段——光标在行尾不落「下一段开头」。
|
|
52
|
+
*/
|
|
53
|
+
export function findVisualLineAt(map, line, col) {
|
|
54
|
+
for (let i = 0; i < map.length; i++) {
|
|
55
|
+
const vl = map[i];
|
|
56
|
+
if (vl.line !== line)
|
|
57
|
+
continue;
|
|
58
|
+
const offset = col - vl.startCol;
|
|
59
|
+
const isLastOfLine = i === map.length - 1 || map[i + 1].line !== vl.line;
|
|
60
|
+
if (offset >= 0 && (offset < vl.length || (isLastOfLine && offset === vl.length)))
|
|
61
|
+
return i;
|
|
62
|
+
}
|
|
63
|
+
return map.length - 1;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* 段内显示列反查 UTF-16 下标:从段起点起累计字素宽,到目标显示列 targetCols
|
|
67
|
+
* 的字素边界(**光标永不落半字**——目标列落在宽字素中间时吸到其首列前,
|
|
68
|
+
* 返回该字素起点的 UTF-16 下标)。targetCols 超段宽返回段尾下标。
|
|
69
|
+
*/
|
|
70
|
+
export function colAtDisplayColumn(line, startCol, targetCols) {
|
|
71
|
+
let used = 0;
|
|
72
|
+
let idx = startCol;
|
|
73
|
+
for (const g of splitGraphemes(line.slice(startCol))) {
|
|
74
|
+
const w = graphemeWidth(g);
|
|
75
|
+
if (used + w > targetCols)
|
|
76
|
+
return idx; // 目标列落在本字素内——吸到首列前(半字防线)
|
|
77
|
+
used += w;
|
|
78
|
+
idx += g.length;
|
|
79
|
+
}
|
|
80
|
+
return idx;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* 位置前缀显示宽:line 内 [0, col) 前缀的字素列宽和(光标视觉列算术的
|
|
84
|
+
* 单源——渲染光标定位 / sticky 列共用)。
|
|
85
|
+
*/
|
|
86
|
+
export function prefixDisplayWidth(line, col) {
|
|
87
|
+
let used = 0;
|
|
88
|
+
let idx = 0;
|
|
89
|
+
for (const g of splitGraphemes(line)) {
|
|
90
|
+
if (idx >= col)
|
|
91
|
+
break;
|
|
92
|
+
used += graphemeWidth(g);
|
|
93
|
+
idx += g.length;
|
|
94
|
+
}
|
|
95
|
+
return used;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* 光标前一个字素的边界(UTF-16):backspace / moveLeft 的字素算术。
|
|
99
|
+
* col 已在行首返回 0。
|
|
100
|
+
*/
|
|
101
|
+
export function prevGraphemeBoundary(line, col) {
|
|
102
|
+
if (col <= 0)
|
|
103
|
+
return 0;
|
|
104
|
+
let idx = 0;
|
|
105
|
+
let prev = 0;
|
|
106
|
+
for (const g of splitGraphemes(line)) {
|
|
107
|
+
if (idx >= col)
|
|
108
|
+
return prev;
|
|
109
|
+
prev = idx;
|
|
110
|
+
idx += g.length;
|
|
111
|
+
}
|
|
112
|
+
return prev;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* 光标后一个字素的终点(UTF-16):deleteForward / moveRight 的字素算术。
|
|
116
|
+
* col 已在行尾返回 line.length。
|
|
117
|
+
*/
|
|
118
|
+
export function nextGraphemeBoundary(line, col) {
|
|
119
|
+
let idx = 0;
|
|
120
|
+
for (const g of splitGraphemes(line)) {
|
|
121
|
+
if (idx >= col)
|
|
122
|
+
return idx + g.length;
|
|
123
|
+
idx += g.length;
|
|
124
|
+
}
|
|
125
|
+
return line.length;
|
|
126
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 词级导航纯函数(多行输入件 Editor 件族基件):findWordBackward /
|
|
3
|
+
* findWordForward——词级删除与词向移动共用的边界判定单源。
|
|
4
|
+
*
|
|
5
|
+
* 机制语义承 pi word-navigation.ts(裁 ⓪ 从零重写):
|
|
6
|
+
* - Intl.Segmenter **word 粒度**切词(与引擎 width 件的 grapheme 粒度不同
|
|
7
|
+
* 面——此处自持单例);
|
|
8
|
+
* - isWordLike 三分支:词内含 ASCII 标点时切到标点边界(foo.bar 一步到
|
|
9
|
+
* '.' 前)、非词非空白的标点 run 整段跳、空白跳过;
|
|
10
|
+
* - 纯函数零状态——调用方(编辑模型)自持光标。
|
|
11
|
+
*/
|
|
12
|
+
const wordSegmenter = new Intl.Segmenter(undefined, { granularity: 'word' });
|
|
13
|
+
/** ASCII 标点判(词内边界细化用——POSIX 图形字符中的非字母数字段) */
|
|
14
|
+
function isPunctuation(ch) {
|
|
15
|
+
const c = ch.charCodeAt(0);
|
|
16
|
+
return (c >= 0x21 && c <= 0x2f) || (c >= 0x3a && c <= 0x40) || (c >= 0x5b && c <= 0x60) || (c >= 0x7b && c <= 0x7e);
|
|
17
|
+
}
|
|
18
|
+
/** 词粒度分段(字素串 + isWordLike 标记) */
|
|
19
|
+
function segmentWords(text) {
|
|
20
|
+
const out = [];
|
|
21
|
+
for (const seg of wordSegmenter.segment(text)) {
|
|
22
|
+
out.push({ segment: seg.segment, isWordLike: seg.isWordLike === true });
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
/** 空白判(词导航语义的空白 = 空白段整体跳过) */
|
|
27
|
+
function isWhitespace(text) {
|
|
28
|
+
return text.length > 0 && text.trim().length === 0;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 光标向后移一词:跳过尾部空白,再跳一词(词内含 ASCII 标点切到最近标点
|
|
32
|
+
* 边界、标点 run 整段跳)。返回新光标(UTF-16 下标)——纯函数不改入参。
|
|
33
|
+
*/
|
|
34
|
+
export function findWordBackward(text, cursor) {
|
|
35
|
+
if (cursor <= 0)
|
|
36
|
+
return 0;
|
|
37
|
+
const segments = segmentWords(text.slice(0, cursor));
|
|
38
|
+
let newCursor = cursor;
|
|
39
|
+
// 先跳尾部空白段
|
|
40
|
+
while (segments.length > 0 && isWhitespace(segments[segments.length - 1].segment)) {
|
|
41
|
+
newCursor -= segments.pop().segment.length;
|
|
42
|
+
}
|
|
43
|
+
if (segments.length === 0)
|
|
44
|
+
return newCursor;
|
|
45
|
+
const last = segments[segments.length - 1];
|
|
46
|
+
if (last.isWordLike) {
|
|
47
|
+
// 词内含 ASCII 标点:切到词内最后一段标点之后(foo.bar → '.' 前)
|
|
48
|
+
const segment = last.segment;
|
|
49
|
+
let cut = -1;
|
|
50
|
+
for (let ci = 0; ci < segment.length; ci++) {
|
|
51
|
+
if (isPunctuation(segment[ci]))
|
|
52
|
+
cut = ci + 1;
|
|
53
|
+
}
|
|
54
|
+
if (cut >= 0)
|
|
55
|
+
newCursor -= segment.length - cut;
|
|
56
|
+
else
|
|
57
|
+
newCursor -= segment.length;
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
// 非词非空白 run(标点串)整段跳
|
|
61
|
+
while (segments.length > 0 &&
|
|
62
|
+
!segments[segments.length - 1].isWordLike &&
|
|
63
|
+
!isWhitespace(segments[segments.length - 1].segment)) {
|
|
64
|
+
newCursor -= segments.pop().segment.length;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return newCursor;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* 光标向前移一词:跳过前导空白,再跳一词(词内标点切到首个标点前、
|
|
71
|
+
* 标点 run 整段跳)。返回新光标(UTF-16 下标)——纯函数。
|
|
72
|
+
*/
|
|
73
|
+
export function findWordForward(text, cursor) {
|
|
74
|
+
if (cursor >= text.length)
|
|
75
|
+
return text.length;
|
|
76
|
+
const segments = segmentWords(text.slice(cursor));
|
|
77
|
+
let newCursor = cursor;
|
|
78
|
+
let i = 0;
|
|
79
|
+
// 先跳前导空白段
|
|
80
|
+
while (i < segments.length && isWhitespace(segments[i].segment)) {
|
|
81
|
+
newCursor += segments[i].segment.length;
|
|
82
|
+
i++;
|
|
83
|
+
}
|
|
84
|
+
if (i >= segments.length)
|
|
85
|
+
return newCursor;
|
|
86
|
+
const first = segments[i];
|
|
87
|
+
if (first.isWordLike) {
|
|
88
|
+
// 词内含 ASCII 标点:切到词内首个标点前(bar.baz → '.' 前)
|
|
89
|
+
let stop = -1;
|
|
90
|
+
for (let ci = 0; ci < first.segment.length; ci++) {
|
|
91
|
+
if (isPunctuation(first.segment[ci])) {
|
|
92
|
+
stop = ci;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
newCursor += stop >= 0 ? stop : first.segment.length;
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
// 非词非空白 run(标点串)整段跳
|
|
100
|
+
while (i < segments.length && !segments[i].isWordLike && !isWhitespace(segments[i].segment)) {
|
|
101
|
+
newCursor += segments[i].segment.length;
|
|
102
|
+
i++;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return newCursor;
|
|
106
|
+
}
|