@fresh-editor/fresh-editor 0.1.97 → 0.1.99

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 CHANGED
@@ -1,5 +1,81 @@
1
1
  # Release Notes
2
2
 
3
+ ## 0.1.99
4
+
5
+ ### Features
6
+
7
+ * **Windows Terminal Support**: Full terminal emulation on Windows using ConPTY (Windows 10 1809+). Handles PowerShell DSR cursor queries, prefers PowerShell over cmd.exe, and supports stdin piping (`type file | fresh`).
8
+
9
+ * **Text Encoding Support**: Detect and convert files in UTF-8, UTF-16 LE/BE, Latin-1, Windows-1252, Windows-1250, GBK, Shift-JIS, EUC-KR, and GB18030. Encoding shown in status bar (clickable to change). "Reload with Encoding..." command in File menu. Confirmation prompt for large files with non-resynchronizable encodings (#488).
10
+
11
+ * **Encoding Selection in File Browser**: Toggle "Detect Encoding" with Alt+E when opening files. When disabled, prompts for manual encoding selection.
12
+
13
+ * **Bundle Package Type**: New package type containing multiple languages, plugins, and themes in a single package. Shown with "B" tag in package manager.
14
+
15
+ * **Space-Separated Fuzzy Search**: Queries with spaces are now split into independent terms, all of which must match. For example, "features groups-view" now matches "/features/groups/groups-view.tsx" (#933).
16
+
17
+ ### Bug Fixes
18
+
19
+ * Fixed Escape key not closing the manual and keyboard shortcuts pages (#840).
20
+
21
+ * Fixed scrollbar and mouse wheel scrolling not working with line wrap enabled.
22
+
23
+ * Fixed scrollbar thumb drag jumping to mouse position instead of following drag movement.
24
+
25
+ * Fixed AltGr character input not working on Windows (#762).
26
+
27
+ * Fixed custom themes not appearing in "Select Theme" on macOS due to incorrect config path resolution.
28
+
29
+ * Fixed LSP servers registered via plugins being disabled by default.
30
+
31
+ * Fixed language packs being installed to plugins directory instead of languages directory.
32
+
33
+ * Fixed theme changes not persisting when selecting the default theme.
34
+
35
+ * Fixed popup positioning not accounting for file explorer width (#898).
36
+
37
+ * Fixed LSP did_open sending wrong language for multi-language LSP servers.
38
+
39
+ * Fixed manual LSP start not working when LSP config was disabled; settings now sync immediately.
40
+
41
+ ### Internal
42
+
43
+ * Refactored config path handling to pass DirectoryContext via call chain instead of static methods.
44
+
45
+ * Added shadow model property-based tests for TextBuffer.
46
+
47
+ * Bumped tree-sitter (0.26.5), actions/checkout (v6), actions/upload-pages-artifact (v4) (@dependabot).
48
+
49
+ ---
50
+
51
+ ## 0.1.98
52
+
53
+ ### Features
54
+
55
+ * **File Explorer Quick Search**: Type to filter files/directories with fuzzy matching. ESC or Backspace clears the search (#892).
56
+
57
+ * **Sort Lines Command**: New command to alphabetically sort selected lines.
58
+
59
+ * **Paragraph Selection**: Ctrl+Shift+Up/Down extends selection to previous/next empty line.
60
+
61
+ * **Local Package Install**: Package manager now supports installing plugins/themes from local file paths (e.g., `/path/to/package`, `~/repos/plugin`).
62
+
63
+ * **Plugin API**: Added `setLineWrap` for plugins to control line wrapping.
64
+
65
+ ### Bug Fixes
66
+
67
+ * Fixed data corruption when saving large files with in-place writes.
68
+
69
+ * Fixed UI hang when loading shortcuts in Open File dialog (#903).
70
+
71
+ * Fixed file explorer failing to open at root path "/" (#902).
72
+
73
+ * Fixed Settings UI search results not scrolling properly (#905).
74
+
75
+ * Fixed multi-cursor cut operations not batching undo correctly.
76
+
77
+ ---
78
+
3
79
  ## 0.1.96
4
80
 
5
81
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fresh-editor/fresh-editor",
3
- "version": "0.1.97",
3
+ "version": "0.1.99",
4
4
  "description": "A modern terminal-based text editor with plugin support",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,397 @@
1
+ /// <reference path="./lib/fresh.d.ts" />
2
+
3
+ /**
4
+ * Code Tour Plugin
5
+ *
6
+ * A JSON-driven walkthrough system that guides users through a codebase
7
+ * using visual overlays and explanatory text.
8
+ *
9
+ * Usage:
10
+ * 1. Create a .fresh-tour.json file in your project root
11
+ * 2. Use "Tour: Load Definition..." command to start a tour
12
+ * 3. Navigate with Space/Right (next), Backspace/Left (prev), Tab (resume), Esc (exit)
13
+ */
14
+
15
+ const editor = getEditor();
16
+
17
+ // ============================================================================
18
+ // Types
19
+ // ============================================================================
20
+
21
+ interface OverlayConfig {
22
+ type: "block" | "line";
23
+ focus_mode: boolean;
24
+ }
25
+
26
+ interface TourStep {
27
+ step_id: number;
28
+ title: string;
29
+ file_path: string;
30
+ lines: [number, number]; // 1-indexed, inclusive
31
+ explanation: string;
32
+ overlay_config?: OverlayConfig;
33
+ }
34
+
35
+ interface TourManifest {
36
+ $schema?: string;
37
+ title: string;
38
+ description: string;
39
+ schema_version: "1.0";
40
+ commit_hash?: string;
41
+ steps: TourStep[];
42
+ }
43
+
44
+ type TourState =
45
+ | { kind: "idle" }
46
+ | { kind: "active"; currentStep: number; isPaused: boolean };
47
+
48
+ interface TourManager {
49
+ state: TourState;
50
+ manifest: TourManifest | null;
51
+ dockBufferId: number | null;
52
+ dockSplitId: number | null;
53
+ contentBufferId: number | null;
54
+ contentSplitId: number | null;
55
+ overlayNamespace: string;
56
+ lastKnownTopByte: number;
57
+ lastKnownBufferId: number;
58
+ }
59
+
60
+ // ============================================================================
61
+ // State
62
+ // ============================================================================
63
+
64
+ const TOUR_NAMESPACE = "code-tour";
65
+
66
+ const tourManager: TourManager = {
67
+ state: { kind: "idle" },
68
+ manifest: null,
69
+ dockBufferId: null,
70
+ dockSplitId: null,
71
+ contentBufferId: null,
72
+ contentSplitId: null,
73
+ overlayNamespace: TOUR_NAMESPACE,
74
+ lastKnownTopByte: 0,
75
+ lastKnownBufferId: 0,
76
+ };
77
+
78
+ // ============================================================================
79
+ // Tour Status Updates
80
+ // ============================================================================
81
+
82
+ const TOUR_POPUP_ID = "code-tour-step";
83
+
84
+ function showStepPopup(
85
+ step: TourStep,
86
+ stepIndex: number,
87
+ totalSteps: number,
88
+ fileError?: string
89
+ ): void {
90
+ const manifest = tourManager.manifest;
91
+ if (!manifest) return;
92
+
93
+ const stepInfo = `Step ${stepIndex + 1}/${totalSteps}: ${step.title}`;
94
+
95
+ // Build message with explanation
96
+ let message = step.explanation;
97
+ if (fileError) {
98
+ message = `ERROR: ${fileError}\n\n${step.explanation}`;
99
+ }
100
+
101
+ // Build actions based on position
102
+ const actions: Array<{ id: string; label: string }> = [];
103
+
104
+ if (stepIndex > 0) {
105
+ actions.push({ id: "prev", label: "← Previous" });
106
+ }
107
+ if (stepIndex < totalSteps - 1) {
108
+ actions.push({ id: "next", label: "Next →" });
109
+ }
110
+ actions.push({ id: "exit", label: "Exit Tour" });
111
+
112
+ editor.showActionPopup({
113
+ id: TOUR_POPUP_ID,
114
+ title: stepInfo,
115
+ message: message,
116
+ actions: actions,
117
+ });
118
+ }
119
+
120
+ // Handle popup button clicks
121
+ interface ActionPopupResultData {
122
+ popup_id: string;
123
+ action_id: string;
124
+ }
125
+
126
+ globalThis.tour_on_action_popup_result = function (data: ActionPopupResultData): void {
127
+ if (data.popup_id !== TOUR_POPUP_ID) return;
128
+
129
+ switch (data.action_id) {
130
+ case "next":
131
+ nextStep();
132
+ break;
133
+ case "prev":
134
+ prevStep();
135
+ break;
136
+ case "exit":
137
+ exitTour();
138
+ break;
139
+ }
140
+ };
141
+
142
+ // ============================================================================
143
+ // Overlay Rendering
144
+ // ============================================================================
145
+
146
+ async function clearTourOverlays(): Promise<void> {
147
+ // Clear overlays from all open buffers
148
+ const buffers = editor.listBuffers();
149
+ for (const buf of buffers) {
150
+ editor.clearNamespace(buf.id, TOUR_NAMESPACE);
151
+ }
152
+ }
153
+
154
+ async function renderStepOverlays(step: TourStep): Promise<void> {
155
+ const bufferId = editor.findBufferByPath(step.file_path);
156
+ if (!bufferId) return;
157
+
158
+ // Clear previous overlays
159
+ await clearTourOverlays();
160
+
161
+ // Get line positions (convert from 1-indexed to 0-indexed)
162
+ const startLine = step.lines[0] - 1;
163
+ const endLine = step.lines[1] - 1;
164
+
165
+ const startPos = await editor.getLineStartPosition(startLine);
166
+ const endPos = await editor.getLineEndPosition(endLine);
167
+
168
+ if (startPos === null || endPos === null) {
169
+ editor.warn(`Tour: Could not get line positions for lines ${step.lines[0]}-${step.lines[1]}`);
170
+ return;
171
+ }
172
+
173
+ // Add highlight overlay for active lines
174
+ editor.addOverlay(bufferId, TOUR_NAMESPACE, startPos, endPos, {
175
+ bg: [42, 74, 106], // Highlighted background color
176
+ extendToLineEnd: true,
177
+ });
178
+
179
+ // If focus mode is enabled, we could dim surrounding lines
180
+ // For now, just the highlight is sufficient
181
+ }
182
+
183
+ // ============================================================================
184
+ // Navigation
185
+ // ============================================================================
186
+
187
+ async function navigateToStep(stepIndex: number): Promise<void> {
188
+ if (!tourManager.manifest) return;
189
+
190
+ const step = tourManager.manifest.steps[stepIndex];
191
+ if (!step) return;
192
+
193
+ // Check if file exists (fileExists is sync, not async)
194
+ const fileExists = editor.fileExists(step.file_path);
195
+
196
+ if (!fileExists) {
197
+ // Show error in popup but allow navigation to continue
198
+ showStepPopup(
199
+ step,
200
+ stepIndex,
201
+ tourManager.manifest.steps.length,
202
+ "File not found"
203
+ );
204
+ return;
205
+ }
206
+
207
+ // Open the file at the starting line
208
+ editor.openFile(step.file_path, step.lines[0], 1);
209
+
210
+ // Wait a bit for the file to open
211
+ await editor.delay(50);
212
+
213
+ // Get the buffer ID after opening
214
+ const bufferId = editor.findBufferByPath(step.file_path);
215
+ if (bufferId) {
216
+ // Center the view on the middle of the highlighted region
217
+ const middleLine = Math.floor((step.lines[0] + step.lines[1]) / 2) - 1;
218
+ const splitId = editor.getActiveSplitId();
219
+ editor.scrollToLineCenter(splitId, bufferId, middleLine);
220
+
221
+ // Render overlays
222
+ await renderStepOverlays(step);
223
+
224
+ // Track for detour detection
225
+ tourManager.lastKnownBufferId = bufferId;
226
+ const viewport = editor.getViewport();
227
+ if (viewport) {
228
+ tourManager.lastKnownTopByte = viewport.topByte;
229
+ }
230
+ }
231
+
232
+ // Show explanation popup
233
+ showStepPopup(step, stepIndex, tourManager.manifest.steps.length);
234
+ }
235
+
236
+ // ============================================================================
237
+ // Tour Lifecycle
238
+ // ============================================================================
239
+
240
+ async function loadTour(manifestPath: string): Promise<void> {
241
+ try {
242
+ // Read and parse manifest
243
+ const content = editor.readFile(manifestPath);
244
+ if (!content) {
245
+ editor.error("Failed to read tour file: " + manifestPath);
246
+ return;
247
+ }
248
+ const manifest: TourManifest = JSON.parse(content);
249
+
250
+ // Validate schema version
251
+ if (manifest.schema_version !== "1.0") {
252
+ editor.error(`Unsupported tour schema version: ${manifest.schema_version}`);
253
+ return;
254
+ }
255
+
256
+ // Validate steps
257
+ if (!manifest.steps || manifest.steps.length === 0) {
258
+ editor.error("Tour has no steps");
259
+ return;
260
+ }
261
+
262
+ // Check commit hash if specified
263
+ if (manifest.commit_hash) {
264
+ const result = await editor.spawnProcess("git", [
265
+ "rev-parse",
266
+ "--short",
267
+ "HEAD",
268
+ ]);
269
+ if (result.exit_code === 0) {
270
+ const currentCommit = result.stdout.trim();
271
+ if (!currentCommit.startsWith(manifest.commit_hash) &&
272
+ !manifest.commit_hash.startsWith(currentCommit)) {
273
+ editor.warn(
274
+ `Tour was created for commit ${manifest.commit_hash}, current: ${currentCommit}`
275
+ );
276
+ }
277
+ }
278
+ }
279
+
280
+ // Initialize tour
281
+ tourManager.manifest = manifest;
282
+ tourManager.state = { kind: "active", currentStep: 0, isPaused: false };
283
+
284
+ // Navigate to first step
285
+ await navigateToStep(0);
286
+
287
+ // Set tour context for keybindings
288
+ editor.setContext("tour-active", true);
289
+ } catch (e) {
290
+ editor.error(`Failed to load tour: ${e}`);
291
+ }
292
+ }
293
+
294
+ function exitTour(): void {
295
+ if (tourManager.state.kind !== "active") return;
296
+
297
+ // Clear overlays
298
+ clearTourOverlays();
299
+
300
+ // Reset state
301
+ tourManager.state = { kind: "idle" };
302
+ tourManager.manifest = null;
303
+
304
+ // Clear context
305
+ editor.setContext("tour-active", false);
306
+
307
+ editor.setStatus("Tour ended");
308
+ }
309
+
310
+ async function nextStep(): Promise<void> {
311
+ if (tourManager.state.kind !== "active" || !tourManager.manifest) return;
312
+
313
+ const newIndex = tourManager.state.currentStep + 1;
314
+ if (newIndex >= tourManager.manifest.steps.length) {
315
+ editor.setStatus("Tour: Already at last step");
316
+ return;
317
+ }
318
+
319
+ tourManager.state = { ...tourManager.state, currentStep: newIndex, isPaused: false };
320
+ await navigateToStep(newIndex);
321
+ }
322
+
323
+ async function prevStep(): Promise<void> {
324
+ if (tourManager.state.kind !== "active" || !tourManager.manifest) return;
325
+
326
+ const newIndex = tourManager.state.currentStep - 1;
327
+ if (newIndex < 0) {
328
+ editor.setStatus("Tour: Already at first step");
329
+ return;
330
+ }
331
+
332
+ tourManager.state = { ...tourManager.state, currentStep: newIndex, isPaused: false };
333
+ await navigateToStep(newIndex);
334
+ }
335
+
336
+ // ============================================================================
337
+ // Command Handlers
338
+ // ============================================================================
339
+
340
+ globalThis.tour_load = async function (): Promise<void> {
341
+ // Prompt for tour file
342
+ const result = await editor.prompt("Enter tour file path:", ".fresh-tour.json");
343
+
344
+ if (result) {
345
+ await loadTour(result);
346
+ }
347
+ };
348
+
349
+ globalThis.tour_next = async function (): Promise<void> {
350
+ await nextStep();
351
+ };
352
+
353
+ globalThis.tour_prev = async function (): Promise<void> {
354
+ await prevStep();
355
+ };
356
+
357
+ globalThis.tour_exit = function (): void {
358
+ exitTour();
359
+ };
360
+
361
+ // ============================================================================
362
+ // Registration
363
+ // ============================================================================
364
+
365
+ // Register commands
366
+ editor.registerCommand(
367
+ "Tour: Load Definition...",
368
+ "Load a .fresh-tour.json file to start a guided code tour",
369
+ "tour_load",
370
+ null
371
+ );
372
+
373
+ editor.registerCommand(
374
+ "Tour: Next Step",
375
+ "Go to the next step in the tour",
376
+ "tour_next",
377
+ "tour-active"
378
+ );
379
+
380
+ editor.registerCommand(
381
+ "Tour: Previous Step",
382
+ "Go to the previous step in the tour",
383
+ "tour_prev",
384
+ "tour-active"
385
+ );
386
+
387
+ editor.registerCommand(
388
+ "Tour: Exit",
389
+ "Exit the current tour",
390
+ "tour_exit",
391
+ "tour-active"
392
+ );
393
+
394
+ // Subscribe to action popup results for navigation buttons
395
+ editor.on("action_popup_result", "tour_on_action_popup_result");
396
+
397
+ editor.debug("Code Tour plugin loaded");
@@ -736,6 +736,22 @@ interface EditorAPI {
736
736
  */
737
737
  getLineStartPosition(line: number): Promise<number | null>;
738
738
  /**
739
+ * Get the byte offset of the end of a line (0-indexed line number)
740
+ * Returns the position after the last character of the line (before newline)
741
+ * Returns null if the line number is out of range
742
+ */
743
+ getLineEndPosition(line: number): Promise<number | null>;
744
+ /**
745
+ * Get the total number of lines in the active buffer
746
+ * Returns null if buffer not found
747
+ */
748
+ getBufferLineCount(): Promise<number | null>;
749
+ /**
750
+ * Scroll a split to center a specific line in the viewport
751
+ * Line is 0-indexed (0 = first line)
752
+ */
753
+ scrollToLineCenter(splitId: number, bufferId: number, line: number): boolean;
754
+ /**
739
755
  * Find buffer by file path, returns buffer ID or 0 if not found
740
756
  */
741
757
  findBufferByPath(path: string): number;
@@ -957,10 +973,13 @@ interface EditorAPI {
957
973
  /**
958
974
  * Submit a view transform for a buffer/split
959
975
  *
960
- * Note: tokens should be ViewTokenWire[], layoutHints should be LayoutHints
961
- * These use manual parsing due to complex enum handling
976
+ * Accepts tokens in the simple format:
977
+ * {kind: "text"|"newline"|"space"|"break", text: "...", sourceOffset: N, style?: {...}}
978
+ *
979
+ * Also accepts the TypeScript-defined format for backwards compatibility:
980
+ * {kind: {Text: "..."} | "Newline" | "Space" | "Break", source_offset: N, style?: {...}}
962
981
  */
963
- submitViewTransform(bufferId: number, splitId: number | null, start: number, end: number, tokens: Record<string, unknown>[], LayoutHints?: Record<string, unknown>): boolean;
982
+ submitViewTransform(bufferId: number, splitId: number | null, start: number, end: number, tokens: Record<string, unknown>[], layoutHints?: Record<string, unknown>): boolean;
964
983
  /**
965
984
  * Clear view transform for a buffer/split
966
985
  */
@@ -1069,6 +1088,10 @@ interface EditorAPI {
1069
1088
  */
1070
1089
  setLineNumbers(bufferId: number, enabled: boolean): boolean;
1071
1090
  /**
1091
+ * Enable or disable line wrapping for a buffer/split
1092
+ */
1093
+ setLineWrap(bufferId: number, splitId: number | null, enabled: boolean): boolean;
1094
+ /**
1072
1095
  * Create a scroll sync group for anchor-based synchronized scrolling
1073
1096
  */
1074
1097
  createScrollSyncGroup(groupId: number, leftSplit: number, rightSplit: number): boolean;