@fresh-editor/fresh-editor 0.1.98 → 0.2.1

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,103 @@
1
1
  # Release Notes
2
2
 
3
+ ## 0.2.0
4
+
5
+ ### Features
6
+
7
+ * Experimental **Session Persistence**: Detach from and reattach to editor sessions with full state preservation. Start with `fresh -a <name>` or `fresh -a` (directory-based), detach via File menu or command palette. Sessions persist across terminal disconnections. Use `fresh --cmd session list/kill/attach` and `fresh --cmd session open-file NAME FILES` to manage sessions from the command line. Allows using Fresh across other applications, e.g. yazi edit action triggers a file open in Fresh.
8
+
9
+ * **Keybinding Editor**: Full-featured editor for customizing keybindings. Search by text or record key, filter by context/source, add/edit/delete bindings with conflict detection and autocomplete. Try menus: Edit..Keybinding Editor, or command palette. Changes are saved in config.json
10
+
11
+ ### Improvements
12
+
13
+ * **Line Editing**: Move lines up/down and duplicate lines, matching modern editor behavior. Multi-cursor support (@Asuka-Minato).
14
+
15
+ * **Triple-Click Selection**: Triple-click selects entire line (#597).
16
+
17
+ * **Vietnamese Localization**: Full Vietnamese (Tiếng Việt) language support.
18
+
19
+ * **Typst Language Support**: Syntax highlighting and tinymist LSP configuration for `.typ` files (#944).
20
+
21
+ * **LSP Improvements**:
22
+ - Per-buffer LSP toggle command to enable/disable LSP for individual files
23
+ - Default LSP configs for bash, lua, ruby, php, yaml, toml (#946)
24
+
25
+ ### Bug Fixes
26
+
27
+ * **LSP Document Sync**: Fixed document corruption when LSP servers received didChange after didOpen, and when bulk edits (selection replacement, multi-cursor) bypassed LSP notifications.
28
+
29
+ * **LSP Completion Popup**: Fixed popup swallowing non-word characters, arrow keys, and other keys. Popup now dismisses correctly allowing keystrokes to pass through (#931)
30
+
31
+ * **LSP Diagnostics**: Fixed diagnostic gutter markers not appearing on implicit trailing lines with zero-width ranges (clangd-style diagnostics).
32
+
33
+ * **Line Wrapping**: End/Home keys now navigate by visual line when wrapping is enabled, matching VS Code/Notepad behavior (#979).
34
+
35
+ * **Syntax Highlighting**: Fixed highlighting lost when saving files without extension (shebang detection) outside working directory (#978).
36
+
37
+ * **Buffer Settings**: User-configured tab size, indentation, and line numbers now preserved across auto-revert.
38
+
39
+ * **Terminal Scrollback**: Any character key exits scrollback mode instead of just 'q' (#863).
40
+
41
+ * **32-bit ARM Build**: Fixed setrlimit type mismatch on ARMv7l platforms (#957).
42
+
43
+ ### Configuration
44
+
45
+ * Added C++20 module extensions (.cppm, .ixx) for C++ syntax highlighting (#955).
46
+
47
+ ### Documentation
48
+
49
+ * Added FreeBSD installation note (@lwhsu).
50
+
51
+ ---
52
+
53
+ ## 0.1.99
54
+
55
+ ### Features
56
+
57
+ * **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`).
58
+
59
+ * **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).
60
+
61
+ * **Encoding Selection in File Browser**: Toggle "Detect Encoding" with Alt+E when opening files. When disabled, prompts for manual encoding selection.
62
+
63
+ * **Bundle Package Type**: New package type containing multiple languages, plugins, and themes in a single package. Shown with "B" tag in package manager.
64
+
65
+ * **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).
66
+
67
+ ### Bug Fixes
68
+
69
+ * Fixed Escape key not closing the manual and keyboard shortcuts pages (#840).
70
+
71
+ * Fixed scrollbar and mouse wheel scrolling not working with line wrap enabled.
72
+
73
+ * Fixed scrollbar thumb drag jumping to mouse position instead of following drag movement.
74
+
75
+ * Fixed AltGr character input not working on Windows (#762).
76
+
77
+ * Fixed custom themes not appearing in "Select Theme" on macOS due to incorrect config path resolution.
78
+
79
+ * Fixed LSP servers registered via plugins being disabled by default.
80
+
81
+ * Fixed language packs being installed to plugins directory instead of languages directory.
82
+
83
+ * Fixed theme changes not persisting when selecting the default theme.
84
+
85
+ * Fixed popup positioning not accounting for file explorer width (#898).
86
+
87
+ * Fixed LSP did_open sending wrong language for multi-language LSP servers.
88
+
89
+ * Fixed manual LSP start not working when LSP config was disabled; settings now sync immediately.
90
+
91
+ ### Internal
92
+
93
+ * Refactored config path handling to pass DirectoryContext via call chain instead of static methods.
94
+
95
+ * Added shadow model property-based tests for TextBuffer.
96
+
97
+ * Bumped tree-sitter (0.26.5), actions/checkout (v6), actions/upload-pages-artifact (v4) (@dependabot).
98
+
99
+ ---
100
+
3
101
  ## 0.1.98
4
102
 
5
103
  ### Features
package/README.md CHANGED
@@ -6,6 +6,8 @@ A terminal-based text editor. [Official Website →](https://sinelaw.github.io/f
6
6
 
7
7
  **[Contributing](#contributing)**
8
8
 
9
+ **[Discord](https://discord.gg/qUutBj9t)**
10
+
9
11
  ## Why?
10
12
 
11
13
  Why another text editor? Fresh brings the intuitive, conventional UX of editors like VS Code and Sublime Text to the terminal.
@@ -57,9 +59,11 @@ Or, pick your preferred method:
57
59
  |----------|--------|
58
60
  | macOS | [brew](#brew) |
59
61
  | Bazzite/Bluefin/Aurora Linux | [brew](#brew) |
62
+ | Windows | [winget](#windows-winget) |
60
63
  | Arch Linux | [AUR](#arch-linux-aur) |
61
64
  | Debian/Ubuntu | [.deb](#debianubuntu-deb) |
62
65
  | Fedora/RHEL | [.rpm](#fedorarhelopensuse-rpm), [Terra](https://terra.fyralabs.com/) |
66
+ | FreeBSD | [ports / pkg](https://www.freshports.org/editors/fresh) |
63
67
  | Linux (any distro) | [AppImage](#appimage), [Flatpak](#flatpak) |
64
68
  | All platforms | [Pre-built binaries](#pre-built-binaries) |
65
69
  | npm | [npm / npx](#npm) |
@@ -79,6 +83,14 @@ brew tap sinelaw/fresh
79
83
  brew install fresh-editor
80
84
  ```
81
85
 
86
+ ### Windows (winget)
87
+
88
+ ```bash
89
+ winget install fresh-editor
90
+ ```
91
+
92
+ Alternatively, Windows users can use [npm](#npm).
93
+
82
94
  ### Arch Linux ([AUR](https://aur.archlinux.org/packages/fresh-editor-bin))
83
95
 
84
96
  **Binary package (recommended, faster install):**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fresh-editor/fresh-editor",
3
- "version": "0.1.98",
3
+ "version": "0.2.1",
4
4
  "description": "A modern terminal-based text editor with plugin support",
5
5
  "repository": {
6
6
  "type": "git",
@@ -755,6 +755,69 @@
755
755
  "panel.help_export_footer": "ЕКСПОРТ: [E]кспорт до .review/session.md | [O]загальний [r]оновити",
756
756
  "debug.loaded": "Плагін рев'ю змін завантажено з підтримкою коментарів"
757
757
  },
758
+ "vi": {
759
+ "cmd.review_diff": "Xem xét khác biệt",
760
+ "cmd.review_diff_desc": "Bắt đầu phiên xem xét mã",
761
+ "cmd.stop_review_diff": "Dừng xem xét",
762
+ "cmd.stop_review_diff_desc": "Dừng phiên xem xét",
763
+ "cmd.refresh_review_diff": "Làm mới khác biệt",
764
+ "cmd.refresh_review_diff_desc": "Làm mới danh sách thay đổi",
765
+ "cmd.side_by_side_diff": "So sánh song song",
766
+ "cmd.side_by_side_diff_desc": "Hiển thị khác biệt song song cho tệp hiện tại",
767
+ "cmd.add_comment": "Xem xét: Thêm nhận xét",
768
+ "cmd.add_comment_desc": "Thêm nhận xét xem xét cho khối hiện tại",
769
+ "cmd.approve_hunk": "Xem xét: Duyệt khối",
770
+ "cmd.approve_hunk_desc": "Đánh dấu khối đã duyệt",
771
+ "cmd.reject_hunk": "Xem xét: Từ chối khối",
772
+ "cmd.reject_hunk_desc": "Đánh dấu khối bị từ chối",
773
+ "cmd.needs_changes": "Xem xét: Cần thay đổi",
774
+ "cmd.needs_changes_desc": "Đánh dấu khối cần thay đổi",
775
+ "cmd.question": "Xem xét: Câu hỏi",
776
+ "cmd.question_desc": "Đánh dấu khối với câu hỏi",
777
+ "cmd.clear_status": "Xem xét: Xóa trạng thái",
778
+ "cmd.clear_status_desc": "Xóa trạng thái xem xét của khối",
779
+ "cmd.overall_feedback": "Xem xét: Phản hồi tổng thể",
780
+ "cmd.overall_feedback_desc": "Đặt phản hồi tổng thể cho xem xét",
781
+ "cmd.export_markdown": "Xem xét: Xuất ra Markdown",
782
+ "cmd.export_markdown_desc": "Xuất xem xét ra .review/session.md",
783
+ "cmd.export_json": "Xem xét: Xuất ra JSON",
784
+ "cmd.export_json_desc": "Xuất xem xét ra .review/session.json",
785
+ "status.refreshing": "Đang làm mới khác biệt xem xét...",
786
+ "status.updated": "Đã cập nhật khác biệt xem xét. Tìm thấy %{count} khối.",
787
+ "status.loading_diff": "Đang tải so sánh song song...",
788
+ "status.not_git_repo": "Không trong kho git",
789
+ "status.failed_old_version": "Không thể tải phiên bản cũ của tệp",
790
+ "status.failed_new_version": "Không thể tải phiên bản mới của tệp",
791
+ "status.diff_summary": "So sánh song song: +%{added} -%{removed} ~%{modified} | 'q' để quay lại",
792
+ "status.no_hunk_selected": "Chưa chọn khối để nhận xét",
793
+ "status.comment_added": "Đã thêm nhận xét vào %{line}",
794
+ "status.comment_cancelled": "Đã hủy nhận xét",
795
+ "status.hunk_approved": "Đã duyệt khối",
796
+ "status.hunk_rejected": "Đã từ chối khối",
797
+ "status.hunk_needs_changes": "Đã đánh dấu khối cần thay đổi",
798
+ "status.hunk_question": "Đã đánh dấu khối với câu hỏi",
799
+ "status.hunk_status_cleared": "Đã xóa trạng thái xem xét của khối",
800
+ "status.feedback_set": "Đã đặt phản hồi tổng thể",
801
+ "status.feedback_cleared": "Đã xóa phản hồi tổng thể",
802
+ "status.exported": "Đã xuất xem xét ra %{path}",
803
+ "status.generating": "Đang tạo luồng khác biệt xem xét...",
804
+ "status.review_summary": "Xem xét: %{count} khối | [c]nhận xét [a]duyệt [x]từ chối [!]thay đổi [?]câu hỏi [E]xuất",
805
+ "status.stopped": "Đã dừng chế độ xem xét khác biệt.",
806
+ "status.no_file_open": "Không có tệp mở - không thể hiển thị khác biệt",
807
+ "status.failed_git_diff": "Không thể lấy git diff cho tệp",
808
+ "status.no_changes": "Không có thay đổi trong tệp này",
809
+ "status.failed_old_new_file": "Không thể tải phiên bản cũ của tệp (tệp có thể là mới)",
810
+ "prompt.comment": "Nhận xét trên %{line}: ",
811
+ "prompt.overall_feedback": "Phản hồi tổng thể: ",
812
+ "panel.no_changes": "Không có thay đổi để xem xét.",
813
+ "panel.help_review": "XEM XÉT: [c]nhận xét [a]duyệt [x]từ chối [!]thay đổi [?]câu hỏi [u]hoàn tác",
814
+ "panel.help_stage": "STAGE: [s]tage [d]bỏ | NAV: [n]tiếp [p]trước [Enter]chi tiết [q]thoát",
815
+ "panel.help_export": "XUẤT: [E] .review/session.md | [O]tổng thể | [r]làm mới",
816
+ "panel.help_review_footer": "XEM XÉT: [c]nhận xét [a]duyệt [x]từ chối [!]cần thay đổi [?]câu hỏi [u]hoàn tác",
817
+ "panel.help_stage_footer": "STAGE: [s]tage [d]bỏ | NAV: [n]tiếp [p]trước [Enter]chi tiết [q]thoát",
818
+ "panel.help_export_footer": "XUẤT: [E]xuất ra .review/session.md | [O]tổng thể [r]làm mới",
819
+ "debug.loaded": "Plugin xem xét khác biệt đã tải với hỗ trợ nhận xét"
820
+ },
758
821
  "zh-CN": {
759
822
  "cmd.review_diff": "审查差异",
760
823
  "cmd.review_diff_desc": "开始代码审查会话",
@@ -59,6 +59,11 @@
59
59
  "status.initialized": "Buffer Modified: ініціалізовано для %{path}",
60
60
  "status.cleared_on_save": "Buffer Modified: очищено при збереженні"
61
61
  },
62
+ "vi": {
63
+ "status.loaded": "Đã tải plugin Buffer Modified",
64
+ "status.initialized": "Buffer Modified: đã khởi tạo cho %{path}",
65
+ "status.cleared_on_save": "Buffer Modified: đã xóa khi lưu"
66
+ },
62
67
  "zh-CN": {
63
68
  "status.loaded": "Buffer Modified插件已加载",
64
69
  "status.initialized": "Buffer Modified: 已为%{path}初始化",
@@ -203,6 +203,23 @@
203
203
  "status.config_not_found": "Не вдалося знайти конфігурацію .clangd у робочому просторі",
204
204
  "status.file_status": "Статус файлу Clangd: %{status}"
205
205
  },
206
+ "vi": {
207
+ "cmd.project_setup": "Clangd: Thiết lập Dự án",
208
+ "cmd.project_setup_desc": "Phân tích trạng thái sẵn sàng clangd C/C++ (compile_commands.json, .clangd)",
209
+ "cmd.switch_source_header": "Clangd: Chuyển đổi Nguồn/Header",
210
+ "cmd.switch_source_header_desc": "Nhảy đến cặp header/nguồn sử dụng clangd",
211
+ "cmd.open_project_config": "Clangd: Mở Cấu hình Dự án",
212
+ "cmd.open_project_config_desc": "Mở tệp .clangd gần nhất",
213
+ "status.plugin_loaded": "Đã tải plugin hỗ trợ Clangd (chuyển đổi header + lệnh cấu hình)",
214
+ "status.no_active_file": "Clangd: không có tệp đang hoạt động để chuyển đổi",
215
+ "status.unsupported_file_type": "Clangd: loại tệp không được hỗ trợ để chuyển đổi header",
216
+ "status.opened_corresponding_file": "Clangd: đã mở tệp tương ứng",
217
+ "status.no_matching_found": "Clangd: không tìm thấy header/nguồn phù hợp",
218
+ "status.switch_failed": "Clangd chuyển đổi nguồn/header thất bại: %{error}",
219
+ "status.opened_config": "Đã mở cấu hình .clangd",
220
+ "status.config_not_found": "Không tìm thấy cấu hình .clangd trong không gian làm việc",
221
+ "status.file_status": "Trạng thái tệp Clangd: %{status}"
222
+ },
206
223
  "zh-CN": {
207
224
  "cmd.project_setup": "Clangd: 项目设置",
208
225
  "cmd.project_setup_desc": "分析C/C++ clangd就绪状态 (compile_commands.json, .clangd)",
@@ -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");
@@ -186,6 +186,7 @@
186
186
  "ru",
187
187
  "th",
188
188
  "uk",
189
+ "vi",
189
190
  "zh-CN"
190
191
  ]
191
192
  },
@@ -71,6 +71,12 @@
71
71
  "status.restore_failed": "Помилка відновлення NuGet: %{error}",
72
72
  "status.restore_error": "Помилка відновлення NuGet: %{error}"
73
73
  },
74
+ "vi": {
75
+ "status.restoring_packages": "Đang khôi phục gói NuGet cho %{project}...",
76
+ "status.restore_completed": "Hoàn tất khôi phục NuGet cho %{project}",
77
+ "status.restore_failed": "Khôi phục NuGet thất bại: %{error}",
78
+ "status.restore_error": "Lỗi khôi phục NuGet: %{error}"
79
+ },
74
80
  "zh-CN": {
75
81
  "status.restoring_packages": "正在为%{project}恢复NuGet包...",
76
82
  "status.restore_completed": "%{project}的NuGet恢复已完成",