@abacus-ai/cli 1.106.25007 → 2.0.0-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.oxlintrc.json +8 -0
- package/dist/index.mjs +12603 -0
- package/package.json +7 -39
- package/resources/abacus.ico +0 -0
- package/resources/entitlements.plist +9 -0
- package/src/__e2e__/README.md +196 -0
- package/src/__e2e__/agent-interactions.e2e.test.tsx +61 -0
- package/src/__e2e__/cli-commands.e2e.test.tsx +77 -0
- package/src/__e2e__/conversation-throttle.e2e.test.ts +453 -0
- package/src/__e2e__/conversation.e2e.test.tsx +56 -0
- package/src/__e2e__/diff-preview.e2e.test.tsx +3399 -0
- package/src/__e2e__/file-creation.e2e.test.tsx +149 -0
- package/src/__e2e__/helpers/test-helpers.ts +450 -0
- package/src/__e2e__/keyboard-navigation.e2e.test.tsx +34 -0
- package/src/__e2e__/llm-models.e2e.test.ts +402 -0
- package/src/__e2e__/mcp/mcp-callback-flow.e2e.test.tsx +71 -0
- package/src/__e2e__/mcp/mcp-full-app-ui.e2e.test.tsx +167 -0
- package/src/__e2e__/mcp/mcp-ui-rendering.e2e.test.tsx +185 -0
- package/src/__e2e__/repl.e2e.test.tsx +78 -0
- package/src/__e2e__/shell-compatibility.e2e.test.tsx +76 -0
- package/src/__e2e__/theme-mcp.e2e.test.tsx +98 -0
- package/src/__e2e__/tool-permissions.e2e.test.tsx +66 -0
- package/src/args.ts +22 -0
- package/src/components/__tests__/react-compiler.test.tsx +78 -0
- package/src/components/__tests__/status-indicator.test.tsx +403 -0
- package/src/components/composer/__tests__/bash-runner.test.tsx +263 -0
- package/src/components/composer/agent-mode-indicator.tsx +63 -0
- package/src/components/composer/bash-runner.tsx +54 -0
- package/src/components/composer/commands/default-commands.tsx +615 -0
- package/src/components/composer/commands/handler.tsx +59 -0
- package/src/components/composer/commands/picker.tsx +273 -0
- package/src/components/composer/commands/registry.ts +233 -0
- package/src/components/composer/commands/types.ts +33 -0
- package/src/components/composer/context.tsx +88 -0
- package/src/components/composer/file-mention-picker.tsx +83 -0
- package/src/components/composer/help.tsx +44 -0
- package/src/components/composer/index.tsx +1006 -0
- package/src/components/composer/mentions.ts +57 -0
- package/src/components/composer/message-queue.tsx +70 -0
- package/src/components/composer/mode-panel.tsx +35 -0
- package/src/components/composer/modes/__tests__/bash-handler.test.tsx +755 -0
- package/src/components/composer/modes/__tests__/bash-renderer.test.tsx +1108 -0
- package/src/components/composer/modes/bash-handler.tsx +132 -0
- package/src/components/composer/modes/bash-renderer.tsx +175 -0
- package/src/components/composer/modes/default-handlers.tsx +33 -0
- package/src/components/composer/modes/index.ts +41 -0
- package/src/components/composer/modes/types.ts +21 -0
- package/src/components/composer/persistent-shell.ts +283 -0
- package/src/components/composer/process.ts +65 -0
- package/src/components/composer/types.ts +9 -0
- package/src/components/composer/use-mention-search.ts +68 -0
- package/src/components/error-boundry.tsx +60 -0
- package/src/components/exit-message.tsx +29 -0
- package/src/components/expanded-view.tsx +74 -0
- package/src/components/file-completion.tsx +127 -0
- package/src/components/header.tsx +47 -0
- package/src/components/logo.tsx +37 -0
- package/src/components/segments.tsx +356 -0
- package/src/components/status-indicator.tsx +306 -0
- package/src/components/tool-group-summary.tsx +263 -0
- package/src/components/tool-permissions/ask-user-question-permission-ui.tsx +312 -0
- package/src/components/tool-permissions/diff-preview.tsx +355 -0
- package/src/components/tool-permissions/index.ts +5 -0
- package/src/components/tool-permissions/permission-options.tsx +375 -0
- package/src/components/tool-permissions/permission-preview-header.tsx +57 -0
- package/src/components/tool-permissions/tool-permission-ui.tsx +398 -0
- package/src/components/tools/agent/ask-user-question.tsx +101 -0
- package/src/components/tools/agent/enter-plan-mode.tsx +49 -0
- package/src/components/tools/agent/exit-plan-mode.tsx +75 -0
- package/src/components/tools/agent/handoff-to-main.tsx +27 -0
- package/src/components/tools/agent/subagent.tsx +37 -0
- package/src/components/tools/agent/todo-write.tsx +104 -0
- package/src/components/tools/browser/close-tab.tsx +58 -0
- package/src/components/tools/browser/computer.tsx +70 -0
- package/src/components/tools/browser/get-interactive-elements.tsx +54 -0
- package/src/components/tools/browser/get-tab-content.tsx +51 -0
- package/src/components/tools/browser/navigate-to.tsx +59 -0
- package/src/components/tools/browser/new-tab.tsx +60 -0
- package/src/components/tools/browser/perform-action.tsx +63 -0
- package/src/components/tools/browser/refresh-tab.tsx +43 -0
- package/src/components/tools/browser/switch-tab.tsx +58 -0
- package/src/components/tools/filesystem/delete-file.tsx +104 -0
- package/src/components/tools/filesystem/edit.tsx +220 -0
- package/src/components/tools/filesystem/list-dir.tsx +78 -0
- package/src/components/tools/filesystem/read-file.tsx +180 -0
- package/src/components/tools/filesystem/upload-image.tsx +76 -0
- package/src/components/tools/ide/ide-diagnostics.tsx +62 -0
- package/src/components/tools/index.ts +91 -0
- package/src/components/tools/mcp/mcp-tool.tsx +158 -0
- package/src/components/tools/search/fetch-url.tsx +73 -0
- package/src/components/tools/search/file-search.tsx +78 -0
- package/src/components/tools/search/grep.tsx +90 -0
- package/src/components/tools/search/semantic-search.tsx +66 -0
- package/src/components/tools/search/web-search.tsx +71 -0
- package/src/components/tools/shared/index.tsx +48 -0
- package/src/components/tools/shared/zod-coercion.ts +35 -0
- package/src/components/tools/terminal/bash-tool-output.tsx +174 -0
- package/src/components/tools/terminal/get-terminal-output.tsx +85 -0
- package/src/components/tools/terminal/run-in-terminal.tsx +106 -0
- package/src/components/tools/types.ts +16 -0
- package/src/components/tools.tsx +66 -0
- package/src/components/ui/__tests__/divider.test.tsx +61 -0
- package/src/components/ui/__tests__/gradient.test.tsx +125 -0
- package/src/components/ui/__tests__/input.test.tsx +166 -0
- package/src/components/ui/__tests__/select.test.tsx +273 -0
- package/src/components/ui/__tests__/shimmer.test.tsx +99 -0
- package/src/components/ui/blinking-indicator.tsx +25 -0
- package/src/components/ui/divider.tsx +162 -0
- package/src/components/ui/gradient.tsx +56 -0
- package/src/components/ui/input.tsx +228 -0
- package/src/components/ui/select.tsx +151 -0
- package/src/components/ui/shimmer.tsx +84 -0
- package/src/context/agent-mode.tsx +95 -0
- package/src/context/extension-file.tsx +136 -0
- package/src/context/network-activity.tsx +45 -0
- package/src/context/notification.tsx +62 -0
- package/src/context/shell-size.tsx +49 -0
- package/src/context/shell-title.tsx +38 -0
- package/src/entrypoints/print-mode.ts +312 -0
- package/src/entrypoints/repl.tsx +401 -0
- package/src/hooks/use-agent.ts +15 -0
- package/src/hooks/use-api-client.ts +1 -0
- package/src/hooks/use-available-height.ts +8 -0
- package/src/hooks/use-cleanup.ts +29 -0
- package/src/hooks/use-interrupt-manager.ts +242 -0
- package/src/hooks/use-models.ts +22 -0
- package/src/index.ts +217 -0
- package/src/lib/__tests__/ansi.test.ts +255 -0
- package/src/lib/__tests__/cli.test.ts +122 -0
- package/src/lib/__tests__/commands.test.ts +325 -0
- package/src/lib/__tests__/constants.test.ts +15 -0
- package/src/lib/__tests__/focusables.test.ts +25 -0
- package/src/lib/__tests__/fs.test.ts +231 -0
- package/src/lib/__tests__/markdown.test.tsx +348 -0
- package/src/lib/__tests__/mcpCommandHandler.test.ts +173 -0
- package/src/lib/__tests__/mcpManagement.test.ts +38 -0
- package/src/lib/__tests__/path-paste.test.ts +144 -0
- package/src/lib/__tests__/path.test.ts +300 -0
- package/src/lib/__tests__/queries.test.ts +39 -0
- package/src/lib/__tests__/standaloneMcpService.test.ts +71 -0
- package/src/lib/__tests__/text-buffer.test.ts +328 -0
- package/src/lib/__tests__/text-utils.test.ts +32 -0
- package/src/lib/__tests__/timing.test.ts +78 -0
- package/src/lib/__tests__/utils.test.ts +238 -0
- package/src/lib/__tests__/vim-buffer-actions.test.ts +154 -0
- package/src/lib/ansi.ts +150 -0
- package/src/lib/cli-push-server.ts +112 -0
- package/src/lib/cli.ts +44 -0
- package/src/lib/clipboard.ts +226 -0
- package/src/lib/command-utils.ts +93 -0
- package/src/lib/commands.ts +270 -0
- package/src/lib/constants.ts +3 -0
- package/src/lib/extension-connection.ts +181 -0
- package/src/lib/focusables.ts +7 -0
- package/src/lib/fs.ts +533 -0
- package/src/lib/markdown/code-block.tsx +63 -0
- package/src/lib/markdown/index.ts +4 -0
- package/src/lib/markdown/link.tsx +19 -0
- package/src/lib/markdown/markdown.tsx +372 -0
- package/src/lib/markdown/types.ts +15 -0
- package/src/lib/mcpCommandHandler.ts +121 -0
- package/src/lib/mcpManagement.ts +44 -0
- package/src/lib/path-paste.ts +185 -0
- package/src/lib/path.ts +179 -0
- package/src/lib/queries.ts +15 -0
- package/src/lib/standaloneMcpService.ts +688 -0
- package/src/lib/status-utils.ts +237 -0
- package/src/lib/test-utils.tsx +72 -0
- package/src/lib/text-buffer.ts +2415 -0
- package/src/lib/text-utils.ts +272 -0
- package/src/lib/timing.ts +63 -0
- package/src/lib/types.ts +295 -0
- package/src/lib/utils.ts +182 -0
- package/src/lib/vim-buffer-actions.ts +732 -0
- package/src/providers/agent.tsx +1075 -0
- package/src/providers/api-client.tsx +43 -0
- package/src/services/logger.ts +85 -0
- package/src/terminal/detection.ts +187 -0
- package/src/terminal/exit.ts +279 -0
- package/src/terminal/notification.ts +83 -0
- package/src/terminal/progress.ts +201 -0
- package/src/terminal/setup.ts +797 -0
- package/src/terminal/suspend.ts +58 -0
- package/src/terminal/types.ts +51 -0
- package/src/theme/context.tsx +57 -0
- package/src/theme/index.ts +4 -0
- package/src/theme/themed.tsx +35 -0
- package/src/theme/themes.json +546 -0
- package/src/theme/types.ts +110 -0
- package/src/tools/types.ts +59 -0
- package/src/tools/utils/__tests__/zod-coercion.test.ts +33 -0
- package/src/tools/utils/tool-ui-components.tsx +631 -0
- package/src/tools/utils/zod-coercion.ts +35 -0
- package/tsconfig.json +11 -0
- package/tsconfig.node.json +29 -0
- package/tsconfig.test.json +27 -0
- package/tsdown.config.ts +17 -0
- package/vitest.config.ts +76 -0
- package/README.md +0 -28
- package/dist/index.js +0 -26
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
isWordCharStrict,
|
|
7
|
+
isWhitespace,
|
|
8
|
+
isCombiningMark,
|
|
9
|
+
isWordCharWithCombining,
|
|
10
|
+
getCharScript,
|
|
11
|
+
isDifferentScript,
|
|
12
|
+
findNextWordStartInLine,
|
|
13
|
+
findPrevWordStartInLine,
|
|
14
|
+
findWordEndInLine,
|
|
15
|
+
offsetToLogicalPos,
|
|
16
|
+
logicalPosToOffset,
|
|
17
|
+
pushUndo,
|
|
18
|
+
replaceRangeInternal,
|
|
19
|
+
type TextBufferState,
|
|
20
|
+
} from "../text-buffer.js";
|
|
21
|
+
|
|
22
|
+
describe.concurrent("text-buffer", () => {
|
|
23
|
+
describe.concurrent("isWordCharStrict", () => {
|
|
24
|
+
it("should identify word characters", () => {
|
|
25
|
+
expect(isWordCharStrict("a")).toBe(true);
|
|
26
|
+
expect(isWordCharStrict("Z")).toBe(true);
|
|
27
|
+
expect(isWordCharStrict("5")).toBe(true);
|
|
28
|
+
expect(isWordCharStrict("_")).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("should identify non-word characters", () => {
|
|
32
|
+
expect(isWordCharStrict(" ")).toBe(false);
|
|
33
|
+
expect(isWordCharStrict(".")).toBe(false);
|
|
34
|
+
expect(isWordCharStrict("!")).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe.concurrent("isWhitespace", () => {
|
|
39
|
+
it("should identify whitespace characters", () => {
|
|
40
|
+
expect(isWhitespace(" ")).toBe(true);
|
|
41
|
+
expect(isWhitespace("\t")).toBe(true);
|
|
42
|
+
expect(isWhitespace("\n")).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("should identify non-whitespace characters", () => {
|
|
46
|
+
expect(isWhitespace("a")).toBe(false);
|
|
47
|
+
expect(isWhitespace("1")).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe.concurrent("isCombiningMark", () => {
|
|
52
|
+
it("should identify combining marks", () => {
|
|
53
|
+
// Combining marks like diacritics
|
|
54
|
+
expect(isCombiningMark("\u0300")).toBe(true); // Combining grave accent
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should identify non-combining characters", () => {
|
|
58
|
+
expect(isCombiningMark("a")).toBe(false);
|
|
59
|
+
expect(isCombiningMark("1")).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe.concurrent("isWordCharWithCombining", () => {
|
|
64
|
+
it("should identify word characters including combining marks", () => {
|
|
65
|
+
expect(isWordCharWithCombining("a")).toBe(true);
|
|
66
|
+
expect(isWordCharWithCombining("\u0300")).toBe(true); // Combining mark
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("should identify non-word characters", () => {
|
|
70
|
+
expect(isWordCharWithCombining(" ")).toBe(false);
|
|
71
|
+
expect(isWordCharWithCombining(".")).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe.concurrent("getCharScript", () => {
|
|
76
|
+
it("should identify Latin script", () => {
|
|
77
|
+
expect(getCharScript("a")).toBe("latin");
|
|
78
|
+
expect(getCharScript("A")).toBe("latin");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("should identify other scripts", () => {
|
|
82
|
+
expect(getCharScript("中")).toBe("han");
|
|
83
|
+
expect(getCharScript("ا")).toBe("arabic");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("should return other for unknown scripts", () => {
|
|
87
|
+
expect(getCharScript(" ")).toBe("other");
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe.concurrent("isDifferentScript", () => {
|
|
92
|
+
it("should detect different scripts", () => {
|
|
93
|
+
expect(isDifferentScript("a", "中")).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("should detect same script", () => {
|
|
97
|
+
expect(isDifferentScript("a", "b")).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("should return false for non-word characters", () => {
|
|
101
|
+
expect(isDifferentScript(" ", "a")).toBe(false);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe.concurrent("findNextWordStartInLine", () => {
|
|
106
|
+
it("should find next word start", () => {
|
|
107
|
+
const result = findNextWordStartInLine("hello world", 0);
|
|
108
|
+
expect(result).toBe(6);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("should return null at end of line", () => {
|
|
112
|
+
const result = findNextWordStartInLine("hello", 0);
|
|
113
|
+
expect(result).toBeNull();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("should handle empty line", () => {
|
|
117
|
+
const result = findNextWordStartInLine("", 0);
|
|
118
|
+
expect(result).toBeNull();
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe.concurrent("findPrevWordStartInLine", () => {
|
|
123
|
+
it("should find previous word start", () => {
|
|
124
|
+
const result = findPrevWordStartInLine("hello world", 11);
|
|
125
|
+
expect(result).toBe(6);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("should return null at start of line", () => {
|
|
129
|
+
const result = findPrevWordStartInLine("hello", 0);
|
|
130
|
+
expect(result).toBeNull();
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe.concurrent("findWordEndInLine", () => {
|
|
135
|
+
it("should find word end", () => {
|
|
136
|
+
const result = findWordEndInLine("hello world", 0);
|
|
137
|
+
expect(result).toBe(4);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("should return null when no word found", () => {
|
|
141
|
+
const result = findWordEndInLine(" ", 0);
|
|
142
|
+
expect(result).toBeNull();
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe.concurrent("offsetToLogicalPos", () => {
|
|
147
|
+
it("should convert offset to position", () => {
|
|
148
|
+
const [row, col] = offsetToLogicalPos("hello\nworld", 0);
|
|
149
|
+
expect(row).toBe(0);
|
|
150
|
+
expect(col).toBe(0);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("should handle offset in middle of line", () => {
|
|
154
|
+
const [row, col] = offsetToLogicalPos("hello\nworld", 3);
|
|
155
|
+
expect(row).toBe(0);
|
|
156
|
+
expect(col).toBe(3);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("should handle offset across lines", () => {
|
|
160
|
+
const [row, col] = offsetToLogicalPos("hello\nworld", 7);
|
|
161
|
+
expect(row).toBe(1);
|
|
162
|
+
expect(col).toBe(1);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("should handle empty text", () => {
|
|
166
|
+
const [row, col] = offsetToLogicalPos("", 0);
|
|
167
|
+
expect(row).toBe(0);
|
|
168
|
+
expect(col).toBe(0);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe.concurrent("logicalPosToOffset", () => {
|
|
173
|
+
it("should convert position to offset", () => {
|
|
174
|
+
const offset = logicalPosToOffset(["hello", "world"], 0, 0);
|
|
175
|
+
expect(offset).toBe(0);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("should handle position in middle of line", () => {
|
|
179
|
+
const offset = logicalPosToOffset(["hello", "world"], 0, 3);
|
|
180
|
+
expect(offset).toBe(3);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("should handle position across lines", () => {
|
|
184
|
+
const offset = logicalPosToOffset(["hello", "world"], 1, 2);
|
|
185
|
+
expect(offset).toBe(8); // 5 (hello) + 1 (newline) + 2 (wo)
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("should be inverse of offsetToLogicalPos", () => {
|
|
189
|
+
// TODO: VERIFICATION - The functions are documented as inverse operations,
|
|
190
|
+
// but this round-trip test should verify they work correctly for all cases,
|
|
191
|
+
// especially with Unicode characters, empty lines, and edge positions.
|
|
192
|
+
// Current test only covers basic case - should add more comprehensive tests.
|
|
193
|
+
const lines = ["hello", "world", "test"];
|
|
194
|
+
const text = lines.join("\n");
|
|
195
|
+
const [row, col] = offsetToLogicalPos(text, 8);
|
|
196
|
+
const offset = logicalPosToOffset(lines, row, col);
|
|
197
|
+
expect(offset).toBe(8);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe.concurrent("pushUndo", () => {
|
|
202
|
+
it("should add state to undo stack", () => {
|
|
203
|
+
const state: TextBufferState = {
|
|
204
|
+
lines: ["hello"],
|
|
205
|
+
cursorRow: 0,
|
|
206
|
+
cursorCol: 5,
|
|
207
|
+
preferredCol: null,
|
|
208
|
+
undoStack: [],
|
|
209
|
+
redoStack: [],
|
|
210
|
+
clipboard: null,
|
|
211
|
+
selectionAnchor: null,
|
|
212
|
+
viewportWidth: 80,
|
|
213
|
+
viewportHeight: 24,
|
|
214
|
+
visualLayout: {
|
|
215
|
+
visualLines: ["hello"],
|
|
216
|
+
logicalToVisualMap: [[[0, 0]]],
|
|
217
|
+
visualToLogicalMap: [[0, 0]],
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const newState = pushUndo(state);
|
|
222
|
+
expect(newState.undoStack.length).toBe(1);
|
|
223
|
+
expect(newState.redoStack.length).toBe(0);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("should clear redo stack", () => {
|
|
227
|
+
const state: TextBufferState = {
|
|
228
|
+
lines: ["hello"],
|
|
229
|
+
cursorRow: 0,
|
|
230
|
+
cursorCol: 5,
|
|
231
|
+
preferredCol: null,
|
|
232
|
+
undoStack: [],
|
|
233
|
+
redoStack: [{ lines: ["old"], cursorRow: 0, cursorCol: 3 }],
|
|
234
|
+
clipboard: null,
|
|
235
|
+
selectionAnchor: null,
|
|
236
|
+
viewportWidth: 80,
|
|
237
|
+
viewportHeight: 24,
|
|
238
|
+
visualLayout: {
|
|
239
|
+
visualLines: ["hello"],
|
|
240
|
+
logicalToVisualMap: [[[0, 0]]],
|
|
241
|
+
visualToLogicalMap: [[0, 0]],
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const newState = pushUndo(state);
|
|
246
|
+
expect(newState.redoStack.length).toBe(0);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
describe.concurrent("replaceRangeInternal", () => {
|
|
251
|
+
it("should replace text in range", () => {
|
|
252
|
+
const state: TextBufferState = {
|
|
253
|
+
lines: ["hello world"],
|
|
254
|
+
cursorRow: 0,
|
|
255
|
+
cursorCol: 5,
|
|
256
|
+
preferredCol: null,
|
|
257
|
+
undoStack: [],
|
|
258
|
+
redoStack: [],
|
|
259
|
+
clipboard: null,
|
|
260
|
+
selectionAnchor: null,
|
|
261
|
+
viewportWidth: 80,
|
|
262
|
+
viewportHeight: 24,
|
|
263
|
+
visualLayout: {
|
|
264
|
+
visualLines: ["hello world"],
|
|
265
|
+
logicalToVisualMap: [[[0, 0]]],
|
|
266
|
+
visualToLogicalMap: [[0, 0]],
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const newState = replaceRangeInternal(state, 0, 0, 0, 5, "hi");
|
|
271
|
+
expect(newState.lines[0]).toBe("hi world");
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("should handle multi-line replacement", () => {
|
|
275
|
+
const state: TextBufferState = {
|
|
276
|
+
lines: ["hello", "world"],
|
|
277
|
+
cursorRow: 0,
|
|
278
|
+
cursorCol: 0,
|
|
279
|
+
preferredCol: null,
|
|
280
|
+
undoStack: [],
|
|
281
|
+
redoStack: [],
|
|
282
|
+
clipboard: null,
|
|
283
|
+
selectionAnchor: null,
|
|
284
|
+
viewportWidth: 80,
|
|
285
|
+
viewportHeight: 24,
|
|
286
|
+
visualLayout: {
|
|
287
|
+
visualLines: ["hello", "world"],
|
|
288
|
+
logicalToVisualMap: [[[0, 0]], [[1, 0]]],
|
|
289
|
+
visualToLogicalMap: [
|
|
290
|
+
[0, 0],
|
|
291
|
+
[1, 0],
|
|
292
|
+
],
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const newState = replaceRangeInternal(state, 0, 0, 1, 5, "test");
|
|
297
|
+
expect(newState.lines).toEqual(["test"]);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("should return original state for invalid range", () => {
|
|
301
|
+
// TODO: DESIGN ISSUE - replaceRangeInternal silently fails for invalid ranges
|
|
302
|
+
// by returning the original state unchanged. This makes it impossible to
|
|
303
|
+
// distinguish between "no change needed" and "invalid input".
|
|
304
|
+
// Consider: Throwing an error, returning a result object with success flag,
|
|
305
|
+
// or at least logging a warning for invalid ranges.
|
|
306
|
+
const state: TextBufferState = {
|
|
307
|
+
lines: ["hello"],
|
|
308
|
+
cursorRow: 0,
|
|
309
|
+
cursorCol: 0,
|
|
310
|
+
preferredCol: null,
|
|
311
|
+
undoStack: [],
|
|
312
|
+
redoStack: [],
|
|
313
|
+
clipboard: null,
|
|
314
|
+
selectionAnchor: null,
|
|
315
|
+
viewportWidth: 80,
|
|
316
|
+
viewportHeight: 24,
|
|
317
|
+
visualLayout: {
|
|
318
|
+
visualLines: ["hello"],
|
|
319
|
+
logicalToVisualMap: [[[0, 0]]],
|
|
320
|
+
visualToLogicalMap: [[0, 0]],
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const newState = replaceRangeInternal(state, 10, 0, 10, 0, "test");
|
|
325
|
+
expect(newState).toBe(state);
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { getAsciiArtWidth, toCodePoints } from "../text-utils.js";
|
|
4
|
+
|
|
5
|
+
describe.concurrent("text-utils", () => {
|
|
6
|
+
describe.concurrent("getAsciiArtWidth", () => {
|
|
7
|
+
it("should return 0 for empty string", () => {
|
|
8
|
+
expect(getAsciiArtWidth("")).toBe(0);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("should return the width of a single line", () => {
|
|
12
|
+
expect(getAsciiArtWidth("hello")).toBe(5);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("should return the maximum width of multiple lines", () => {
|
|
16
|
+
const asciiArt = "hello\nworld\nhi";
|
|
17
|
+
expect(getAsciiArtWidth(asciiArt)).toBe(5);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe.concurrent("toCodePoints", () => {
|
|
22
|
+
it("should split ASCII string into characters", () => {
|
|
23
|
+
expect(toCodePoints("abc")).toEqual(["a", "b", "c"]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("should handle emoji correctly", () => {
|
|
27
|
+
const result = toCodePoints("👋");
|
|
28
|
+
expect(result.length).toBe(1);
|
|
29
|
+
expect(result[0]).toBe("👋");
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
4
|
+
|
|
5
|
+
import { Timing } from "../timing.js";
|
|
6
|
+
|
|
7
|
+
describe.concurrent("Timing", () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
vi.useFakeTimers();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("should initialize with current time", () => {
|
|
13
|
+
const timing = new Timing();
|
|
14
|
+
expect(timing.getElapsedMs()).toBe(0);
|
|
15
|
+
expect(timing.getElapsedSeconds()).toBe(0);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("should calculate elapsed time in milliseconds", () => {
|
|
19
|
+
const timing = new Timing();
|
|
20
|
+
vi.advanceTimersByTime(100);
|
|
21
|
+
expect(timing.getElapsedMs()).toBe(100);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("should calculate elapsed time in seconds", () => {
|
|
25
|
+
const timing = new Timing();
|
|
26
|
+
vi.advanceTimersByTime(2500);
|
|
27
|
+
expect(timing.getElapsedSeconds()).toBe(2.5);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("should format duration correctly", () => {
|
|
31
|
+
const timing = new Timing();
|
|
32
|
+
vi.advanceTimersByTime(1234);
|
|
33
|
+
const formatted = timing.getFormattedDuration();
|
|
34
|
+
expect(formatted).toBe("1s");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("should format duration with minutes", () => {
|
|
38
|
+
const timing = new Timing();
|
|
39
|
+
vi.advanceTimersByTime(125000); // 2 minutes 5 seconds
|
|
40
|
+
const formatted = timing.getFormattedDuration();
|
|
41
|
+
expect(formatted).toBe("2m 5s");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should format duration with hours", () => {
|
|
45
|
+
const timing = new Timing();
|
|
46
|
+
vi.advanceTimersByTime(3665000); // 1 hour 1 minute 5 seconds
|
|
47
|
+
const formatted = timing.getFormattedDuration();
|
|
48
|
+
expect(formatted).toBe("1h 1m 5s");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("should reset the timer", () => {
|
|
52
|
+
const timing = new Timing();
|
|
53
|
+
vi.advanceTimersByTime(1000);
|
|
54
|
+
expect(timing.getElapsedMs()).toBeGreaterThan(0);
|
|
55
|
+
|
|
56
|
+
timing.reset();
|
|
57
|
+
expect(timing.getElapsedMs()).toBe(0);
|
|
58
|
+
expect(timing.getElapsedSeconds()).toBe(0);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("should continue timing after reset", () => {
|
|
62
|
+
const timing = new Timing();
|
|
63
|
+
vi.advanceTimersByTime(1000);
|
|
64
|
+
timing.reset();
|
|
65
|
+
vi.advanceTimersByTime(500);
|
|
66
|
+
expect(timing.getElapsedMs()).toBe(500);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("should handle multiple resets", () => {
|
|
70
|
+
const timing = new Timing();
|
|
71
|
+
vi.advanceTimersByTime(1000);
|
|
72
|
+
timing.reset();
|
|
73
|
+
vi.advanceTimersByTime(500);
|
|
74
|
+
timing.reset();
|
|
75
|
+
vi.advanceTimersByTime(250);
|
|
76
|
+
expect(timing.getElapsedMs()).toBe(250);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
|
|
5
|
+
import { clamp, deepClone, createDeferredPromise, getSegmentContent, isWindows } from "../utils.js";
|
|
6
|
+
|
|
7
|
+
describe.concurrent("utils", () => {
|
|
8
|
+
describe.concurrent("clamp", () => {
|
|
9
|
+
it("should return value when within range", () => {
|
|
10
|
+
expect(clamp(5, 0, 10)).toBe(5);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("should return min when value is less than min", () => {
|
|
14
|
+
expect(clamp(-5, 0, 10)).toBe(0);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("should return max when value is greater than max", () => {
|
|
18
|
+
expect(clamp(15, 0, 10)).toBe(10);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("should handle edge cases", () => {
|
|
22
|
+
expect(clamp(0, 0, 10)).toBe(0);
|
|
23
|
+
expect(clamp(10, 0, 10)).toBe(10);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("should handle negative ranges", () => {
|
|
27
|
+
expect(clamp(-15, -10, -5)).toBe(-10);
|
|
28
|
+
expect(clamp(-3, -10, -5)).toBe(-5);
|
|
29
|
+
expect(clamp(-7, -10, -5)).toBe(-7);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe.concurrent("deepClone", () => {
|
|
34
|
+
it("should clone primitive values", () => {
|
|
35
|
+
expect(deepClone(5)).toBe(5);
|
|
36
|
+
expect(deepClone("test")).toBe("test");
|
|
37
|
+
expect(deepClone(true)).toBe(true);
|
|
38
|
+
expect(deepClone(null)).toBe(null);
|
|
39
|
+
expect(deepClone(undefined)).toBe(undefined);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("should clone arrays", () => {
|
|
43
|
+
const original = [1, 2, 3];
|
|
44
|
+
const cloned = deepClone(original);
|
|
45
|
+
expect(cloned).toEqual(original);
|
|
46
|
+
expect(cloned).not.toBe(original);
|
|
47
|
+
cloned.push(4);
|
|
48
|
+
expect(original).toHaveLength(3);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("should clone objects", () => {
|
|
52
|
+
const original: Record<string, number> = { a: 1, b: 2 };
|
|
53
|
+
const cloned = deepClone(original);
|
|
54
|
+
expect(cloned).toEqual(original);
|
|
55
|
+
expect(cloned).not.toBe(original);
|
|
56
|
+
cloned.c = 3;
|
|
57
|
+
expect(original).not.toHaveProperty("c");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("should deep clone nested objects", () => {
|
|
61
|
+
const original = { a: { b: { c: 1 } } };
|
|
62
|
+
const cloned = deepClone(original);
|
|
63
|
+
expect(cloned).toEqual(original);
|
|
64
|
+
expect(cloned.a).not.toBe(original.a);
|
|
65
|
+
expect(cloned.a.b).not.toBe(original.a.b);
|
|
66
|
+
cloned.a.b.c = 2;
|
|
67
|
+
expect(original.a.b.c).toBe(1);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("should deep clone nested arrays", () => {
|
|
71
|
+
const original = [
|
|
72
|
+
[1, 2],
|
|
73
|
+
[3, 4],
|
|
74
|
+
];
|
|
75
|
+
const cloned = deepClone(original);
|
|
76
|
+
expect(cloned).toEqual(original);
|
|
77
|
+
expect(cloned).not.toBe(original);
|
|
78
|
+
expect(cloned[0]).not.toBe(original[0]);
|
|
79
|
+
cloned[0].push(5);
|
|
80
|
+
expect(original[0]).toHaveLength(2);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("should handle mixed nested structures", () => {
|
|
84
|
+
const original = { a: [1, { b: 2 }], c: { d: [3, 4] } };
|
|
85
|
+
const cloned = deepClone(original);
|
|
86
|
+
expect(cloned).toEqual(original);
|
|
87
|
+
(cloned.a[1] as { b: number }).b = 5;
|
|
88
|
+
expect((original.a[1] as { b: number }).b).toBe(2);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("should return RegExp as-is", () => {
|
|
92
|
+
// TODO: LIMITATION - RegExp objects are returned as-is (not cloned).
|
|
93
|
+
// This means mutations to the cloned object's regex will affect the original.
|
|
94
|
+
// Consider: Whether this is intentional or if RegExp should be cloned.
|
|
95
|
+
const regex = /test/gi;
|
|
96
|
+
const cloned = deepClone(regex);
|
|
97
|
+
expect(cloned).toBe(regex);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("should handle empty objects and arrays", () => {
|
|
101
|
+
expect(deepClone({})).toEqual({});
|
|
102
|
+
expect(deepClone([])).toEqual([]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("should handle Date objects", () => {
|
|
106
|
+
// TODO: BUG - deepClone does not properly handle Date objects.
|
|
107
|
+
// Dates will be cloned as plain objects, losing their Date prototype methods.
|
|
108
|
+
// Fix: Add Date handling: if (obj instanceof Date) return new Date(obj.getTime())
|
|
109
|
+
const date = new Date("2023-01-01");
|
|
110
|
+
const cloned = deepClone(date);
|
|
111
|
+
// Current behavior: cloned is a plain object, not a Date
|
|
112
|
+
expect(cloned).not.toBeInstanceOf(Date);
|
|
113
|
+
expect(typeof cloned).toBe("object");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("should handle circular references", () => {
|
|
117
|
+
// TODO: BUG - deepClone will cause infinite recursion with circular references.
|
|
118
|
+
// Fix: Add a WeakSet to track visited objects and skip circular references.
|
|
119
|
+
// This test documents the current broken behavior - it will stack overflow.
|
|
120
|
+
const obj: any = { a: 1 };
|
|
121
|
+
obj.self = obj; // Circular reference
|
|
122
|
+
// Note: This will cause a stack overflow in the current implementation
|
|
123
|
+
// Uncomment the expect below to verify the bug, but it will crash the test
|
|
124
|
+
// expect(() => deepClone(obj)).toThrow();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("should handle Map and Set", () => {
|
|
128
|
+
// TODO: LIMITATION - deepClone does not handle Map or Set objects.
|
|
129
|
+
// They will be cloned as plain objects, losing their Map/Set behavior.
|
|
130
|
+
// Fix: Add Map/Set handling similar to arrays.
|
|
131
|
+
const map = new Map([["key", "value"]]);
|
|
132
|
+
const cloned = deepClone(map);
|
|
133
|
+
expect(cloned).not.toBeInstanceOf(Map);
|
|
134
|
+
expect(typeof cloned).toBe("object");
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe.concurrent("createDeferredPromise", () => {
|
|
139
|
+
it("should create a promise with resolve and reject functions", async () => {
|
|
140
|
+
const deferred = createDeferredPromise<string>();
|
|
141
|
+
expect(deferred).toHaveProperty("promise");
|
|
142
|
+
expect(deferred).toHaveProperty("resolve");
|
|
143
|
+
expect(deferred).toHaveProperty("reject");
|
|
144
|
+
expect(typeof deferred.resolve).toBe("function");
|
|
145
|
+
expect(typeof deferred.reject).toBe("function");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("should resolve the promise when resolve is called", async () => {
|
|
149
|
+
const deferred = createDeferredPromise<string>();
|
|
150
|
+
deferred.resolve("test");
|
|
151
|
+
await expect(deferred.promise).resolves.toBe("test");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("should reject the promise when reject is called", async () => {
|
|
155
|
+
const deferred = createDeferredPromise<string>();
|
|
156
|
+
const error = new Error("test error");
|
|
157
|
+
deferred.reject(error);
|
|
158
|
+
await expect(deferred.promise).rejects.toBe(error);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("should work with void type", async () => {
|
|
162
|
+
const deferred = createDeferredPromise<void>();
|
|
163
|
+
deferred.resolve();
|
|
164
|
+
await expect(deferred.promise).resolves.toBeUndefined();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("should work with object types", async () => {
|
|
168
|
+
const deferred = createDeferredPromise<{ value: number }>();
|
|
169
|
+
deferred.resolve({ value: 42 });
|
|
170
|
+
await expect(deferred.promise).resolves.toEqual({ value: 42 });
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe.concurrent("getSegmentContent", () => {
|
|
175
|
+
it("should return string when segment is a string", () => {
|
|
176
|
+
expect(getSegmentContent("test")).toBe("test");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("should return segment property when segment is an object", () => {
|
|
180
|
+
expect(getSegmentContent({ segment: "test" })).toBe("test");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("should return null when segment is undefined", () => {
|
|
184
|
+
expect(getSegmentContent(undefined)).toBe(null);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("should return null when segment is null", () => {
|
|
188
|
+
expect(getSegmentContent(null as unknown as undefined)).toBe(null);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("should return null when segment is an object without segment property", () => {
|
|
192
|
+
expect(getSegmentContent({ other: "value" } as unknown as { segment: string })).toBe(null);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("should handle empty strings", () => {
|
|
196
|
+
expect(getSegmentContent("")).toBe("");
|
|
197
|
+
expect(getSegmentContent({ segment: "" })).toBe("");
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe.concurrent("isWindows", () => {
|
|
202
|
+
it("should return true on Windows platform", () => {
|
|
203
|
+
const originalPlatform = process.platform;
|
|
204
|
+
Object.defineProperty(process, "platform", {
|
|
205
|
+
value: "win32",
|
|
206
|
+
writable: true,
|
|
207
|
+
configurable: true,
|
|
208
|
+
});
|
|
209
|
+
expect(isWindows()).toBe(true);
|
|
210
|
+
Object.defineProperty(process, "platform", {
|
|
211
|
+
value: originalPlatform,
|
|
212
|
+
writable: true,
|
|
213
|
+
configurable: true,
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("should return false on non-Windows platforms", () => {
|
|
218
|
+
const originalPlatform = process.platform;
|
|
219
|
+
Object.defineProperty(process, "platform", {
|
|
220
|
+
value: "linux",
|
|
221
|
+
writable: true,
|
|
222
|
+
configurable: true,
|
|
223
|
+
});
|
|
224
|
+
expect(isWindows()).toBe(false);
|
|
225
|
+
Object.defineProperty(process, "platform", {
|
|
226
|
+
value: "darwin",
|
|
227
|
+
writable: true,
|
|
228
|
+
configurable: true,
|
|
229
|
+
});
|
|
230
|
+
expect(isWindows()).toBe(false);
|
|
231
|
+
Object.defineProperty(process, "platform", {
|
|
232
|
+
value: originalPlatform,
|
|
233
|
+
writable: true,
|
|
234
|
+
configurable: true,
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
});
|