@involvex/fresh-editor 0.1.76 → 0.1.78

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.
Files changed (90) hide show
  1. package/bin/CHANGELOG.md +1017 -0
  2. package/bin/LICENSE +117 -0
  3. package/bin/README.md +248 -0
  4. package/bin/fresh.exe +0 -0
  5. package/bin/plugins/README.md +71 -0
  6. package/bin/plugins/audit_mode.i18n.json +821 -0
  7. package/bin/plugins/audit_mode.ts +1810 -0
  8. package/bin/plugins/buffer_modified.i18n.json +67 -0
  9. package/bin/plugins/buffer_modified.ts +281 -0
  10. package/bin/plugins/calculator.i18n.json +93 -0
  11. package/bin/plugins/calculator.ts +770 -0
  12. package/bin/plugins/clangd-lsp.ts +168 -0
  13. package/bin/plugins/clangd_support.i18n.json +223 -0
  14. package/bin/plugins/clangd_support.md +20 -0
  15. package/bin/plugins/clangd_support.ts +325 -0
  16. package/bin/plugins/color_highlighter.i18n.json +145 -0
  17. package/bin/plugins/color_highlighter.ts +304 -0
  18. package/bin/plugins/config-schema.json +768 -0
  19. package/bin/plugins/csharp-lsp.ts +147 -0
  20. package/bin/plugins/csharp_support.i18n.json +80 -0
  21. package/bin/plugins/csharp_support.ts +170 -0
  22. package/bin/plugins/css-lsp.ts +143 -0
  23. package/bin/plugins/diagnostics_panel.i18n.json +236 -0
  24. package/bin/plugins/diagnostics_panel.ts +642 -0
  25. package/bin/plugins/examples/README.md +85 -0
  26. package/bin/plugins/examples/async_demo.ts +165 -0
  27. package/bin/plugins/examples/bookmarks.ts +329 -0
  28. package/bin/plugins/examples/buffer_query_demo.ts +110 -0
  29. package/bin/plugins/examples/git_grep.ts +262 -0
  30. package/bin/plugins/examples/hello_world.ts +93 -0
  31. package/bin/plugins/examples/virtual_buffer_demo.ts +116 -0
  32. package/bin/plugins/find_references.i18n.json +275 -0
  33. package/bin/plugins/find_references.ts +359 -0
  34. package/bin/plugins/git_blame.i18n.json +496 -0
  35. package/bin/plugins/git_blame.ts +707 -0
  36. package/bin/plugins/git_find_file.i18n.json +314 -0
  37. package/bin/plugins/git_find_file.ts +300 -0
  38. package/bin/plugins/git_grep.i18n.json +171 -0
  39. package/bin/plugins/git_grep.ts +191 -0
  40. package/bin/plugins/git_gutter.i18n.json +93 -0
  41. package/bin/plugins/git_gutter.ts +477 -0
  42. package/bin/plugins/git_log.i18n.json +481 -0
  43. package/bin/plugins/git_log.ts +1285 -0
  44. package/bin/plugins/go-lsp.ts +143 -0
  45. package/bin/plugins/html-lsp.ts +145 -0
  46. package/bin/plugins/json-lsp.ts +145 -0
  47. package/bin/plugins/lib/fresh.d.ts +1321 -0
  48. package/bin/plugins/lib/index.ts +24 -0
  49. package/bin/plugins/lib/navigation-controller.ts +214 -0
  50. package/bin/plugins/lib/panel-manager.ts +220 -0
  51. package/bin/plugins/lib/types.ts +72 -0
  52. package/bin/plugins/lib/virtual-buffer-factory.ts +130 -0
  53. package/bin/plugins/live_grep.i18n.json +171 -0
  54. package/bin/plugins/live_grep.ts +422 -0
  55. package/bin/plugins/markdown_compose.i18n.json +223 -0
  56. package/bin/plugins/markdown_compose.ts +630 -0
  57. package/bin/plugins/merge_conflict.i18n.json +821 -0
  58. package/bin/plugins/merge_conflict.ts +1810 -0
  59. package/bin/plugins/path_complete.i18n.json +80 -0
  60. package/bin/plugins/path_complete.ts +165 -0
  61. package/bin/plugins/python-lsp.ts +162 -0
  62. package/bin/plugins/rust-lsp.ts +166 -0
  63. package/bin/plugins/search_replace.i18n.json +405 -0
  64. package/bin/plugins/search_replace.ts +484 -0
  65. package/bin/plugins/test_i18n.i18n.json +67 -0
  66. package/bin/plugins/test_i18n.ts +18 -0
  67. package/bin/plugins/theme_editor.i18n.json +3746 -0
  68. package/bin/plugins/theme_editor.ts +2063 -0
  69. package/bin/plugins/todo_highlighter.i18n.json +184 -0
  70. package/bin/plugins/todo_highlighter.ts +206 -0
  71. package/bin/plugins/typescript-lsp.ts +167 -0
  72. package/bin/plugins/vi_mode.i18n.json +1549 -0
  73. package/bin/plugins/vi_mode.ts +2747 -0
  74. package/bin/plugins/welcome.i18n.json +236 -0
  75. package/bin/plugins/welcome.ts +76 -0
  76. package/bin/themes/dark.json +102 -0
  77. package/bin/themes/dracula.json +62 -0
  78. package/bin/themes/high-contrast.json +102 -0
  79. package/bin/themes/light.json +102 -0
  80. package/bin/themes/nord.json +62 -0
  81. package/bin/themes/nostalgia.json +102 -0
  82. package/bin/themes/solarized-dark.json +62 -0
  83. package/binary-install.js +1 -1
  84. package/dist/bin/fresh.js +9 -0
  85. package/dist/binary-install.js +149 -0
  86. package/dist/binary.js +30 -0
  87. package/dist/fresh-6yhknp07.exe +0 -0
  88. package/dist/install.js +158 -0
  89. package/dist/run-fresh.js +43 -0
  90. package/package.json +7 -2
