@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.
- package/bin/CHANGELOG.md +1017 -0
- package/bin/LICENSE +117 -0
- package/bin/README.md +248 -0
- package/bin/fresh.exe +0 -0
- package/bin/plugins/README.md +71 -0
- package/bin/plugins/audit_mode.i18n.json +821 -0
- package/bin/plugins/audit_mode.ts +1810 -0
- package/bin/plugins/buffer_modified.i18n.json +67 -0
- package/bin/plugins/buffer_modified.ts +281 -0
- package/bin/plugins/calculator.i18n.json +93 -0
- package/bin/plugins/calculator.ts +770 -0
- package/bin/plugins/clangd-lsp.ts +168 -0
- package/bin/plugins/clangd_support.i18n.json +223 -0
- package/bin/plugins/clangd_support.md +20 -0
- package/bin/plugins/clangd_support.ts +325 -0
- package/bin/plugins/color_highlighter.i18n.json +145 -0
- package/bin/plugins/color_highlighter.ts +304 -0
- package/bin/plugins/config-schema.json +768 -0
- package/bin/plugins/csharp-lsp.ts +147 -0
- package/bin/plugins/csharp_support.i18n.json +80 -0
- package/bin/plugins/csharp_support.ts +170 -0
- package/bin/plugins/css-lsp.ts +143 -0
- package/bin/plugins/diagnostics_panel.i18n.json +236 -0
- package/bin/plugins/diagnostics_panel.ts +642 -0
- package/bin/plugins/examples/README.md +85 -0
- package/bin/plugins/examples/async_demo.ts +165 -0
- package/bin/plugins/examples/bookmarks.ts +329 -0
- package/bin/plugins/examples/buffer_query_demo.ts +110 -0
- package/bin/plugins/examples/git_grep.ts +262 -0
- package/bin/plugins/examples/hello_world.ts +93 -0
- package/bin/plugins/examples/virtual_buffer_demo.ts +116 -0
- package/bin/plugins/find_references.i18n.json +275 -0
- package/bin/plugins/find_references.ts +359 -0
- package/bin/plugins/git_blame.i18n.json +496 -0
- package/bin/plugins/git_blame.ts +707 -0
- package/bin/plugins/git_find_file.i18n.json +314 -0
- package/bin/plugins/git_find_file.ts +300 -0
- package/bin/plugins/git_grep.i18n.json +171 -0
- package/bin/plugins/git_grep.ts +191 -0
- package/bin/plugins/git_gutter.i18n.json +93 -0
- package/bin/plugins/git_gutter.ts +477 -0
- package/bin/plugins/git_log.i18n.json +481 -0
- package/bin/plugins/git_log.ts +1285 -0
- package/bin/plugins/go-lsp.ts +143 -0
- package/bin/plugins/html-lsp.ts +145 -0
- package/bin/plugins/json-lsp.ts +145 -0
- package/bin/plugins/lib/fresh.d.ts +1321 -0
- package/bin/plugins/lib/index.ts +24 -0
- package/bin/plugins/lib/navigation-controller.ts +214 -0
- package/bin/plugins/lib/panel-manager.ts +220 -0
- package/bin/plugins/lib/types.ts +72 -0
- package/bin/plugins/lib/virtual-buffer-factory.ts +130 -0
- package/bin/plugins/live_grep.i18n.json +171 -0
- package/bin/plugins/live_grep.ts +422 -0
- package/bin/plugins/markdown_compose.i18n.json +223 -0
- package/bin/plugins/markdown_compose.ts +630 -0
- package/bin/plugins/merge_conflict.i18n.json +821 -0
- package/bin/plugins/merge_conflict.ts +1810 -0
- package/bin/plugins/path_complete.i18n.json +80 -0
- package/bin/plugins/path_complete.ts +165 -0
- package/bin/plugins/python-lsp.ts +162 -0
- package/bin/plugins/rust-lsp.ts +166 -0
- package/bin/plugins/search_replace.i18n.json +405 -0
- package/bin/plugins/search_replace.ts +484 -0
- package/bin/plugins/test_i18n.i18n.json +67 -0
- package/bin/plugins/test_i18n.ts +18 -0
- package/bin/plugins/theme_editor.i18n.json +3746 -0
- package/bin/plugins/theme_editor.ts +2063 -0
- package/bin/plugins/todo_highlighter.i18n.json +184 -0
- package/bin/plugins/todo_highlighter.ts +206 -0
- package/bin/plugins/typescript-lsp.ts +167 -0
- package/bin/plugins/vi_mode.i18n.json +1549 -0
- package/bin/plugins/vi_mode.ts +2747 -0
- package/bin/plugins/welcome.i18n.json +236 -0
- package/bin/plugins/welcome.ts +76 -0
- package/bin/themes/dark.json +102 -0
- package/bin/themes/dracula.json +62 -0
- package/bin/themes/high-contrast.json +102 -0
- package/bin/themes/light.json +102 -0
- package/bin/themes/nord.json +62 -0
- package/bin/themes/nostalgia.json +102 -0
- package/bin/themes/solarized-dark.json +62 -0
- package/binary-install.js +1 -1
- package/dist/bin/fresh.js +9 -0
- package/dist/binary-install.js +149 -0
- package/dist/binary.js +30 -0
- package/dist/fresh-6yhknp07.exe +0 -0
- package/dist/install.js +158 -0
- package/dist/run-fresh.js +43 -0
- 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"));
|