@oh-my-pi/pi-coding-agent 14.1.0 → 14.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +79 -0
- package/package.json +8 -8
- package/src/async/job-manager.ts +43 -10
- package/src/commit/agentic/tools/analyze-file.ts +1 -2
- package/src/config/mcp-schema.json +1 -1
- package/src/config/model-equivalence.ts +1 -0
- package/src/config/model-registry.ts +63 -34
- package/src/config/model-resolver.ts +111 -15
- package/src/config/settings-schema.ts +4 -3
- package/src/config/settings.ts +1 -1
- package/src/cursor.ts +64 -23
- package/src/edit/index.ts +254 -89
- package/src/edit/modes/chunk.ts +336 -57
- package/src/edit/modes/hashline.ts +51 -26
- package/src/edit/modes/patch.ts +16 -10
- package/src/edit/modes/replace.ts +15 -7
- package/src/edit/renderer.ts +248 -94
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +6 -4
- package/src/extensibility/custom-tools/types.ts +0 -3
- package/src/extensibility/extensions/loader.ts +16 -0
- package/src/extensibility/extensions/runner.ts +2 -7
- package/src/extensibility/extensions/types.ts +8 -4
- package/src/internal-urls/docs-index.generated.ts +3 -3
- package/src/ipy/executor.ts +447 -52
- package/src/ipy/kernel.ts +39 -13
- package/src/lsp/client.ts +54 -0
- package/src/lsp/index.ts +8 -0
- package/src/lsp/types.ts +6 -0
- package/src/main.ts +0 -1
- package/src/modes/acp/acp-agent.ts +4 -1
- package/src/modes/components/bash-execution.ts +16 -4
- package/src/modes/components/status-line/presets.ts +17 -6
- package/src/modes/components/status-line/segments.ts +15 -0
- package/src/modes/components/status-line-segment-editor.ts +1 -0
- package/src/modes/components/status-line.ts +7 -1
- package/src/modes/components/tool-execution.ts +145 -75
- package/src/modes/controllers/command-controller.ts +24 -1
- package/src/modes/controllers/event-controller.ts +4 -1
- package/src/modes/controllers/extension-ui-controller.ts +28 -5
- package/src/modes/controllers/input-controller.ts +9 -3
- package/src/modes/controllers/selector-controller.ts +4 -1
- package/src/modes/interactive-mode.ts +19 -3
- package/src/modes/print-mode.ts +13 -4
- package/src/modes/prompt-action-autocomplete.ts +3 -5
- package/src/modes/rpc/rpc-mode.ts +8 -2
- package/src/modes/shared.ts +2 -2
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/prompts/tools/bash.md +2 -2
- package/src/prompts/tools/chunk-edit.md +191 -163
- package/src/prompts/tools/hashline.md +11 -11
- package/src/prompts/tools/patch.md +10 -5
- package/src/prompts/tools/{await.md → poll.md} +1 -1
- package/src/prompts/tools/read-chunk.md +3 -3
- package/src/prompts/tools/task.md +2 -2
- package/src/prompts/tools/vim.md +98 -0
- package/src/sdk.ts +754 -724
- package/src/session/agent-session.ts +164 -34
- package/src/session/session-manager.ts +50 -4
- package/src/slash-commands/builtin-registry.ts +17 -0
- package/src/task/executor.ts +4 -4
- package/src/task/index.ts +3 -5
- package/src/task/types.ts +2 -2
- package/src/tools/bash.ts +26 -8
- package/src/tools/find.ts +5 -2
- package/src/tools/grep.ts +77 -8
- package/src/tools/index.ts +48 -19
- package/src/tools/{await-tool.ts → poll-tool.ts} +36 -30
- package/src/tools/python.ts +293 -278
- package/src/tools/submit-result.ts +5 -2
- package/src/tools/todo-write.ts +8 -2
- package/src/tools/vim.ts +966 -0
- package/src/utils/edit-mode.ts +2 -1
- package/src/utils/session-color.ts +55 -0
- package/src/utils/title-generator.ts +15 -6
- package/src/vim/buffer.ts +309 -0
- package/src/vim/commands.ts +382 -0
- package/src/vim/engine.ts +2426 -0
- package/src/vim/parser.ts +151 -0
- package/src/vim/render.ts +252 -0
- package/src/vim/types.ts +197 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { VimInputError as VimError, type VimKeyToken } from "./types";
|
|
2
|
+
|
|
3
|
+
const SPECIAL_KEYS = new Map<string, string>([
|
|
4
|
+
["esc", "Esc"],
|
|
5
|
+
["escape", "Esc"],
|
|
6
|
+
["cr", "CR"],
|
|
7
|
+
["enter", "CR"],
|
|
8
|
+
["return", "CR"],
|
|
9
|
+
["bs", "BS"],
|
|
10
|
+
["backspace", "BS"],
|
|
11
|
+
["tab", "Tab"],
|
|
12
|
+
["c-d", "C-d"],
|
|
13
|
+
["c-u", "C-u"],
|
|
14
|
+
["c-r", "C-r"],
|
|
15
|
+
["c-w", "C-w"],
|
|
16
|
+
["c-o", "C-o"],
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
function normalizeSpecialKey(raw: string): string | undefined {
|
|
20
|
+
return SPECIAL_KEYS.get(raw.trim().toLowerCase());
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function toDisplayToken(value: string): string {
|
|
24
|
+
switch (value) {
|
|
25
|
+
case " ":
|
|
26
|
+
return "<Space>";
|
|
27
|
+
default:
|
|
28
|
+
return value.length === 1 ? value : `<${value}>`;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function parseKeySequences(sequences: string[]): VimKeyToken[] {
|
|
33
|
+
const tokens: VimKeyToken[] = [];
|
|
34
|
+
|
|
35
|
+
for (let sequenceIndex = 0; sequenceIndex < sequences.length; sequenceIndex += 1) {
|
|
36
|
+
const sequence = sequences[sequenceIndex] ?? "";
|
|
37
|
+
for (let offset = 0; offset < sequence.length; offset += 1) {
|
|
38
|
+
const char = sequence[offset] ?? "";
|
|
39
|
+
// Handle literal escape byte (\x1b / \u001b)
|
|
40
|
+
if (char === "\x1b") {
|
|
41
|
+
tokens.push({
|
|
42
|
+
value: "Esc",
|
|
43
|
+
display: "<Esc>",
|
|
44
|
+
sequenceIndex,
|
|
45
|
+
offset,
|
|
46
|
+
});
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
// Handle literal carriage return
|
|
50
|
+
if (char === "\r") {
|
|
51
|
+
tokens.push({
|
|
52
|
+
value: "CR",
|
|
53
|
+
display: "<CR>",
|
|
54
|
+
sequenceIndex,
|
|
55
|
+
offset,
|
|
56
|
+
});
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
// Handle escaped sequences: \r → CR, \e → Esc, \n → newline, \t → Tab
|
|
60
|
+
if (char === "\\" && offset + 1 < sequence.length) {
|
|
61
|
+
const next = sequence[offset + 1];
|
|
62
|
+
if (next === "r") {
|
|
63
|
+
tokens.push({ value: "CR", display: "\\r", sequenceIndex, offset });
|
|
64
|
+
offset += 1;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (next === "e") {
|
|
68
|
+
tokens.push({ value: "Esc", display: "\\e", sequenceIndex, offset });
|
|
69
|
+
offset += 1;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (next === "n") {
|
|
73
|
+
tokens.push({ value: "\n", display: "\\n", sequenceIndex, offset });
|
|
74
|
+
offset += 1;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (next === "t") {
|
|
78
|
+
tokens.push({ value: "Tab", display: "\\t", sequenceIndex, offset });
|
|
79
|
+
offset += 1;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (char !== "<") {
|
|
84
|
+
tokens.push({
|
|
85
|
+
value: char,
|
|
86
|
+
display: toDisplayToken(char),
|
|
87
|
+
sequenceIndex,
|
|
88
|
+
offset,
|
|
89
|
+
});
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const close = sequence.indexOf(">", offset + 1);
|
|
94
|
+
if (close === -1) {
|
|
95
|
+
throw new VimError(`Unterminated special key in sequence ${sequenceIndex + 1}`, {
|
|
96
|
+
value: char,
|
|
97
|
+
display: char,
|
|
98
|
+
sequenceIndex,
|
|
99
|
+
offset,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const rawSpecial = sequence.slice(offset + 1, close);
|
|
104
|
+
const special = normalizeSpecialKey(rawSpecial);
|
|
105
|
+
if (!special) {
|
|
106
|
+
throw new VimError(`Unknown special key <${rawSpecial}> in sequence ${sequenceIndex + 1}`, {
|
|
107
|
+
value: rawSpecial,
|
|
108
|
+
display: `<${rawSpecial}>`,
|
|
109
|
+
sequenceIndex,
|
|
110
|
+
offset,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
tokens.push({
|
|
115
|
+
value: special,
|
|
116
|
+
display: `<${rawSpecial}>`,
|
|
117
|
+
sequenceIndex,
|
|
118
|
+
offset,
|
|
119
|
+
});
|
|
120
|
+
offset = close;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return tokens;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function tokensToReplay(tokens: readonly VimKeyToken[]): string[] {
|
|
128
|
+
return tokens.map(token => token.value);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function replayTokens(values: readonly string[]): VimKeyToken[] {
|
|
132
|
+
return values.map((value, index) => ({
|
|
133
|
+
value,
|
|
134
|
+
display: toDisplayToken(value),
|
|
135
|
+
sequenceIndex: 0,
|
|
136
|
+
offset: index,
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function formatVimError(error: unknown): string {
|
|
141
|
+
if (!(error instanceof VimError)) {
|
|
142
|
+
return error instanceof Error ? error.message : String(error);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const base = error.message;
|
|
146
|
+
if (!error.location) {
|
|
147
|
+
return base;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return `${base} (sequence ${error.location.sequenceIndex + 1}, token ${error.location.offset + 1})`;
|
|
151
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { extractSegments } from "@oh-my-pi/pi-tui";
|
|
2
|
+
import { truncateToWidth } from "../tools/render-utils";
|
|
3
|
+
import type {
|
|
4
|
+
VimErrorLocation,
|
|
5
|
+
VimFocusLine,
|
|
6
|
+
VimMode,
|
|
7
|
+
VimPendingInput,
|
|
8
|
+
VimSelection,
|
|
9
|
+
VimToolDetails,
|
|
10
|
+
VimViewport,
|
|
11
|
+
VimViewportLine,
|
|
12
|
+
} from "./types";
|
|
13
|
+
|
|
14
|
+
export const VIM_OPEN_VIEWPORT_LINES = 80;
|
|
15
|
+
export const VIM_DEFAULT_VIEWPORT_LINES = 10;
|
|
16
|
+
export const VIM_TAB_DISPLAY = "→";
|
|
17
|
+
const VIM_INLINE_CURSOR = "▏";
|
|
18
|
+
|
|
19
|
+
const VIM_VIEWPORT_WIDTH = 140;
|
|
20
|
+
const VIM_FOCUS_WIDTH = 100;
|
|
21
|
+
|
|
22
|
+
interface ViewportRenderInput {
|
|
23
|
+
file: string;
|
|
24
|
+
mode: VimMode;
|
|
25
|
+
cursor: { line: number; col: number };
|
|
26
|
+
totalLines: number;
|
|
27
|
+
modified: boolean;
|
|
28
|
+
lines: string[];
|
|
29
|
+
viewport: VimViewport;
|
|
30
|
+
selection?: VimSelection;
|
|
31
|
+
statusMessage?: string;
|
|
32
|
+
lastCommand?: string;
|
|
33
|
+
pendingInput?: VimPendingInput;
|
|
34
|
+
errorLocation?: VimErrorLocation;
|
|
35
|
+
closed?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function renderHeader(details: Pick<VimToolDetails, "file" | "modified" | "mode" | "cursor" | "totalLines">): string {
|
|
39
|
+
const modified = details.modified ? "[+]" : "[ ]";
|
|
40
|
+
return `${details.file} ${modified} ${details.mode} L${details.cursor.line}:${details.cursor.col} (${details.totalLines} lines)`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function selectionContainsLine(selection: VimSelection | undefined, lineNumber: number): boolean {
|
|
44
|
+
if (!selection) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
return lineNumber >= selection.start.line && lineNumber <= selection.end.line;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function visibleWidthForChar(char: string): number {
|
|
51
|
+
return char === "\t" ? VIM_TAB_DISPLAY.length : Math.max(1, Bun.stringWidth(char));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function renderVisibleText(input: string): string {
|
|
55
|
+
let output = "";
|
|
56
|
+
for (const char of input) {
|
|
57
|
+
output += char === "\t" ? VIM_TAB_DISPLAY : char;
|
|
58
|
+
}
|
|
59
|
+
return output;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function renderedColumnForRawColumn(input: string, rawCol: number): number {
|
|
63
|
+
let column = 0;
|
|
64
|
+
let index = 0;
|
|
65
|
+
for (const char of input) {
|
|
66
|
+
if (index >= rawCol) {
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
column += visibleWidthForChar(char);
|
|
70
|
+
index += 1;
|
|
71
|
+
}
|
|
72
|
+
return column;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function cropVisibleText(text: string, startCol: number, width: number): { text: string; startCol: number } {
|
|
76
|
+
if (text.length <= width) {
|
|
77
|
+
return { text, startCol: 0 };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const maxStart = Math.max(0, text.length - width);
|
|
81
|
+
const clampedStart = Math.max(0, Math.min(startCol, maxStart));
|
|
82
|
+
let window = text.slice(clampedStart, clampedStart + width);
|
|
83
|
+
if (clampedStart > 0 && window.length > 0) {
|
|
84
|
+
window = `…${window.slice(1)}`;
|
|
85
|
+
}
|
|
86
|
+
if (clampedStart + width < text.length && window.length > 0) {
|
|
87
|
+
window = `${window.slice(0, -1)}…`;
|
|
88
|
+
}
|
|
89
|
+
return { text: window, startCol: clampedStart };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function buildFocusLine(lineNumber: number, rawText: string, rawCursorCol: number): VimFocusLine {
|
|
93
|
+
const visibleText = renderVisibleText(rawText);
|
|
94
|
+
const caretCol = renderedColumnForRawColumn(rawText, rawCursorCol);
|
|
95
|
+
const desiredStart = Math.max(0, caretCol - Math.floor(VIM_FOCUS_WIDTH / 2));
|
|
96
|
+
const cropped = cropVisibleText(visibleText, desiredStart, VIM_FOCUS_WIDTH);
|
|
97
|
+
return {
|
|
98
|
+
line: lineNumber,
|
|
99
|
+
text: cropped.text,
|
|
100
|
+
windowStartCol: cropped.startCol + 1,
|
|
101
|
+
windowEndCol: cropped.startCol + cropped.text.length,
|
|
102
|
+
caretCol: Math.max(0, caretCol - cropped.startCol),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function buildViewportLines(
|
|
107
|
+
input: Pick<ViewportRenderInput, "lines" | "viewport" | "cursor" | "selection">,
|
|
108
|
+
): VimViewportLine[] {
|
|
109
|
+
const lines: VimViewportLine[] = [];
|
|
110
|
+
for (let lineNumber = input.viewport.start; lineNumber <= input.viewport.end; lineNumber += 1) {
|
|
111
|
+
const rawText = input.lines[lineNumber - 1] ?? "";
|
|
112
|
+
const visibleText = renderVisibleText(rawText);
|
|
113
|
+
const isCursor = lineNumber === input.cursor.line;
|
|
114
|
+
if (isCursor) {
|
|
115
|
+
const cursorCol = renderedColumnForRawColumn(rawText, input.cursor.col - 1);
|
|
116
|
+
const desiredStart = Math.max(0, cursorCol - Math.floor(VIM_VIEWPORT_WIDTH / 2));
|
|
117
|
+
const cropped = cropVisibleText(visibleText, desiredStart, VIM_VIEWPORT_WIDTH);
|
|
118
|
+
lines.push({
|
|
119
|
+
line: lineNumber,
|
|
120
|
+
text: cropped.text,
|
|
121
|
+
isCursor: true,
|
|
122
|
+
isSelected: selectionContainsLine(input.selection, lineNumber),
|
|
123
|
+
cursorCol: Math.max(0, cursorCol - cropped.startCol),
|
|
124
|
+
});
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
lines.push({
|
|
128
|
+
line: lineNumber,
|
|
129
|
+
text: truncateToWidth(visibleText, VIM_VIEWPORT_WIDTH),
|
|
130
|
+
isCursor: false,
|
|
131
|
+
isSelected: selectionContainsLine(input.selection, lineNumber),
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return lines;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function computeViewport(
|
|
138
|
+
cursorLine: number,
|
|
139
|
+
totalLines: number,
|
|
140
|
+
size: number,
|
|
141
|
+
preferredStart?: number,
|
|
142
|
+
): VimViewport {
|
|
143
|
+
const lineCount = Math.max(totalLines, 1);
|
|
144
|
+
const clampedSize = Math.max(1, Math.min(size, lineCount));
|
|
145
|
+
const maxStart = Math.max(1, lineCount - clampedSize + 1);
|
|
146
|
+
const centered = Math.max(1, Math.min(cursorLine - Math.floor(clampedSize / 2), maxStart));
|
|
147
|
+
let start = preferredStart ? Math.max(1, Math.min(preferredStart, maxStart)) : centered;
|
|
148
|
+
const end = Math.min(lineCount, start + clampedSize - 1);
|
|
149
|
+
if (cursorLine < start) {
|
|
150
|
+
start = cursorLine;
|
|
151
|
+
}
|
|
152
|
+
if (cursorLine > end) {
|
|
153
|
+
start = Math.max(1, cursorLine - clampedSize + 1);
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
start,
|
|
157
|
+
end: Math.min(lineCount, start + clampedSize - 1),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function formatPendingInput(pending: VimPendingInput | undefined): string | undefined {
|
|
162
|
+
if (!pending) {
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
if (pending.kind === "insert") {
|
|
166
|
+
return "Pending: INSERT mode";
|
|
167
|
+
}
|
|
168
|
+
const prefix = pending.kind === "command" ? ":" : pending.kind === "search-forward" ? "/" : "?";
|
|
169
|
+
return `Pending: ${prefix}${truncateToWidth(renderVisibleText(pending.text), 80)}`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function renderPlainViewportCursor(line: VimViewportLine): string {
|
|
173
|
+
if (!line.isCursor || line.cursorCol === undefined) {
|
|
174
|
+
return line.text;
|
|
175
|
+
}
|
|
176
|
+
const totalWidth = Bun.stringWidth(line.text);
|
|
177
|
+
const cursorCol = Math.max(0, Math.min(line.cursorCol, totalWidth));
|
|
178
|
+
const segments = extractSegments(line.text, cursorCol, cursorCol, Math.max(0, totalWidth - cursorCol), true);
|
|
179
|
+
return `${segments.before}${VIM_INLINE_CURSOR}${segments.after}`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function renderVimDetails(details: VimToolDetails): string {
|
|
183
|
+
const lines: string[] = [renderHeader(details)];
|
|
184
|
+
|
|
185
|
+
// Explicit cursor position indicator (models miss it in header)
|
|
186
|
+
lines.push(`[CURSOR] Line ${details.cursor.line}, Column ${details.cursor.col} (of ${details.totalLines} lines)`);
|
|
187
|
+
|
|
188
|
+
if (details.lastCommand) {
|
|
189
|
+
lines.push(`Command: ${truncateToWidth(details.lastCommand, 80)}`);
|
|
190
|
+
}
|
|
191
|
+
if (details.statusMessage) {
|
|
192
|
+
lines.push(`Status: ${details.statusMessage}`);
|
|
193
|
+
}
|
|
194
|
+
if (details.errorLocation) {
|
|
195
|
+
lines.push(
|
|
196
|
+
`Error location: sequence ${details.errorLocation.sequenceIndex + 1}, token ${details.errorLocation.offset + 1}`,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const pending = formatPendingInput(details.pendingInput);
|
|
201
|
+
if (pending) {
|
|
202
|
+
lines.push(pending);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (details.closed) {
|
|
206
|
+
return lines.join("\n");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (details.focus) {
|
|
210
|
+
const focusPrefix = `>${String(details.focus.line).padStart(String(details.viewport.end).length, " ")}│`;
|
|
211
|
+
const caretPrefix = `${" ".repeat(focusPrefix.length)} `;
|
|
212
|
+
const caretPadding = " ".repeat(Math.max(0, details.focus.caretCol));
|
|
213
|
+
lines.push("Focus:");
|
|
214
|
+
lines.push(`${focusPrefix}${details.focus.text}`);
|
|
215
|
+
lines.push(`${caretPrefix}${caretPadding}^`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (details.viewportLines && details.viewportLines.length > 0) {
|
|
219
|
+
const padWidth = String(details.viewport.end).length;
|
|
220
|
+
lines.push("Viewport:");
|
|
221
|
+
for (const line of details.viewportLines) {
|
|
222
|
+
const prefix = line.isCursor ? ">" : line.isSelected ? "*" : " ";
|
|
223
|
+
lines.push(`${prefix}${String(line.line).padStart(padWidth, " ")}│${renderPlainViewportCursor(line)}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return lines.join("\n");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function buildDetails(input: ViewportRenderInput): VimToolDetails {
|
|
231
|
+
const details: VimToolDetails = {
|
|
232
|
+
file: input.file,
|
|
233
|
+
mode: input.mode,
|
|
234
|
+
cursor: input.cursor,
|
|
235
|
+
totalLines: input.totalLines,
|
|
236
|
+
modified: input.modified,
|
|
237
|
+
viewport: input.viewport,
|
|
238
|
+
selection: input.selection,
|
|
239
|
+
lastCommand: input.lastCommand,
|
|
240
|
+
statusMessage: input.statusMessage,
|
|
241
|
+
pendingInput: input.pendingInput,
|
|
242
|
+
errorLocation: input.errorLocation,
|
|
243
|
+
closed: input.closed,
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
if (!input.closed) {
|
|
247
|
+
details.focus = buildFocusLine(input.cursor.line, input.lines[input.cursor.line - 1] ?? "", input.cursor.col - 1);
|
|
248
|
+
details.viewportLines = buildViewportLines(input);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return details;
|
|
252
|
+
}
|
package/src/vim/types.ts
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import type { FileDiagnosticsResult } from "../lsp";
|
|
2
|
+
import type { OutputMeta } from "../tools/output-meta";
|
|
3
|
+
|
|
4
|
+
export type VimMode = "NORMAL" | "INSERT" | "VISUAL" | "VISUAL-LINE" | "COMMAND";
|
|
5
|
+
|
|
6
|
+
export type VimInputMode =
|
|
7
|
+
| "normal"
|
|
8
|
+
| "insert"
|
|
9
|
+
| "visual"
|
|
10
|
+
| "visual-line"
|
|
11
|
+
| "command"
|
|
12
|
+
| "search-forward"
|
|
13
|
+
| "search-backward";
|
|
14
|
+
|
|
15
|
+
export interface Position {
|
|
16
|
+
line: number;
|
|
17
|
+
col: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface VimViewport {
|
|
21
|
+
start: number;
|
|
22
|
+
end: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface VimSelection {
|
|
26
|
+
kind: "char" | "line";
|
|
27
|
+
start: Position;
|
|
28
|
+
end: Position;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface VimFocusLine {
|
|
32
|
+
line: number;
|
|
33
|
+
text: string;
|
|
34
|
+
windowStartCol: number;
|
|
35
|
+
windowEndCol: number;
|
|
36
|
+
caretCol: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface VimViewportLine {
|
|
40
|
+
line: number;
|
|
41
|
+
text: string;
|
|
42
|
+
isCursor: boolean;
|
|
43
|
+
isSelected: boolean;
|
|
44
|
+
cursorCol?: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface VimPendingInput {
|
|
48
|
+
kind: "insert" | "command" | "search-forward" | "search-backward";
|
|
49
|
+
text: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface VimErrorLocation {
|
|
53
|
+
sequenceIndex: number;
|
|
54
|
+
offset: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface VimToolDetails {
|
|
58
|
+
file: string;
|
|
59
|
+
mode: VimMode;
|
|
60
|
+
cursor: { line: number; col: number };
|
|
61
|
+
totalLines: number;
|
|
62
|
+
modified: boolean;
|
|
63
|
+
viewport: VimViewport;
|
|
64
|
+
focus?: VimFocusLine;
|
|
65
|
+
viewportLines?: VimViewportLine[];
|
|
66
|
+
selection?: VimSelection;
|
|
67
|
+
pendingInput?: VimPendingInput;
|
|
68
|
+
errorLocation?: VimErrorLocation;
|
|
69
|
+
closed?: boolean;
|
|
70
|
+
meta?: OutputMeta;
|
|
71
|
+
lastCommand?: string;
|
|
72
|
+
statusMessage?: string;
|
|
73
|
+
diagnostics?: FileDiagnosticsResult;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface VimFingerprint {
|
|
77
|
+
exists: boolean;
|
|
78
|
+
size: number;
|
|
79
|
+
mtimeMs: number;
|
|
80
|
+
hash: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface VimLoadedFile {
|
|
84
|
+
absolutePath: string;
|
|
85
|
+
displayPath: string;
|
|
86
|
+
lines: string[];
|
|
87
|
+
trailingNewline: boolean;
|
|
88
|
+
fingerprint: VimFingerprint | null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface VimKeyToken {
|
|
92
|
+
value: string;
|
|
93
|
+
display: string;
|
|
94
|
+
sequenceIndex: number;
|
|
95
|
+
offset: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface VimRegister {
|
|
99
|
+
kind: "char" | "line";
|
|
100
|
+
text: string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface VimSearchState {
|
|
104
|
+
pattern: string;
|
|
105
|
+
direction: 1 | -1;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface VimBufferSnapshot {
|
|
109
|
+
displayPath: string;
|
|
110
|
+
filePath: string;
|
|
111
|
+
lines: string[];
|
|
112
|
+
cursor: Position;
|
|
113
|
+
modified: boolean;
|
|
114
|
+
trailingNewline: boolean;
|
|
115
|
+
baseFingerprint: VimFingerprint | null;
|
|
116
|
+
editabilityChecked: boolean;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface VimUndoEntry {
|
|
120
|
+
before: VimBufferSnapshot;
|
|
121
|
+
after: VimBufferSnapshot;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface VimLineRange {
|
|
125
|
+
start: number;
|
|
126
|
+
end: number;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export type VimExCommand =
|
|
130
|
+
| { kind: "write"; force: boolean }
|
|
131
|
+
| { kind: "update"; force: boolean }
|
|
132
|
+
| { kind: "quit"; force: boolean }
|
|
133
|
+
| { kind: "write-quit"; force: boolean }
|
|
134
|
+
| { kind: "edit"; force: boolean; path?: string }
|
|
135
|
+
| { kind: "goto-line"; line: number }
|
|
136
|
+
| { kind: "substitute"; range?: VimLineRange | "all"; pattern: string; replacement: string; flags: string }
|
|
137
|
+
| { kind: "delete"; range?: VimLineRange | "all" }
|
|
138
|
+
| { kind: "yank"; range?: VimLineRange | "all" }
|
|
139
|
+
| { kind: "put"; range?: VimLineRange | "all"; before: boolean }
|
|
140
|
+
| { kind: "copy"; range?: VimLineRange | "all"; destination: number }
|
|
141
|
+
| { kind: "move"; range?: VimLineRange | "all"; destination: number }
|
|
142
|
+
| { kind: "sort"; range?: VimLineRange | "all"; flags: string }
|
|
143
|
+
| { kind: "join"; range?: VimLineRange | "all"; trimWhitespace: boolean }
|
|
144
|
+
| { kind: "global"; range?: VimLineRange | "all"; pattern: string; command: string; invert: boolean }
|
|
145
|
+
| { kind: "append"; range?: VimLineRange; text: string }
|
|
146
|
+
| { kind: "insert-before"; range?: VimLineRange; text: string };
|
|
147
|
+
|
|
148
|
+
export class VimInputError extends Error {
|
|
149
|
+
location?: { sequenceIndex: number; offset: number };
|
|
150
|
+
|
|
151
|
+
constructor(message: string, token?: VimKeyToken) {
|
|
152
|
+
super(message);
|
|
153
|
+
this.name = "VimInputError";
|
|
154
|
+
if (token) {
|
|
155
|
+
this.location = {
|
|
156
|
+
sequenceIndex: token.sequenceIndex,
|
|
157
|
+
offset: token.offset,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function clonePosition(position: Position): Position {
|
|
164
|
+
return { line: position.line, col: position.col };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function comparePositions(left: Position, right: Position): number {
|
|
168
|
+
if (left.line !== right.line) {
|
|
169
|
+
return left.line - right.line;
|
|
170
|
+
}
|
|
171
|
+
return left.col - right.col;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function minPosition(left: Position, right: Position): Position {
|
|
175
|
+
return comparePositions(left, right) <= 0 ? clonePosition(left) : clonePosition(right);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function maxPosition(left: Position, right: Position): Position {
|
|
179
|
+
return comparePositions(left, right) >= 0 ? clonePosition(left) : clonePosition(right);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function toPublicMode(mode: VimInputMode): VimMode {
|
|
183
|
+
switch (mode) {
|
|
184
|
+
case "insert":
|
|
185
|
+
return "INSERT";
|
|
186
|
+
case "visual":
|
|
187
|
+
return "VISUAL";
|
|
188
|
+
case "visual-line":
|
|
189
|
+
return "VISUAL-LINE";
|
|
190
|
+
case "command":
|
|
191
|
+
case "search-forward":
|
|
192
|
+
case "search-backward":
|
|
193
|
+
return "COMMAND";
|
|
194
|
+
default:
|
|
195
|
+
return "NORMAL";
|
|
196
|
+
}
|
|
197
|
+
}
|