@@ -0,0 +1,770 @@
1
+ /// <reference path="../types/fresh.d.ts" />
2
+ const editor = getEditor();
3
+
4
+
5
+ /**
6
+ * Calculator Plugin for Fresh Editor
7
+ *
8
+ * A sleek visual calculator with:
9
+ * - Mouse-clickable buttons (anywhere in button area)
10
+ * - Keyboard input support
11
+ * - Expression parsing with parentheses and basic arithmetic
12
+ * - Modern calculator styling with ANSI colors
13
+ * - Compact fixed-size layout centered in view
14
+ */
15
+
16
+ // ANSI color codes
17
+ const C = {
18
+ RESET: "\x1b[0m",
19
+ BOLD: "\x1b[1m",
20
+ DIM: "\x1b[2m",
21
+ // Colors
22
+ RED: "\x1b[31m",
23
+ GREEN: "\x1b[32m",
24
+ YELLOW: "\x1b[33m",
25
+ BLUE: "\x1b[34m",
26
+ MAGENTA: "\x1b[35m",
27
+ CYAN: "\x1b[36m",
28
+ WHITE: "\x1b[37m",
29
+ BRIGHT_RED: "\x1b[91m",
30
+ BRIGHT_GREEN: "\x1b[92m",
31
+ BRIGHT_YELLOW: "\x1b[93m",
32
+ BRIGHT_BLUE: "\x1b[94m",
33
+ BRIGHT_MAGENTA: "\x1b[95m",
34
+ BRIGHT_CYAN: "\x1b[96m",
35
+ // Backgrounds
36
+ BG_BLACK: "\x1b[40m",
37
+ BG_RED: "\x1b[41m",
38
+ BG_GREEN: "\x1b[42m",
39
+ BG_YELLOW: "\x1b[43m",
40
+ BG_BLUE: "\x1b[44m",
41
+ BG_MAGENTA: "\x1b[45m",
42
+ BG_CYAN: "\x1b[46m",
43
+ BG_WHITE: "\x1b[47m",
44
+ BG_BRIGHT_BLACK: "\x1b[100m",
45
+ };
46
+
47
+ // Calculator state
48
+ interface CalculatorState {
49
+ expression: string;
50
+ result: string;
51
+ error: string;
52
+ bufferId: number;
53
+ splitId: number;
54
+ lastViewport: ViewportInfo | null;
55
+ }
56
+
57
+ const state: CalculatorState = {
58
+ expression: "",
59
+ result: "0",
60
+ error: "",
61
+ bufferId: 0,
62
+ splitId: 0,
63
+ lastViewport: null,
64
+ };
65
+
66
+ // Cache the layout so it doesn't jump around
67
+ let cachedLayout: LayoutMetrics | null = null;
68
+
69
+ // Track hovered button for visual feedback
70
+ let hoveredButton: { row: number; col: number } | null = null;
71
+
72
+ // Track if copy button is hovered
73
+ let copyButtonHovered = false;
74
+
75
+ // Button definitions
76
+ interface Button {
77
+ label: string;
78
+ action: string;
79
+ type: "number" | "operator" | "function" | "clear" | "equals";
80
+ }
81
+
82
+ const BUTTON_LAYOUT: Button[][] = [
83
+ [
84
+ { label: "C", action: "clear", type: "clear" },
85
+ { label: "(", action: "(", type: "function" },
86
+ { label: ")", action: ")", type: "function" },
87
+ { label: "^", action: "^", type: "operator" },
88
+ { label: "÷", action: "/", type: "operator" },
89
+ ],
90
+ [
91
+ { label: "sqrt", action: "sqrt(", type: "function" },
92
+ { label: "ln", action: "ln(", type: "function" },
93
+ { label: "log", action: "log(", type: "function" },
94
+ { label: "π", action: "pi", type: "number" },
95
+ { label: "×", action: "*", type: "operator" },
96
+ ],
97
+ [
98
+ { label: "sin", action: "sin(", type: "function" },
99
+ { label: "cos", action: "cos(", type: "function" },
100
+ { label: "tan", action: "tan(", type: "function" },
101
+ { label: "e", action: "e", type: "number" },
102
+ { label: "-", action: "-", type: "operator" },
103
+ ],
104
+ [
105
+ { label: "7", action: "7", type: "number" },
106
+ { label: "8", action: "8", type: "number" },
107
+ { label: "9", action: "9", type: "number" },
108
+ { label: "⌫", action: "backspace", type: "clear" },
109
+ { label: "+", action: "+", type: "operator" },
110
+ ],
111
+ [
112
+ { label: "4", action: "4", type: "number" },
113
+ { label: "5", action: "5", type: "number" },
114
+ { label: "6", action: "6", type: "number" },
115
+ { label: "±", action: "negate", type: "function" },
116
+ { label: "=", action: "equals", type: "equals" },
117
+ ],
118
+ [
119
+ { label: "1", action: "1", type: "number" },
120
+ { label: "2", action: "2", type: "number" },
121
+ { label: "3", action: "3", type: "number" },
122
+ { label: "0", action: "0", type: "number" },
123
+ { label: ".", action: ".", type: "number" },
124
+ ],
125
+ ];
126
+
127
+ // Fixed layout constants
128
+ const BUTTON_WIDTH = 5;
129
+ const NUM_COLS = 5;
130
+ const NUM_ROWS = 6;
131
+ const CALC_WIDTH = BUTTON_WIDTH * NUM_COLS + 1; // 26 chars
132
+ const DISPLAY_LINES = 2;
133
+
134
+ // Get color for button type (with optional hover highlight)
135
+ function getButtonColor(type: Button["type"], isHovered: boolean): string {
136
+ if (isHovered) {
137
+ // Bright/inverted colors for hover
138
+ return C.BG_WHITE + "\x1b[30m"; // White background, black text
139
+ }
140
+ switch (type) {
141
+ case "number": return C.WHITE;
142
+ case "operator": return C.BRIGHT_YELLOW;
143
+ case "function": return C.BRIGHT_CYAN;
144
+ case "clear": return C.BRIGHT_RED;
145
+ case "equals": return C.BRIGHT_GREEN;
146
+ default: return C.WHITE;
147
+ }
148
+ }
149
+
150
+ // Layout metrics
151
+ interface LayoutMetrics {
152
+ startX: number;
153
+ startY: number;
154
+ }
155
+
156
+ function calculateLayout(_viewport: ViewportInfo): LayoutMetrics {
157
+ // Position at top-left with 1 row/column gap
158
+ const startX = 1;
159
+ const startY = 1;
160
+
161
+ return { startX, startY };
162
+ }
163
+
164
+ // Render the calculator with ANSI colors
165
+ function renderCalculator(): TextPropertyEntry[] {
166
+ const viewport = editor.getViewport();
167
+ if (!viewport) {
168
+ return [{ text: "No viewport\n", properties: {} }];
169
+ }
170
+
171
+ state.lastViewport = viewport;
172
+
173
+ // Use cached layout to prevent jumping, or calculate new one
174
+ if (!cachedLayout) {
175
+ cachedLayout = calculateLayout(viewport);
176
+ }
177
+ const layout = cachedLayout;
178
+ const entries: TextPropertyEntry[] = [];
179
+
180
+ const addLine = (text: string): void => {
181
+ entries.push({ text: text + "\n", properties: {} });
182
+ };
183
+
184
+ // Top margin
185
+ for (let i = 0; i < layout.startY; i++) {
186
+ addLine("");
187
+ }
188
+
189
+ const pad = " ".repeat(layout.startX);
190
+
191
+ // Unicode box drawing chars
192
+ const TL = "╭", TR = "╮", BL = "╰", BR = "╯";
193
+ const V = "│";
194
+ const LT = "├", RT = "┤", X = "┼";
195
+
196
+ // Generate border patterns dynamically
197
+ const cellWidth = BUTTON_WIDTH - 1; // 4 dashes per cell
198
+ const topBorder = TL + "─".repeat(CALC_WIDTH - 2) + TR;
199
+ const sepTop = LT + Array(NUM_COLS).fill("─".repeat(cellWidth)).join("┬") + RT;
200
+ const sepMid = LT + Array(NUM_COLS).fill("─".repeat(cellWidth)).join("┼") + RT;
201
+ const sepBot = BL + Array(NUM_COLS).fill("─".repeat(cellWidth)).join("┴") + BR;
202
+
203
+ // Display - top border
204
+ addLine(`${pad}${C.CYAN}${topBorder}${C.RESET}`);
205
+
206
+ // Expression line
207
+ let expr = state.expression || "";
208
+ const maxLen = CALC_WIDTH - 4;
209
+ if (expr.length > maxLen) expr = expr.slice(-maxLen);
210
+ addLine(`${pad}${C.CYAN}${V}${C.RESET} ${C.BRIGHT_GREEN}${expr.padStart(maxLen)}${C.RESET} ${C.CYAN}${V}${C.RESET}`);
211
+
212
+ // Result line with copy button on left - slightly different background
213
+ let result = state.error || state.result;
214
+ const copyBtnWidth = 6; // "Copy" + 2 spaces
215
+ const resultMaxLen = maxLen - copyBtnWidth;
216
+ if (result.length > resultMaxLen) result = result.slice(0, resultMaxLen);
217
+ const resultColor = state.error ? C.BRIGHT_RED : C.BRIGHT_GREEN;
218
+ const copyBtnColor = copyButtonHovered ? (C.BG_WHITE + "\x1b[30m") : (C.BG_BRIGHT_BLACK + C.BRIGHT_MAGENTA);
219
+ const resultBg = C.BG_BRIGHT_BLACK;
220
+ addLine(`${pad}${C.CYAN}${V}${C.RESET}${copyBtnColor}Copy${C.RESET}${resultBg} ${C.BOLD}${resultColor}${result.padStart(resultMaxLen)}${C.RESET}${resultBg} ${C.RESET}${C.CYAN}${V}${C.RESET}`);
221
+
222
+ // Separator between display and buttons
223
+ addLine(`${pad}${C.CYAN}${sepTop}${C.RESET}`);
224
+
225
+ // Button rows
226
+ for (let rowIdx = 0; rowIdx < BUTTON_LAYOUT.length; rowIdx++) {
227
+ const buttonRow = BUTTON_LAYOUT[rowIdx];
228
+ let line = `${pad}${C.CYAN}${V}${C.RESET}`;
229
+
230
+ for (let colIdx = 0; colIdx < buttonRow.length; colIdx++) {
231
+ const btn = buttonRow[colIdx];
232
+ const isHovered = hoveredButton?.row === rowIdx && hoveredButton?.col === colIdx;
233
+ const color = getButtonColor(btn.type, isHovered);
234
+ const label = btn.label;
235
+ const innerWidth = BUTTON_WIDTH - 1;
236
+ const leftSpace = Math.floor((innerWidth - label.length) / 2);
237
+ const rightSpace = innerWidth - label.length - leftSpace;
238
+ line += `${color}${C.BOLD}${" ".repeat(leftSpace)}${label}${" ".repeat(rightSpace)}${C.RESET}${C.CYAN}${V}${C.RESET}`;
239
+ }
240
+
241
+ addLine(line);
242
+
243
+ // Row separator (except after last row)
244
+ if (rowIdx < BUTTON_LAYOUT.length - 1) {
245
+ addLine(`${pad}${C.CYAN}${sepMid}${C.RESET}`);
246
+ }
247
+ }
248
+
249
+ // Bottom border
250
+ addLine(`${pad}${C.CYAN}${sepBot}${C.RESET}`);
251
+
252
+ // Help line
253
+ addLine("");
254
+ addLine(`${pad}${C.DIM} Esc:close =/Enter:calc Del:clear${C.RESET}`);
255
+
256
+ return entries;
257
+ }
258
+
259
+ // Check if click is on copy button (returns true if on copy button)
260
+ function isCopyButtonAt(contentCol: number, contentRow: number): boolean {
261
+ if (!cachedLayout) return false;
262
+
263
+ // Copy button is on result line (row 2 after top margin)
264
+ const resultLineY = cachedLayout.startY + 2; // top border + expression line
265
+ const copyBtnStartX = cachedLayout.startX + 1; // after left border
266
+ const copyBtnEndX = copyBtnStartX + 4; // "Copy" is 4 chars
267
+
268
+ return contentRow === resultLineY &&
269
+ contentCol >= copyBtnStartX &&
270
+ contentCol < copyBtnEndX;
271
+ }
272
+
273
+ // Copy result to clipboard
274
+ function copyResultToClipboard(): void {
275
+ const textToCopy = state.error || state.result;
276
+ editor.copyToClipboard(textToCopy);
277
+ editor.setStatus(editor.t("status.copied", { value: textToCopy }));
278
+ }
279
+
280
+ // Get button position at content-relative coordinates
281
+ function getButtonPosition(contentCol: number, contentRow: number): { row: number; col: number } | null {
282
+ if (!cachedLayout) return null;
283
+
284
+ // Button area starts after: marginY + display(2 lines) + borders(2)
285
+ const buttonAreaStartY = cachedLayout.startY + DISPLAY_LINES + 2;
286
+ const buttonAreaStartX = cachedLayout.startX + 1; // +1 for left border
287
+
288
+ const relY = contentRow - buttonAreaStartY;
289
+ const relX = contentCol - buttonAreaStartX;
290
+
291
+ if (relX < 0 || relY < 0) return null;
292
+ if (relX >= BUTTON_WIDTH * NUM_COLS) return null;
293
+
294
+ // Check if on horizontal separator line (odd rows are separators)
295
+ if (relY % 2 === 1) return null;
296
+
297
+ // Check if on vertical border (every BUTTON_WIDTH chars, minus 1 for the separator)
298
+ const posInButton = relX % BUTTON_WIDTH;
299
+ if (posInButton === BUTTON_WIDTH - 1) return null; // On the | border
300
+
301
+ // Each button row = 2 lines (content + separator)
302
+ const buttonRowIdx = Math.floor(relY / 2);
303
+ if (buttonRowIdx < 0 || buttonRowIdx >= NUM_ROWS) return null;
304
+
305
+ // Column
306
+ const buttonColIdx = Math.floor(relX / BUTTON_WIDTH);
307
+ if (buttonColIdx < 0 || buttonColIdx >= NUM_COLS) return null;
308
+
309
+ return { row: buttonRowIdx, col: buttonColIdx };
310
+ }
311
+
312
+ // Get button at content-relative position
313
+ function getButtonAt(contentCol: number, contentRow: number): Button | null {
314
+ const pos = getButtonPosition(contentCol, contentRow);
315
+ if (!pos) return null;
316
+ return BUTTON_LAYOUT[pos.row][pos.col];
317
+ }
318
+
319
+ // Expression parser
320
+ interface Token {
321
+ type: "number" | "operator" | "lparen" | "rparen" | "function" | "constant";
322
+ value: string | number;
323
+ }
324
+
325
+ // Known functions and constants
326
+ const FUNCTIONS = ["sqrt", "ln", "log", "sin", "cos", "tan", "asin", "acos", "atan", "abs"];
327
+ const CONSTANTS: Record<string, number> = {
328
+ pi: Math.PI,
329
+ e: Math.E,
330
+ };
331
+
332
+ function tokenize(expr: string): Token[] {
333
+ const tokens: Token[] = [];
334
+ let i = 0;
335
+
336
+ while (i < expr.length) {
337
+ const ch = expr[i];
338
+
339
+ if (/\s/.test(ch)) { i++; continue; }
340
+
341
+ // Numbers
342
+ if (/[0-9.]/.test(ch)) {
343
+ let num = "";
344
+ while (i < expr.length && /[0-9.]/.test(expr[i])) {
345
+ num += expr[i];
346
+ i++;
347
+ }
348
+ tokens.push({ type: "number", value: parseFloat(num) });
349
+ continue;
350
+ }
351
+
352
+ // Identifiers (functions and constants)
353
+ if (/[a-zA-Z]/.test(ch)) {
354
+ let ident = "";
355
+ while (i < expr.length && /[a-zA-Z0-9]/.test(expr[i])) {
356
+ ident += expr[i];
357
+ i++;
358
+ }
359
+ if (FUNCTIONS.includes(ident)) {
360
+ tokens.push({ type: "function", value: ident });
361
+ } else if (ident in CONSTANTS) {
362
+ tokens.push({ type: "constant", value: ident });
363
+ } else {
364
+ throw new Error(`Unknown: ${ident}`);
365
+ }
366
+ continue;
367
+ }
368
+
369
+ if (ch === "(") { tokens.push({ type: "lparen", value: "(" }); i++; continue; }
370
+ if (ch === ")") { tokens.push({ type: "rparen", value: ")" }); i++; continue; }
371
+ if (/[+\-*/^]/.test(ch)) { tokens.push({ type: "operator", value: ch }); i++; continue; }
372
+
373
+ i++;
374
+ }
375
+
376
+ return tokens;
377
+ }
378
+
379
+ // Precedence: + - < * / < ^ < unary - < functions
380
+ function parseExpression(tokens: Token[], pos: { idx: number }): number {
381
+ let left = parseTerm(tokens, pos);
382
+
383
+ while (pos.idx < tokens.length) {
384
+ const token = tokens[pos.idx];
385
+ if (token.type === "operator" && (token.value === "+" || token.value === "-")) {
386
+ pos.idx++;
387
+ const right = parseTerm(tokens, pos);
388
+ left = token.value === "+" ? left + right : left - right;
389
+ } else {
390
+ break;
391
+ }
392
+ }
393
+
394
+ return left;
395
+ }
396
+
397
+ function parseTerm(tokens: Token[], pos: { idx: number }): number {
398
+ let left = parsePower(tokens, pos);
399
+
400
+ while (pos.idx < tokens.length) {
401
+ const token = tokens[pos.idx];
402
+ if (token.type === "operator" && (token.value === "*" || token.value === "/")) {
403
+ pos.idx++;
404
+ const right = parsePower(tokens, pos);
405
+ if (token.value === "*") {
406
+ left = left * right;
407
+ } else {
408
+ if (right === 0) throw new Error("Div by 0");
409
+ left = left / right;
410
+ }
411
+ } else {
412
+ break;
413
+ }
414
+ }
415
+
416
+ return left;
417
+ }
418
+
419
+ function parsePower(tokens: Token[], pos: { idx: number }): number {
420
+ const base = parseUnary(tokens, pos);
421
+
422
+ if (pos.idx < tokens.length && tokens[pos.idx].type === "operator" && tokens[pos.idx].value === "^") {
423
+ pos.idx++;
424
+ const exp = parsePower(tokens, pos); // Right associative
425
+ return Math.pow(base, exp);
426
+ }
427
+
428
+ return base;
429
+ }
430
+
431
+ function parseUnary(tokens: Token[], pos: { idx: number }): number {
432
+ if (pos.idx >= tokens.length) throw new Error("Unexpected end");
433
+
434
+ const token = tokens[pos.idx];
435
+
436
+ if (token.type === "operator" && token.value === "-") {
437
+ pos.idx++;
438
+ return -parseUnary(tokens, pos);
439
+ }
440
+
441
+ return parsePrimary(tokens, pos);
442
+ }
443
+
444
+ function parsePrimary(tokens: Token[], pos: { idx: number }): number {
445
+ if (pos.idx >= tokens.length) throw new Error("Unexpected end");
446
+
447
+ const token = tokens[pos.idx];
448
+
449
+ // Function call
450
+ if (token.type === "function") {
451
+ const fname = token.value as string;
452
+ pos.idx++;
453
+ if (pos.idx >= tokens.length || tokens[pos.idx].type !== "lparen") {
454
+ throw new Error(`Expected ( after ${fname}`);
455
+ }
456
+ pos.idx++; // skip (
457
+ const arg = parseExpression(tokens, pos);
458
+ if (pos.idx >= tokens.length || tokens[pos.idx].type !== "rparen") {
459
+ throw new Error("Missing )");
460
+ }
461
+ pos.idx++; // skip )
462
+
463
+ switch (fname) {
464
+ case "sqrt": return Math.sqrt(arg);
465
+ case "ln": return Math.log(arg);
466
+ case "log": return Math.log10(arg);
467
+ case "sin": return Math.sin(arg);
468
+ case "cos": return Math.cos(arg);
469
+ case "tan": return Math.tan(arg);
470
+ case "asin": return Math.asin(arg);
471
+ case "acos": return Math.acos(arg);
472
+ case "atan": return Math.atan(arg);
473
+ case "abs": return Math.abs(arg);
474
+ default: throw new Error(`Unknown function: ${fname}`);
475
+ }
476
+ }
477
+
478
+ // Constant
479
+ if (token.type === "constant") {
480
+ pos.idx++;
481
+ return CONSTANTS[token.value as string];
482
+ }
483
+
484
+ // Number
485
+ if (token.type === "number") {
486
+ pos.idx++;
487
+ return token.value as number;
488
+ }
489
+
490
+ // Parenthesized expression
491
+ if (token.type === "lparen") {
492
+ pos.idx++;
493
+ const result = parseExpression(tokens, pos);
494
+ if (pos.idx >= tokens.length || tokens[pos.idx].type !== "rparen") {
495
+ throw new Error("Missing )");
496
+ }
497
+ pos.idx++;
498
+ return result;
499
+ }
500
+
501
+ throw new Error("Syntax error");
502
+ }
503
+
504
+ function evaluateExpression(expr: string): string {
505
+ if (!expr.trim()) return "0";
506
+
507
+ const tokens = tokenize(expr);
508
+ if (tokens.length === 0) return "0";
509
+
510
+ const pos = { idx: 0 };
511
+ const result = parseExpression(tokens, pos);
512
+
513
+ if (pos.idx < tokens.length) throw new Error("Syntax error");
514
+
515
+ if (Number.isInteger(result)) {
516
+ return result.toString();
517
+ } else {
518
+ return parseFloat(result.toFixed(10)).toString();
519
+ }
520
+ }
521
+
522
+ // Handle button press
523
+ function handleButton(button: Button): void {
524
+ state.error = "";
525
+
526
+ switch (button.action) {
527
+ case "clear":
528
+ state.expression = "";
529
+ state.result = "0";
530
+ break;
531
+ case "backspace":
532
+ if (state.expression.length > 0) {
533
+ state.expression = state.expression.slice(0, -1);
534
+ }
535
+ break;
536
+ case "negate":
537
+ // Toggle sign: if expression is empty, negate last result; otherwise toggle current number
538
+ if (state.expression === "") {
539
+ // Use negated result as new expression
540
+ if (state.result !== "0") {
541
+ const num = parseFloat(state.result);
542
+ state.expression = (-num).toString();
543
+ state.result = state.expression;
544
+ } else {
545
+ state.expression = "-";
546
+ }
547
+ } else {
548
+ // Try to toggle sign of last number in expression
549
+ const match = state.expression.match(/(-?\d+\.?\d*)$/);
550
+ if (match) {
551
+ const numStr = match[1];
552
+ const prefix = state.expression.slice(0, state.expression.length - numStr.length);
553
+ const num = parseFloat(numStr);
554
+ state.expression = prefix + (-num).toString();
555
+ } else {
556
+ // No number at end, just add minus
557
+ state.expression += "-";
558
+ }
559
+ }
560
+ break;
561
+ case "equals":
562
+ try {
563
+ state.result = evaluateExpression(state.expression);
564
+ } catch (e) {
565
+ state.error = e instanceof Error ? e.message : "Error";
566
+ }
567
+ break;
568
+ default:
569
+ state.expression += button.action;
570
+ break;
571
+ }
572
+
573
+ updateDisplay();
574
+ }
575
+
576
+ function updateDisplay(): void {
577
+ if (state.bufferId) {
578
+ const entries = renderCalculator();
579
+ editor.setVirtualBufferContent(state.bufferId, entries);
580
+ }
581
+ }
582
+
583
+ // Mouse click handler
584
+ globalThis.onCalculatorMouseClick = function (data: {
585
+ column: number;
586
+ row: number;
587
+ button: string;
588
+ modifiers: string;
589
+ content_x: number;
590
+ content_y: number;
591
+ }): boolean {
592
+ if (data.button !== "left") return true;
593
+
594
+ const activeBuffer = editor.getActiveBufferId();
595
+ if (activeBuffer !== state.bufferId || state.bufferId === 0) return true;
596
+
597
+ // Convert screen coordinates to content-relative coordinates
598
+ const relCol = data.column - data.content_x;
599
+ const relRow = data.row - data.content_y;
600
+
601
+ // Check for copy button click
602
+ if (isCopyButtonAt(relCol, relRow)) {
603
+ copyResultToClipboard();
604
+ return false;
605
+ }
606
+
607
+ const button = getButtonAt(relCol, relRow);
608
+ if (button) {
609
+ handleButton(button);
610
+ return false;
611
+ }
612
+
613
+ return true;
614
+ };
615
+
616
+ // Keyboard handlers
617
+ globalThis.calc_digit_0 = function (): void { handleButton({ label: "0", action: "0", type: "number" }); };
618
+ globalThis.calc_digit_1 = function (): void { handleButton({ label: "1", action: "1", type: "number" }); };
619
+ globalThis.calc_digit_2 = function (): void { handleButton({ label: "2", action: "2", type: "number" }); };
620
+ globalThis.calc_digit_3 = function (): void { handleButton({ label: "3", action: "3", type: "number" }); };
621
+ globalThis.calc_digit_4 = function (): void { handleButton({ label: "4", action: "4", type: "number" }); };
622
+ globalThis.calc_digit_5 = function (): void { handleButton({ label: "5", action: "5", type: "number" }); };
623
+ globalThis.calc_digit_6 = function (): void { handleButton({ label: "6", action: "6", type: "number" }); };
624
+ globalThis.calc_digit_7 = function (): void { handleButton({ label: "7", action: "7", type: "number" }); };
625
+ globalThis.calc_digit_8 = function (): void { handleButton({ label: "8", action: "8", type: "number" }); };
626
+ globalThis.calc_digit_9 = function (): void { handleButton({ label: "9", action: "9", type: "number" }); };
627
+
628
+ globalThis.calc_add = function (): void { handleButton({ label: "+", action: "+", type: "operator" }); };
629
+ globalThis.calc_subtract = function (): void { handleButton({ label: "-", action: "-", type: "operator" }); };
630
+ globalThis.calc_multiply = function (): void { handleButton({ label: "×", action: "*", type: "operator" }); };
631
+ globalThis.calc_divide = function (): void { handleButton({ label: "÷", action: "/", type: "operator" }); };
632
+ globalThis.calc_lparen = function (): void { handleButton({ label: "(", action: "(", type: "function" }); };
633
+ globalThis.calc_rparen = function (): void { handleButton({ label: ")", action: ")", type: "function" }); };
634
+ globalThis.calc_dot = function (): void { handleButton({ label: ".", action: ".", type: "number" }); };
635
+ globalThis.calc_equals = function (): void { handleButton({ label: "=", action: "equals", type: "equals" }); };
636
+ globalThis.calc_clear = function (): void { handleButton({ label: "C", action: "clear", type: "clear" }); };
637
+ globalThis.calc_backspace = function (): void { handleButton({ label: "⌫", action: "backspace", type: "clear" }); };
638
+ globalThis.calc_power = function (): void { handleButton({ label: "^", action: "^", type: "operator" }); };
639
+
640
+ // Letter handlers for typing function names
641
+ const letterHandler = (ch: string) => () => {
642
+ state.error = "";
643
+ state.expression += ch;
644
+ updateDisplay();
645
+ };
646
+ for (const ch of "abcdefghijklmnopqrstuvwxyz") {
647
+ (globalThis as Record<string, unknown>)[`calc_letter_${ch}`] = letterHandler(ch);
648
+ }
649
+
650
+ globalThis.calc_close = function (): void {
651
+ if (state.bufferId) {
652
+ editor.closeBuffer(state.bufferId);
653
+ state.bufferId = 0;
654
+ }
655
+ };
656
+
657
+ // Open calculator
658
+ globalThis.calculator_open = async function (): Promise<void> {
659
+ if (state.bufferId) {
660
+ const bufferInfo = editor.getBufferInfo(state.bufferId);
661
+ if (bufferInfo) {
662
+ editor.showBuffer(state.bufferId);
663
+ return;
664
+ }
665
+ state.bufferId = 0;
666
+ }
667
+
668
+ state.expression = "";
669
+ state.result = "0";
670
+ state.error = "";
671
+ cachedLayout = null; // Reset layout for fresh calculation
672
+ hoveredButton = null; // Reset hover state
673
+ copyButtonHovered = false; // Reset copy button hover state
674
+
675
+ const modeBindings: [string, string][] = [
676
+ ["0", "calc_digit_0"], ["1", "calc_digit_1"], ["2", "calc_digit_2"],
677
+ ["3", "calc_digit_3"], ["4", "calc_digit_4"], ["5", "calc_digit_5"],
678
+ ["6", "calc_digit_6"], ["7", "calc_digit_7"], ["8", "calc_digit_8"],
679
+ ["9", "calc_digit_9"],
680
+ ["+", "calc_add"], ["-", "calc_subtract"], ["*", "calc_multiply"],
681
+ ["/", "calc_divide"], ["(", "calc_lparen"], [")", "calc_rparen"],
682
+ [".", "calc_dot"], ["^", "calc_power"],
683
+ ["Return", "calc_equals"], ["=", "calc_equals"],
684
+ ["Delete", "calc_clear"],
685
+ ["Backspace", "calc_backspace"],
686
+ ["Escape", "calc_close"],
687
+ ];
688
+ // Add letter bindings for typing function names
689
+ for (const ch of "abcdefghijklmnopqrstuvwxyz") {
690
+ modeBindings.push([ch, `calc_letter_${ch}`]);
691
+ }
692
+ editor.defineMode("calculator", "special", modeBindings, true);
693
+
694
+ const cmds = [
695
+ ["calc_digit_0", "0"], ["calc_digit_1", "1"], ["calc_digit_2", "2"],
696
+ ["calc_digit_3", "3"], ["calc_digit_4", "4"], ["calc_digit_5", "5"],
697
+ ["calc_digit_6", "6"], ["calc_digit_7", "7"], ["calc_digit_8", "8"],
698
+ ["calc_digit_9", "9"], ["calc_add", "+"], ["calc_subtract", "-"],
699
+ ["calc_multiply", "*"], ["calc_divide", "/"], ["calc_lparen", "("],
700
+ ["calc_rparen", ")"], ["calc_dot", "."], ["calc_equals", "="],
701
+ ["calc_clear", "C"], ["calc_backspace", "BS"], ["calc_close", "close"],
702
+ ["calc_power", "^"],
703
+ ];
704
+ for (const [name, desc] of cmds) {
705
+ editor.registerCommand(name, `Calc: ${desc}`, name, "calculator");
706
+ }
707
+ // Register letter commands
708
+ for (const ch of "abcdefghijklmnopqrstuvwxyz") {
709
+ editor.registerCommand(`calc_letter_${ch}`, `Calc: ${ch}`, `calc_letter_${ch}`, "calculator");
710
+ }
711
+
712
+ const entries = renderCalculator();
713
+
714
+ state.bufferId = await editor.createVirtualBuffer({
715
+ name: "*Calculator*",
716
+ mode: "calculator",
717
+ read_only: true,
718
+ entries,
719
+ show_line_numbers: false,
720
+ show_cursors: false,
721
+ editing_disabled: true,
722
+ });
723
+
724
+ state.splitId = editor.getActiveSplitId();
725
+
726
+ editor.setStatus(editor.t("status.opened"));
727
+ };
728
+
729
+ // Mouse move handler for hover effect
730
+ globalThis.onCalculatorMouseMove = function (data: {
731
+ column: number;
732
+ row: number;
733
+ content_x: number;
734
+ content_y: number;
735
+ }): boolean {
736
+ const activeBuffer = editor.getActiveBufferId();
737
+ if (activeBuffer !== state.bufferId || state.bufferId === 0) return true;
738
+
739
+ // Convert screen coordinates to content-relative coordinates
740
+ const relCol = data.column - data.content_x;
741
+ const relRow = data.row - data.content_y;
742
+
743
+ const newHover = getButtonPosition(relCol, relRow);
744
+ const newCopyHover = isCopyButtonAt(relCol, relRow);
745
+
746
+ // Check if hover changed
747
+ const buttonChanged =
748
+ (newHover === null && hoveredButton !== null) ||
749
+ (newHover !== null && hoveredButton === null) ||
750
+ (newHover !== null && hoveredButton !== null &&
751
+ (newHover.row !== hoveredButton.row || newHover.col !== hoveredButton.col));
752
+ const copyChanged = newCopyHover !== copyButtonHovered;
753
+
754
+ if (buttonChanged || copyChanged) {
755
+ hoveredButton = newHover;
756
+ copyButtonHovered = newCopyHover;
757
+ updateDisplay();
758
+ }
759
+
760
+ return true;
761
+ };
762
+
763
+ // Register hooks
764
+ editor.on("mouse_click", "onCalculatorMouseClick");
765
+ editor.on("mouse_move", "onCalculatorMouseMove");
766
+
767
+ // Register main command
768
+ editor.registerCommand("%cmd.calculator", "%cmd.calculator_desc", "calculator_open", "normal");
769
+
770
+ editor.setStatus(editor.t("status.loaded"));