@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 +98 -0
- package/README.md +12 -0
- package/package.json +1 -1
- package/plugins/audit_mode.i18n.json +63 -0
- package/plugins/buffer_modified.i18n.json +5 -0
- package/plugins/clangd_support.i18n.json +17 -0
- package/plugins/code-tour.ts +397 -0
- package/plugins/config-schema.json +1 -0
- package/plugins/csharp_support.i18n.json +6 -0
- package/plugins/diagnostics_panel.i18n.json +18 -0
- package/plugins/find_references.i18n.json +21 -0
- package/plugins/git_blame.i18n.json +39 -0
- package/plugins/git_find_file.i18n.json +24 -0
- package/plugins/git_grep.i18n.json +13 -0
- package/plugins/git_gutter.i18n.json +7 -0
- package/plugins/git_log.i18n.json +37 -0
- package/plugins/lib/fresh.d.ts +22 -3
- package/plugins/live_grep.i18n.json +13 -0
- package/plugins/markdown_compose.i18n.json +17 -0
- package/plugins/merge_conflict.i18n.json +63 -0
- package/plugins/path_complete.i18n.json +6 -0
- package/plugins/pkg.i18n.json +24 -0
- package/plugins/pkg.ts +377 -82
- package/plugins/schemas/package.schema.json +146 -1
- package/plugins/schemas/tour.schema.json +103 -0
- package/plugins/search_replace.i18n.json +31 -0
- package/plugins/test_i18n.i18n.json +5 -0
- package/plugins/theme_editor.i18n.json +294 -0
- package/plugins/vi_mode.i18n.json +121 -0
- package/plugins/welcome.i18n.json +18 -0
- package/themes/light.json +1 -1
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
|
@@ -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");
|
|
@@ -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恢复已完成",
|