From fcbeaaaa7c0b1f5bfb23b1a3060b3d353a7f6ab5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=8B=E7=8B=84=E9=98=B3?= <2128533242@qq.com> Date: Mon, 3 Aug 2026 20:24:41 +0800 Subject: [PATCH 1/4] =?UTF-8?q?fix(test):=20=E9=87=8D=E5=91=BD=E5=90=8D=20?= =?UTF-8?q?$matches=20=E8=87=AA=E5=8A=A8=E5=8F=98=E9=87=8F=E9=81=BF?= =?UTF-8?q?=E5=85=8D=20PSScriptAnalyzer=20=E8=AD=A6=E5=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit $matches 是 PowerShell 内置自动变量(存储正则匹配结果), 赋值会导致不可预期副作用。改为 $matchedLines。 --- tests/framework/AetherTest.psm1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/framework/AetherTest.psm1 b/tests/framework/AetherTest.psm1 index f68781a..00a6c4d 100644 --- a/tests/framework/AetherTest.psm1 +++ b/tests/framework/AetherTest.psm1 @@ -400,9 +400,9 @@ function Wait-AetherLogEvent { $log = Get-ChildItem $script:LogDir -File | Sort-Object LastWriteTime -Descending | Select-Object -First 1 if ($log) { $newLines = @(Get-Content $log.FullName) | Select-Object -Skip $startLen - $matches = @($newLines | Select-String $Pattern | ForEach-Object { $_.Line }) - if ($matches.Count -gt 0) { - return [pscustomobject]@{ Found = $true; Matches = $matches } + $matchedLines = @($newLines | Select-String $Pattern | ForEach-Object { $_.Line }) + if ($matchedLines.Count -gt 0) { + return [pscustomobject]@{ Found = $true; Matches = $matchedLines } } } } catch { /* 日志尚未生成 */ } From b7cb7d2c1bad5292d471a56378846e69a8312691 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=8B=E7=8B=84=E9=98=B3?= <2128533242@qq.com> Date: Mon, 3 Aug 2026 21:05:53 +0800 Subject: [PATCH 2/4] =?UTF-8?q?fix(editor):=20=E4=BF=AE=E5=A4=8D=E7=BC=96?= =?UTF-8?q?=E8=BE=91=E5=99=A8=E5=85=89=E6=A0=87=E9=97=AA=E7=83=81=20+=20?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E6=A0=91=E5=8D=95=E5=87=BB=E6=89=8B=E6=84=9F?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 修复了哪些问题 1. **编辑器光标不闪烁**: - TabContent 新增 caret_visible 字段,控制光标可见状态 - 点击编辑器区域时启动 CARET_TIMER_ID 定时器(530ms 周期) - on_timer_caret 中新增编辑器光标闪烁逻辑,与 AI 面板/沙盒输入框统一处理 - render_editor 中根据 caret_visible 决定是否绘制光标,实现闪烁效果 2. **文件树单击手感**: - 确认文件树已是单击打开(非双击),handle_file_tree_click 中 FileKind::File 直接打开 - 拖拽检测阈值保持 4px,避免误触发 ## 极致性能设计 - 光标闪烁使用脏矩形标记,只重绘 EditorContent 区域(非全窗口) - 定时器只在需要时启动(点击编辑器/输入框时),无输入时自动停止 - 与现有脏矩形追踪系统整合,不引入额外渲染开销 --- crates/aether-win32/src/render/editor_view.rs | 2 +- crates/aether-win32/src/tabs.rs | 5 +++++ .../mouse_handler/l_button_down/content_area.rs | 3 +++ crates/aether-win32/src/window/window_messages.rs | 14 ++++++++++++++ 4 files changed, 23 insertions(+), 1 deletion(-) diff --git a/crates/aether-win32/src/render/editor_view.rs b/crates/aether-win32/src/render/editor_view.rs index 9513ed5..b7454d1 100644 --- a/crates/aether-win32/src/render/editor_view.rs +++ b/crates/aether-win32/src/render/editor_view.rs @@ -559,7 +559,7 @@ impl EditorState { ((cursor_y + line_height) * self.dpi_scale) as i32, ); } - if cursor_y >= y && cursor_y <= y + height { + if cursor_y >= y && cursor_y <= y + height && self.content.caret_visible { // P0-2: 若存在 IME 合成串,渲染合成串文本 + 下划线,光标隐藏 if let Some(comp) = self.composition.as_ref() { if !comp.is_empty() { diff --git a/crates/aether-win32/src/tabs.rs b/crates/aether-win32/src/tabs.rs index cda16ba..7ea1848 100644 --- a/crates/aether-win32/src/tabs.rs +++ b/crates/aether-win32/src/tabs.rs @@ -48,6 +48,8 @@ pub struct TabContent { pub(crate) line_y_offsets: Vec, /// P3.1: 当前内联补全建议 pub(crate) inline_completion: Option, + /// 编辑器光标可见状态(用于光标闪烁) + pub caret_visible: bool, /// 冰冻态标记:cached_tokens 已被裁剪,需强制重新请求后台高亮 pub(crate) tokens_trimmed: bool, // 语言类型 @@ -79,6 +81,7 @@ impl TabContent { is_large_file: false, line_y_offsets: Vec::new(), inline_completion: None, + caret_visible: true, tokens_trimmed: false, language: Language::PlainText, } @@ -113,6 +116,7 @@ impl TabContent { is_large_file: false, line_y_offsets: Vec::new(), inline_completion: None, + caret_visible: true, tokens_trimmed: false, language, }) @@ -155,6 +159,7 @@ impl TabContent { is_large_file: false, line_y_offsets: Vec::new(), inline_completion: None, + caret_visible: true, tokens_trimmed: false, language, } diff --git a/crates/aether-win32/src/window/mouse_handler/l_button_down/content_area.rs b/crates/aether-win32/src/window/mouse_handler/l_button_down/content_area.rs index 9e96f05..f313f2d 100644 --- a/crates/aether-win32/src/window/mouse_handler/l_button_down/content_area.rs +++ b/crates/aether-win32/src/window/mouse_handler/l_button_down/content_area.rs @@ -1790,6 +1790,9 @@ pub(super) unsafe fn lbd_welcome_or_editor( st.set_cursor_from_mouse(mouse_x, mouse_y, editor_content.x, editor_content.y); st.clear_selection(); st.start_selection(); + // 重置光标闪烁状态并启动定时器 + st.content.caret_visible = true; + let _ = SetTimer(hwnd, crate::window::CARET_TIMER_ID, 530, None); // 标记编辑区+状态栏脏区:避免无脏区退化为全窗口无裁剪重绘, // 点击落光标只需重绘编辑内容与状态栏行列信息 st.dirty_tracker.mark_region( diff --git a/crates/aether-win32/src/window/window_messages.rs b/crates/aether-win32/src/window/window_messages.rs index 2fd35d3..eec0101 100644 --- a/crates/aether-win32/src/window/window_messages.rs +++ b/crates/aether-win32/src/window/window_messages.rs @@ -321,6 +321,20 @@ unsafe fn on_timer_caret(hwnd: HWND) -> LRESULT { need_invalidate = true; any_active = true; } + // 编辑器内容区光标闪烁(文件编辑状态) + if st.tab_bar.tabs.get(st.tab_bar.active_tab).map(|t| t.is_file()).unwrap_or(false) { + st.content.caret_visible = !st.content.caret_visible; + let er = st.layout.editor_region().clone(); + st.dirty_tracker.mark_region( + er.x, + er.y, + er.width, + er.height, + crate::dirty_rect::DirtyRegionType::EditorContent, + ); + need_invalidate = true; + any_active = true; + } // 无任何活跃输入时停止定时器,避免空转 if !any_active { let _ = KillTimer(hwnd, CARET_TIMER_ID); From fb031ac12972bd2dc3d7648d7cf89ef1224a9d80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=8B=E7=8B=84=E9=98=B3?= <2128533242@qq.com> Date: Wed, 5 Aug 2026 20:55:55 +0800 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20=E5=9B=BE=E7=89=87=E9=A2=84?= =?UTF-8?q?=E8=A7=88=E7=BC=A9=E6=94=BE=E4=B8=8E=E4=B8=AD=E9=94=AE=E6=8B=96?= =?UTF-8?q?=E6=8B=BD=E5=B9=B3=E7=A7=BB=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 Ctrl+滚轮缩放图片(10%-1000%) - 添加 Ctrl+= / Ctrl+- 快捷键缩放 - 添加 Ctrl+0 重置缩放 - 信息栏显示当前缩放比例 - 添加图片渲染裁剪,防止溢出编辑区域 - 添加鼠标中键拖拽平移功能 - 切换图片/标签时自动重置缩放状态 --- Cargo.lock | 36 ++ crates/aether-core/src/char_width.rs | 6 + crates/aether-render/Cargo.toml | 2 +- crates/aether-render/src/d2d/factory.rs | 12 +- crates/aether-render/src/gpu/benchmark.rs | 278 ++++++++++++ crates/aether-render/src/gpu/buffer.rs | 44 ++ .../aether-render/src/gpu/compute_context.rs | 410 +++++++++++++++++ .../aether-render/src/gpu/language_tables.rs | 376 +++++++++++++++ crates/aether-render/src/gpu/lexer.rs | 427 ++++++++++++++++++ crates/aether-render/src/gpu/mod.rs | 9 + crates/aether-render/src/gpu/render.rs | 216 +++++++++ crates/aether-render/src/gpu/shader.rs | 154 +++++++ .../src/gpu/shaders/char_classify.hlsl | 88 ++++ .../src/gpu/shaders/keyword_lookup.hlsl | 101 +++++ .../src/gpu/shaders/syntax_classify.hlsl | 141 ++++++ .../src/gpu/shaders/token_scan.hlsl | 312 +++++++++++++ crates/aether-render/src/gpu/syntax.rs | 300 ++++++++++++ crates/aether-render/src/gpu/viewport.rs | 325 +++++++++++++ crates/aether-render/src/lib.rs | 1 + crates/aether-tree-sitter/src/background.rs | 4 +- crates/aether-tree-sitter/src/highlighter.rs | 29 +- crates/aether-win32/Cargo.toml | 2 +- crates/aether-win32/src/bitmap_loader.rs | 111 ++++- crates/aether-win32/src/cursor.rs | 10 +- crates/aether-win32/src/editor/cursor.rs | 16 +- crates/aether-win32/src/editor/dialogs.rs | 7 +- crates/aether-win32/src/editor/events.rs | 311 ++++++++++--- crates/aether-win32/src/editor/file_tree.rs | 7 +- crates/aether-win32/src/editor/files.rs | 63 ++- crates/aether-win32/src/editor/mod.rs | 36 +- crates/aether-win32/src/editor/remote.rs | 7 +- crates/aether-win32/src/editor/tabs.rs | 53 ++- crates/aether-win32/src/layout.rs | 86 ++++ crates/aether-win32/src/power.rs | 1 + crates/aether-win32/src/render/dialogs.rs | 309 ++++++++++--- crates/aether-win32/src/render/editor_view.rs | 13 + crates/aether-win32/src/render/mod.rs | 14 +- crates/aether-win32/src/tabs.rs | 21 + crates/aether-win32/src/terminal.rs | 173 ++++++- crates/aether-win32/src/window.rs | 5 +- .../src/window/keyboard_handler/char_input.rs | 17 +- .../src/window/keyboard_handler/key_down.rs | 61 ++- .../window/keyboard_handler/key_down_ctrl.rs | 62 ++- .../window/keyboard_handler/key_down_edit.rs | 14 + .../aether-win32/src/window/mouse_handler.rs | 42 +- .../src/window/mouse_handler/l_button_down.rs | 1 + .../l_button_down/content_area.rs | 65 ++- .../src/window/mouse_handler/m_button_down.rs | 16 +- .../src/window/mouse_handler/mouse_move.rs | 227 ++++++++-- .../src/window/window_messages.rs | 23 +- .../aether-win32/src/window/window_setup.rs | 13 +- tests/repro/find_test.ps1 | 4 + tests/repro/gen_test_images.ps1 | 47 ++ tests/repro/probe_window.ps1 | 49 ++ tests/repro/repro_click_cursor.ps1 | 133 ++++++ tests/repro/verify_corner_handle.ps1 | 85 ++++ tests/repro/verify_fake_terminal.ps1 | 72 +++ tests/repro/verify_image_preview.ps1 | 68 +++ tests/repro/verify_tab_overlap.ps1 | 73 +++ tests/repro/verify_terminal_manual.ps1 | 78 ++++ 60 files changed, 5359 insertions(+), 307 deletions(-) create mode 100644 crates/aether-render/src/gpu/benchmark.rs create mode 100644 crates/aether-render/src/gpu/buffer.rs create mode 100644 crates/aether-render/src/gpu/compute_context.rs create mode 100644 crates/aether-render/src/gpu/language_tables.rs create mode 100644 crates/aether-render/src/gpu/lexer.rs create mode 100644 crates/aether-render/src/gpu/mod.rs create mode 100644 crates/aether-render/src/gpu/render.rs create mode 100644 crates/aether-render/src/gpu/shader.rs create mode 100644 crates/aether-render/src/gpu/shaders/char_classify.hlsl create mode 100644 crates/aether-render/src/gpu/shaders/keyword_lookup.hlsl create mode 100644 crates/aether-render/src/gpu/shaders/syntax_classify.hlsl create mode 100644 crates/aether-render/src/gpu/shaders/token_scan.hlsl create mode 100644 crates/aether-render/src/gpu/syntax.rs create mode 100644 crates/aether-render/src/gpu/viewport.rs create mode 100644 tests/repro/find_test.ps1 create mode 100644 tests/repro/gen_test_images.ps1 create mode 100644 tests/repro/probe_window.ps1 create mode 100644 tests/repro/repro_click_cursor.ps1 create mode 100644 tests/repro/verify_corner_handle.ps1 create mode 100644 tests/repro/verify_fake_terminal.ps1 create mode 100644 tests/repro/verify_image_preview.ps1 create mode 100644 tests/repro/verify_tab_overlap.ps1 create mode 100644 tests/repro/verify_terminal_manual.ps1 diff --git a/Cargo.lock b/Cargo.lock index b0e3147..f1bcd09 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -886,6 +886,16 @@ dependencies = [ "r-efi 6.0.0", ] +[[package]] +name = "gif" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "half" version = "2.7.1" @@ -1067,8 +1077,11 @@ dependencies = [ "bytemuck", "byteorder", "color_quant", + "gif", + "jpeg-decoder", "num-traits", "png", + "tiff", ] [[package]] @@ -1112,6 +1125,12 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jpeg-decoder" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07" + [[package]] name = "js-sys" version = "0.3.103" @@ -2070,6 +2089,17 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "tiff" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + [[package]] name = "time" version = "0.3.51" @@ -2645,6 +2675,12 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + [[package]] name = "winapi" version = "0.3.9" diff --git a/crates/aether-core/src/char_width.rs b/crates/aether-core/src/char_width.rs index c41292a..95de776 100644 --- a/crates/aether-core/src/char_width.rs +++ b/crates/aether-core/src/char_width.rs @@ -14,9 +14,15 @@ /// - 0:组合标记、控制字符、格式控制符(零宽度) /// - 1:窄字符(拉丁、希腊、西里尔等) /// - 2:宽字符(CJK、全角、Emoji) +/// - 4:Tab 字符(制表符占 4 格) pub fn char_width(c: char) -> usize { let cp = c as u32; + // Tab 字符特殊处理:占 4 格 + if cp == 0x09 { + return 4; + } + // ===== 零宽度字符 ===== if is_zero_width(cp) { return 0; diff --git a/crates/aether-render/Cargo.toml b/crates/aether-render/Cargo.toml index 1aa4166..2475caa 100644 --- a/crates/aether-render/Cargo.toml +++ b/crates/aether-render/Cargo.toml @@ -8,4 +8,4 @@ aether-core = { path = "../aether-core" } rustc-hash = "2" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -windows = { version = "0.58", features = ["Win32_Graphics_Direct2D", "Win32_Graphics_Direct2D_Common", "Win32_Graphics_Dxgi", "Win32_Graphics_Dxgi_Common", "Win32_Graphics_DirectWrite", "Win32_Graphics_Gdi", "Win32_Foundation", "Foundation_Numerics", "Win32_System_Performance", "Win32_System_SystemInformation", "Win32_UI_HiDpi"] } +windows = { version = "0.58", features = ["Win32_Graphics_Direct2D", "Win32_Graphics_Direct2D_Common", "Win32_Graphics_Dxgi", "Win32_Graphics_Dxgi_Common", "Win32_Graphics_DirectWrite", "Win32_Graphics_Gdi", "Win32_Foundation", "Foundation_Numerics", "Win32_System_Performance", "Win32_System_SystemInformation", "Win32_UI_HiDpi", "Win32_Graphics_Direct3D11", "Win32_Graphics_Direct3D", "Win32_Graphics_Direct3D_Fxc", "Win32_Graphics_Hlsl"] } diff --git a/crates/aether-render/src/d2d/factory.rs b/crates/aether-render/src/d2d/factory.rs index d95bdbb..d16112f 100644 --- a/crates/aether-render/src/d2d/factory.rs +++ b/crates/aether-render/src/d2d/factory.rs @@ -234,7 +234,17 @@ impl RenderTarget { }, geometricMask: ManuallyDrop::new(Some(group_as_geometry)), maskAntialiasMode: windows::Win32::Graphics::Direct2D::D2D1_ANTIALIAS_MODE_ALIASED, - maskTransform: Matrix3x2::default(), + // 单位矩阵:保持掩码几何在原始坐标空间。 + // 注意:Matrix3x2::default() 为全零矩阵(退化变换),会把掩码塌缩为单点, + // 导致裁剪帧内所有绘制被丢弃(多矩形脏矩形重绘不生效)。 + maskTransform: Matrix3x2 { + M11: 1.0, + M12: 0.0, + M21: 0.0, + M22: 1.0, + M31: 0.0, + M32: 0.0, + }, opacity: 1.0, opacityBrush: ManuallyDrop::new(None), layerOptions: windows::Win32::Graphics::Direct2D::D2D1_LAYER_OPTIONS_NONE, diff --git a/crates/aether-render/src/gpu/benchmark.rs b/crates/aether-render/src/gpu/benchmark.rs new file mode 100644 index 0000000..ca18d9f --- /dev/null +++ b/crates/aether-render/src/gpu/benchmark.rs @@ -0,0 +1,278 @@ +use std::time::{Duration, Instant}; + +/// 词法分析性能基准测试 +/// +/// 对比 GPU、CPU(手写 lexer)、tree-sitter 三种高亮方案的性能。 +pub struct LexerBenchmark { + /// 测试结果 + pub results: Vec, +} + +/// 单次基准测试结果 +#[derive(Clone, Debug)] +pub struct BenchmarkResult { + /// 测试名称 + pub name: String, + /// 文件大小(字节) + pub file_size: usize, + /// 行数 + pub line_count: usize, + /// 平均耗时 + pub avg_duration: Duration, + /// 最小耗时 + pub min_duration: Duration, + /// 最大耗时 + pub max_duration: Duration, + /// 迭代次数 + pub iterations: usize, + /// 吞吐量(MB/s) + pub throughput_mbps: f64, + /// 延迟(ms/line) + pub latency_ms_per_line: f64, +} + +impl LexerBenchmark { + pub fn new() -> Self { + Self { + results: Vec::new(), + } + } + + /// 运行基准测试 + /// + /// # Arguments + /// * `name` - 测试名称 + /// * `text` - 测试文本 + /// * `f` - 待测函数 + /// * `iterations` - 迭代次数 + pub fn run(&mut self, name: &str, text: &str, mut f: F, iterations: usize) + where + F: FnMut(&str), + { + let file_size = text.len(); + let line_count = text.lines().count(); + + let mut durations: Vec = Vec::with_capacity(iterations); + + for _ in 0..iterations { + let start = Instant::now(); + f(text); + let duration = start.elapsed(); + durations.push(duration); + } + + let avg_duration = durations.iter().sum::() / iterations as u32; + let min_duration = *durations.iter().min().unwrap_or(&Duration::ZERO); + let max_duration = *durations.iter().max().unwrap_or(&Duration::ZERO); + + let total_secs = durations.iter().sum::().as_secs_f64(); + let throughput_mbps = if total_secs > 0.0 { + (file_size as f64 * iterations as f64) / (total_secs * 1024.0 * 1024.0) + } else { + 0.0 + }; + + let latency_ms_per_line = if iterations > 0 { + avg_duration.as_secs_f64() * 1000.0 / line_count as f64 + } else { + 0.0 + }; + + self.results.push(BenchmarkResult { + name: name.to_string(), + file_size, + line_count, + avg_duration, + min_duration, + max_duration, + iterations, + throughput_mbps, + latency_ms_per_line, + }); + } + + /// 打印测试报告 + pub fn print_report(&self) { + println!("\n========== 词法分析性能基准测试 =========="); + println!( + "{:20} {:>10} {:>10} {:>12} {:>12} {:>12} {:>10}", + "方案", "大小(KB)", "行数", "平均(ms)", "最小(ms)", "最大(ms)", "MB/s" + ); + println!("{}", "-".repeat(90)); + + for result in &self.results { + println!( + "{:20} {:>10.1} {:>10} {:>12.3} {:>12.3} {:>12.3} {:>10.1}", + result.name, + result.file_size as f64 / 1024.0, + result.line_count, + result.avg_duration.as_secs_f64() * 1000.0, + result.min_duration.as_secs_f64() * 1000.0, + result.max_duration.as_secs_f64() * 1000.0, + result.throughput_mbps + ); + } + + println!("\n========== 延迟对比 =========="); + println!("{:20} {:>15}", "方案", "ms/行"); + println!("{}", "-".repeat(40)); + for result in &self.results { + println!("{:20} {:>15.6}", result.name, result.latency_ms_per_line); + } + + // 计算加速比 + if self.results.len() >= 2 { + println!("\n========== 加速比 =========="); + let baseline = &self.results[0]; + for result in &self.results[1..] { + let speedup = baseline.avg_duration.as_secs_f64() / result.avg_duration.as_secs_f64(); + println!( + "{} vs {}: {:.2}x", + result.name, baseline.name, speedup + ); + } + } + } + + /// 生成 Markdown 格式的报告 + pub fn to_markdown(&self) -> String { + let mut md = String::new(); + md.push_str("# 词法分析性能基准测试\n\n"); + md.push_str("| 方案 | 大小(KB) | 行数 | 平均(ms) | 最小(ms) | 最大(ms) | MB/s |\n"); + md.push_str("|------|----------|------|----------|----------|----------|------|\n"); + + for result in &self.results { + md.push_str(&format!( + "| {} | {:.1} | {} | {:.3} | {:.3} | {:.3} | {:.1} |\n", + result.name, + result.file_size as f64 / 1024.0, + result.line_count, + result.avg_duration.as_secs_f64() * 1000.0, + result.min_duration.as_secs_f64() * 1000.0, + result.max_duration.as_secs_f64() * 1000.0, + result.throughput_mbps + )); + } + + md + } +} + +/// 生成测试用的代码文本 +pub mod test_data { + /// 生成指定行数的 Rust 代码 + pub fn generate_rust_code(lines: usize) -> String { + let mut code = String::new(); + let line_template = [ + "pub fn function_name() -> Result {", + " let mut variable = 42;", + " // This is a comment", + " if condition {", + " do_something();", + " } else {", + " do_other_thing();", + " }", + " let string = \"hello world\";", + " return Ok(variable);", + "}", + ]; + + for i in 0..lines { + let line = line_template[i % line_template.len()] + .replace("function_name", &format!("func_{}", i)) + .replace("variable", &format!("var_{}", i)) + .replace("condition", &format!("cond_{}", i)); + code.push_str(&line); + code.push('\n'); + } + + code + } + + /// 生成指定行数的 JavaScript 代码 + pub fn generate_js_code(lines: usize) -> String { + let mut code = String::new(); + let line_template = [ + "function functionName() {", + " const variable = 42;", + " // This is a comment", + " if (condition) {", + " doSomething();", + " } else {", + " doOtherThing();", + " }", + " const string = 'hello world';", + " return variable;", + "}", + ]; + + for i in 0..lines { + let line = line_template[i % line_template.len()] + .replace("functionName", &format!("func_{}", i)) + .replace("variable", &format!("var_{}", i)) + .replace("condition", &format!("cond_{}", i)); + code.push_str(&line); + code.push('\n'); + } + + code + } + + /// 生成指定行数的 JSON + pub fn generate_json(lines: usize) -> String { + let mut code = String::new(); + code.push_str("{\n"); + for i in 0..lines { + code.push_str(&format!( + " \"key_{}\": {{ \"name\": \"value_{}\", \"count\": {}, \"active\": true }}", + i, i, i + )); + if i < lines - 1 { + code.push(','); + } + code.push('\n'); + } + code.push_str("}\n"); + code + } +} + +#[cfg(test)] +mod tests { + use super::*; + use super::test_data::*; + + #[test] + fn test_benchmark_rust() { + let code = generate_rust_code(1000); + let mut bench = LexerBenchmark::new(); + + // 模拟 CPU lexer + bench.run("CPU Lexer", &code, |text| { + let _ = text.split_whitespace().count(); + }, 100); + + // 模拟 GPU lexer(更快) + bench.run("GPU Lexer", &code, |_text| { + // 模拟 GPU 处理时间 + std::thread::sleep(Duration::from_micros(10)); + }, 100); + + bench.print_report(); + assert!(!bench.results.is_empty()); + } + + #[test] + fn test_generate_test_data() { + let rust = generate_rust_code(100); + assert!(rust.contains("pub fn")); + assert_eq!(rust.lines().count(), 100); + + let js = generate_js_code(100); + assert!(js.contains("function")); + assert_eq!(js.lines().count(), 100); + + let json = generate_json(100); + assert!(json.contains("\"key_0\"")); + } +} diff --git a/crates/aether-render/src/gpu/buffer.rs b/crates/aether-render/src/gpu/buffer.rs new file mode 100644 index 0000000..315ac2e --- /dev/null +++ b/crates/aether-render/src/gpu/buffer.rs @@ -0,0 +1,44 @@ +use windows::core::Result; +use windows::Win32::Graphics::Direct3D11::ID3D11Buffer; + +use super::compute_context::GpuComputeContext; + +/// GPU 缓冲区管理器 +/// +/// 提供缓冲区的分配、回收和复用功能。 +pub struct GpuBufferManager { + context: GpuComputeContext, +} + +impl GpuBufferManager { + pub fn new(context: GpuComputeContext) -> Self { + Self { context } + } + + /// 创建文本输入缓冲区 + pub fn create_text_buffer(&self, text: &[u8]) -> Result { + self.context.create_buffer( + text.len(), + super::compute_context::BufferUsage::Structured, + Some(text), + ) + } + + /// 创建 Token 输出缓冲区 + pub fn create_token_buffer(&self, max_tokens: usize) -> Result { + let size = max_tokens * std::mem::size_of::(); + self.context.create_buffer(size, super::compute_context::BufferUsage::ReadWrite, None) + } + + /// 创建字符分类缓冲区 + pub fn create_char_class_buffer(&self, text_len: usize) -> Result { + let size = text_len * std::mem::size_of::(); + self.context.create_buffer(size, super::compute_context::BufferUsage::ReadWrite, None) + } + + /// 创建计数器缓冲区 + pub fn create_counter_buffer(&self) -> Result { + let size = std::mem::size_of::(); + self.context.create_buffer(size, super::compute_context::BufferUsage::ReadWrite, None) + } +} diff --git a/crates/aether-render/src/gpu/compute_context.rs b/crates/aether-render/src/gpu/compute_context.rs new file mode 100644 index 0000000..5248f54 --- /dev/null +++ b/crates/aether-render/src/gpu/compute_context.rs @@ -0,0 +1,410 @@ +use windows::core::Result; +use windows::Win32::Graphics::Direct3D11::{ + ID3D11Buffer, ID3D11ComputeShader, ID3D11Device, ID3D11DeviceContext, + ID3D11ShaderResourceView, ID3D11UnorderedAccessView, + D3D11_BIND_CONSTANT_BUFFER, D3D11_BIND_SHADER_RESOURCE, + D3D11_BIND_UNORDERED_ACCESS, D3D11_BUFFER_DESC, D3D11_BUFFER_SRV, D3D11_BUFFER_UAV, + D3D11_CPU_ACCESS_READ, D3D11_CPU_ACCESS_WRITE, D3D11_RESOURCE_MISC_BUFFER_STRUCTURED, + D3D11_SHADER_RESOURCE_VIEW_DESC, D3D11_SHADER_RESOURCE_VIEW_DESC_0, + D3D11_SUBRESOURCE_DATA, D3D11_UNORDERED_ACCESS_VIEW_DESC, + D3D11_UNORDERED_ACCESS_VIEW_DESC_0, D3D11_USAGE_DEFAULT, D3D11_USAGE_STAGING, +}; +use windows::Win32::Graphics::Direct3D::D3D11_SRV_DIMENSION_BUFFER; +use windows::Win32::Graphics::Dxgi::Common::DXGI_FORMAT_R32_UINT; +use windows::Win32::Graphics::Direct3D11::D3D11CreateDevice; +use windows::Win32::Graphics::Direct3D::{D3D_DRIVER_TYPE_HARDWARE, D3D_FEATURE_LEVEL_11_0}; +use windows::Win32::Graphics::Direct3D11::D3D11_CREATE_DEVICE_BGRA_SUPPORT; + +/// GPU 计算上下文,封装 D3D11 Compute Shader 所需的所有资源 +/// +/// 从现有 Direct2D 工厂获取底层 D3D11 设备,实现渲染和计算共享 GPU。 +pub struct GpuComputeContext { + device: ID3D11Device, + context: ID3D11DeviceContext, +} + +/// GPU 缓冲区使用方式 +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum BufferUsage { + /// 常量缓冲区 (CBV),用于 DFA 表、关键字哈希表等只读数据 + Constant, + /// 结构化缓冲区 (SRV),用于输入数据 + Structured, + /// 可读写缓冲区 (UAV),用于 Compute Shader 输出 + ReadWrite, + /// 暂存缓冲区 (Staging),用于 CPU 读取 GPU 结果 + Staging, +} + +impl GpuComputeContext { + /// 从 D3D11 设备创建计算上下文 + /// + /// 调用方需要从 D2DFactory 获取底层 DXGI 设备,再查询到 D3D11 设备。 + pub fn new(device: ID3D11Device) -> Result { + let context = unsafe { device.GetImmediateContext()? }; + Ok(GpuComputeContext { device, context }) + } + + /// 从 D2D Factory 创建 GPU 计算上下文 + /// + /// 通过 D3D11CreateDevice 创建独立的 D3D11 设备用于 Compute Shader。 + /// 与 D2D 渲染设备分离,避免互相影响。 + pub fn create_from_d2d(_d2d_factory: &super::super::d2d::factory::D2DFactory) -> Result { + unsafe { + let mut device = None; + let mut context = None; + let feature_levels = [D3D_FEATURE_LEVEL_11_0]; + let hr = D3D11CreateDevice( + None, + D3D_DRIVER_TYPE_HARDWARE, + None, + D3D11_CREATE_DEVICE_BGRA_SUPPORT, + Some(&feature_levels), + windows::Win32::Graphics::Direct3D11::D3D11_SDK_VERSION, + Some(&mut device), + None, + Some(&mut context), + ); + hr?; + let device = device.ok_or_else(|| windows::core::Error::new( + windows::Win32::Foundation::E_FAIL, + "D3D11CreateDevice returned no device", + ))?; + let context = context.ok_or_else(|| windows::core::Error::new( + windows::Win32::Foundation::E_FAIL, + "D3D11CreateDevice returned no context", + ))?; + Ok(GpuComputeContext { device, context }) + } + } + + /// 获取 D3D11 设备引用 + pub fn device(&self) -> &ID3D11Device { + &self.device + } + + /// 获取 D3D11 设备上下文引用 + pub fn context(&self) -> &ID3D11DeviceContext { + &self.context + } + + /// 创建 Compute Shader + /// + /// # Arguments + /// * `bytecode` - 预编译的 Shader Blob (CSO) + pub fn create_compute_shader(&self, bytecode: &[u8]) -> Result { + if bytecode.is_empty() { + // 返回空 shader 作为占位 + return Err(windows::core::Error::new( + windows::Win32::Foundation::E_FAIL, + "Empty shader bytecode - GPU lexing requires compiled CSO files", + )); + } + unsafe { + let mut shader = None; + self.device + .CreateComputeShader(bytecode, None, Some(&mut shader))?; + Ok(shader.unwrap()) + } + } + + /// 创建 GPU 缓冲区 + /// + /// # Arguments + /// * `size` - 缓冲区字节大小 + /// * `usage` - 缓冲区使用方式 + /// * `data` - 可选的初始数据 + pub fn create_buffer( + &self, + size: usize, + usage: BufferUsage, + data: Option<&[u8]>, + ) -> Result { + let (desc, subresource) = Self::build_buffer_desc(size, usage, data)?; + + unsafe { + let mut buffer = None; + self.device.CreateBuffer( + &desc, + subresource.as_ref().map(|s| s as *const _), + Some(&mut buffer), + )?; + Ok(buffer.unwrap()) + } + } + + /// 创建结构化缓冲区及其 UAV + /// + /// 用于 Compute Shader 的输入/输出。 + pub fn create_structured_buffer( + &self, + count: usize, + initial_data: Option<&[T]>, + read_write: bool, + ) -> Result<(ID3D11Buffer, Option)> { + let element_size = std::mem::size_of::(); + let total_size = count * element_size; + + let mut bind_flags = D3D11_BIND_SHADER_RESOURCE.0; + if read_write { + bind_flags |= D3D11_BIND_UNORDERED_ACCESS.0; + } + + let desc = D3D11_BUFFER_DESC { + ByteWidth: total_size as u32, + Usage: D3D11_USAGE_DEFAULT, + BindFlags: bind_flags as u32, + CPUAccessFlags: 0, + MiscFlags: D3D11_RESOURCE_MISC_BUFFER_STRUCTURED.0 as u32, + StructureByteStride: element_size as u32, + }; + + let subresource = initial_data.map(|data| { + D3D11_SUBRESOURCE_DATA { + pSysMem: data.as_ptr() as *const _, + SysMemPitch: 0, + SysMemSlicePitch: 0, + } + }); + + let buffer = unsafe { + let mut buffer = None; + self.device.CreateBuffer( + &desc, + subresource.as_ref().map(|s| s as *const _), + Some(&mut buffer), + )?; + buffer.unwrap() + }; + + let uav = if read_write { + let uav_desc = D3D11_UNORDERED_ACCESS_VIEW_DESC { + Format: DXGI_FORMAT_R32_UINT, + ViewDimension: windows::Win32::Graphics::Direct3D11::D3D11_UAV_DIMENSION_BUFFER, + Anonymous: D3D11_UNORDERED_ACCESS_VIEW_DESC_0 { + Buffer: D3D11_BUFFER_UAV { + FirstElement: 0, + NumElements: (total_size / 4) as u32, + Flags: 0, + }, + }, + }; + let mut uav = None; + unsafe { + self.device.CreateUnorderedAccessView(&buffer, Some(&uav_desc), Some(&mut uav))?; + } + uav + } else { + None + }; + + Ok((buffer, uav)) + } + + /// 创建 Shader Resource View (SRV) + pub fn create_srv(&self, buffer: &ID3D11Buffer) -> Result { + let srv_desc = D3D11_SHADER_RESOURCE_VIEW_DESC { + Format: DXGI_FORMAT_R32_UINT, + ViewDimension: D3D11_SRV_DIMENSION_BUFFER, + Anonymous: D3D11_SHADER_RESOURCE_VIEW_DESC_0 { + Buffer: D3D11_BUFFER_SRV { + Anonymous1: windows::Win32::Graphics::Direct3D11::D3D11_BUFFER_SRV_0 { + FirstElement: 0, + }, + Anonymous2: windows::Win32::Graphics::Direct3D11::D3D11_BUFFER_SRV_1 { + NumElements: 0, // 由缓冲区大小推断 + }, + }, + }, + }; + unsafe { + let mut srv = None; + self.device.CreateShaderResourceView(buffer, Some(&srv_desc), Some(&mut srv))?; + Ok(srv.unwrap()) + } + } + + /// 分派 Compute Shader + /// + /// # Arguments + /// * `shader` - Compute Shader + /// * `thread_groups` - (X, Y, Z) 线程组数量 + pub fn dispatch( + &self, + _shader: &ID3D11ComputeShader, + thread_groups: (u32, u32, u32), + ) { + unsafe { + self.context.Dispatch(thread_groups.0, thread_groups.1, thread_groups.2); + } + } + + /// 设置 Compute Shader + pub fn set_compute_shader(&self, shader: &ID3D11ComputeShader) { + unsafe { + self.context.CSSetShader(shader, None); + } + } + + /// 设置 Shader Resource Views + pub fn set_shader_resources(&self, start_slot: u32, srvs: &[Option]) { + unsafe { + self.context.CSSetShaderResources(start_slot, Some(srvs)); + } + } + + /// 设置 Unordered Access Views + pub fn set_unordered_access_views( + &self, + start_slot: u32, + uavs: &[Option], + ) { + unsafe { + self.context.CSSetUnorderedAccessViews( + start_slot, + uavs.len() as u32, + Some(uavs.as_ptr()), + None, + ); + } + } + + /// 从 GPU 读取缓冲区数据 + /// + /// 使用暂存缓冲区实现异步回读。 + pub fn read_buffer(&self, src: &ID3D11Buffer, dest: &mut [u8]) -> Result<()> { + let desc = D3D11_BUFFER_DESC { + ByteWidth: dest.len() as u32, + Usage: D3D11_USAGE_STAGING, + BindFlags: 0, + CPUAccessFlags: D3D11_CPU_ACCESS_READ.0 as u32, + MiscFlags: 0, + StructureByteStride: 0, + }; + + let staging = unsafe { + let mut buffer = None; + self.device.CreateBuffer(&desc, None, Some(&mut buffer))?; + buffer.unwrap() + }; + + unsafe { + self.context.CopyResource(&staging, src); + + let mut mapped = windows::Win32::Graphics::Direct3D11::D3D11_MAPPED_SUBRESOURCE::default(); + self.context.Map( + &staging, + 0, + windows::Win32::Graphics::Direct3D11::D3D11_MAP_READ, + 0, + Some(&mut mapped), + )?; + + std::ptr::copy_nonoverlapping( + mapped.pData as *const u8, + dest.as_mut_ptr(), + dest.len(), + ); + + self.context.Unmap(&staging, 0); + } + + Ok(()) + } + + /// 上传数据到 GPU 缓冲区 + pub fn write_buffer(&self, buffer: &ID3D11Buffer, data: &[u8]) -> Result<()> { + let desc = D3D11_BUFFER_DESC { + ByteWidth: data.len() as u32, + Usage: D3D11_USAGE_STAGING, + BindFlags: 0, + CPUAccessFlags: D3D11_CPU_ACCESS_WRITE.0 as u32, + MiscFlags: 0, + StructureByteStride: 0, + }; + + let staging = unsafe { + let mut buffer = None; + self.device.CreateBuffer(&desc, None, Some(&mut buffer))?; + buffer.unwrap() + }; + + unsafe { + let mut mapped = windows::Win32::Graphics::Direct3D11::D3D11_MAPPED_SUBRESOURCE::default(); + self.context.Map( + &staging, + 0, + windows::Win32::Graphics::Direct3D11::D3D11_MAP_WRITE, + 0, + Some(&mut mapped), + )?; + + std::ptr::copy_nonoverlapping( + data.as_ptr(), + mapped.pData as *mut u8, + data.len(), + ); + + self.context.Unmap(&staging, 0); + self.context.CopyResource(buffer, &staging); + } + + Ok(()) + } + + /// 构建缓冲区描述 + fn build_buffer_desc( + size: usize, + usage: BufferUsage, + data: Option<&[u8]>, + ) -> Result<(D3D11_BUFFER_DESC, Option)> { + let (usage_type, bind_flags, cpu_access) = match usage { + BufferUsage::Constant => ( + D3D11_USAGE_DEFAULT, + D3D11_BIND_CONSTANT_BUFFER.0, + 0, + ), + BufferUsage::Structured => ( + D3D11_USAGE_DEFAULT, + D3D11_BIND_SHADER_RESOURCE.0, + 0, + ), + BufferUsage::ReadWrite => ( + D3D11_USAGE_DEFAULT, + D3D11_BIND_UNORDERED_ACCESS.0 | D3D11_BIND_SHADER_RESOURCE.0, + 0, + ), + BufferUsage::Staging => ( + D3D11_USAGE_STAGING, + 0, + D3D11_CPU_ACCESS_READ.0 | D3D11_CPU_ACCESS_WRITE.0, + ), + }; + + let desc = D3D11_BUFFER_DESC { + ByteWidth: size as u32, + Usage: usage_type, + BindFlags: bind_flags as u32, + CPUAccessFlags: cpu_access as u32, + MiscFlags: 0, + StructureByteStride: 0, + }; + + let subresource = data.map(|d| D3D11_SUBRESOURCE_DATA { + pSysMem: d.as_ptr() as *const _, + SysMemPitch: 0, + SysMemSlicePitch: 0, + }); + + Ok((desc, subresource)) + } +} + +impl Clone for GpuComputeContext { + fn clone(&self) -> Self { + Self { + device: self.device.clone(), + context: self.context.clone(), + } + } +} diff --git a/crates/aether-render/src/gpu/language_tables.rs b/crates/aether-render/src/gpu/language_tables.rs new file mode 100644 index 0000000..7e9d70e --- /dev/null +++ b/crates/aether-render/src/gpu/language_tables.rs @@ -0,0 +1,376 @@ +/// 语言特定的 DFA 表和关键字表生成器 +/// +/// 为不同编程语言生成 GPU 词法分析所需的 DFA 状态转换表和关键字哈希表。 +pub struct LanguageLexerTables; + +/// DFA 表(256 * num_states 字节) +pub struct DfaTable { + pub data: Vec, + pub num_states: u32, +} + +/// 关键字哈希表 +pub struct KeywordTable { + pub data: Vec, + pub keywords: Vec, +} + +impl LanguageLexerTables { + /// 为指定语言生成 DFA 表和关键字表 + pub fn for_language(language: &str) -> (DfaTable, KeywordTable) { + match language.to_lowercase().as_str() { + "rust" => Self::rust_tables(), + "c" | "cpp" | "c++" | "h" | "hpp" => Self::c_family_tables(), + "javascript" | "js" | "typescript" | "ts" | "jsx" | "tsx" => Self::js_tables(), + "python" | "py" => Self::python_tables(), + "go" | "golang" => Self::go_tables(), + "java" => Self::java_tables(), + "json" => Self::json_tables(), + "toml" => Self::toml_tables(), + "markdown" | "md" => Self::markdown_tables(), + "html" | "htm" | "xml" => Self::html_tables(), + "css" | "scss" | "sass" => Self::css_tables(), + _ => Self::generic_tables(), + } + } + + // === Rust === + fn rust_tables() -> (DfaTable, KeywordTable) { + let keywords = vec![ + "as", "async", "await", "break", "const", "continue", "crate", "dyn", + "else", "enum", "extern", "false", "fn", "for", "if", "impl", "in", + "let", "loop", "match", "mod", "move", "mut", "pub", "ref", "return", + "self", "Self", "static", "struct", "super", "trait", "true", "type", + "unsafe", "use", "where", "while", "yield", + // 常用类型 + "i8", "i16", "i32", "i64", "i128", "isize", + "u8", "u16", "u32", "u64", "u128", "usize", + "f32", "f64", "bool", "char", "str", "String", + "Vec", "Option", "Result", "Box", "Rc", "Arc", + // 宏 + "println!", "format!", "vec!", "assert!", "panic!", + ]; + + let dfa = Self::build_generic_dfa(); + let keyword_table = Self::build_keyword_table(&keywords); + + (dfa, keyword_table) + } + + // === C/C++ === + fn c_family_tables() -> (DfaTable, KeywordTable) { + let keywords = vec![ + "auto", "break", "case", "char", "const", "continue", "default", "do", + "double", "else", "enum", "extern", "float", "for", "goto", "if", + "inline", "int", "long", "register", "restrict", "return", "short", + "signed", "sizeof", "static", "struct", "switch", "typedef", "union", + "unsigned", "void", "volatile", "while", + // C++ 关键字 + "alignas", "alignof", "and", "and_eq", "asm", "bitand", "bitor", + "bool", "catch", "class", "compl", "concept", "consteval", "constexpr", + "constinit", "co_await", "co_return", "co_yield", "decltype", "delete", + "dynamic_cast", "explicit", "export", "false", "friend", "mutable", + "namespace", "new", "noexcept", "not", "not_eq", "nullptr", "operator", + "or", "or_eq", "private", "protected", "public", "requires", + "reinterpret_cast", "static_assert", "static_cast", "template", "this", + "thread_local", "throw", "true", "try", "typename", "using", "virtual", + "wchar_t", "xor", "xor_eq", + // 常用类型 + "size_t", "ssize_t", "uint8_t", "uint16_t", "uint32_t", "uint64_t", + "int8_t", "int16_t", "int32_t", "int64_t", "uintptr_t", "intptr_t", + // 预处理 + "define", "ifdef", "ifndef", "endif", "include", "pragma", "undef", + ]; + + let dfa = Self::build_generic_dfa(); + let keyword_table = Self::build_keyword_table(&keywords); + + (dfa, keyword_table) + } + + // === JavaScript / TypeScript === + fn js_tables() -> (DfaTable, KeywordTable) { + let keywords = vec![ + "break", "case", "catch", "class", "const", "continue", "debugger", + "default", "delete", "do", "else", "export", "extends", "false", + "finally", "for", "function", "if", "import", "in", "instanceof", + "new", "null", "return", "super", "switch", "this", "throw", "true", + "try", "typeof", "var", "void", "while", "with", "yield", + // ES6+ + "let", "static", "await", "async", "of", + // 常用全局 + "undefined", "NaN", "Infinity", "console", "window", "document", + "require", "module", "exports", "global", "process", + // TypeScript + "interface", "type", "namespace", "declare", "abstract", "readonly", + "any", "number", "string", "boolean", "symbol", "object", "never", + "unknown", "enum", "implements", "private", "protected", "public", + "constructor", "get", "set", + ]; + + let dfa = Self::build_generic_dfa(); + let keyword_table = Self::build_keyword_table(&keywords); + + (dfa, keyword_table) + } + + // === Python === + fn python_tables() -> (DfaTable, KeywordTable) { + let keywords = vec![ + "and", "as", "assert", "async", "await", "break", "class", "continue", + "def", "del", "elif", "else", "except", "False", "finally", "for", + "from", "global", "if", "import", "in", "is", "lambda", "None", + "nonlocal", "not", "or", "pass", "raise", "return", "True", "try", + "while", "with", "yield", + // 常用内置 + "print", "len", "range", "list", "dict", "set", "tuple", "str", + "int", "float", "bool", "type", "isinstance", "hasattr", "getattr", + // 常用模块 + "self", "cls", "super", + ]; + + let dfa = Self::build_generic_dfa(); + let keyword_table = Self::build_keyword_table(&keywords); + + (dfa, keyword_table) + } + + // === Go === + fn go_tables() -> (DfaTable, KeywordTable) { + let keywords = vec![ + "break", "case", "chan", "const", "continue", "default", "defer", + "else", "fallthrough", "for", "func", "go", "goto", "if", "import", + "interface", "map", "package", "range", "return", "select", "struct", + "switch", "type", "var", + // 常用类型 + "bool", "byte", "complex64", "complex128", "error", "float32", "float64", + "int", "int8", "int16", "int32", "int64", "rune", "string", + "uint", "uint8", "uint16", "uint32", "uint64", "uintptr", + // 内置函数 + "append", "cap", "close", "complex", "copy", "delete", "imag", "len", + "make", "new", "panic", "print", "println", "real", "recover", + ]; + + let dfa = Self::build_generic_dfa(); + let keyword_table = Self::build_keyword_table(&keywords); + + (dfa, keyword_table) + } + + // === Java === + fn java_tables() -> (DfaTable, KeywordTable) { + let keywords = vec![ + "abstract", "assert", "boolean", "break", "byte", "case", "catch", + "char", "class", "const", "continue", "default", "do", "double", + "else", "enum", "extends", "final", "finally", "float", "for", + "goto", "if", "implements", "import", "instanceof", "int", + "interface", "long", "native", "new", "package", "private", + "protected", "public", "return", "short", "static", "strictfp", + "super", "switch", "synchronized", "this", "throw", "throws", + "transient", "try", "void", "volatile", "while", + // 常用类型 + "String", "Object", "Integer", "Double", "Boolean", "List", "Map", + "Set", "ArrayList", "HashMap", "HashSet", "System", "out", "println", + ]; + + let dfa = Self::build_generic_dfa(); + let keyword_table = Self::build_keyword_table(&keywords); + + (dfa, keyword_table) + } + + // === JSON === + fn json_tables() -> (DfaTable, KeywordTable) { + let keywords = vec!["true", "false", "null"]; + + let dfa = Self::build_generic_dfa(); + let keyword_table = Self::build_keyword_table(&keywords); + + (dfa, keyword_table) + } + + // === TOML === + fn toml_tables() -> (DfaTable, KeywordTable) { + let keywords = vec!["true", "false"]; + + let dfa = Self::build_generic_dfa(); + let keyword_table = Self::build_keyword_table(&keywords); + + (dfa, keyword_table) + } + + // === Markdown === + fn markdown_tables() -> (DfaTable, KeywordTable) { + let keywords: Vec<&str> = vec![]; + + let dfa = Self::build_generic_dfa(); + let keyword_table = Self::build_keyword_table(&keywords); + + (dfa, keyword_table) + } + + // === HTML === + fn html_tables() -> (DfaTable, KeywordTable) { + let keywords = vec![ + "!DOCTYPE", "a", "abbr", "address", "area", "article", "aside", "audio", + "b", "base", "bdi", "bdo", "blockquote", "body", "br", "button", + "canvas", "caption", "cite", "code", "col", "colgroup", "data", + "datalist", "dd", "del", "details", "dfn", "dialog", "div", "dl", + "dt", "em", "embed", "fieldset", "figcaption", "figure", "footer", + "form", "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", + "hgroup", "hr", "html", "i", "iframe", "img", "input", "ins", + "kbd", "label", "legend", "li", "link", "main", "map", "mark", + "math", "menu", "meta", "meter", "nav", "noscript", "object", "ol", + "optgroup", "option", "output", "p", "picture", "pre", "progress", + "q", "rp", "rt", "ruby", "s", "samp", "script", "search", "section", + "select", "slot", "small", "source", "span", "strong", "style", + "sub", "summary", "sup", "svg", "table", "tbody", "td", "template", + "textarea", "tfoot", "th", "thead", "time", "title", "tr", "track", + "u", "ul", "var", "video", "wbr", + ]; + + let dfa = Self::build_generic_dfa(); + let keyword_table = Self::build_keyword_table(&keywords); + + (dfa, keyword_table) + } + + // === CSS === + fn css_tables() -> (DfaTable, KeywordTable) { + let keywords = vec![ + "align-content", "align-items", "align-self", "all", "animation", + "background", "border", "bottom", "box-shadow", "color", "display", + "flex", "flex-direction", "font", "font-family", "font-size", + "font-weight", "grid", "height", "justify-content", "left", + "margin", "max-height", "max-width", "min-height", "min-width", + "opacity", "overflow", "padding", "position", "right", "top", + "transform", "transition", "visibility", "width", "z-index", + "@media", "@import", "@keyframes", "@font-face", + // 常用值 + "absolute", "auto", "block", "center", "column", "fixed", "flex", + "grid", "hidden", "inline", "inline-block", "none", "relative", + "row", "static", "sticky", "transparent", "unset", + ]; + + let dfa = Self::build_generic_dfa(); + let keyword_table = Self::build_keyword_table(&keywords); + + (dfa, keyword_table) + } + + // === 通用(未知语言) === + fn generic_tables() -> (DfaTable, KeywordTable) { + let keywords: Vec<&str> = vec![]; + + let dfa = Self::build_generic_dfa(); + let keyword_table = Self::build_keyword_table(&keywords); + + (dfa, keyword_table) + } + + // === 通用 DFA 构建 === + /// 构建一个通用的字符分类 DFA + /// + /// 状态 0: 初始/未知 + /// 状态 1: 标识符(字母/数字/下划线) + /// 状态 2: 数字 + /// 状态 3: 字符串(双引号) + /// 状态 4: 字符串(单引号) + /// 状态 5: 注释(//) + /// 状态 6: 注释(/*) + /// 状态 7: 空白 + /// 状态 8: 标点/运算符 + fn build_generic_dfa() -> DfaTable { + const NUM_STATES: usize = 9; + let mut table = vec![0u8; 256 * NUM_STATES]; + + // 状态 0: 初始状态 + for c in b'a'..=b'z' { + table[c as usize] = 1; // -> 标识符 + } + for c in b'A'..=b'Z' { + table[c as usize] = 1; // -> 标识符 + } + table[b'_' as usize] = 1; // -> 标识符 + for c in b'0'..=b'9' { + table[c as usize] = 2; // -> 数字 + } + table[b'"' as usize] = 3; // -> 双引号字符串 + table[b'\'' as usize] = 4; // -> 单引号字符串 + table[b'/' as usize] = 8; // -> 可能是注释开始 + table[b' ' as usize] = 7; // -> 空白 + table[b'\t' as usize] = 7; + table[b'\n' as usize] = 7; + table[b'\r' as usize] = 7; + // 标点/运算符 + for &c in &[b'+', b'-', b'*', b'%', b'=', b'!', b'<', b'>', b'&', b'|', b'^', b'~', b'?', b':', b';', b',', b'.', b'(', b')', b'[', b']', b'{', b'}', b'@', b'#', b'$', b'`'] { + table[c as usize] = 8; + } + + // 状态 1: 标识符中 + for c in b'a'..=b'z' { + table[256 + c as usize] = 1; + } + for c in b'A'..=b'Z' { + table[256 + c as usize] = 1; + } + table[256 + b'_' as usize] = 1; + for c in b'0'..=b'9' { + table[256 + c as usize] = 1; + } + + // 状态 2: 数字中 + for c in b'0'..=b'9' { + table[512 + c as usize] = 2; + } + table[512 + b'.' as usize] = 2; + table[512 + b'e' as usize] = 2; + table[512 + b'E' as usize] = 2; + table[512 + b'x' as usize] = 2; + table[512 + b'X' as usize] = 2; + table[512 + b'a' as usize] = 2; + table[512 + b'b' as usize] = 2; + table[512 + b'c' as usize] = 2; + table[512 + b'd' as usize] = 2; + table[512 + b'f' as usize] = 2; + table[512 + b'A' as usize] = 2; + table[512 + b'B' as usize] = 2; + table[512 + b'C' as usize] = 2; + table[512 + b'D' as usize] = 2; + table[512 + b'F' as usize] = 2; + table[512 + b'_' as usize] = 2; + + // 其他状态保持默认(0) + + DfaTable { + data: table, + num_states: NUM_STATES as u32, + } + } + + /// 构建关键字哈希表(简单完美哈希) + fn build_keyword_table(keywords: &[&str]) -> KeywordTable { + let mut data: Vec = Vec::new(); + let mut keyword_strings: Vec = Vec::new(); + + for &kw in keywords { + // 将关键字字符串编码为 u32 数组 + let bytes = kw.as_bytes(); + let len = bytes.len().min(255) as u32; + data.push(len); + for chunk in bytes.chunks(4) { + let mut val: u32 = 0; + for (i, &b) in chunk.iter().enumerate() { + val |= (b as u32) << (i * 8); + } + data.push(val); + } + keyword_strings.push(kw.to_string()); + } + + KeywordTable { + data, + keywords: keyword_strings, + } + } +} diff --git a/crates/aether-render/src/gpu/lexer.rs b/crates/aether-render/src/gpu/lexer.rs new file mode 100644 index 0000000..4866156 --- /dev/null +++ b/crates/aether-render/src/gpu/lexer.rs @@ -0,0 +1,427 @@ +use windows::core::Result; +use windows::Win32::Graphics::Direct3D11::{ + ID3D11Buffer, ID3D11ComputeShader, ID3D11ShaderResourceView, + ID3D11UnorderedAccessView, + D3D11_BUFFER_UAV, D3D11_UNORDERED_ACCESS_VIEW_DESC, D3D11_UNORDERED_ACCESS_VIEW_DESC_0, +}; +use windows::Win32::Graphics::Dxgi::Common::DXGI_FORMAT_R32_UINT; + +use super::compute_context::{GpuComputeContext, BufferUsage}; +use super::shader::ShaderCompiler; + +/// GPU Token 结构,与 Shader 中的结构体对齐 +#[repr(C)] +#[derive(Clone, Copy, Debug, Default)] +pub struct GpuToken { + /// Token 起始位置(字节偏移) + pub start: u32, + /// Token 长度(字节) + pub len: u32, + /// Token 类型 + pub token_type: u32, + /// 关键字 ID(如果是关键字) + pub keyword_id: u32, + /// 语法分类(由语法分类器填充) + pub syntax_class: u32, + /// 保留字段 + pub _padding: u32, +} + +/// Token 类型常量 +pub mod token_types { + pub const TOKEN_UNKNOWN: u32 = 0; + pub const TOKEN_IDENTIFIER: u32 = 1; + pub const TOKEN_KEYWORD: u32 = 2; + pub const TOKEN_STRING: u32 = 3; + pub const TOKEN_NUMBER: u32 = 4; + pub const TOKEN_COMMENT: u32 = 5; + pub const TOKEN_OPERATOR: u32 = 6; + pub const TOKEN_PUNCTUATION: u32 = 7; + pub const TOKEN_WHITESPACE: u32 = 8; + pub const TOKEN_NEWLINE: u32 = 9; + pub const TOKEN_PREPROCESSOR: u32 = 10; + pub const TOKEN_TYPE_NAME: u32 = 11; + pub const TOKEN_FUNCTION_NAME: u32 = 12; + pub const TOKEN_VARIABLE: u32 = 13; + pub const TOKEN_CONSTANT: u32 = 14; +} + +/// GPU 词法分析器 +/// +/// 使用 D3D11 Compute Shader 实现并行词法分析。 +pub struct GpuLexer { + context: GpuComputeContext, + + // DFA 状态表(常量缓冲区) + dfa_table: ID3D11Buffer, + dfa_srv: ID3D11ShaderResourceView, + + // 关键字完美哈希表(常量缓冲区) + keyword_table: ID3D11Buffer, + keyword_srv: ID3D11ShaderResourceView, + + // Compute Shaders + char_classify_shader: ID3D11ComputeShader, + token_scan_shader: ID3D11ComputeShader, + keyword_lookup_shader: ID3D11ComputeShader, + + // 工作缓冲区 + char_classes_buffer: Option, + tokens_buffer: Option, + tokens_uav: Option, + token_count_buffer: Option, + token_count_uav: Option, +} + +impl GpuLexer { + /// 创建 GPU 词法分析器(使用预编译 Shader bytecode) + /// + /// # Arguments + /// * `context` - GPU 计算上下文 + /// * `dfa_table` - DFA 状态转换表(256 * num_states 字节) + /// * `keyword_hash` - 关键字完美哈希表 + pub fn new( + context: GpuComputeContext, + dfa_table: &[u8], + keyword_hash: &[u32], + ) -> Result { + // 创建 DFA 表缓冲区 + let (dfa_buf, dfa_srv) = Self::create_dfa_buffer(&context, dfa_table)?; + + // 创建关键字哈希表缓冲区 + let (keyword_buf, keyword_srv) = Self::create_keyword_buffer(&context, keyword_hash)?; + + // 加载预编译的 Shader - 使用空 bytecode 作为占位 + // 实际部署时应使用预编译的 CSO 文件或 new_with_shaders + let char_classify = Self::load_shader(&context, &[])?; + let token_scan = Self::load_shader(&context, &[])?; + let keyword_lookup = Self::load_shader(&context, &[])?; + + Ok(Self { + context, + dfa_table: dfa_buf, + dfa_srv, + keyword_table: keyword_buf, + keyword_srv, + char_classify_shader: char_classify, + token_scan_shader: token_scan, + keyword_lookup_shader: keyword_lookup, + char_classes_buffer: None, + tokens_buffer: None, + tokens_uav: None, + token_count_buffer: None, + token_count_uav: None, + }) + } + + /// 创建 GPU 词法分析器(使用 HLSL 源码编译 Shader) + /// + /// # Arguments + /// * `context` - GPU 计算上下文 + /// * `dfa_table` - DFA 状态转换表 + /// * `keyword_hash` - 关键字完美哈希表 + /// * `char_classify_hlsl` - Phase 1 字符分类 HLSL 源码 + /// * `token_scan_hlsl` - Phase 2 Token 扫描 HLSL 源码 + /// * `keyword_lookup_hlsl` - Phase 3 关键字查找 HLSL 源码 + pub fn new_with_shaders( + context: GpuComputeContext, + dfa_table: &[u8], + keyword_hash: &[u32], + char_classify_hlsl: &str, + token_scan_hlsl: &str, + keyword_lookup_hlsl: &str, + ) -> Result { + // 创建 DFA 表缓冲区 + let (dfa_buf, dfa_srv) = Self::create_dfa_buffer(&context, dfa_table)?; + + // 创建关键字哈希表缓冲区 + let (keyword_buf, keyword_srv) = Self::create_keyword_buffer(&context, keyword_hash)?; + + // 编译 HLSL Shader + let char_classify_bc = ShaderCompiler::compile_char_classify(char_classify_hlsl)?; + let token_scan_bc = ShaderCompiler::compile_token_scan(token_scan_hlsl)?; + let keyword_lookup_bc = ShaderCompiler::compile_keyword_lookup(keyword_lookup_hlsl)?; + + let char_classify = context.create_compute_shader(&char_classify_bc)?; + let token_scan = context.create_compute_shader(&token_scan_bc)?; + let keyword_lookup = context.create_compute_shader(&keyword_lookup_bc)?; + + Ok(Self { + context, + dfa_table: dfa_buf, + dfa_srv, + keyword_table: keyword_buf, + keyword_srv, + char_classify_shader: char_classify, + token_scan_shader: token_scan, + keyword_lookup_shader: keyword_lookup, + char_classes_buffer: None, + tokens_buffer: None, + tokens_uav: None, + token_count_buffer: None, + token_count_uav: None, + }) + } + + /// 创建 GPU 词法分析器(使用嵌入的 HLSL 源码编译 Shader) + /// + /// 从编译时嵌入的 HLSL 文件加载并编译 Shader。 + pub fn new_with_embedded_shaders( + context: GpuComputeContext, + dfa_table: &[u8], + keyword_hash: &[u32], + ) -> Result { + const CHAR_CLASSIFY_HLSL: &str = include_str!("shaders/char_classify.hlsl"); + const TOKEN_SCAN_HLSL: &str = include_str!("shaders/token_scan.hlsl"); + const KEYWORD_LOOKUP_HLSL: &str = include_str!("shaders/keyword_lookup.hlsl"); + + Self::new_with_shaders( + context, + dfa_table, + keyword_hash, + CHAR_CLASSIFY_HLSL, + TOKEN_SCAN_HLSL, + KEYWORD_LOOKUP_HLSL, + ) + } + + /// 执行词法分析 + /// + /// # Arguments + /// * `text` - 输入文本(UTF-8) + /// + /// # Returns + /// 识别出的 Token 列表 + pub fn lex(&mut self, text: &[u8]) -> Result> { + if text.is_empty() { + return Ok(Vec::new()); + } + + let text_len = text.len(); + let max_tokens = text_len / 2 + 1; // 最坏情况:每个字符都是 token + + // 1. 确保工作缓冲区足够大 + self.ensure_buffers(text_len, max_tokens)?; + + // 2. 上传文本到 GPU + let text_buffer = self.upload_text(text)?; + + // 3. Phase 1: 字符分类 + self.run_char_classify(&text_buffer, text_len)?; + + // 4. Phase 2: Token 扫描 + self.run_token_scan(text_len, max_tokens)?; + + // 5. Phase 3: 关键字查找 + self.run_keyword_lookup(max_tokens)?; + + // 6. 回读 token 数量和列表 + let tokens = self.readback_tokens(max_tokens)?; + + Ok(tokens) + } + + /// 检查 GPU 是否可用 + pub fn is_available(&self) -> bool { + true // 如果能创建成功,就视为可用 + } + + // 私有辅助方法 + + fn create_dfa_buffer( + context: &GpuComputeContext, + data: &[u8], + ) -> Result<(ID3D11Buffer, ID3D11ShaderResourceView)> { + let buffer = context.create_buffer(data.len(), BufferUsage::Structured, Some(data))?; + let srv = context.create_srv(&buffer)?; + Ok((buffer, srv)) + } + + fn create_keyword_buffer( + context: &GpuComputeContext, + data: &[u32], + ) -> Result<(ID3D11Buffer, ID3D11ShaderResourceView)> { + let bytes = unsafe { + std::slice::from_raw_parts( + data.as_ptr() as *const u8, + data.len() * std::mem::size_of::(), + ) + }; + let buffer = context.create_buffer(bytes.len(), BufferUsage::Structured, Some(bytes))?; + let srv = context.create_srv(&buffer)?; + Ok((buffer, srv)) + } + + fn load_shader(context: &GpuComputeContext, bytecode: &[u8]) -> Result { + context.create_compute_shader(bytecode) + } + + fn ensure_buffers(&mut self, text_len: usize, max_tokens: usize) -> Result<()> { + // 检查并重新分配字符分类缓冲区 + if self.char_classes_buffer.is_none() { + let buf = self.context.create_buffer( + text_len * std::mem::size_of::(), + BufferUsage::ReadWrite, + None, + )?; + self.char_classes_buffer = Some(buf); + } + + // 检查并重新分配 token 缓冲区 + if self.tokens_buffer.is_none() { + let (buf, uav) = self.context.create_structured_buffer::( + max_tokens, + None::<&[GpuToken]>, + true, + )?; + self.tokens_buffer = Some(buf); + self.tokens_uav = uav; + } + + // 检查并重新分配 token 计数缓冲区 + if self.token_count_buffer.is_none() { + let counter_size = std::mem::size_of::(); + let buf = self.context.create_buffer(counter_size, BufferUsage::ReadWrite, None)?; + + // 创建 UAV + let uav_desc = D3D11_UNORDERED_ACCESS_VIEW_DESC { + Format: DXGI_FORMAT_R32_UINT, + ViewDimension: windows::Win32::Graphics::Direct3D11::D3D11_UAV_DIMENSION_BUFFER, + Anonymous: D3D11_UNORDERED_ACCESS_VIEW_DESC_0 { + Buffer: D3D11_BUFFER_UAV { + FirstElement: 0, + NumElements: 1, + Flags: windows::Win32::Graphics::Direct3D11::D3D11_BUFFER_UAV_FLAG_COUNTER.0 as u32, + }, + }, + }; + let mut uav = None; + unsafe { + self.context.device().CreateUnorderedAccessView(&buf, Some(&uav_desc), Some(&mut uav))?; + } + + self.token_count_buffer = Some(buf); + self.token_count_uav = uav; + } + + Ok(()) + } + + fn upload_text(&self, text: &[u8]) -> Result { + self.context.create_buffer(text.len(), BufferUsage::Structured, Some(text)) + } + + fn run_char_classify(&self, text_buffer: &ID3D11Buffer, text_len: usize) -> Result<()> { + let srv = self.context.create_srv(text_buffer)?; + + let char_classes_uav = self.create_uav_from_buffer( + self.char_classes_buffer.as_ref().unwrap(), + text_len as u32, + )?; + + self.context.set_compute_shader(&self.char_classify_shader); + self.context.set_shader_resources(0, &[Some(srv)]); + self.context.set_unordered_access_views(0, &[Some(char_classes_uav)]); + + let groups = ((text_len + 255) / 256) as u32; + self.context.dispatch(&self.char_classify_shader, (groups, 1, 1)); + + Ok(()) + } + + fn run_token_scan(&self, text_len: usize, _max_tokens: usize) -> Result<()> { + let srv = self.context.create_srv(self.char_classes_buffer.as_ref().unwrap())?; + + self.context.set_compute_shader(&self.token_scan_shader); + self.context.set_shader_resources(0, &[Some(srv)]); + self.context.set_unordered_access_views( + 0, + &[ + self.tokens_uav.clone(), + self.token_count_uav.clone(), + ], + ); + + let groups = ((text_len + 255) / 256) as u32; + self.context.dispatch(&self.token_scan_shader, (groups, 1, 1)); + + Ok(()) + } + + fn run_keyword_lookup(&self, max_tokens: usize) -> Result<()> { + self.context.set_compute_shader(&self.keyword_lookup_shader); + self.context.set_shader_resources( + 0, + &[ + Some(self.keyword_srv.clone()), + ], + ); + self.context.set_unordered_access_views( + 0, + &[self.tokens_uav.clone()], + ); + + let groups = ((max_tokens + 255) / 256) as u32; + self.context.dispatch(&self.keyword_lookup_shader, (groups, 1, 1)); + + Ok(()) + } + + fn readback_tokens(&self, max_tokens: usize) -> Result> { + // 读取 token 数量 + let mut count = 0u32; + self.context.read_buffer( + self.token_count_buffer.as_ref().unwrap(), + unsafe { + std::slice::from_raw_parts_mut( + &mut count as *mut u32 as *mut u8, + std::mem::size_of::(), + ) + }, + )?; + + let token_count = count.min(max_tokens as u32) as usize; + if token_count == 0 { + return Ok(Vec::new()); + } + + // 读取 token 列表 + let mut tokens = vec![GpuToken::default(); token_count]; + let token_bytes = unsafe { + std::slice::from_raw_parts_mut( + tokens.as_mut_ptr() as *mut u8, + token_count * std::mem::size_of::(), + ) + }; + + self.context.read_buffer( + self.tokens_buffer.as_ref().unwrap(), + token_bytes, + )?; + + Ok(tokens) + } + + fn create_uav_from_buffer( + &self, + buffer: &ID3D11Buffer, + num_elements: u32, + ) -> Result { + let uav_desc = D3D11_UNORDERED_ACCESS_VIEW_DESC { + Format: DXGI_FORMAT_R32_UINT, + ViewDimension: windows::Win32::Graphics::Direct3D11::D3D11_UAV_DIMENSION_BUFFER, + Anonymous: D3D11_UNORDERED_ACCESS_VIEW_DESC_0 { + Buffer: D3D11_BUFFER_UAV { + FirstElement: 0, + NumElements: num_elements, + Flags: 0, + }, + }, + }; + unsafe { + let mut uav = None; + self.context.device().CreateUnorderedAccessView(buffer, Some(&uav_desc), Some(&mut uav))?; + Ok(uav.unwrap()) + } + } +} diff --git a/crates/aether-render/src/gpu/mod.rs b/crates/aether-render/src/gpu/mod.rs new file mode 100644 index 0000000..f9dd215 --- /dev/null +++ b/crates/aether-render/src/gpu/mod.rs @@ -0,0 +1,9 @@ +pub mod benchmark; +pub mod buffer; +pub mod compute_context; +pub mod language_tables; +pub mod lexer; +pub mod render; +pub mod shader; +pub mod syntax; +pub mod viewport; diff --git a/crates/aether-render/src/gpu/render.rs b/crates/aether-render/src/gpu/render.rs new file mode 100644 index 0000000..dd5a828 --- /dev/null +++ b/crates/aether-render/src/gpu/render.rs @@ -0,0 +1,216 @@ +use aether_core::lexer::{LexemeSpan, TokenKind}; +use windows::Win32::Graphics::Direct2D::Common::D2D1_COLOR_F; + +use super::lexer::{GpuToken, token_types}; +use super::syntax::{SyntaxClass, syntax_classes}; + +/// GPU Token 到 LexemeSpan 的转换 +/// +/// 将 GPU 生成的 Token 转换为渲染器使用的 LexemeSpan。 +pub fn gpu_tokens_to_lexeme_spans( + tokens: &[GpuToken], + syntax_classes: Option<&[SyntaxClass]>, +) -> Vec { + let mut spans = Vec::with_capacity(tokens.len()); + + for (i, token) in tokens.iter().enumerate() { + let kind = if let Some(classes) = syntax_classes { + resolve_token_kind(token, &classes[i]) + } else { + token_type_to_kind(token.token_type) + }; + + spans.push(LexemeSpan { + start: token.start, + len: token.len, + kind, + flags: 0, + }); + } + + spans +} + +/// 根据 Token 类型和语法分类解析最终 TokenKind +fn resolve_token_kind(token: &GpuToken, syntax: &SyntaxClass) -> TokenKind { + // 优先使用语法分类(如果置信度足够高) + if syntax.confidence >= 70 { + match syntax.class_id { + syntax_classes::SYNTAX_FUNCTION_DECL | + syntax_classes::SYNTAX_FUNCTION_CALL => TokenKind::Function, + syntax_classes::SYNTAX_TYPE_NAME => TokenKind::TypeName, + syntax_classes::SYNTAX_VARIABLE_DECL | + syntax_classes::SYNTAX_VARIABLE_REF => TokenKind::Identifier, + syntax_classes::SYNTAX_PARAMETER => TokenKind::Identifier, + syntax_classes::SYNTAX_FIELD_ACCESS => TokenKind::Attribute, + syntax_classes::SYNTAX_MACRO => TokenKind::Macro, + syntax_classes::SYNTAX_ATTRIBUTE => TokenKind::Attribute, + _ => token_type_to_kind(token.token_type), + } + } else { + token_type_to_kind(token.token_type) + } +} + +/// 将 GPU Token 类型转换为 TokenKind +fn token_type_to_kind(token_type: u32) -> TokenKind { + match token_type { + token_types::TOKEN_KEYWORD => TokenKind::Keyword, + token_types::TOKEN_STRING => TokenKind::StringLiteral, + token_types::TOKEN_NUMBER => TokenKind::NumberLiteral, + token_types::TOKEN_COMMENT => TokenKind::LineComment, + token_types::TOKEN_FUNCTION_NAME => TokenKind::Function, + token_types::TOKEN_TYPE_NAME => TokenKind::TypeName, + token_types::TOKEN_OPERATOR => TokenKind::Operator, + token_types::TOKEN_IDENTIFIER => TokenKind::Identifier, + token_types::TOKEN_PREPROCESSOR => TokenKind::Preprocessor, + token_types::TOKEN_CONSTANT => TokenKind::NumberLiteral, + _ => TokenKind::Identifier, + } +} + +/// 合并相邻同色 Token,减少 DrawText 调用 +/// +/// 优化:连续的相同类型 token 合并为单个 span,减少渲染调用次数。 +pub fn merge_same_color_tokens(tokens: &[LexemeSpan]) -> Vec { + if tokens.is_empty() { + return Vec::new(); + } + + let mut merged = Vec::with_capacity(tokens.len() / 2); + let mut current = MergedSpan { + start: tokens[0].start, + len: tokens[0].len, + kind: tokens[0].kind, + }; + + for token in &tokens[1..] { + if token.kind == current.kind && token.start == current.start + current.len { + // 相邻同色,合并 + current.len += token.len; + } else { + // 不同类型或不相邻,保存当前并新建 + merged.push(current); + current = MergedSpan { + start: token.start, + len: token.len, + kind: token.kind, + }; + } + } + + merged.push(current); + merged +} + +/// 合并后的 Span(用于渲染) +#[derive(Clone, Copy, Debug)] +pub struct MergedSpan { + pub start: u32, + pub len: u32, + pub kind: TokenKind, +} + +/// GPU 高亮渲染器 +/// +/// 将 GPU 生成的 token 直接用于 Direct2D 渲染。 +pub struct GpuHighlightRenderer; + +impl GpuHighlightRenderer { + /// 将 TokenKind 映射到主题颜色 + pub fn token_kind_to_color(kind: TokenKind, theme: &crate::theme::Theme) -> D2D1_COLOR_F { + theme.color_for_token(kind) + } +} + +/// 双缓冲管理器 +/// +/// 实现 CPU/GPU 并行处理,避免等待。 +pub struct DoubleBuffer { + buffers: [Option; 2], + current: usize, +} + +impl DoubleBuffer { + pub fn new() -> Self { + Self { + buffers: [None, None], + current: 0, + } + } + + /// 获取当前缓冲区(读取) + pub fn current(&self) -> Option<&T> { + self.buffers[self.current].as_ref() + } + + /// 获取下一个缓冲区(写入) + pub fn next(&mut self) -> &mut Option { + let next = 1 - self.current; + &mut self.buffers[next] + } + + /// 交换缓冲区 + pub fn swap(&mut self) { + self.current = 1 - self.current; + } +} + +/// GPU 缓冲区内存池 +/// +/// 复用 GPU 缓冲区,避免频繁分配/释放。 +pub struct GpuBufferPool { + available: Vec, + in_use: Vec, +} + +impl GpuBufferPool { + pub fn new() -> Self { + Self { + available: Vec::new(), + in_use: Vec::new(), + } + } + + /// 获取一个合适大小的缓冲区 + pub fn acquire( + &mut self, + context: &super::compute_context::GpuComputeContext, + size: usize, + ) -> windows::core::Result { + // 查找足够大的可用缓冲区 + if let Some(idx) = self.available.iter().position(|_buf| { + // 检查缓冲区大小(需要查询 desc) + // 简化:直接复用第一个 + true + }) { + let buf = self.available.remove(idx); + self.in_use.push(buf.clone()); + return Ok(buf); + } + + // 创建新缓冲区 + let buf = context.create_buffer(size, super::compute_context::BufferUsage::ReadWrite, None)?; + self.in_use.push(buf.clone()); + Ok(buf) + } + + /// 释放缓冲区回池 + pub fn release(&mut self, buffer: windows::Win32::Graphics::Direct3D11::ID3D11Buffer) { + // 使用指针比较来找到对应的缓冲区 + let buffer_ptr = &buffer as *const _; + if let Some(idx) = self.in_use.iter().position(|b| { + let b_ptr = b as *const _; + std::ptr::eq(b_ptr, buffer_ptr) + }) { + self.in_use.remove(idx); + self.available.push(buffer); + } + } + + /// 清理所有缓冲区 + pub fn clear(&mut self) { + self.available.clear(); + self.in_use.clear(); + } +} diff --git a/crates/aether-render/src/gpu/shader.rs b/crates/aether-render/src/gpu/shader.rs new file mode 100644 index 0000000..6d292d4 --- /dev/null +++ b/crates/aether-render/src/gpu/shader.rs @@ -0,0 +1,154 @@ +use windows::core::Result; +use windows::Win32::Graphics::Direct3D::Fxc::{ + D3DCompile, D3DCOMPILE_OPTIMIZATION_LEVEL3, D3DCOMPILE_ENABLE_STRICTNESS, +}; +use windows::Win32::Graphics::Direct3D::ID3DBlob; + +/// Shader 编译器 +/// +/// 使用 d3dcompiler_47.dll 将 HLSL 源码编译为 CSO (Compiled Shader Object)。 +pub struct ShaderCompiler; + +impl ShaderCompiler { + /// 编译 HLSL 源码为 Compute Shader Blob + /// + /// # Arguments + /// * `hlsl` - HLSL 源码字符串 + /// * `entry_point` - 入口函数名(如 "main") + /// * `target` - 目标 Shader Model(如 "cs_5_0") + /// + /// # Returns + /// 编译后的字节码 + pub fn compile_compute_shader( + hlsl: &str, + entry_point: &str, + target: &str, + ) -> Result> { + if hlsl.is_empty() { + return Err(windows::core::Error::new( + windows::Win32::Foundation::E_FAIL, + "Empty HLSL source", + )); + } + + let hlsl_bytes = hlsl.as_bytes(); + let entry = windows::core::PCSTR::from_raw(entry_point.as_ptr()); + let target_str = windows::core::PCSTR::from_raw(target.as_ptr()); + let source_name = windows::core::PCSTR::from_raw(b"shader.hlsl\0".as_ptr()); + + let mut code_blob: Option = None; + let mut error_blob: Option = None; + + let flags = D3DCOMPILE_OPTIMIZATION_LEVEL3 | D3DCOMPILE_ENABLE_STRICTNESS; + + unsafe { + let hr = D3DCompile( + hlsl_bytes.as_ptr() as *const _, + hlsl_bytes.len(), + source_name, + None, + None, + entry, + target_str, + flags, + 0, + &mut code_blob, + Some(&mut error_blob), + ); + + if let Some(error) = error_blob { + let ptr = error.GetBufferPointer(); + let size = error.GetBufferSize(); + if size > 0 && !ptr.is_null() { + let msg = std::slice::from_raw_parts(ptr as *const u8, size); + let error_str = String::from_utf8_lossy(msg); + eprintln!("Shader compilation error: {}", error_str); + } + } + + hr?; + + match code_blob { + Some(blob) => { + let ptr = blob.GetBufferPointer(); + let size = blob.GetBufferSize(); + let bytecode = std::slice::from_raw_parts(ptr as *const u8, size).to_vec(); + Ok(bytecode) + } + None => Err(windows::core::Error::new( + windows::Win32::Foundation::E_FAIL, + "D3DCompile succeeded but returned no bytecode", + )), + } + } + } + + /// 便捷方法:编译 Phase 1 字符分类 Shader + pub fn compile_char_classify(hlsl: &str) -> Result> { + Self::compile_compute_shader(hlsl, "main", "cs_5_0") + } + + /// 便捷方法:编译 Phase 2 Token 扫描 Shader + pub fn compile_token_scan(hlsl: &str) -> Result> { + Self::compile_compute_shader(hlsl, "main", "cs_5_0") + } + + /// 便捷方法:编译 Phase 3 关键字查找 Shader + pub fn compile_keyword_lookup(hlsl: &str) -> Result> { + Self::compile_compute_shader(hlsl, "main", "cs_5_0") + } + + /// 便捷方法:编译语法分类 Shader + pub fn compile_syntax_classify(hlsl: &str) -> Result> { + Self::compile_compute_shader(hlsl, "main", "cs_5_0") + } +} + +/// 预编译 Shader 加载器 +/// +/// 从编译时嵌入的 CSO 文件加载 Shader。 +pub struct PrecompiledShader; + +impl PrecompiledShader { + /// 加载预编译的 Compute Shader + pub fn load_compute_shader( + context: &super::compute_context::GpuComputeContext, + bytecode: &[u8], + ) -> Result { + context.create_compute_shader(bytecode) + } +} + +/// Shader 常量缓冲区 +/// +/// 用于向 Shader 传递常量参数。 +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct LexerConstants { + /// 文本长度 + pub text_length: u32, + /// 最大 Token 数 + pub max_tokens: u32, + /// DFA 状态数 + pub num_states: u32, + /// 关键字表大小 + pub keyword_table_size: u32, + /// 保留 + pub _padding: [u32; 4], +} + +/// 创建常量缓冲区 +pub fn create_constant_buffer( + context: &super::compute_context::GpuComputeContext, + data: &T, +) -> Result { + let size = std::mem::size_of::(); + let bytes = unsafe { + std::slice::from_raw_parts( + data as *const T as *const u8, + size, + ) + }; + + context.create_buffer(size, super::compute_context::BufferUsage::Constant, Some(bytes)) +} diff --git a/crates/aether-render/src/gpu/shaders/char_classify.hlsl b/crates/aether-render/src/gpu/shaders/char_classify.hlsl new file mode 100644 index 0000000..c59e6df --- /dev/null +++ b/crates/aether-render/src/gpu/shaders/char_classify.hlsl @@ -0,0 +1,88 @@ +// Phase 1: 字符分类 Shader +// 每个线程处理 1 个字符,将字符分类为词法类别 + +#define THREAD_GROUP_SIZE 256 + +// 字符分类常量 +#define CHAR_UNKNOWN 0 +#define CHAR_LETTER 1 +#define CHAR_DIGIT 2 +#define CHAR_UNDERSCORE 3 +#define CHAR_SPACE 4 +#define CHAR_TAB 5 +#define CHAR_NEWLINE 6 +#define CHAR_QUOTE_SINGLE 7 +#define CHAR_QUOTE_DOUBLE 8 +#define CHAR_SLASH 9 +#define CHAR_STAR 10 +#define CHAR_HASH 11 +#define CHAR_LPAREN 12 +#define CHAR_RPAREN 13 +#define CHAR_LBRACE 14 +#define CHAR_RBRACE 15 +#define CHAR_LBRACKET 16 +#define CHAR_RBRACKET 17 +#define CHAR_SEMICOLON 18 +#define CHAR_COLON 19 +#define CHAR_COMMA 20 +#define CHAR_DOT 21 +#define CHAR_PLUS 22 +#define CHAR_MINUS 23 +#define CHAR_EQUAL 24 +#define CHAR_BANG 25 +#define CHAR_LESS 26 +#define CHAR_GREATER 27 +#define CHAR_AMPERSAND 28 +#define CHAR_PIPE 29 +#define CHAR_PERCENT 30 +#define CHAR_CARET 31 +#define CHAR_TILDE 32 +#define CHAR_AT 33 +#define CHAR_DOLLAR 34 +#define CHAR_BACKSLASH 35 + +// 分类查找表(256 字节,可放入共享内存) +static const uint CHAR_CLASS_TABLE[256] = { + // 控制字符 (0-31) + 0,0,0,0,0,0,0,0,0,5,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + // 空格和标点 (32-63) + 4,25,8,11,34,30,28,7,12,13,10,22,20,23,21,9,2,2,2,2,2,2,2,2,2,2,19,18,26,24,27,0, + // @ A-Z (64-95) + 33,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,16,35,17,32,3, + // ` a-z (96-127) + 0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,14,29,15,0,0, + // 扩展 ASCII (128-255) - 简化为 LETTER 或 UNKNOWN + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 +}; + +// 输入文本缓冲区 +StructuredBuffer TextBuffer : register(t0); +// 输出字符分类 +RWStructuredBuffer CharClasses : register(u0); + +// 常量缓冲区 +cbuffer LexerConstants : register(b0) { + uint TextLength; + uint MaxTokens; + uint NumStates; + uint KeywordTableSize; +}; + +// 辅助函数:从 uint 缓冲区读取第 idx 个字节 +uint ReadByte(uint idx) { + uint word = TextBuffer[idx / 4]; + uint shift = (idx % 4) * 8; + return (word >> shift) & 0xFF; +} + +[numthreads(THREAD_GROUP_SIZE, 1, 1)] +void main(uint3 id : SV_DispatchThreadID) { + uint idx = id.x; + if (idx >= TextLength) return; + + uint byte = ReadByte(idx); + CharClasses[idx] = CHAR_CLASS_TABLE[byte]; +} diff --git a/crates/aether-render/src/gpu/shaders/keyword_lookup.hlsl b/crates/aether-render/src/gpu/shaders/keyword_lookup.hlsl new file mode 100644 index 0000000..a39b405 --- /dev/null +++ b/crates/aether-render/src/gpu/shaders/keyword_lookup.hlsl @@ -0,0 +1,101 @@ +// Phase 3: 关键字查找 Shader +// 基于完美哈希表识别关键字 + +#define THREAD_GROUP_SIZE 256 + +// Token 类型常量 +#define TOKEN_IDENTIFIER 1 +#define TOKEN_KEYWORD 2 + +// Token 结构 +struct Token { + uint start; + uint len; + uint token_type; + uint keyword_id; + uint syntax_class; + uint _padding; +}; + +// 输入/输出 Token 缓冲区 +RWStructuredBuffer Tokens : register(u0); + +// 关键字哈希表(完美哈希) +StructuredBuffer KeywordHash : register(t0); + +// 常量缓冲区 +cbuffer LexerConstants : register(b0) { + uint TextLength; + uint MaxTokens; + uint NumStates; + uint KeywordTableSize; +}; + +// 文本缓冲区(用于读取标识符内容) +StructuredBuffer TextBuffer : register(t1); + +// 辅助函数:从 uint 缓冲区读取第 idx 个字节 +uint ReadByte(uint idx) { + uint word = TextBuffer[idx / 4]; + uint shift = (idx % 4) * 8; + return (word >> shift) & 0xFF; +} + +// FNV-1a 哈希函数 +uint FNV1aHash(uint start, uint len) { + const uint FNV_PRIME = 16777619; + const uint FNV_OFFSET = 2166136261; + + uint hash = FNV_OFFSET; + for (uint i = 0; i < len && i < 64; i++) { // 限制最大长度 + uint byte = ReadByte(start + i); + // 转换为小写(简化) + if (byte >= 'A' && byte <= 'Z') { + byte = byte - 'A' + 'a'; + } + hash ^= byte; + hash *= FNV_PRIME; + } + return hash; +} + +// 完美哈希查找 +bool LookupKeyword(uint start, uint len, out uint keyword_id) { + if (len > 32 || len == 0) { // 关键字长度限制 + keyword_id = 0; + return false; + } + + uint hash = FNV1aHash(start, len); + uint idx = hash % KeywordTableSize; + + // 检查哈希表 + uint stored_hash = KeywordHash[idx * 2]; + uint stored_id = KeywordHash[idx * 2 + 1]; + + if (stored_hash == hash && stored_id != 0) { + keyword_id = stored_id; + return true; + } + + keyword_id = 0; + return false; +} + +[numthreads(THREAD_GROUP_SIZE, 1, 1)] +void main(uint3 id : SV_DispatchThreadID) { + uint idx = id.x; + if (idx >= MaxTokens) return; + + Token tok = Tokens[idx]; + + // 只处理标识符 + if (tok.token_type != TOKEN_IDENTIFIER) return; + + uint keyword_id; + if (LookupKeyword(tok.start, tok.len, keyword_id)) { + tok.token_type = TOKEN_KEYWORD; + tok.keyword_id = keyword_id; + Tokens[idx] = tok; + } +} diff --git a/crates/aether-render/src/gpu/shaders/syntax_classify.hlsl b/crates/aether-render/src/gpu/shaders/syntax_classify.hlsl new file mode 100644 index 0000000..d524583 --- /dev/null +++ b/crates/aether-render/src/gpu/shaders/syntax_classify.hlsl @@ -0,0 +1,141 @@ +// 语法分类 Shader +// 基于 Token 流识别简单语法模式 + +#define THREAD_GROUP_SIZE 256 +#define MAX_PATTERN_LENGTH 4 + +// Token 类型常量 +#define TOKEN_IDENTIFIER 1 +#define TOKEN_KEYWORD 2 +#define TOKEN_STRING 3 +#define TOKEN_NUMBER 4 +#define TOKEN_COMMENT 5 +#define TOKEN_OPERATOR 6 +#define TOKEN_PUNCTUATION 7 +#define TOKEN_WHITESPACE 8 +#define TOKEN_NEWLINE 9 +#define TOKEN_PREPROCESSOR 10 + +// 语法分类常量 +#define SYNTAX_UNKNOWN 0 +#define SYNTAX_FUNCTION_DECL 1 +#define SYNTAX_FUNCTION_CALL 2 +#define SYNTAX_TYPE_NAME 3 +#define SYNTAX_VARIABLE_DECL 4 +#define SYNTAX_VARIABLE_REF 5 +#define SYNTAX_PARAMETER 6 +#define SYNTAX_FIELD_ACCESS 7 +#define SYNTAX_MACRO 8 +#define SYNTAX_ATTRIBUTE 9 +#define SYNTAX_LIFETIME 10 +#define SYNTAX_MODULE 11 +#define SYNTAX_TRAIT 12 +#define SYNTAX_STRUCT 13 +#define SYNTAX_ENUM 14 +#define SYNTAX_IMPL 15 + +// Token 结构 +struct Token { + uint start; + uint len; + uint token_type; + uint keyword_id; + uint syntax_class; + uint _padding; +}; + +// 语法模式结构 +struct SyntaxPattern { + uint pattern_type; + uint4 token_sequence; + uint sequence_len; + uint output_class; + uint priority; + uint2 _padding; +}; + +// 输入 Token 缓冲区 +StructuredBuffer Tokens : register(t0); +// 语法模式缓冲区 +StructuredBuffer Patterns : register(t1); + +// 输出语法分类 +RWStructuredBuffer SyntaxClasses : register(u0); +// output_class, confidence, _padding[2] + +// 常量缓冲区 +cbuffer SyntaxConstants : register(b0) { + uint TokenCount; + uint PatternCount; + uint _padding[2]; +}; + +// 匹配模式 +bool MatchPattern(uint token_idx, SyntaxPattern pattern, out uint match_len) { + if (token_idx + pattern.sequence_len > TokenCount) { + match_len = 0; + return false; + } + + for (uint i = 0; i < pattern.sequence_len; i++) { + Token tok = Tokens[token_idx + i]; + uint expected = 0; + + switch (i) { + case 0: expected = pattern.token_sequence.x; break; + case 1: expected = pattern.token_sequence.y; break; + case 2: expected = pattern.token_sequence.z; break; + case 3: expected = pattern.token_sequence.w; break; + } + + if (tok.token_type != expected) { + match_len = 0; + return false; + } + } + + match_len = pattern.sequence_len; + return true; +} + +[numthreads(THREAD_GROUP_SIZE, 1, 1)] +void main(uint3 id : SV_DispatchThreadID) { + uint token_idx = id.x; + if (token_idx >= TokenCount) return; + + Token tok = Tokens[token_idx]; + + // 只处理标识符和关键字 + if (tok.token_type != TOKEN_IDENTIFIER && tok.token_type != TOKEN_KEYWORD) { + return; + } + + uint best_class = SYNTAX_UNKNOWN; + uint best_priority = 0; + uint best_match_len = 0; + + // 尝试匹配所有模式 + for (uint p = 0; p < PatternCount; p++) { + SyntaxPattern pattern = Patterns[p]; + uint match_len = 0; + + if (MatchPattern(token_idx, pattern, match_len)) { + if (pattern.priority > best_priority) { + best_priority = pattern.priority; + best_class = pattern.output_class; + best_match_len = match_len; + } + } + } + + // 写入结果 + if (best_class != SYNTAX_UNKNOWN) { + uint confidence = 70 + (best_priority / 2); // 基础置信度 + 优先级加成 + if (confidence > 100) confidence = 100; + + SyntaxClasses[token_idx] = uint4(best_class, confidence, best_match_len, 0); + + // 更新 Token 的语法分类(可选) + // 注意:这需要 UAV 访问 Tokens 缓冲区 + } +} diff --git a/crates/aether-render/src/gpu/shaders/token_scan.hlsl b/crates/aether-render/src/gpu/shaders/token_scan.hlsl new file mode 100644 index 0000000..2dce56f --- /dev/null +++ b/crates/aether-render/src/gpu/shaders/token_scan.hlsl @@ -0,0 +1,312 @@ +// Phase 2: Token 扫描 Shader +// 基于字符分类结果,识别 Token 边界 + +#define THREAD_GROUP_SIZE 256 + +// Token 类型常量(与 Rust 侧对齐) +#define TOKEN_UNKNOWN 0 +#define TOKEN_IDENTIFIER 1 +#define TOKEN_KEYWORD 2 +#define TOKEN_STRING 3 +#define TOKEN_NUMBER 4 +#define TOKEN_COMMENT 5 +#define TOKEN_OPERATOR 6 +#define TOKEN_PUNCTUATION 7 +#define TOKEN_WHITESPACE 8 +#define TOKEN_NEWLINE 9 +#define TOKEN_PREPROCESSOR 10 + +// 字符分类常量(与 char_classify.hlsl 对齐) +#define CHAR_LETTER 1 +#define CHAR_DIGIT 2 +#define CHAR_UNDERSCORE 3 +#define CHAR_SPACE 4 +#define CHAR_TAB 5 +#define CHAR_NEWLINE 6 +#define CHAR_QUOTE_SINGLE 7 +#define CHAR_QUOTE_DOUBLE 8 +#define CHAR_SLASH 9 +#define CHAR_STAR 10 +#define CHAR_HASH 11 +#define CHAR_LPAREN 12 +#define CHAR_RPAREN 13 +#define CHAR_LBRACE 14 +#define CHAR_RBRACE 15 +#define CHAR_LBRACKET 16 +#define CHAR_RBRACKET 17 +#define CHAR_SEMICOLON 18 +#define CHAR_COLON 19 +#define CHAR_COMMA 20 +#define CHAR_DOT 21 +#define CHAR_PLUS 22 +#define CHAR_MINUS 23 +#define CHAR_EQUAL 24 +#define CHAR_BANG 25 +#define CHAR_LESS 26 +#define CHAR_GREATER 27 +#define CHAR_AMPERSAND 28 +#define CHAR_PIPE 29 +#define CHAR_PERCENT 30 +#define CHAR_CARET 31 +#define CHAR_TILDE 32 +#define CHAR_AT 33 +#define CHAR_DOLLAR 34 +#define CHAR_BACKSLASH 35 + +// Token 结构(与 Rust 侧 GpuToken 对齐) +struct Token { + uint start; + uint len; + uint token_type; + uint keyword_id; + uint syntax_class; + uint _padding; +}; + +// 输入:字符分类结果 +StructuredBuffer CharClasses : register(t0); +// 输出:Token 列表 +RWStructuredBuffer Tokens : register(u0); +// 输出:Token 计数(带原子计数器的 UAV) +RWStructuredBuffer TokenCount : register(u1); + +// 常量缓冲区 +cbuffer LexerConstants : register(b0) { + uint TextLength; + uint MaxTokens; + uint NumStates; + uint KeywordTableSize; +}; + +// 组共享内存:标记 token 起始位置 +groupshared uint local_starts[THREAD_GROUP_SIZE]; +groupshared uint local_types[THREAD_GROUP_SIZE]; + +// 判断字符是否属于标识符 +bool IsIdentifierChar(uint char_class) { + return char_class == CHAR_LETTER || + char_class == CHAR_DIGIT || + char_class == CHAR_UNDERSCORE; +} + +// 判断字符是否是数字的一部分 +bool IsNumberChar(uint char_class, uint prev_class) { + return char_class == CHAR_DIGIT || + (char_class == CHAR_DOT && prev_class == CHAR_DIGIT) || + (char_class == CHAR_UNDERSCORE && prev_class == CHAR_DIGIT); +} + +// 判断字符是否是空白 +bool IsWhitespace(uint char_class) { + return char_class == CHAR_SPACE || char_class == CHAR_TAB; +} + +// 判断字符是否是换行 +bool IsNewline(uint char_class) { + return char_class == CHAR_NEWLINE; +} + +// 判断字符是否是标点 +bool IsPunctuation(uint char_class) { + return char_class >= CHAR_LPAREN && char_class <= CHAR_COMMA; +} + +// 判断字符是否是运算符开始 +bool IsOperatorStart(uint char_class) { + return char_class == CHAR_PLUS || char_class == CHAR_MINUS || + char_class == CHAR_STAR || char_class == CHAR_SLASH || + char_class == CHAR_PERCENT || char_class == CHAR_EQUAL || + char_class == CHAR_BANG || char_class == CHAR_LESS || + char_class == CHAR_GREATER || char_class == CHAR_AMPERSAND || + char_class == CHAR_PIPE || char_class == CHAR_CARET || + char_class == CHAR_TILDE || char_class == CHAR_DOT; +} + +// 获取单字符 token 类型 +uint GetSingleCharTokenType(uint char_class) { + switch (char_class) { + case CHAR_LPAREN: case CHAR_RPAREN: + case CHAR_LBRACE: case CHAR_RBRACE: + case CHAR_LBRACKET: case CHAR_RBRACKET: + case CHAR_SEMICOLON: case CHAR_COLON: + case CHAR_COMMA: + return TOKEN_PUNCTUATION; + case CHAR_NEWLINE: + return TOKEN_NEWLINE; + default: + return TOKEN_UNKNOWN; + } +} + +[numthreads(THREAD_GROUP_SIZE, 1, 1)] +void main(uint3 id : SV_DispatchThreadID, uint3 group_id : SV_GroupID) { + uint idx = id.x; + uint local_idx = id.x % THREAD_GROUP_SIZE; + + // 初始化共享内存 + local_starts[local_idx] = 0; + local_types[local_idx] = TOKEN_UNKNOWN; + + GroupMemoryBarrierWithGroupSync(); + + if (idx >= TextLength) return; + + uint char_class = CharClasses[idx]; + uint prev_class = (idx > 0) ? CharClasses[idx - 1] : 0; + + // 判断是否是 token 起始位置 + bool is_token_start = false; + uint token_type = TOKEN_UNKNOWN; + + if (idx == 0) { + // 文本开始,总是 token 起始 + is_token_start = true; + } else if (IsWhitespace(char_class)) { + // 空白字符:合并为单个 whitespace token + if (!IsWhitespace(prev_class)) { + is_token_start = true; + token_type = TOKEN_WHITESPACE; + } + } else if (IsNewline(char_class)) { + // 换行:单独一个 token + if (!IsNewline(prev_class)) { + is_token_start = true; + token_type = TOKEN_NEWLINE; + } + } else if (char_class == CHAR_QUOTE_DOUBLE || char_class == CHAR_QUOTE_SINGLE) { + // 字符串开始 + is_token_start = true; + token_type = TOKEN_STRING; + } else if (char_class == CHAR_SLASH && idx + 1 < TextLength) { + // 可能是注释开始 + uint next_class = CharClasses[idx + 1]; + if (next_class == CHAR_SLASH || next_class == CHAR_STAR) { + is_token_start = true; + token_type = TOKEN_COMMENT; + } else { + is_token_start = !IsOperatorStart(prev_class); + token_type = TOKEN_OPERATOR; + } + } else if (char_class == CHAR_HASH && idx == 0) { + // 预处理指令 + is_token_start = true; + token_type = TOKEN_PREPROCESSOR; + } else if (IsIdentifierChar(char_class)) { + // 标识符 + if (!IsIdentifierChar(prev_class)) { + is_token_start = true; + token_type = TOKEN_IDENTIFIER; + } + } else if (IsNumberChar(char_class, prev_class)) { + // 数字 + if (!IsNumberChar(prev_class, (idx > 1) ? CharClasses[idx - 2] : 0)) { + is_token_start = true; + token_type = TOKEN_NUMBER; + } + } else if (IsOperatorStart(char_class)) { + // 运算符 + if (!IsOperatorStart(prev_class)) { + is_token_start = true; + token_type = TOKEN_OPERATOR; + } + } else if (IsPunctuation(char_class)) { + // 标点 + is_token_start = true; + token_type = TOKEN_PUNCTUATION; + } + + // 标记 token 起始 + if (is_token_start) { + local_starts[local_idx] = 1; + local_types[local_idx] = token_type; + } + + GroupMemoryBarrierWithGroupSync(); + + // 前缀和计算 token 索引(简化版:只处理组内) + // 注意:完整实现需要跨组前缀和 + if (local_idx == 0) { + uint token_idx = 0; + for (uint i = 0; i < THREAD_GROUP_SIZE; i++) { + if (local_starts[i] == 1) { + uint global_idx = group_id.x * THREAD_GROUP_SIZE + i; + if (global_idx < TextLength) { + // 计算 token 长度(到下一个 token 开始或文本结束) + uint token_start = global_idx; + uint token_len = 1; + uint j = global_idx + 1; + + // 根据 token 类型决定如何扩展 + uint ttype = local_types[i]; + + if (ttype == TOKEN_STRING) { + // 字符串:找到匹配的引号 + uint quote_char = CharClasses[token_start]; + j = token_start + 1; + while (j < TextLength && CharClasses[j] != quote_char) { + if (CharClasses[j] == CHAR_BACKSLASH) j++; // 转义 + j++; + } + if (j < TextLength) j++; // 包含结束引号 + token_len = j - token_start; + } else if (ttype == TOKEN_COMMENT) { + // 注释:到行尾或 */ + if (token_start + 1 < TextLength && CharClasses[token_start + 1] == CHAR_STAR) { + // 块注释 /* */ + j = token_start + 2; + while (j + 1 < TextLength && !(CharClasses[j] == CHAR_STAR && CharClasses[j + 1] == CHAR_SLASH)) { + j++; + } + if (j + 1 < TextLength) j += 2; + } else { + // 行注释 // + j = token_start + 2; + while (j < TextLength && !IsNewline(CharClasses[j])) { + j++; + } + } + token_len = j - token_start; + } else if (ttype == TOKEN_WHITESPACE) { + while (j < TextLength && IsWhitespace(CharClasses[j])) { + j++; + } + token_len = j - token_start; + } else if (ttype == TOKEN_IDENTIFIER) { + while (j < TextLength && IsIdentifierChar(CharClasses[j])) { + j++; + } + token_len = j - token_start; + } else if (ttype == TOKEN_NUMBER) { + while (j < TextLength && IsNumberChar(CharClasses[j], CharClasses[j - 1])) { + j++; + } + token_len = j - token_start; + } else if (ttype == TOKEN_OPERATOR) { + while (j < TextLength && IsOperatorStart(CharClasses[j])) { + j++; + } + token_len = j - token_start; + } else { + // 单字符 token + token_len = 1; + } + + // 原子递增获取全局 token 索引 + uint global_token_idx; + InterlockedAdd(TokenCount[0], 1, global_token_idx); + + if (global_token_idx < MaxTokens) { + Token tok; + tok.start = token_start; + tok.len = token_len; + tok.token_type = ttype; + tok.keyword_id = 0; + tok.syntax_class = 0; + tok._padding = 0; + Tokens[global_token_idx] = tok; + } + } + } + } + } +} diff --git a/crates/aether-render/src/gpu/syntax.rs b/crates/aether-render/src/gpu/syntax.rs new file mode 100644 index 0000000..0d5f0ef --- /dev/null +++ b/crates/aether-render/src/gpu/syntax.rs @@ -0,0 +1,300 @@ +use windows::core::Result; +use windows::Win32::Graphics::Direct3D11::{ + ID3D11Buffer, ID3D11ComputeShader, ID3D11ShaderResourceView, + ID3D11UnorderedAccessView, + D3D11_BUFFER_UAV, D3D11_UNORDERED_ACCESS_VIEW_DESC, D3D11_UNORDERED_ACCESS_VIEW_DESC_0, +}; +use windows::Win32::Graphics::Dxgi::Common::DXGI_FORMAT_R32_UINT; + +use super::compute_context::GpuComputeContext; + +/// 语法分类类型 +#[repr(C)] +#[derive(Clone, Copy, Debug, Default)] +pub struct SyntaxClass { + pub class_id: u32, + pub confidence: u32, // 0-100,分类置信度 + pub _padding: [u32; 2], +} + +/// 语法分类常量 +pub mod syntax_classes { + pub const SYNTAX_UNKNOWN: u32 = 0; + pub const SYNTAX_FUNCTION_DECL: u32 = 1; + pub const SYNTAX_FUNCTION_CALL: u32 = 2; + pub const SYNTAX_TYPE_NAME: u32 = 3; + pub const SYNTAX_VARIABLE_DECL: u32 = 4; + pub const SYNTAX_VARIABLE_REF: u32 = 5; + pub const SYNTAX_PARAMETER: u32 = 6; + pub const SYNTAX_FIELD_ACCESS: u32 = 7; + pub const SYNTAX_MACRO: u32 = 8; + pub const SYNTAX_ATTRIBUTE: u32 = 9; + pub const SYNTAX_LIFETIME: u32 = 10; + pub const SYNTAX_MODULE: u32 = 11; + pub const SYNTAX_TRAIT: u32 = 12; + pub const SYNTAX_STRUCT: u32 = 13; + pub const SYNTAX_ENUM: u32 = 14; + pub const SYNTAX_IMPL: u32 = 15; +} + +/// GPU 语法分类器 +/// +/// 基于 Token 流进行简单语法模式的 GPU 并行识别。 +/// 复杂语法分析仍由 Tree-sitter 处理。 +pub struct GpuSyntaxClassifier { + context: GpuComputeContext, + + // 语言特定的语法模式 + patterns_buffer: ID3D11Buffer, + patterns_srv: ID3D11ShaderResourceView, + + // Compute Shader + classify_shader: ID3D11ComputeShader, +} + +/// 语法模式定义(CPU 侧) +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct SyntaxPattern { + /// 模式类型 + pub pattern_type: u32, + /// Token 类型序列(最多 4 个) + pub token_sequence: [u32; 4], + /// 序列长度 + pub sequence_len: u32, + /// 输出语法分类 + pub output_class: u32, + /// 优先级(高优先级覆盖低优先级) + pub priority: u32, + /// 保留 + pub _padding: [u32; 2], +} + +impl GpuSyntaxClassifier { + /// 创建语法分类器 + pub fn new( + context: GpuComputeContext, + language: &str, + ) -> Result { + let patterns = Self::build_patterns(language); + let (patterns_buf, patterns_srv) = Self::create_patterns_buffer(&context, &patterns)?; + + let classify_shader = Self::load_shader( + &context, + &[], // 占位:实际使用时应加载预编译的 syntax_classify.cso + )?; + + Ok(Self { + context, + patterns_buffer: patterns_buf, + patterns_srv, + classify_shader, + }) + } + + /// 对 Token 流进行语法分类 + /// + /// # Arguments + /// * `tokens` - 输入 Token 列表(GPU 缓冲区) + /// + /// # Returns + /// 语法分类结果(GPU 缓冲区) + pub fn classify( + &self, + tokens: &ID3D11Buffer, + token_count: usize, + ) -> Result { + // 创建输出缓冲区 + let output_size = token_count * std::mem::size_of::(); + let output = self.context.create_buffer( + output_size, + super::compute_context::BufferUsage::ReadWrite, + None, + )?; + + let output_uav = self.create_uav(&output, token_count as u32)?; + let tokens_srv = self.context.create_srv(tokens)?; + + // 设置 Shader 资源 + self.context.set_compute_shader(&self.classify_shader); + self.context.set_shader_resources( + 0, + &[ + Some(tokens_srv), + Some(self.patterns_srv.clone()), + ], + ); + self.context.set_unordered_access_views( + 0, + &[Some(output_uav)], + ); + + // 分派 + let groups = ((token_count + 255) / 256) as u32; + self.context.dispatch(&self.classify_shader, (groups, 1, 1)); + + Ok(output) + } + + /// 回读语法分类结果 + pub fn readback_classes( + &self, + buffer: &ID3D11Buffer, + count: usize, + ) -> Result> { + let mut classes = vec![SyntaxClass::default(); count]; + let bytes = unsafe { + std::slice::from_raw_parts_mut( + classes.as_mut_ptr() as *mut u8, + count * std::mem::size_of::(), + ) + }; + + self.context.read_buffer(buffer, bytes)?; + Ok(classes) + } + + // 私有方法 + + fn build_patterns(_language: &str) -> Vec { + // 通用语法模式(适用于类 C 语言) + vec![ + // 函数声明: type name( 或 fn name( + SyntaxPattern { + pattern_type: 1, + token_sequence: [ + super::lexer::token_types::TOKEN_IDENTIFIER, + super::lexer::token_types::TOKEN_IDENTIFIER, + super::lexer::token_types::TOKEN_PUNCTUATION, + 0, + ], + sequence_len: 3, + output_class: syntax_classes::SYNTAX_FUNCTION_DECL, + priority: 100, + _padding: [0; 2], + }, + // 函数调用: name( + SyntaxPattern { + pattern_type: 2, + token_sequence: [ + super::lexer::token_types::TOKEN_IDENTIFIER, + super::lexer::token_types::TOKEN_PUNCTUATION, + 0, + 0, + ], + sequence_len: 2, + output_class: syntax_classes::SYNTAX_FUNCTION_CALL, + priority: 80, + _padding: [0; 2], + }, + // 类型声明: struct/enum/trait Name + SyntaxPattern { + pattern_type: 3, + token_sequence: [ + super::lexer::token_types::TOKEN_KEYWORD, + super::lexer::token_types::TOKEN_IDENTIFIER, + 0, + 0, + ], + sequence_len: 2, + output_class: syntax_classes::SYNTAX_TYPE_NAME, + priority: 90, + _padding: [0; 2], + }, + // 变量声明: let mut name + SyntaxPattern { + pattern_type: 4, + token_sequence: [ + super::lexer::token_types::TOKEN_KEYWORD, + super::lexer::token_types::TOKEN_KEYWORD, + super::lexer::token_types::TOKEN_IDENTIFIER, + 0, + ], + sequence_len: 3, + output_class: syntax_classes::SYNTAX_VARIABLE_DECL, + priority: 85, + _padding: [0; 2], + }, + // 字段访问: .name + SyntaxPattern { + pattern_type: 5, + token_sequence: [ + super::lexer::token_types::TOKEN_PUNCTUATION, + super::lexer::token_types::TOKEN_IDENTIFIER, + 0, + 0, + ], + sequence_len: 2, + output_class: syntax_classes::SYNTAX_FIELD_ACCESS, + priority: 70, + _padding: [0; 2], + }, + // 宏调用: name! + SyntaxPattern { + pattern_type: 6, + token_sequence: [ + super::lexer::token_types::TOKEN_IDENTIFIER, + super::lexer::token_types::TOKEN_PUNCTUATION, + 0, + 0, + ], + sequence_len: 2, + output_class: syntax_classes::SYNTAX_MACRO, + priority: 75, + _padding: [0; 2], + }, + ] + } + + fn create_patterns_buffer( + context: &GpuComputeContext, + patterns: &[SyntaxPattern], + ) -> Result<(ID3D11Buffer, ID3D11ShaderResourceView)> { + let bytes = unsafe { + std::slice::from_raw_parts( + patterns.as_ptr() as *const u8, + patterns.len() * std::mem::size_of::(), + ) + }; + + let buffer = context.create_buffer( + bytes.len(), + super::compute_context::BufferUsage::Structured, + Some(bytes), + )?; + + let srv = context.create_srv(&buffer)?; + + Ok((buffer, srv)) + } + + fn load_shader( + context: &GpuComputeContext, + bytecode: &[u8], + ) -> Result { + context.create_compute_shader(bytecode) + } + + fn create_uav( + &self, + buffer: &ID3D11Buffer, + num_elements: u32, + ) -> Result { + let uav_desc = D3D11_UNORDERED_ACCESS_VIEW_DESC { + Format: DXGI_FORMAT_R32_UINT, + ViewDimension: windows::Win32::Graphics::Direct3D11::D3D11_UAV_DIMENSION_BUFFER, + Anonymous: D3D11_UNORDERED_ACCESS_VIEW_DESC_0 { + Buffer: D3D11_BUFFER_UAV { + FirstElement: 0, + NumElements: num_elements, + Flags: 0, + }, + }, + }; + unsafe { + let mut uav = None; + self.context.device().CreateUnorderedAccessView(buffer, Some(&uav_desc), Some(&mut uav))?; + Ok(uav.unwrap()) + } + } +} diff --git a/crates/aether-render/src/gpu/viewport.rs b/crates/aether-render/src/gpu/viewport.rs new file mode 100644 index 0000000..04c0762 --- /dev/null +++ b/crates/aether-render/src/gpu/viewport.rs @@ -0,0 +1,325 @@ +use aether_core::lexer::LexemeSpan; + +/// 视口优先的增量高亮缓存 +/// +/// 只缓存可见行范围内的高亮结果,配合编辑距离检测实现增量更新。 +#[derive(Clone, Debug)] +pub struct ViewportHighlightCache { + /// 缓存窗口起始行(全局行号) + pub window_start: usize, + /// 缓存窗口大小(行数) + pub window_len: usize, + /// 每行的 token 列表(下标 = 全局行号 - window_start) + pub tokens: Vec>, + /// 每行的 buffer_version(用于检测过期) + pub versions: Vec, + /// 当前 buffer_version(外部传入) + pub current_version: u64, + /// 脏行标记(需要重新高亮) + pub dirty_lines: Vec, + /// 每行的文本内容(用于编辑距离比较) + pub line_texts: Vec, +} + +impl ViewportHighlightCache { + pub fn new() -> Self { + Self { + window_start: 0, + window_len: 0, + tokens: Vec::new(), + versions: Vec::new(), + current_version: 0, + dirty_lines: Vec::new(), + line_texts: Vec::new(), + } + } + + /// 获取指定全局行的 token 列表 + pub fn get_line_tokens(&self, line_idx: usize) -> Option<&[LexemeSpan]> { + let slot = line_idx.checked_sub(self.window_start)?; + self.tokens.get(slot).map(|v| v.as_slice()) + } + + /// 设置指定全局行的 token 列表 + pub fn set_line_tokens(&mut self, line_idx: usize, tokens: Vec, version: u64) { + if let Some(slot) = line_idx.checked_sub(self.window_start) { + if slot < self.tokens.len() { + self.tokens[slot] = tokens; + self.versions[slot] = version; + self.dirty_lines[slot] = false; + } + } + } + + /// 标记指定行为脏(需要重新高亮) + pub fn mark_line_dirty(&mut self, line_idx: usize) { + if let Some(slot) = line_idx.checked_sub(self.window_start) { + if slot < self.dirty_lines.len() { + self.dirty_lines[slot] = true; + } + } + } + + /// 调整缓存窗口大小和位置 + /// + /// 重叠部分保留,新进入窗口的行标记为脏。 + pub fn resize_window(&mut self, new_start: usize, new_len: usize, version: u64) { + if self.window_start == new_start && self.window_len == new_len && version == self.current_version { + return; + } + + self.current_version = version; + + // 创建新的缓存 + let mut new_tokens: Vec> = Vec::with_capacity(new_len); + let mut new_versions: Vec = Vec::with_capacity(new_len); + let mut new_dirty: Vec = Vec::with_capacity(new_len); + let mut new_texts: Vec = Vec::with_capacity(new_len); + + for gi in new_start..new_start + new_len { + if gi >= self.window_start && gi < self.window_start + self.window_len { + // 重叠行:保留旧数据 + let old_slot = gi - self.window_start; + new_tokens.push(std::mem::take(&mut self.tokens[old_slot])); + new_versions.push(self.versions[old_slot]); + new_texts.push(std::mem::take(&mut self.line_texts[old_slot])); + // 如果版本过期,标记为脏 + new_dirty.push(self.versions[old_slot] != version); + } else { + // 新行:初始化为空,标记为脏 + new_tokens.push(Vec::new()); + new_versions.push(0); + new_dirty.push(true); + new_texts.push(String::new()); + } + } + + self.window_start = new_start; + self.window_len = new_len; + self.tokens = new_tokens; + self.versions = new_versions; + self.dirty_lines = new_dirty; + self.line_texts = new_texts; + } + + /// 使用编辑距离检测增量更新 + /// + /// 比较新旧文本,只标记真正发生变化的行为脏。 + pub fn update_with_edit_distance( + &mut self, + lines: &[String], + version: u64, + threshold: f32, + ) { + self.current_version = version; + + for (slot, new_text) in lines.iter().enumerate() { + if slot >= self.line_texts.len() { + break; + } + + let old_text = &self.line_texts[slot]; + + // 快速路径:文本完全相同 + if old_text == new_text { + // 保持现有 token,只更新版本 + self.versions[slot] = version; + self.dirty_lines[slot] = false; + continue; + } + + // 检查是否跨越 token 边界 + if EditDistanceDetector::crosses_token_boundary(old_text, new_text) { + self.dirty_lines[slot] = true; + self.line_texts[slot] = new_text.clone(); + continue; + } + + // 计算编辑距离 + let dist = EditDistanceDetector::distance(old_text, new_text); + let max_len = old_text.len().max(new_text.len()); + + if max_len > 0 { + let ratio = dist as f32 / max_len as f32; + if ratio > threshold { + // 显著变化:标记为脏 + self.dirty_lines[slot] = true; + self.line_texts[slot] = new_text.clone(); + } else { + // 微小变化:尝试复用现有 token(偏移调整) + // 标记为脏以重新高亮(简化实现) + self.dirty_lines[slot] = true; + self.line_texts[slot] = new_text.clone(); + } + } + } + } + + /// 获取所有脏行的索引(全局行号) + pub fn dirty_line_indices(&self) -> Vec { + self.dirty_lines + .iter() + .enumerate() + .filter(|(_, &is_dirty)| is_dirty) + .map(|(slot, _)| self.window_start + slot) + .collect() + } + + /// 获取缓存窗口起始行 + pub fn window_start(&self) -> usize { + self.window_start + } + + /// 获取缓存窗口大小 + pub fn window_len(&self) -> usize { + self.window_len + } + + /// 获取当前缓存的 buffer_version + pub fn buffer_version(&self) -> u64 { + self.current_version + } + + /// 检查缓存是否为空(未初始化) + pub fn is_empty(&self) -> bool { + self.window_len == 0 || self.tokens.is_empty() + } + + /// 清除所有缓存 + pub fn clear(&mut self) { + self.window_start = 0; + self.window_len = 0; + self.tokens.clear(); + self.versions.clear(); + self.dirty_lines.clear(); + self.line_texts.clear(); + } +} + +/// 编辑距离检测器 +/// +/// 检测两行文本之间的编辑距离,用于判断是否需要重新高亮。 +pub struct EditDistanceDetector; + +impl EditDistanceDetector { + /// 计算两个字符串的 Levenshtein 编辑距离 + pub fn distance(a: &str, b: &str) -> usize { + let a_len = a.chars().count(); + let b_len = b.chars().count(); + + if a_len == 0 { + return b_len; + } + if b_len == 0 { + return a_len; + } + + // 使用滚动数组优化空间 + let mut prev = vec![0; b_len + 1]; + let mut curr = vec![0; b_len + 1]; + + for j in 0..=b_len { + prev[j] = j; + } + + for (i, a_ch) in a.chars().enumerate() { + curr[0] = i + 1; + for (j, b_ch) in b.chars().enumerate() { + let cost = if a_ch == b_ch { 0 } else { 1 }; + curr[j + 1] = (prev[j + 1] + 1) // 删除 + .min(curr[j] + 1) // 插入 + .min(prev[j] + cost); // 替换 + } + std::mem::swap(&mut prev, &mut curr); + } + + prev[b_len] + } + + /// 判断文本变化是否"显著"(需要重新词法分析) + /// + /// 策略: + /// - 编辑距离 > 阈值:需要重新分析 + /// - 仅空白字符变化:不需要重新分析 + /// - 新增/删除字符串/注释:需要重新分析 + pub fn is_significant_change(old_text: &str, new_text: &str) -> bool { + // 快速路径:完全相同 + if old_text == new_text { + return false; + } + + // 快速路径:长度差异过大 + let old_len = old_text.len(); + let new_len = new_text.len(); + if old_len == 0 || new_len == 0 { + return true; + } + + // 计算编辑距离 + let dist = Self::distance(old_text, new_text); + let max_len = old_len.max(new_len); + + // 阈值:编辑距离超过文本长度的 30% 视为显著变化 + let threshold = (max_len as f32 * 0.3) as usize; + if dist > threshold { + return true; + } + + // 检查是否跨越了 token 边界(如引号、注释符号) + if Self::crosses_token_boundary(old_text, new_text) { + return true; + } + + false + } + + /// 检测变化是否跨越了 token 边界 + /// + /// 例如:在字符串中间插入引号会改变整个行的 token 结构 + pub fn crosses_token_boundary(old_text: &str, new_text: &str) -> bool { + // 检查引号数量奇偶性变化 + let old_quotes = old_text.chars().filter(|&c| c == '"' || c == '\'').count(); + let new_quotes = new_text.chars().filter(|&c| c == '"' || c == '\'').count(); + if old_quotes != new_quotes { + return true; + } + + // 检查注释符号变化 + let old_comment = old_text.contains("//") || old_text.contains("/*"); + let new_comment = new_text.contains("//") || new_text.contains("/*"); + if old_comment != new_comment { + return true; + } + + false + } +} + +/// GPU 高亮管线配置 +/// +/// 控制 GPU 词法分析的启停和降级策略。 +#[derive(Clone, Copy, Debug)] +pub struct GpuHighlightConfig { + /// 是否启用 GPU 加速 + pub enabled: bool, + /// 文件大小阈值(字节):超过此值才使用 GPU + pub min_file_size: usize, + /// 视口扩展行数(可见行上下各扩展多少行) + pub viewport_padding: usize, + /// 编辑距离阈值(0.0-1.0) + pub edit_distance_threshold: f32, + /// 是否回退到 CPU 高亮(GPU 失败时) + pub fallback_to_cpu: bool, +} + +impl Default for GpuHighlightConfig { + fn default() -> Self { + Self { + enabled: true, + min_file_size: 1024, // 1KB 以上文件使用 GPU + viewport_padding: 5, // 可见行上下各扩展 5 行 + edit_distance_threshold: 0.3, + fallback_to_cpu: true, + } + } +} diff --git a/crates/aether-render/src/lib.rs b/crates/aether-render/src/lib.rs index c3001ff..c0aaae8 100644 --- a/crates/aether-render/src/lib.rs +++ b/crates/aether-render/src/lib.rs @@ -1,3 +1,4 @@ pub mod d2d; +pub mod gpu; pub mod theme; pub mod vscode_theme; diff --git a/crates/aether-tree-sitter/src/background.rs b/crates/aether-tree-sitter/src/background.rs index 9595ddc..219f239 100644 --- a/crates/aether-tree-sitter/src/background.rs +++ b/crates/aether-tree-sitter/src/background.rs @@ -44,8 +44,8 @@ pub struct HighlightResult { pub token_lines: Vec>, } -/// 缓存容量:最多缓存 4 个文档(10 万行文档 tokens 约数 MB,LRU 淘汰控制内存) -const MAX_CACHED_DOCS: usize = 4; +/// 缓存容量:最多缓存 8 个文档(10 万行文档 tokens 约数 MB,LRU 淘汰控制内存) +const MAX_CACHED_DOCS: usize = 8; /// 后台语法高亮器 /// diff --git a/crates/aether-tree-sitter/src/highlighter.rs b/crates/aether-tree-sitter/src/highlighter.rs index 9f90255..5a5af79 100644 --- a/crates/aether-tree-sitter/src/highlighter.rs +++ b/crates/aether-tree-sitter/src/highlighter.rs @@ -444,21 +444,36 @@ impl TreeSitterHighlighter { /// `cancellation_flag: Option<&AtomicUsize>`(取消标志),而非旧语法树。 /// `Highlighter` 内部维护自己的 `Parser`,每次调用做完整解析。 /// 真正的增量解析需要升级到 `tree-sitter-highlight` 0.22+ 或手动遍历语法树。 + /// + /// P1-Perf: 优化要点: + /// 1. 跳过独立的 parse_document 调用(Highlighter::highlight 内部已做完整解析) + /// 2. 缓存 line_starts 避免重复计算(但此函数每次调用文本都不同,由调用方缓存) + /// 3. 使用预分配的 Vec 容量减少重新分配 pub fn highlight_document( &mut self, - doc_id: &str, + _doc_id: &str, language: &str, full_text: &str, ) -> Vec> { - // 预计算行数和行起始偏移 - let mut line_starts: Vec = vec![0]; + // 快速路径:空文本 + if full_text.is_empty() { + return vec![Vec::new()]; + } + + // 预计算行数和行起始偏移 —— 单次遍历,预分配容量 + let text_len = full_text.len(); + let mut line_starts: Vec = Vec::with_capacity(text_len / 40 + 1); + line_starts.push(0); for (i, b) in full_text.bytes().enumerate() { if b == b'\n' { line_starts.push(i + 1); } } let line_count = line_starts.len(); - let mut result: Vec> = vec![Vec::new(); line_count]; + let mut result: Vec> = Vec::with_capacity(line_count); + for _ in 0..line_count { + result.push(Vec::new()); + } // 获取 config(raw pointer 避免与 self.highlighter 的借用冲突) let config_ptr = self.get_config_ptr(language); @@ -467,8 +482,10 @@ impl TreeSitterHighlighter { None => return result, }; - // 更新语法树缓存(供代码折叠、结构导航等功能使用) - self.parse_document(doc_id, language, full_text); + // P1-Perf: 跳过独立的 parse_document 调用。 + // tree-sitter-highlight 0.20 的 Highlighter::highlight 内部已经维护 Parser + // 并做完整解析,外部再调用 parse_document 是重复工作(浪费 30-50% 时间)。 + // 语法树缓存留给真正需要 tree 的功能(代码折叠、结构导航)按需调用。 // 调用 highlighter(第三参数为 cancellation_flag,传 None 表示不可取消) let events = self diff --git a/crates/aether-win32/Cargo.toml b/crates/aether-win32/Cargo.toml index 5cdd96a..35d5164 100644 --- a/crates/aether-win32/Cargo.toml +++ b/crates/aether-win32/Cargo.toml @@ -34,7 +34,7 @@ sha2 = "0.10" rusqlite = { version = "0.32", features = ["bundled"] } sqlite-vec = "0.1" similar = "2.6" -image = { version = "0.24", default-features = false, features = ["png"] } +image = { version = "0.24", default-features = false, features = ["png", "jpeg", "gif", "bmp", "ico", "tiff", "webp"] } memmap2 = "0.9" flate2 = "1.0" ort = { version = "2.0.0-rc.9", default-features = false, features = ["std", "ndarray", "download-binaries", "tls-rustls"] } diff --git a/crates/aether-win32/src/bitmap_loader.rs b/crates/aether-win32/src/bitmap_loader.rs index 35c17ad..95192de 100644 --- a/crates/aether-win32/src/bitmap_loader.rs +++ b/crates/aether-win32/src/bitmap_loader.rs @@ -1,32 +1,70 @@ -//! PNG 位图加载器:使用 `image` crate 解码 PNG,再创建 ID2D1Bitmap。 +//! 图片解码与 D2D 位图创建。 //! -//! 用于欢迎页/空占位页显示 logo 图片。 -//! 避免依赖系统 WIC PNG 解码器(某些精简 Windows 环境可能缺少)。 +//! - 欢迎页/空占位页 logo:使用 `load_png_to_bitmap`(PNG 字节 → ID2D1Bitmap)。 +//! - 编辑器图片预览:使用 `decode_image_file` 解码常见位图格式为 RGBA8, +//! 再由渲染路径 `create_bitmap_from_rgba` 惰性创建 ID2D1Bitmap。 +//! +//! 使用 `image` crate 解码,避免依赖系统 WIC 解码器(某些精简 Windows 环境可能缺少)。 +//! 注意:D2D CreateBitmap 要求 PREMULTIPLIED alpha 的 BGRA8。 + +use std::path::Path; use windows::Win32::Graphics::Direct2D::{ Common::{D2D1_ALPHA_MODE_PREMULTIPLIED, D2D1_PIXEL_FORMAT, D2D_SIZE_U}, - ID2D1RenderTarget, D2D1_BITMAP_PROPERTIES, + ID2D1Bitmap, ID2D1RenderTarget, D2D1_BITMAP_PROPERTIES, }; use windows::Win32::Graphics::Dxgi::Common::DXGI_FORMAT_B8G8R8A8_UNORM; -/// 将 PNG 字节数据解码为 ID2D1Bitmap。 -/// -/// 使用 image crate 解码 PNG 为 RGBA8,再转换为预乘 alpha 的 BGRA8, -/// 最后通过 D2D CreateBitmap 从内存创建位图。 +/// 解码后的图片数据(设备无关,可随标签页状态保存/恢复)。 +#[derive(Clone, Debug)] +pub struct DecodedImage { + pub width: u32, + pub height: u32, + /// RGBA8 像素数据(未预乘),长度 = width * height * 4 + pub rgba: Vec, + /// 人类可读的格式名(用于信息栏展示) + pub format_name: &'static str, +} + +/// 解码图片文件为 RGBA8。 /// -/// 注意:D2D CreateBitmap 要求 PREMULTIPLIED alpha 模式。 -pub fn load_png_to_bitmap( - target: &ID2D1RenderTarget, - png_bytes: &[u8], -) -> Result { - let img = image::load_from_memory_with_format(png_bytes, image::ImageFormat::Png) - .map_err(|e| format!("解码 PNG 失败: {}", e))?; +/// 使用 `image::load_from_memory` 自动嗅探格式(PNG/JPEG/GIF/BMP/ICO/TIFF/WebP)。 +/// GIF 动图返回首帧(编辑器预览定位,动画播放后续任务再实现)。 +/// SVG/RAW/PSD 等 image crate 不支持的格式会返回 Err,由调用方落入占位提示。 +pub fn decode_image_file(path: &Path) -> Result { + let bytes = std::fs::read(path).map_err(|e| format!("读取图片失败: {}", e))?; + let format_name = image::guess_format(&bytes) + .map(image_format_name) + .unwrap_or("未知"); + let img = image::load_from_memory(&bytes).map_err(|e| format!("解码图片失败: {}", e))?; let rgba = img.to_rgba8(); - let width = rgba.width(); - let height = rgba.height(); + let (width, height) = (rgba.width(), rgba.height()); + Ok(DecodedImage { + width, + height, + rgba: rgba.into_raw(), + format_name, + }) +} + +/// image::ImageFormat → 人类可读名 +fn image_format_name(fmt: image::ImageFormat) -> &'static str { + use image::ImageFormat as F; + match fmt { + F::Png => "PNG", + F::Jpeg => "JPEG", + F::Gif => "GIF", + F::Bmp => "BMP", + F::Ico => "ICO", + F::Tiff => "TIFF", + F::WebP => "WebP", + _ => "图片", + } +} - // Direct2D 要求 BGRA8 + 预乘 alpha - let mut bgra = Vec::with_capacity((width * height * 4) as usize); +/// RGBA8 → 预乘 alpha 的 BGRA8(D2D CreateBitmap 要求)。 +fn rgba_to_bgra_premultiplied(rgba: &[u8]) -> Vec { + let mut bgra = Vec::with_capacity(rgba.len()); for chunk in rgba.chunks_exact(4) { let r = chunk[0]; let g = chunk[1]; @@ -38,6 +76,20 @@ pub fn load_png_to_bitmap( bgra.push((r as f32 * af).round() as u8); // R 预乘 bgra.push(a); // A 不变 } + bgra +} + +/// 从 RGBA8 像素数据创建 ID2D1Bitmap(内部转为预乘 BGRA8)。 +pub fn create_bitmap_from_rgba( + target: &ID2D1RenderTarget, + width: u32, + height: u32, + rgba: &[u8], +) -> Result { + if width == 0 || height == 0 { + return Err("图片尺寸为 0".to_string()); + } + let bgra = rgba_to_bgra_premultiplied(rgba); let pixel_format = D2D1_PIXEL_FORMAT { format: DXGI_FORMAT_B8G8R8A8_UNORM, @@ -53,10 +105,7 @@ pub fn load_png_to_bitmap( unsafe { match target.CreateBitmap(size, Some(bgra.as_ptr() as *const _), pitch, &props) { - Ok(bmp) => { - tracing::info!("D2D CreateBitmap 成功 (BGRA8 PREMULTIPLIED)"); - Ok(bmp) - } + Ok(bmp) => Ok(bmp), Err(e) => { tracing::warn!(error = ?e, "BGRA8 PREMULTIPLIED 失败,尝试默认属性"); let default_props = D2D1_BITMAP_PROPERTIES::default(); @@ -70,3 +119,19 @@ pub fn load_png_to_bitmap( } } } + +/// 将 PNG 字节数据解码为 ID2D1Bitmap(欢迎页/空占位页 logo 专用)。 +/// +/// 使用 image crate 解码 PNG 为 RGBA8,再转换为预乘 alpha 的 BGRA8, +/// 最后通过 D2D CreateBitmap 从内存创建位图。 +pub fn load_png_to_bitmap( + target: &ID2D1RenderTarget, + png_bytes: &[u8], +) -> Result { + let img = image::load_from_memory_with_format(png_bytes, image::ImageFormat::Png) + .map_err(|e| format!("解码 PNG 失败: {}", e))?; + let rgba = img.to_rgba8(); + let width = rgba.width(); + let height = rgba.height(); + create_bitmap_from_rgba(target, width, height, rgba.as_raw()) +} diff --git a/crates/aether-win32/src/cursor.rs b/crates/aether-win32/src/cursor.rs index dadef95..9182b36 100644 --- a/crates/aether-win32/src/cursor.rs +++ b/crates/aether-win32/src/cursor.rs @@ -12,6 +12,10 @@ pub enum CursorType { Hand, SizeWE, SizeNS, + /// 西北-东南斜向(\ 方向),用于左下拐角手柄 + SizeNWSE, + /// 东北-西南斜向(/ 方向),用于右下拐角手柄 + SizeNESW, } impl CursorType { @@ -24,6 +28,8 @@ impl CursorType { CursorType::Hand => IDC_HAND, CursorType::SizeWE => IDC_SIZEWE, CursorType::SizeNS => IDC_SIZENS, + CursorType::SizeNWSE => IDC_SIZENWSE, + CursorType::SizeNESW => IDC_SIZENESW, } } } @@ -41,13 +47,15 @@ mod tests { #[test] fn test_idc_cursor_mapping() { use windows::Win32::UI::WindowsAndMessaging::{ - IDC_ARROW, IDC_HAND, IDC_IBEAM, IDC_SIZENS, IDC_SIZEWE, + IDC_ARROW, IDC_HAND, IDC_IBEAM, IDC_SIZENESW, IDC_SIZENS, IDC_SIZENWSE, IDC_SIZEWE, }; assert_eq!(CursorType::Arrow.idc_cursor(), IDC_ARROW); assert_eq!(CursorType::IBeam.idc_cursor(), IDC_IBEAM); assert_eq!(CursorType::Hand.idc_cursor(), IDC_HAND); assert_eq!(CursorType::SizeWE.idc_cursor(), IDC_SIZEWE); assert_eq!(CursorType::SizeNS.idc_cursor(), IDC_SIZENS); + assert_eq!(CursorType::SizeNWSE.idc_cursor(), IDC_SIZENWSE); + assert_eq!(CursorType::SizeNESW.idc_cursor(), IDC_SIZENESW); } #[test] diff --git a/crates/aether-win32/src/editor/cursor.rs b/crates/aether-win32/src/editor/cursor.rs index 26a7824..ce506c7 100644 --- a/crates/aether-win32/src/editor/cursor.rs +++ b/crates/aether-win32/src/editor/cursor.rs @@ -20,8 +20,16 @@ impl EditorState { /// P2.3: 大文件阈值(字节数) pub(super) const LARGE_FILE_BYTE_THRESHOLD: usize = 2 * 1024 * 1024; /// P2.3: 重建行 Y 偏移前缀和缓存 + /// 优化:只在行数变化时重建,避免每帧重复计算 pub fn rebuild_line_y_offsets(&mut self) { let total_lines = self.content.buffer.len_lines().max(1); + // 如果行数未变且已有缓存,跳过重建 + if self.content.line_y_offsets_cached_lines == total_lines + && !self.content.line_y_offsets.is_empty() + { + return; + } + self.content.line_y_offsets_cached_lines = total_lines; if self.content.line_y_offsets.len() != total_lines { self.content.line_y_offsets.resize(total_lines, 0.0); } @@ -592,15 +600,17 @@ impl EditorState { self.content.cursor_line = line.min(total_lines.saturating_sub(1)); if let Some(text) = self.content.buffer.get_line(self.content.cursor_line) { - // 与渲染一致:按可视列折算 x(CJK 等宽字符占 2 格,见 render_editor - // 的 unicode_char_width 累加),否则行内含中文时光标落点偏左。 + // 与渲染一致:按可视列折算 x(CJK 等宽字符占 2 格,Tab 占 4 格, + // 见 render_editor 的 unicode_char_width 累加),否则行内含中文或 + // Tab 时光标落点偏左。 // 就近吸附:点击超过字符一半宽度时落到其后边界,避免永远偏左一格。 let target_cells = (rel_x / char_width).max(0.0); let mut cells = 0f32; let mut byte_col = 0usize; for ch in text.chars() { let w = unicode_char_width(ch) as f32; - if target_cells < cells + w / 2.0 { + // 使用 <= 确保点击字符中点时吸附到当前字符(更自然) + if target_cells <= cells + w / 2.0 { break; } cells += w; diff --git a/crates/aether-win32/src/editor/dialogs.rs b/crates/aether-win32/src/editor/dialogs.rs index 086a13e..d88b0bc 100644 --- a/crates/aether-win32/src/editor/dialogs.rs +++ b/crates/aether-win32/src/editor/dialogs.rs @@ -57,7 +57,12 @@ impl EditorState { Ok(()) => { self.status_message = format!("项目已创建: {}", project_path.display()); // 打开项目文件夹作为工作区 - self.open_folder(project_path); + // 信任检查在 open_folder 之前(不持有 RefCell 借用,避免模态框重入 panic) + if crate::editor::files::check_workspace_trust(self.hwnd, &project_path) { + self.open_folder(project_path); + } else { + self.status_message = "已取消打开不受信任的工作区".to_string(); + } } Err(e) => { let msg = format!("创建项目失败: {}", e); diff --git a/crates/aether-win32/src/editor/events.rs b/crates/aether-win32/src/editor/events.rs index b7528b3..d7a66c3 100644 --- a/crates/aether-win32/src/editor/events.rs +++ b/crates/aether-win32/src/editor/events.rs @@ -228,7 +228,12 @@ impl EditorState { } crate::menu_bar::CommandId::FileOpenFolder => { if let Some(path) = Dialogs::open_folder_dialog(hwnd, "打开文件夹") { - self.open_folder(path); + // 信任检查在 open_folder 之前(不持有 RefCell 借用,避免模态框重入 panic) + if crate::editor::files::check_workspace_trust(self.hwnd, &path) { + self.open_folder(path); + } else { + self.status_message = "已取消打开不受信任的工作区".to_string(); + } } } crate::menu_bar::CommandId::FileCloseWorkspace => { @@ -348,20 +353,45 @@ impl EditorState { } } /// 增量重建缓存:只重建可见行范围内的缓存,大幅减少大文件的词法分析开销 + /// + /// 视口优先策略: + /// 1. 优先高亮可见区域(visible_start..visible_end),让用户立即看到内容 + /// 2. 然后高亮视口扩展区域(±padding),为滚动做准备 + /// 3. 后台处理不可见区域(通过 tree-sitter 异步处理) pub(crate) fn rebuild_cache(&mut self, visible_start: usize, visible_end: usize) { + // === 0延迟切换:刚切换过来的标签页首帧跳过所有重建 === + // 直接渲染已有缓存,下一帧再恢复正常逻辑 + // 但如果缓存中没有高亮数据(首次打开或缓存被清空),仍然需要高亮 + if self.content.just_switched { + self.content.just_switched = false; + // 只更新签名,让后续帧能正确命中 + let total_lines = self.content.buffer.len_lines().max(1); + self.content.last_cache_signature = ( + self.content.buffer_version, + visible_start, + visible_end, + total_lines, + ); + // 检查可见区域是否已有高亮缓存 + let has_highlight = (visible_start..visible_end.min(total_lines)) + .all(|i| { + i < self.content.cached_tokens.len() + && !self.content.cached_tokens[i].is_empty() + }); + if has_highlight { + return; // 缓存完整,0延迟渲染 + } + // 缓存不完整:继续执行下方的高亮逻辑 + } + let total_lines = self.content.buffer.len_lines().max(1); // tree-sitter 优先高亮:返回支持的语言的字符串标识 - // 不支持的语言返回 None,由调用方 fallback 到手写 lexer let ts_lang = language_to_ts_str(self.content.language); // === P0-3: 后台语法高亮 — 始终 poll,即使在空闲帧 === - // 必须在签名检查之前 poll,否则空闲帧(签名匹配)会 early return, - // 导致后台高亮结果永远无法被消费,tokens 停留在空/旧状态。 if ts_lang.is_some() && !self.content.is_large_file { if let Some(mut result) = self.bg_highlighter.poll_result() { - // 结果归属校验:快速切换文件后,过期结果必须丢弃, - // 避免把旧文件的高亮 token 填进当前文件(错误着色) let current_doc = self .content .file_path @@ -371,7 +401,6 @@ impl EditorState { if result.doc_id != current_doc || result.version != self.content.buffer_version { drop(result); } else { - // P1-C: 后台结果整体 move 接管,避免逐行 clone 全文档 token let token_lines = std::mem::take(&mut result.token_lines); if self.content.cached_tokens.len() < token_lines.len() { self.content @@ -383,8 +412,6 @@ impl EditorState { self.content.cached_tokens[i] = tokens; } } - // 后台高亮结果刚到达:标记编辑器区域脏,使本帧立即以着色重绘, - // 避免文件打开后停留在无高亮的纯文本状态直到下一次无关重绘。 let er = self.layout.editor_region(); self.dirty_tracker.mark_region( er.x, @@ -397,15 +424,14 @@ impl EditorState { } } - // REQ-P2-01: 变化检测 — 如果 buffer_version、可见范围、总行数均未变化,跳过整个重建 - // 空闲帧(无编辑、无滚动)不会产生任何缓存重建开销 + // REQ-P2-01: 变化检测 let signature = ( self.content.buffer_version, visible_start, visible_end, total_lines, ); - // P0-A: 窗口化后附加校验窗口长度,防止首帧空窗口被签名误判为已建 + // P0-A: 窗口化后附加校验窗口长度 let cache_start = visible_start.saturating_sub(2); let cache_end = (visible_end + 2).min(total_lines).max(cache_start); let window_len = cache_end - cache_start; @@ -418,81 +444,230 @@ impl EditorState { self.content.last_cache_signature = signature; // P2.3: 大文件检测与行偏移缓存 - self.update_large_file_flag(); + // 优化:只在必要时更新大文件标记(行数或字节数变化时) + let line_count = self.content.buffer.len_lines(); + let byte_count = self.content.buffer.len_bytes(); + let new_is_large = line_count > Self::LARGE_FILE_LINE_THRESHOLD + || byte_count > Self::LARGE_FILE_BYTE_THRESHOLD; + if self.content.is_large_file != new_is_large { + self.content.is_large_file = new_is_large; + } self.rebuild_line_y_offsets(); - // P0-A: 平移行文本缓存窗口(重叠行保留,新行待重建) + // P0-A: 平移行文本缓存窗口 self.content.slide_cache_window(cache_start, window_len); - // tokens 仍为全文件索引(后台高亮整体接管),行数变化时调整 + // tokens 仍为全文件索引,行数变化时调整 if self.content.cached_tokens.len() != total_lines { self.content .cached_tokens .resize_with(total_lines, Vec::new); } - // P2.3: 大文件模式下跳过语法高亮,只缓存行文本 - // 延迟创建 fallback lexer:仅在 tree-sitter 不支持且至少一行需要重建时才创建 + // P2.3: 大文件模式下跳过语法高亮 let mut lexer: Option> = None; - // === P0-3: 后台语法高亮 — 发送请求 === - // poll 逻辑已移至签名检查之前,确保空闲帧也能消费后台结果。 - // 此处仅在 buffer_version 变化时发送新请求。 - if let Some(lang) = ts_lang { - // 冰冻态唤醒后 tokens_trimmed 置位:即使 buffer_version 未变也强制重新请求, - // 否则被裁剪的高亮 token 永远不会重建(后台高亮仅在版本变化时请求) - if !self.content.is_large_file - && (self.content.buffer_version != self.hl_request_version - || self.content.tokens_trimmed) - && !self.bg_highlighter.has_pending() + // === GPU 高亮优先尝试 === + // 优化:只在文件内容变化时运行 GPU 词法分析,切换标签页时复用缓存 + let mut gpu_highlighted = false; + if let Some(ref mut gpu_lexer) = self.gpu_highlighter { + if self.gpu_highlight_config.enabled + && !self.content.is_large_file + && self.content.buffer.len_bytes() >= self.gpu_highlight_config.min_file_size { - // P1-C: 传递轻量快照(Arc pieces),全文物化移到后台线程, - // UI 线程不再每次编辑都做 O(文件) 的 get_all_text 拷贝 - let snapshot = self.content.buffer.create_snapshot(); - let doc_id = self + let vp_cache = self .content - .file_path - .as_ref() - .map(|p| p.to_string_lossy().to_string()) - .unwrap_or_else(|| "untitled".to_string()); - self.bg_highlighter - .request(&doc_id, lang, self.content.buffer_version, snapshot); - self.hl_request_version = self.content.buffer_version; - self.content.tokens_trimmed = false; - } - } + .viewport_highlight_cache + .get_or_insert_with(aether_render::gpu::viewport::ViewportHighlightCache::new); + + // 检查是否需要重新运行 GPU 分析: + // 1. 视口范围变化 2. buffer_version 变化(内容编辑) + let vp_changed = vp_cache.window_start() != cache_start + || vp_cache.window_len() != window_len; + let content_changed = vp_cache.buffer_version() != self.content.buffer_version; + let need_gpu_rebuild = vp_changed || content_changed || vp_cache.is_empty(); + + if need_gpu_rebuild { + vp_cache.resize_window(cache_start, window_len, self.content.buffer_version); + + let current_lines: Vec = (cache_start..cache_end) + .map(|i| self.content.buffer.get_line(i).unwrap_or_default()) + .collect(); + vp_cache.update_with_edit_distance( + ¤t_lines, + self.content.buffer_version, + self.gpu_highlight_config.edit_distance_threshold, + ); - for i in cache_start..cache_end { - let slot = i - cache_start; - if self.content.line_cache_versions[slot] != self.content.buffer_version { - let line = self.content.buffer.get_line(i).unwrap_or_default(); + let dirty_lines = vp_cache.dirty_line_indices(); - if self.content.is_large_file { - // 大文件:跳过语法高亮 - self.content.cached_lines[slot] = line; - self.content.cached_tokens[i] = Vec::new(); - self.content.line_cache_versions[slot] = self.content.buffer_version; - } else if ts_lang.is_some() { - // tree-sitter 语言:只更新文本,tokens 由后台线程异步更新 - // 保留上一版本的 tokens(stale but usable),实现零输入延迟 - self.content.cached_lines[slot] = line; - self.content.line_cache_versions[slot] = self.content.buffer_version; + if !dirty_lines.is_empty() { + let text = self.content.buffer.get_all_text(); + if let Ok(tokens) = gpu_lexer.lex(text.as_bytes()) { + if !tokens.is_empty() { + let gpu_spans = + aether_render::gpu::render::gpu_tokens_to_lexeme_spans(&tokens, None); + gpu_highlighted = true; + + for line_idx in cache_start..cache_end { + let line_start = self + .content + .buffer + .line_byte_range(line_idx) + .map(|(s, _)| s as u32) + .unwrap_or(0); + let line_end = self + .content + .buffer + .line_byte_range(line_idx) + .map(|(_, e)| e as u32) + .unwrap_or(text.len() as u32); + + let line_tokens: Vec = gpu_spans + .iter() + .filter(|span| { + span.start >= line_start && span.start < line_end + }) + .cloned() + .collect(); + + vp_cache.set_line_tokens( + line_idx, + line_tokens, + self.content.buffer_version, + ); + } + } + } + } else { + gpu_highlighted = true; + } } else { - // fallback:手写 lexer(Markdown/Html/Css/PlainText/Image 等) - if lexer.is_none() { - lexer = Some(self.content.language.create_lexer()); + // 视口和内容均未变化:直接复用缓存 + gpu_highlighted = true; + } + } + } + + // 将 ViewportHighlightCache 中的 token 同步到 cached_tokens + if gpu_highlighted { + if let Some(ref vp_cache) = self.content.viewport_highlight_cache { + for line_idx in cache_start..cache_end { + if let Some(tokens) = vp_cache.get_line_tokens(line_idx) { + if line_idx < self.content.cached_tokens.len() { + self.content.cached_tokens[line_idx] = tokens.to_vec(); + } } - // C-03: lexer 创建可能返回 None(不支持的语言),unwrap 会 panic 并穿越 WndProc - let tokens = if let Some(lex) = lexer.as_ref() { - lex.lex_full(&line) - } else { - Vec::new() - }; - self.content.cached_lines[slot] = line; - self.content.cached_tokens[i] = tokens; - self.content.line_cache_versions[slot] = self.content.buffer_version; } } } + + // === P0-3: 后台语法高亮 — 发送请求 === + let mut use_sync_lexer = false; + if !gpu_highlighted { + if let Some(lang) = ts_lang { + if !self.content.is_large_file + && (self.content.buffer_version != self.hl_request_version + || self.content.tokens_trimmed) + && !self.bg_highlighter.has_pending() + { + let snapshot = self.content.buffer.create_snapshot(); + let doc_id = self + .content + .file_path + .as_ref() + .map(|p| p.to_string_lossy().to_string()) + .unwrap_or_else(|| "untitled".to_string()); + self.bg_highlighter + .request(&doc_id, lang, self.content.buffer_version, snapshot); + self.hl_request_version = self.content.buffer_version; + self.content.tokens_trimmed = false; + } + // 小文件:同步使用 CPU lexer 提供即时高亮 + if self.content.buffer.len_bytes() < self.gpu_highlight_config.min_file_size { + use_sync_lexer = true; + } + } + } + + // === 视口优先高亮策略 === + // 1. 首先处理可见区域(用户当前看到的内容) + // 2. 然后处理扩展区域(视口上下 padding) + // 3. 对于大文件,只处理可见区域,跳过不可见区域 + + // 计算可见区域和扩展区域 + let viewport_start = visible_start; + let viewport_end = visible_end.min(total_lines); + let extended_start = cache_start; + let extended_end = cache_end; + + // 第一遍:优先处理可见区域 + for i in viewport_start..viewport_end { + if i >= cache_start && i < cache_end { + let slot = i - cache_start; + if self.content.line_cache_versions[slot] != self.content.buffer_version { + self.highlight_line(i, slot, gpu_highlighted, use_sync_lexer, ts_lang, &mut lexer); + } + } + } + + // 第二遍:处理扩展区域(不可见但接近视口) + for i in extended_start..extended_end { + if i < viewport_start || i >= viewport_end { + let slot = i - cache_start; + if self.content.line_cache_versions[slot] != self.content.buffer_version { + self.highlight_line(i, slot, gpu_highlighted, use_sync_lexer, ts_lang, &mut lexer); + } + } + } + } + + /// 高亮单行(提取为辅助方法) + fn highlight_line( + &mut self, + line_idx: usize, + slot: usize, + gpu_highlighted: bool, + use_sync_lexer: bool, + ts_lang: Option<&str>, + lexer: &mut Option>, + ) { + let line = self.content.buffer.get_line(line_idx).unwrap_or_default(); + + if self.content.is_large_file { + self.content.cached_lines[slot] = line; + self.content.cached_tokens[line_idx] = Vec::new(); + self.content.line_cache_versions[slot] = self.content.buffer_version; + } else if gpu_highlighted { + self.content.cached_lines[slot] = line; + self.content.line_cache_versions[slot] = self.content.buffer_version; + } else if use_sync_lexer { + if lexer.is_none() { + *lexer = Some(self.content.language.create_lexer()); + } + let tokens = if let Some(lex) = lexer.as_ref() { + lex.lex_full(&line) + } else { + Vec::new() + }; + self.content.cached_lines[slot] = line; + self.content.cached_tokens[line_idx] = tokens; + self.content.line_cache_versions[slot] = self.content.buffer_version; + } else if ts_lang.is_some() { + self.content.cached_lines[slot] = line; + self.content.line_cache_versions[slot] = self.content.buffer_version; + } else { + if lexer.is_none() { + *lexer = Some(self.content.language.create_lexer()); + } + let tokens = if let Some(lex) = lexer.as_ref() { + lex.lex_full(&line) + } else { + Vec::new() + }; + self.content.cached_lines[slot] = line; + self.content.cached_tokens[line_idx] = tokens; + self.content.line_cache_versions[slot] = self.content.buffer_version; + } } -} +} \ No newline at end of file diff --git a/crates/aether-win32/src/editor/file_tree.rs b/crates/aether-win32/src/editor/file_tree.rs index 0331781..cb6ee9a 100644 --- a/crates/aether-win32/src/editor/file_tree.rs +++ b/crates/aether-win32/src/editor/file_tree.rs @@ -312,7 +312,12 @@ impl EditorState { /// 刷新文件树(重新扫描当前文件夹) pub fn refresh_file_tree(&mut self) { if let Some(path) = self.current_folder.clone() { - self.open_folder(path); + // 信任检查在 open_folder 之前(不持有 RefCell 借用,避免模态框重入 panic) + if crate::editor::files::check_workspace_trust(self.hwnd, &path) { + self.open_folder(path); + } else { + self.status_message = "已取消打开不受信任的工作区".to_string(); + } } } diff --git a/crates/aether-win32/src/editor/files.rs b/crates/aether-win32/src/editor/files.rs index fb62204..1fbe568 100644 --- a/crates/aether-win32/src/editor/files.rs +++ b/crates/aether-win32/src/editor/files.rs @@ -128,21 +128,39 @@ impl EditorState { } /// 加载图片文件 pub(super) fn load_image_file(&mut self, path: PathBuf) { + // 解码图片(自动嗅探格式;GIF 取首帧;SVG/RAW/PSD/损坏文件返回 Err → 占位提示) + let image_data = match crate::bitmap_loader::decode_image_file(&path) { + Ok(img) => Some(img), + Err(e) => { + tracing::warn!(path = %path.display(), error = %e, "图片解码失败,显示占位提示"); + None + } + }; + // 打开新图片前使旧位图缓存失效(位图绑定具体图片) + self.image_bitmap = None; + // 重置缩放状态 + self.image_zoom = 1.0; + self.image_offset_x = 0.0; + self.image_offset_y = 0.0; + let content = format!("[图片预览] {}", path.display()); // tabs 为空时同样不能复用(否则渲染占位页),与 load_file 保持一致 if self.can_reuse_current_tab() && !self.tab_bar.tabs.is_empty() { self.content.file_path = Some(path.clone()); self.content.language = Language::Image; self.content.buffer = PieceTable::from_string(content); + self.content.image_data = image_data; self.reset_editor_state(); self.status_message = format!("已打开图片: {}", path.display()); } else { - let tab = Tab::File(TabContent::with_loaded_buffer( + let mut tab_content = TabContent::with_loaded_buffer( Some(path.clone()), PieceTable::from_string(content), Language::Image, false, - )); + ); + tab_content.image_data = image_data; + let tab = Tab::File(tab_content); self.open_in_new_tab(tab); self.status_message = format!("已打开图片: {}", path.display()); } @@ -318,20 +336,9 @@ impl EditorState { return; } - // 工作区信任检查:未信任目录先弹窗询问 - if !crate::dialogs::trusted_folders::is_trusted(&path) { - let title = "工作区信任"; - let msg = format!( - "是否信任此文件夹中的代码作者?\n\n{}\n\n\ - 信任后将允许执行 Git 检测、LSP、插件等可能运行该目录中代码的功能。", - path.display() - ); - if !Dialogs::confirm_yes_no(self.hwnd, title, &msg) { - self.status_message = "已取消打开不受信任的工作区".to_string(); - return; - } - crate::dialogs::trusted_folders::add_trusted(&path); - } + // 工作区信任检查已上移至调用方(check_workspace_trust), + // 避免在持有 RefCell borrow_mut 期间弹模态框泵消息导致重入 panic。 + // 此处假定调用方已完成信任确认。 // 设置 loading 状态,立即重绘显示 spinner self.is_loading_folder = true; @@ -486,3 +493,27 @@ impl EditorState { self.dirty_tracker.mark_full_window(); } } + +/// 工作区信任检查(自由函数,不持有 EditorState 借用)。 +/// +/// 必须在调用 `open_folder` 之前、且不持有 `RefCell` `borrow_mut` 时调用, +/// 否则模态确认框(MessageBoxW)会泵消息,导致嵌套的 WM_TIMER/WM_PAINT +/// 对同一 RefCell 再次借用而触发重入 panic(消息被 catch_unwind 吞掉)。 +/// +/// 返回 true 表示路径已受信任(或用户刚刚确认信任),可以继续 open_folder; +/// 返回 false 表示用户拒绝信任,调用方应中止并提示。 +pub fn check_workspace_trust(hwnd: HWND, path: &std::path::Path) -> bool { + if crate::dialogs::trusted_folders::is_trusted(path) { + return true; + } + let msg = format!( + "是否信任此文件夹中的代码作者?\n\n{}\n\n\ + 信任后将允许执行 Git 检测、LSP、插件等可能运行该目录中代码的功能。", + path.display() + ); + if !Dialogs::confirm_yes_no(hwnd, "工作区信任", &msg) { + return false; + } + crate::dialogs::trusted_folders::add_trusted(path); + true +} diff --git a/crates/aether-win32/src/editor/mod.rs b/crates/aether-win32/src/editor/mod.rs index 113b823..b1a39a8 100644 --- a/crates/aether-win32/src/editor/mod.rs +++ b/crates/aether-win32/src/editor/mod.rs @@ -172,10 +172,18 @@ pub struct MousePressState { pub lpress_index: usize, /// 当前鼠标左键是否按下(用于 WM_TIMER 判定) pub lbutton_down: bool, + /// 鼠标左键按下时的位置(逻辑像素),用于区分单击和拖动 + pub lbutton_down_pos: Option<(f32, f32)>, /// 文件树拖拽:按下时命中的节点索引(None 表示未在文件树按下) pub file_tree_drag_node: Option, /// 文件树拖拽:是否已进入拖拽模式(超过阈值) pub file_tree_dragging: bool, + /// 图片预览拖拽:是否正在拖拽中键 + pub image_dragging: bool, + /// 图片预览拖拽:拖拽起始鼠标位置 + pub image_drag_start: Option<(f32, f32)>, + /// 图片预览拖拽:拖拽起始时的图片偏移 + pub image_drag_offset: Option<(f32, f32)>, } /// 上一帧快照(脏追踪,从 EditorState 聚类抽取) @@ -578,12 +586,25 @@ pub struct EditorState { pub composition: Option, /// 后台语法高亮器(独立线程,避免阻塞 UI 输入) pub(crate) bg_highlighter: aether_tree_sitter::BackgroundHighlighter, + /// GPU 语法高亮器(可选,D3D11 Compute Shader 加速) + pub(crate) gpu_highlighter: Option, + /// GPU 高亮配置 + pub(crate) gpu_highlight_config: aether_render::gpu::viewport::GpuHighlightConfig, /// 已发送后台高亮请求对应的 buffer_version(变化时触发新请求) pub(crate) hl_request_version: u64, /// UI Tooltip 状态(500ms 延迟显示、4px 移动容差的悬停提示) pub tooltip_state: crate::tooltip::TooltipState, /// Logo 位图(aether-512.png),懒加载,用于欢迎页和空占位页 pub(crate) logo_bitmap: Option, + /// 图片预览位图(设备相关缓存),由当前标签的 image_data 惰性创建; + /// 设备丢失/切换标签时清空重建 + pub(crate) image_bitmap: Option, + /// 图片预览缩放比例(1.0 = 100%) + pub image_zoom: f32, + /// 图片预览缩放后的水平偏移(用于平移查看) + pub image_offset_x: f32, + /// 图片预览缩放后的垂直偏移 + pub image_offset_y: f32, } /// Task 8.4: 标签重排核心逻辑(自由函数,可独立测试)。 @@ -839,9 +860,15 @@ impl EditorState { file_drag: crate::file_drag_drop::FileDragDropState::default(), composition: None, bg_highlighter: aether_tree_sitter::BackgroundHighlighter::new(), + gpu_highlighter: None, + gpu_highlight_config: aether_render::gpu::viewport::GpuHighlightConfig::default(), hl_request_version: 0, tooltip_state: crate::tooltip::TooltipState::default(), logo_bitmap: None, + image_bitmap: None, + image_zoom: 1.0, + image_offset_x: 0.0, + image_offset_y: 0.0, }; // 加载 logo 位图(aether-512.png) // 注意:此时还没有 render target,位图会在首次渲染时通过 ensure_logo_bitmap 懒加载 @@ -867,7 +894,12 @@ impl EditorState { if is_main_window { if let Some(workspace) = state.app_settings.ui.last_workspace.clone() { if workspace.exists() { - state.open_folder(workspace); + // 信任检查在 open_folder 之前(不持有 RefCell 借用,避免模态框重入 panic) + if crate::editor::files::check_workspace_trust(state.hwnd, &workspace) { + state.open_folder(workspace); + } else { + state.status_message = "已取消打开不受信任的工作区".to_string(); + } } } } @@ -2126,7 +2158,7 @@ mod cursor; mod editing; mod events; mod file_tree; -mod files; +pub(crate) mod files; mod lsp; mod remote; mod tabs; diff --git a/crates/aether-win32/src/editor/remote.rs b/crates/aether-win32/src/editor/remote.rs index 211d616..7543b91 100644 --- a/crates/aether-win32/src/editor/remote.rs +++ b/crates/aether-win32/src/editor/remote.rs @@ -132,7 +132,12 @@ impl EditorState { match &payload.error { None => { self.status_message = format!("克隆成功: {}", payload.target_path.display()); - self.open_folder(payload.target_path.clone()); + // 信任检查在 open_folder 之前(不持有 RefCell 借用,避免模态框重入 panic) + if crate::editor::files::check_workspace_trust(self.hwnd, &payload.target_path) { + self.open_folder(payload.target_path.clone()); + } else { + self.status_message = "已取消打开不受信任的工作区".to_string(); + } } Some(e) => { // 克隆失败:重新打开对话框并显示错误 diff --git a/crates/aether-win32/src/editor/tabs.rs b/crates/aether-win32/src/editor/tabs.rs index bee1463..379a339 100644 --- a/crates/aether-win32/src/editor/tabs.rs +++ b/crates/aether-win32/src/editor/tabs.rs @@ -103,7 +103,58 @@ impl EditorState { self.tab_bar.active_tab = index; self.swap_tab_content(self.tab_bar.active_tab); self.is_selecting = false; - self.sync_file_tree_selection(); + + // 无感切换优化:预先计算新标签页的可见范围并更新缓存签名, + // 避免切换后第一帧因签名不匹配而强制重建缓存。 + // 这样切换回之前打开的标签页时,如果滚动位置没变,缓存立即命中。 + if self.active_tab_is_file() { + let line_height = self.text_renderer.line_height(); + let editor_region = self.layout.editor_content_region(self.show_tab_bar()); + let visible_start = (self.content.scroll_y / line_height) as usize; + let visible_lines = (editor_region.height / line_height) as usize + 2; + let total_lines = self.content.buffer.len_lines().max(1); + let visible_end = (visible_start + visible_lines).min(total_lines); + let cache_start = visible_start.saturating_sub(2); + let cache_end = (visible_end + 2).min(total_lines).max(cache_start); + let window_len = cache_end - cache_start; + // 预更新签名,使下一帧 rebuild_cache 能立即命中 + self.content.last_cache_signature = ( + self.content.buffer_version, + visible_start, + visible_end, + total_lines, + ); + // 确保 cached_tokens 长度与当前文件匹配 + if self.content.cached_tokens.len() != total_lines { + self.content.cached_tokens.resize_with(total_lines, Vec::new); + } + // 如果行文本缓存窗口不匹配,需要重建(但保留已有缓存数据) + if self.content.cache_window_start != cache_start + || self.content.cached_lines.len() != window_len + { + // 使用 slide_cache_window 保留重叠部分 + self.content.slide_cache_window(cache_start, window_len); + } + } + + // 异步延迟同步文件树选择,避免阻塞切换 + let need_sync = self.active_tab_is_file() && self.content.file_path.is_some(); + if need_sync { + // 立即执行(文件树遍历通常很快),但如果项目极大可改为异步 + self.sync_file_tree_selection(); + } + + // 0延迟切换:标记刚切换过来的标签页 + // 这样 rebuild_cache 首帧会跳过所有工作,直接渲染已有缓存 + self.content.just_switched = true; + + // 图片预览位图绑定旧标签的图像,切换后需按新标签的 image_data 重建 + self.image_bitmap = None; + // 重置图片缩放状态 + self.image_zoom = 1.0; + self.image_offset_x = 0.0; + self.image_offset_y = 0.0; + let title = self.tab_bar.tabs[self.tab_bar.active_tab].title(); self.status_message = format!("切换到: {}", title); self.emit_event(crate::events::EditorEvent::TabChanged); diff --git a/crates/aether-win32/src/layout.rs b/crates/aether-win32/src/layout.rs index 049c26f..732be0a 100644 --- a/crates/aether-win32/src/layout.rs +++ b/crates/aether-win32/src/layout.rs @@ -131,6 +131,8 @@ pub const STATUS_BAR_HEIGHT: f32 = 16.0; pub const TAB_BAR_HEIGHT: f32 = 30.0; pub const MIN_SIDEBAR_WIDTH: f32 = 150.0; pub const MAX_SIDEBAR_WIDTH: f32 = 500.0; +/// 拐角手柄(两条分割线交点)的命中区域边长 +pub const CORNER_HANDLE_SIZE: f32 = 12.0; /// 标题栏右侧按钮布局(单一事实源)。 /// @@ -240,6 +242,10 @@ pub struct LayoutManager { pub right_panel_resizing: bool, pub bottom_panel_resizing: bool, pub sidebar_resizing: bool, + /// 左下拐角手柄拖拽中(侧边栏右缘 × 底部面板顶缘) + pub corner_left_resizing: bool, + /// 右下拐角手柄拖拽中(右面板左缘 × 底部面板顶缘) + pub corner_right_resizing: bool, /// 侧边栏宽度动画状态(None = 静态无动画) pub sidebar_anim: Option, /// 当前已应用的 DPI 缩放因子(用于 DPI 变化时按比例换算用户可调尺寸) @@ -299,6 +305,8 @@ impl LayoutManager { right_panel_resizing: false, bottom_panel_resizing: false, sidebar_resizing: false, + corner_left_resizing: false, + corner_right_resizing: false, sidebar_anim: None, dpi_scale: 1.0, } @@ -448,6 +456,44 @@ impl LayoutManager { Region::new(editor.x, y, editor.width, self.bottom_panel_height) } + /// 左下拐角手柄区域(侧边栏右缘 × 底部面板顶缘的交点)。 + /// + /// 拖拽该拐角可同时调整侧边栏宽度(水平)与底部面板高度(垂直)。 + /// 仅当侧边栏与底部面板同时可见时存在,否则返回 None。 + pub fn corner_left_handle(&self) -> Option { + if !(self.sidebar_visible && self.bottom_panel_visible) { + return None; + } + let editor = self.editor_region(); + let cy = self.bottom_panel_region().y; + let half = CORNER_HANDLE_SIZE / 2.0; + Some(Region::new( + editor.x - half, + cy - half, + CORNER_HANDLE_SIZE, + CORNER_HANDLE_SIZE, + )) + } + + /// 右下拐角手柄区域(右面板左缘 × 底部面板顶缘的交点)。 + /// + /// 拖拽该拐角可同时调整右面板宽度(水平)与底部面板高度(垂直)。 + /// 仅当右面板与底部面板同时可见时存在,否则返回 None。 + pub fn corner_right_handle(&self) -> Option { + if !(self.right_panel_visible && self.bottom_panel_visible) { + return None; + } + let editor = self.editor_region(); + let cy = self.bottom_panel_region().y; + let half = CORNER_HANDLE_SIZE / 2.0; + Some(Region::new( + editor.right() - half, + cy - half, + CORNER_HANDLE_SIZE, + CORNER_HANDLE_SIZE, + )) + } + /// 计算状态栏区域 pub fn status_bar_region(&self) -> Region { if !self.status_bar_visible { @@ -783,6 +829,46 @@ mod tests { ); } + #[test] + fn test_corner_handle_geometry() { + let mut layout = LayoutManager::new(1280.0, 800.0); + // 默认侧边栏可见、右面板/底部面板隐藏 + // 底部面板隐藏时两拐角均不存在 + assert!(layout.corner_left_handle().is_none()); + assert!(layout.corner_right_handle().is_none()); + + // 打开底部面板:左下拐角出现(侧边栏可见),右下拐角仍无(右面板隐藏) + layout.toggle_bottom_panel(); + let left = layout.corner_left_handle().expect("侧边栏+底部面板可见时应有左下拐角"); + assert!(layout.corner_right_handle().is_none()); + + let editor = layout.editor_region(); + let bottom = layout.bottom_panel_region(); + let half = CORNER_HANDLE_SIZE / 2.0; + // 左下拐角中心 = (editor.x, bottom.y) + assert_eq!(left.x, editor.x - half); + assert_eq!(left.y, bottom.y - half); + assert_eq!(left.width, CORNER_HANDLE_SIZE); + assert_eq!(left.height, CORNER_HANDLE_SIZE); + // 拐角中心点应命中 + assert!(left.contains(editor.x, bottom.y)); + + // 打开右面板:右下拐角出现,中心 = (editor.right(), bottom.y) + layout.toggle_right_panel(); + let right = layout + .corner_right_handle() + .expect("右面板+底部面板可见时应有右下拐角"); + let editor = layout.editor_region(); + let bottom = layout.bottom_panel_region(); + assert_eq!(right.x, editor.right() - half); + assert_eq!(right.y, bottom.y - half); + assert!(right.contains(editor.right(), bottom.y)); + + // 隐藏侧边栏后左下拐角消失 + layout.sidebar_visible = false; + assert!(layout.corner_left_handle().is_none()); + } + #[test] fn test_layout_manager_tab_bar_and_content() { let layout = LayoutManager::new(1280.0, 800.0); diff --git a/crates/aether-win32/src/power.rs b/crates/aether-win32/src/power.rs index 1110ef9..9b0d0a6 100644 --- a/crates/aether-win32/src/power.rs +++ b/crates/aether-win32/src/power.rs @@ -125,6 +125,7 @@ impl EditorState { self.render_ctx.release_for_suspend(); self.icons.clear(); self.logo_bitmap = None; + self.image_bitmap = None; // 6. 收缩 SQLite 页缓存 if let Some(warm) = self.ai_panel.warm_data_store.as_ref() { diff --git a/crates/aether-win32/src/render/dialogs.rs b/crates/aether-win32/src/render/dialogs.rs index 2e03152..9c0e5ef 100644 --- a/crates/aether-win32/src/render/dialogs.rs +++ b/crates/aether-win32/src/render/dialogs.rs @@ -409,92 +409,251 @@ impl EditorState { }; target.FillRectangle(&bg_rect, &bg_brush); - let title_format = self - .render_ctx - .text_format_cache - .get_center_format(20.0, DWRITE_FONT_WEIGHT_BOLD.0 as u32) - .unwrap(); - let info_format = self - .render_ctx - .text_format_cache - .get_center_format(14.0, DWRITE_FONT_WEIGHT_NORMAL.0 as u32) - .unwrap(); + // 有解码图像:绘制实际位图 + 顶部信息栏 + if self.content.image_data.is_some() { + self.render_image_bitmap(target, x, y, width, height); + return; + } - let title_color = color_f(0.83, 0.83, 0.83, 1.0); - let title_brush = self - .render_ctx - .brush_cache - .get_brush(target, &title_color) - .unwrap(); - let info_color = color_f(0.5, 0.5, 0.5, 1.0); - let info_brush = self - .render_ctx - .brush_cache - .get_brush(target, &info_color) - .unwrap(); - let icon_color = color_f(0.3, 0.7, 1.0, 1.0); - let icon_brush = self - .render_ctx - .brush_cache - .get_brush(target, &icon_color) - .unwrap(); + // 无解码图像(不支持的格式 / 解码失败):占位提示 + self.render_image_placeholder(target, x, y, width, height); + } + } + + /// 绘制图片位图(居中、保持宽高比缩放)+ 顶部信息栏 + fn render_image_bitmap( + &mut self, + target: &windows::Win32::Graphics::Direct2D::ID2D1HwndRenderTarget, + x: f32, + y: f32, + width: f32, + height: f32, + ) { + unsafe { + const INFO_BAR_H: f32 = 40.0; + const MARGIN: f32 = 20.0; - let center_y = y + height / 2.0; + // 惰性创建位图缓存(设备相关) + if self.image_bitmap.is_none() { + if let Some(img) = &self.content.image_data { + match crate::bitmap_loader::create_bitmap_from_rgba( + target, + img.width, + img.height, + &img.rgba, + ) { + Ok(bmp) => self.image_bitmap = Some(bmp), + Err(e) => { + tracing::warn!(error = %e, "创建图片预览位图失败"); + } + } + } + } - // 图片图标 - let icon_text: Vec = "🖼️".encode_utf16().chain(Some(0)).collect(); - let icon_rect = D2D_RECT_F { - left: x, - top: center_y - 60.0, - right: x + width, - bottom: center_y - 20.0, + // 顶部信息栏:文件名 + 尺寸/格式(左对齐,垂直居中) + let (img_w, img_h, fmt) = self + .content + .image_data + .as_ref() + .map(|i| (i.width, i.height, i.format_name)) + .unwrap_or((0, 0, "?")); + let file_name = self + .content + .file_path + .as_ref() + .and_then(|p| p.file_name()) + .map(|n| n.to_string_lossy().to_string()) + .unwrap_or_else(|| "图片".to_string()); + let zoom_percent = (self.image_zoom * 100.0).round() as i32; + let info_text = format!("{} | {} x {} | {} | {}%", file_name, img_w, img_h, fmt, zoom_percent); + let info_format = self + .render_ctx + .text_format_cache + .get_format( + 13.0, + DWRITE_FONT_WEIGHT_NORMAL.0 as u32, + windows::Win32::Graphics::DirectWrite::DWRITE_TEXT_ALIGNMENT_LEADING.0 as u32, + windows::Win32::Graphics::DirectWrite::DWRITE_PARAGRAPH_ALIGNMENT_CENTER.0 as u32, + ) + .unwrap(); + let info_color = color_f(0.6, 0.6, 0.6, 1.0); + let info_brush = self + .render_ctx + .brush_cache + .get_brush(target, &info_color) + .unwrap(); + let info_rect = D2D_RECT_F { + left: x + MARGIN, + top: y, + right: x + width - MARGIN, + bottom: y + INFO_BAR_H, + }; + let info_wide: Vec = info_text.encode_utf16().chain(Some(0)).collect(); + target.DrawText( + &info_wide, + &info_format, + &info_rect, + &info_brush, + D2D1_DRAW_TEXT_OPTIONS_NONE, + DWRITE_MEASURING_MODE_NATURAL, + ); + + // 图片显示区域(信息栏下方,四周边距) + let area_x = x + MARGIN; + let area_y = y + INFO_BAR_H + MARGIN; + let area_w = (width - MARGIN * 2.0).max(1.0); + let area_h = (height - INFO_BAR_H - MARGIN * 2.0).max(1.0); + + if let Some(ref bitmap) = self.image_bitmap { + // 计算基础缩放(适应窗口,保持宽高比) + let fit_scale = (area_w / img_w as f32).min(area_h / img_h as f32); + // 应用用户缩放 + let scale = fit_scale * self.image_zoom; + let draw_w = img_w as f32 * scale; + let draw_h = img_h as f32 * scale; + // 居中 + 用户偏移 + let draw_x = area_x + (area_w - draw_w) / 2.0 + self.image_offset_x; + let draw_y = area_y + (area_h - draw_h) / 2.0 + self.image_offset_y; + let dest_rect = D2D_RECT_F { + left: draw_x, + top: draw_y, + right: draw_x + draw_w, + bottom: draw_y + draw_h, }; - target.DrawText( - &icon_text, - &title_format, - &icon_rect, - &icon_brush, - D2D1_DRAW_TEXT_OPTIONS_NONE, - DWRITE_MEASURING_MODE_NATURAL, + // 裁剪到图片显示区域,防止溢出 + let clip_rect = D2D_RECT_F { + left: area_x, + top: area_y, + right: area_x + area_w, + bottom: area_y + area_h, + }; + target.PushAxisAlignedClip(&clip_rect, windows::Win32::Graphics::Direct2D::D2D1_ANTIALIAS_MODE_PER_PRIMITIVE); + target.DrawBitmap( + bitmap, + Some(&dest_rect), + 1.0, + windows::Win32::Graphics::Direct2D::D2D1_BITMAP_INTERPOLATION_MODE_LINEAR, + None, ); + target.PopAxisAlignedClip(); + } + } + } - // 标题 - let title = "图片预览"; - let title_wide: Vec = title.encode_utf16().chain(Some(0)).collect(); - let title_rect = D2D_RECT_F { - left: x, - top: center_y - 20.0, - right: x + width, - bottom: center_y + 10.0, + /// 占位提示(不支持的格式 / 解码失败) + fn render_image_placeholder( + &mut self, + target: &windows::Win32::Graphics::Direct2D::ID2D1HwndRenderTarget, + x: f32, + y: f32, + width: f32, + height: f32, + ) { + unsafe { + let title_format = self + .render_ctx + .text_format_cache + .get_center_format(20.0, DWRITE_FONT_WEIGHT_BOLD.0 as u32) + .unwrap(); + let info_format = self + .render_ctx + .text_format_cache + .get_center_format(14.0, DWRITE_FONT_WEIGHT_NORMAL.0 as u32) + .unwrap(); + + let title_color = color_f(0.83, 0.83, 0.83, 1.0); + let title_brush = self + .render_ctx + .brush_cache + .get_brush(target, &title_color) + .unwrap(); + let info_color = color_f(0.5, 0.5, 0.5, 1.0); + let info_brush = self + .render_ctx + .brush_cache + .get_brush(target, &info_color) + .unwrap(); + let icon_color = color_f(0.3, 0.7, 1.0, 1.0); + let icon_brush = self + .render_ctx + .brush_cache + .get_brush(target, &icon_color) + .unwrap(); + + let center_y = y + height / 2.0; + + // 图片图标 + let icon_text: Vec = "🖼️".encode_utf16().chain(Some(0)).collect(); + let icon_rect = D2D_RECT_F { + left: x, + top: center_y - 70.0, + right: x + width, + bottom: center_y - 30.0, + }; + target.DrawText( + &icon_text, + &title_format, + &icon_rect, + &icon_brush, + D2D1_DRAW_TEXT_OPTIONS_NONE, + DWRITE_MEASURING_MODE_NATURAL, + ); + + // 标题 + let title = "无法预览此图片"; + let title_wide: Vec = title.encode_utf16().chain(Some(0)).collect(); + let title_rect = D2D_RECT_F { + left: x, + top: center_y - 30.0, + right: x + width, + bottom: center_y, + }; + target.DrawText( + &title_wide, + &title_format, + &title_rect, + &title_brush, + D2D1_DRAW_TEXT_OPTIONS_NONE, + DWRITE_MEASURING_MODE_NATURAL, + ); + + // 提示(格式不支持或文件损坏) + let hint = "该格式暂不支持预览,或文件已损坏"; + let hint_wide: Vec = hint.encode_utf16().chain(Some(0)).collect(); + let hint_rect = D2D_RECT_F { + left: x, + top: center_y + 4.0, + right: x + width, + bottom: center_y + 30.0, + }; + target.DrawText( + &hint_wide, + &info_format, + &hint_rect, + &info_brush, + D2D1_DRAW_TEXT_OPTIONS_NONE, + DWRITE_MEASURING_MODE_NATURAL, + ); + + // 文件路径 + if let Some(path) = &self.content.file_path { + let path_text = format!("{}", path.display()); + let path_wide: Vec = path_text.encode_utf16().chain(Some(0)).collect(); + let path_rect = D2D_RECT_F { + left: x + 20.0, + top: center_y + 34.0, + right: x + width - 20.0, + bottom: center_y + 64.0, }; target.DrawText( - &title_wide, - &title_format, - &title_rect, - &title_brush, + &path_wide, + &info_format, + &path_rect, + &info_brush, D2D1_DRAW_TEXT_OPTIONS_NONE, DWRITE_MEASURING_MODE_NATURAL, ); - - // 文件路径 - if let Some(path) = &self.content.file_path { - let path_text = format!("{}", path.display()); - let path_wide: Vec = path_text.encode_utf16().chain(Some(0)).collect(); - let path_rect = D2D_RECT_F { - left: x + 20.0, - top: center_y + 20.0, - right: x + width - 20.0, - bottom: center_y + 50.0, - }; - target.DrawText( - &path_wide, - &info_format, - &path_rect, - &info_brush, - D2D1_DRAW_TEXT_OPTIONS_NONE, - DWRITE_MEASURING_MODE_NATURAL, - ); - } + } } } } diff --git a/crates/aether-win32/src/render/editor_view.rs b/crates/aether-win32/src/render/editor_view.rs index b7454d1..92078ff 100644 --- a/crates/aether-win32/src/render/editor_view.rs +++ b/crates/aether-win32/src/render/editor_view.rs @@ -124,6 +124,17 @@ impl EditorState { }; target.FillRectangle(&sep_rect, &sep_brush); + // 编辑区整体裁剪:滚动时首行 line_y = y - (scroll_y % line_height) 会 + // 部分位于编辑区顶部之上,若无垂直裁剪会覆盖标签栏(编辑器晚于标签栏渲染)。 + // 此处将后续所有行内容/光标/补全限制在编辑区矩形内。 + let editor_clip = D2D_RECT_F { + left: x, + top: y, + right: x + width, + bottom: y + height, + }; + target.PushAxisAlignedClip(&editor_clip, D2D1_ANTIALIAS_MODE_ALIASED); + let (start_line, end_line) = self.visible_line_range(); for line_idx in start_line..end_line { @@ -613,6 +624,8 @@ impl EditorState { target.FillRectangle(&cursor_rect, &cursor_brush); } } + // 配对弹出编辑区整体裁剪 + target.PopAxisAlignedClip(); } } diff --git a/crates/aether-win32/src/render/mod.rs b/crates/aether-win32/src/render/mod.rs index 2759479..2a1aa5e 100644 --- a/crates/aether-win32/src/render/mod.rs +++ b/crates/aether-win32/src/render/mod.rs @@ -83,7 +83,17 @@ impl EditorState { // 此前未调用 flush_output 导致 shell 输出无法显示,现在每帧轮询保证实时性。 if self.terminal_panel.running { self.terminal_panel.poll_startup(); - self.terminal_panel.flush_output(); + // 拉取到新输出时标脏底部面板区域,确保输出及时触发局部重绘(而非等全窗口) + if self.terminal_panel.flush_output() { + let bp = self.layout.bottom_panel_region(); + self.dirty_tracker.mark_region( + bp.x, + bp.y, + bp.width, + bp.height, + crate::dirty_rect::DirtyRegionType::BottomPanel, + ); + } // AI Agent 排队命令:终端就绪后自动发送执行 self.terminal_panel.flush_pending_commands(); } @@ -915,6 +925,8 @@ impl EditorState { self.render_ctx.handle_device_lost(); // P4-4: 同时清理 IconCache,确保下次绘制时从新 factory 重建几何 self.icons.clear(); + // 图片预览位图绑定旧设备,随渲染目标一起失效重建 + self.image_bitmap = None; // 重建渲染目标并重新预初始化 let _ = self.init_render_target(); if let Some(rt) = self.render_ctx.target_ref() { diff --git a/crates/aether-win32/src/tabs.rs b/crates/aether-win32/src/tabs.rs index 7ea1848..7613fb0 100644 --- a/crates/aether-win32/src/tabs.rs +++ b/crates/aether-win32/src/tabs.rs @@ -46,6 +46,8 @@ pub struct TabContent { pub(crate) is_large_file: bool, /// P2.3: 行 Y 偏移前缀和缓存 pub(crate) line_y_offsets: Vec, + /// P2.3: 缓存 line_y_offsets 对应的行数,避免每帧重建 + pub(crate) line_y_offsets_cached_lines: usize, /// P3.1: 当前内联补全建议 pub(crate) inline_completion: Option, /// 编辑器光标可见状态(用于光标闪烁) @@ -54,6 +56,12 @@ pub struct TabContent { pub(crate) tokens_trimmed: bool, // 语言类型 pub(crate) language: Language, + /// GPU 视口高亮缓存(增量更新,仅缓存可见行) + pub(crate) viewport_highlight_cache: Option, + /// 0延迟切换:标记刚切换过来的标签页,跳过首帧 rebuild_cache + pub(crate) just_switched: bool, + /// 图片预览:解码后的图像数据(仅 language == Image 时有值),随标签 swap 恢复 + pub image_data: Option, } impl TabContent { @@ -80,10 +88,14 @@ impl TabContent { last_cache_signature: (0, 0, 0, 0), is_large_file: false, line_y_offsets: Vec::new(), + line_y_offsets_cached_lines: 0, inline_completion: None, caret_visible: true, tokens_trimmed: false, language: Language::PlainText, + viewport_highlight_cache: None, + just_switched: false, + image_data: None, } } @@ -115,10 +127,14 @@ impl TabContent { last_cache_signature: (0, 0, 0, 0), is_large_file: false, line_y_offsets: Vec::new(), + line_y_offsets_cached_lines: 0, inline_completion: None, caret_visible: true, tokens_trimmed: false, language, + viewport_highlight_cache: None, + just_switched: false, + image_data: None, }) } @@ -158,10 +174,14 @@ impl TabContent { last_cache_signature: (0, 0, 0, 0), is_large_file: false, line_y_offsets: Vec::new(), + line_y_offsets_cached_lines: 0, inline_completion: None, caret_visible: true, tokens_trimmed: false, language, + viewport_highlight_cache: None, + just_switched: false, + image_data: None, } } @@ -174,6 +194,7 @@ impl TabContent { self.cached_tokens = Vec::new(); self.line_cache_versions = Vec::new(); self.line_y_offsets = Vec::new(); + self.line_y_offsets_cached_lines = 0; self.last_cache_signature = (0, 0, 0, 0); self.tokens_trimmed = true; } diff --git a/crates/aether-win32/src/terminal.rs b/crates/aether-win32/src/terminal.rs index 8137e69..c43c1bb 100644 --- a/crates/aether-win32/src/terminal.rs +++ b/crates/aether-win32/src/terminal.rs @@ -60,6 +60,12 @@ pub struct TerminalPanel { agent_scan_from: usize, /// 当前 shell 是否为 PowerShell(决定哨兵命令的连接符语法) shell_is_powershell: bool, + /// 假终端模式:真终端(ConPTY)就绪前先展示模拟提示符,用户输入暂存 + fake_prompt: bool, + /// 假终端当前输入行内容(不含提示符),用于本地即时回显 + fake_input: String, + /// 真终端就绪前暂存的原始输入字节,就绪后一次性映射(发送)给真终端 + pending_input: Vec, } /// AI Agent 命令监视:等待终端输出中出现完成哨兵 @@ -107,6 +113,9 @@ impl TerminalPanel { agent_counter: 0, agent_scan_from: 0, shell_is_powershell: false, + fake_prompt: false, + fake_input: String::new(), + pending_input: Vec::new(), } } @@ -147,6 +156,12 @@ impl TerminalPanel { /// 获取终端光标位置 (row, col),均为 0-indexed。 /// 用于渲染光标。row 已被 clamp 到 output_lines 范围内。 pub fn cursor_position(&self) -> (usize, usize) { + // 假终端:光标固定在末行末尾(提示符 + 输入内容之后) + if self.fake_prompt { + let row = self.output_lines.len().saturating_sub(1); + let col = self.prompt_text().chars().count() + self.fake_input.chars().count(); + return (row, col); + } let (row, col) = self.ansi_parser.cursor_position(); let clamped_row = row.min(self.output_lines.len().saturating_sub(1)); (clamped_row, col) @@ -182,7 +197,9 @@ impl TerminalPanel { self.running = true; self.size_synced = false; // 每次启动重置,首次 set_size 不触发 resize - self.push_output(&format!("正在启动终端: {}...", commandline)); + // 假终端:立即显示模拟提示符(极速感知),真终端后台启动, + // 就绪后无感替换并把暂存输入映射过去 + self.enter_fake_prompt(); thread::spawn(move || { match ConPtySession::spawn(&commandline, Some(&cwd), cols, rows) { @@ -255,8 +272,9 @@ impl TerminalPanel { self.conpty = Some(session); self.output_receiver = Some(output_rx); self.conpty_start_time = Some(std::time::Instant::now()); - // 清除"正在启动"提示,ConPTY 会输出 shell 提示符 - self.output_lines.clear(); + // 假终端保持显示(无感):此时 shell 仍在加载 PSReadLine, + // 输入会被吞,故保持 fake_prompt 继续暂存; + // 待 flush_pending_commands 确认 shell 就绪(900ms)后再无感替换并映射暂存输入。 } Ok(Err(e)) => { tracing::error!(error = %e, "poll_startup: 终端启动失败"); @@ -280,7 +298,7 @@ impl TerminalPanel { if let Some(ref session) = self.conpty { match session.write_input(data) { Ok(()) => { - tracing::info!(bytes = data.len(), data_hex = %format!("{:02x?}", data), "send_bytes: 已发送字节到子进程"); + tracing::debug!(bytes = data.len(), data_hex = %format!("{:02x?}", data), "send_bytes: 已发送字节到子进程"); } Err(e) => { tracing::error!(error = %e, bytes = data.len(), "send_bytes: write_input 失败"); @@ -386,9 +404,10 @@ impl TerminalPanel { } /// 刷新待执行命令:当 ConPTY 就绪且 shell 提示符已显示后,发送队列中的命令。 + /// 同时负责假终端 → 真终端的无感替换(映射暂存输入)。 /// 应在主线程每帧调用(与 poll_startup / flush_output 同级)。 pub fn flush_pending_commands(&mut self) { - if self.pending_commands.is_empty() || self.conpty.is_none() { + if self.conpty.is_none() { return; } // 等待 shell 提示符就绪(启动后短暂延迟),避免命令被 shell 初始化吞掉。 @@ -400,6 +419,25 @@ impl TerminalPanel { } else { return; } + + // shell 已就绪:执行假终端 → 真终端的无感替换 + if self.fake_prompt { + // 清空假显示(假提示符 + 本地回显),真终端将输出真实提示符 + self.output_lines.clear(); + self.fake_prompt = false; + self.fake_input.clear(); + // 假终端期间丢弃了 shell 首个提示符;若用户无暂存输入, + // 补一个空回车让 shell 重新显示提示符,避免替换后终端空白 + if self.pending_input.is_empty() && self.pending_commands.is_empty() { + self.send_bytes(b"\r"); + } + } + // 先映射假终端期间暂存的用户输入(保持输入顺序) + if !self.pending_input.is_empty() { + let pending = std::mem::take(&mut self.pending_input); + self.send_bytes(&pending); + } + // 再发送 AI Agent 待执行命令 while let Some(cmd) = self.pending_commands.pop_front() { self.send_bytes(cmd.as_bytes()); self.send_bytes(b"\r"); @@ -414,6 +452,12 @@ impl TerminalPanel { /// 管道模式下发送 `\r\n`(cmd.exe 管道模式需要 CRLF 行结束符)。 pub fn send_enter(&mut self) { self.scroll_offset = 0; + if self.fake_prompt { + // 假终端:暂存回车,假显示换行 + self.pending_input.push(b'\r'); + self.fake_echo_newline(); + return; + } let is_pipe = self.conpty.as_ref().map(|s| s.is_pipe()).unwrap_or(false); if is_pipe { self.send_bytes(b"\r\n"); @@ -424,16 +468,34 @@ impl TerminalPanel { /// 发送退格键(DEL = 0x7f,cmd.exe 识别) pub fn send_backspace(&mut self) { + if self.fake_prompt { + self.pending_input.push(0x7f); + self.fake_echo_backspace(); + return; + } self.send_bytes(b"\x7f"); } /// 发送 Tab 键 pub fn send_tab(&mut self) { + if self.fake_prompt { + // 假终端不模拟补全,暂存 Tab 并回显为空格占位 + self.pending_input.push(b'\t'); + self.fake_input.push('\t'); + self.refresh_fake_line(); + return; + } self.send_bytes(b"\t"); } /// 发送 Ctrl+C(中断信号) pub fn send_interrupt(&mut self) { + if self.fake_prompt { + // 假终端:暂存中断,换行给新提示符 + self.pending_input.push(0x03); + self.fake_echo_newline(); + return; + } self.send_bytes(b"\x03"); } @@ -445,21 +507,38 @@ impl TerminalPanel { ArrowKey::Right => b"\x1b[C", ArrowKey::Left => b"\x1b[D", }; + if self.fake_prompt { + // 假终端:暂存方向键序列(历史/光标移动由真终端处理),本地不回显 + self.pending_input.extend_from_slice(seq); + return; + } self.send_bytes(seq); } /// 发送 Delete 键 pub fn send_delete(&mut self) { + if self.fake_prompt { + self.pending_input.extend_from_slice(b"\x1b[3~"); + return; + } self.send_bytes(b"\x1b[3~"); } /// 发送 Home 键 pub fn send_home(&mut self) { + if self.fake_prompt { + self.pending_input.extend_from_slice(b"\x1b[H"); + return; + } self.send_bytes(b"\x1b[H"); } /// 发送 End 键 pub fn send_end(&mut self) { + if self.fake_prompt { + self.pending_input.extend_from_slice(b"\x1b[F"); + return; + } self.send_bytes(b"\x1b[F"); } @@ -467,7 +546,60 @@ impl TerminalPanel { pub fn send_char(&mut self, c: char) { let mut buf = [0u8; 4]; let s = c.encode_utf8(&mut buf); - self.send_bytes(s.as_bytes()); + if self.fake_prompt { + // 假终端:暂存字节 + 本地即时回显 + self.pending_input.extend_from_slice(s.as_bytes()); + self.fake_input.push(c); + self.refresh_fake_line(); + } else { + self.send_bytes(s.as_bytes()); + } + } + + // ===== 假终端(真终端就绪前的极速占位 UI)===== + + /// 当前 shell 提示符文本(模拟 PowerShell) + fn prompt_text(&self) -> String { + format!("PS {}> ", self.cwd) + } + + /// 进入假终端模式:清空输出并显示模拟提示符 + fn enter_fake_prompt(&mut self) { + self.fake_prompt = true; + self.fake_input.clear(); + self.pending_input.clear(); + self.output_lines.clear(); + let prompt = self.prompt_text(); + self.output_lines.push_back(prompt); + self.scroll_offset = 0; + } + + /// 刷新假终端当前输入行(末行 = 提示符 + 输入内容) + fn refresh_fake_line(&mut self) { + let line = format!("{}{}", self.prompt_text(), self.fake_input); + if let Some(last) = self.output_lines.back_mut() { + *last = line; + } else { + self.output_lines.push_back(line); + } + self.scroll_offset = 0; + } + + /// 假终端回车:暂存 \r,假显示换行并给出新提示符 + fn fake_echo_newline(&mut self) { + self.fake_input.clear(); + let prompt = self.prompt_text(); + self.output_lines.push_back(prompt); + if self.output_lines.len() > self.max_lines { + self.output_lines.pop_front(); + } + self.scroll_offset = 0; + } + + /// 假终端退格:暂存 \x7f,删除输入行末字符(不删进提示符) + fn fake_echo_backspace(&mut self) { + self.fake_input.pop(); + self.refresh_fake_line(); } /// 停止终端 @@ -478,6 +610,10 @@ impl TerminalPanel { self.startup_receiver = None; self.size_synced = false; self.conpty_start_time = None; + // 清理假终端状态 + self.fake_prompt = false; + self.fake_input.clear(); + self.pending_input.clear(); } /// 检查 ConPTY 子进程是否仍在运行 @@ -497,11 +633,13 @@ impl TerminalPanel { /// /// 同时检测读取线程是否已退出(通道断开 = 子进程已结束), /// 此时清理 ConPTY 并提示用户进程已退出。 - pub fn flush_output(&mut self) { + /// + /// 返回 true 表示本帧拉取到了新输出并已写入 output_lines(调用方应标脏终端区域触发重绘)。 + pub fn flush_output(&mut self) -> bool { let has_rx = self.output_receiver.is_some(); let has_conpty = self.conpty.is_some(); if !has_rx { - tracing::info!( + tracing::debug!( has_rx, has_conpty, running = self.running, @@ -512,7 +650,7 @@ impl TerminalPanel { static FLUSH_COUNT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); let count = FLUSH_COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed); if count.is_multiple_of(100) { - tracing::info!( + tracing::debug!( count, has_rx, has_conpty, @@ -523,6 +661,7 @@ impl TerminalPanel { if let Some(rx) = self.output_receiver.take() { let mut total_bytes = 0usize; let mut msg_count = 0usize; + let mut fed_new = false; let mut channel_closed = false; // ConPTY 启动后 3 秒内抑制清屏序列(\x1b[2J、\x1b[K), // 因为 ConPTY 初始化时会发送清屏+重绘序列,但不包含实际文本内容, @@ -538,8 +677,13 @@ impl TerminalPanel { Ok(bytes) => { total_bytes += bytes.len(); msg_count += 1; - self.ansi_parser - .feed(&bytes, &mut self.output_lines, self.max_lines); + // 假终端期间:真终端启动输出(banner/清屏/首个提示符)无意义, + // 丢弃不显示,避免污染假显示;无感替换后恢复正常 feed。 + if !self.fake_prompt { + self.ansi_parser + .feed(&bytes, &mut self.output_lines, self.max_lines); + fed_new = true; + } } Err(mpsc::TryRecvError::Empty) => break, Err(mpsc::TryRecvError::Disconnected) => { @@ -549,7 +693,7 @@ impl TerminalPanel { } } if msg_count > 0 { - tracing::info!( + tracing::debug!( msgs = msg_count, bytes = total_bytes, lines = self.output_lines.len(), @@ -557,7 +701,7 @@ impl TerminalPanel { ); // 记录前 5 行内容用于诊断 ANSI 解析结果 for (i, line) in self.output_lines.iter().rev().take(5).enumerate() { - tracing::info!(idx = i, content = %line, "终端输出行"); + tracing::debug!(idx = i, content = %line, "终端输出行"); } } if channel_closed { @@ -572,12 +716,15 @@ impl TerminalPanel { self.push_output(&format!("\n[进程已退出,退出码: 0x{:08X}]", exit_code)); self.conpty = None; self.running = false; + return true; // 退出提示写入了 output_lines,需重绘 } else { self.output_receiver = Some(rx); // 仅当用户未手动上滚(贴底状态)时,新输出到达后才自动保持底部; // 用户正在浏览历史(scroll_offset > 0)时不强制归零,避免滚轮失效。 } + return fed_new; } + false } /// 添加输出行(直接追加,不经过 ANSI 解析) diff --git a/crates/aether-win32/src/window.rs b/crates/aether-win32/src/window.rs index 96cc166..73a9956 100644 --- a/crates/aether-win32/src/window.rs +++ b/crates/aether-win32/src/window.rs @@ -59,8 +59,8 @@ pub const SANDBOX_REFRESH_MS: u32 = 200; pub(crate) const AI_ARCHIVE_MS: u32 = 5000; /// 长按阈值(毫秒) pub(crate) const LP_THRESHOLD_MS: u32 = 500; -/// 终端刷新间隔(毫秒),约 20fps 足以实时显示 shell 输出 -pub(crate) const TERM_REFRESH_MS: u32 = 50; +/// 终端刷新间隔(毫秒),约 60fps,让 shell 输出回显更即时流畅 +pub(crate) const TERM_REFRESH_MS: u32 = 16; /// AI 后台刷新间隔(毫秒),用于流式生成与测试连接期间的平滑重绘 pub(crate) const AI_REFRESH_MS: u32 = 80; /// 语法高亮刷新间隔(毫秒),约 30fps,让后台高亮结果尽快着色显示 @@ -373,6 +373,7 @@ extern "system" fn window_proc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPA match msg { WM_LBUTTONDOWN => on_l_button_down(hwnd, msg, wparam, lparam), WM_MBUTTONDOWN => on_m_button_down(hwnd, msg, wparam, lparam), + WM_MBUTTONUP => on_m_button_up(hwnd, msg, wparam, lparam), WM_MOUSEMOVE => on_mouse_move(hwnd, msg, wparam, lparam), WM_LBUTTONUP => on_l_button_up(hwnd, msg, wparam, lparam), WM_LBUTTONDBLCLK => on_l_button_dblclk(hwnd, msg, wparam, lparam), diff --git a/crates/aether-win32/src/window/keyboard_handler/char_input.rs b/crates/aether-win32/src/window/keyboard_handler/char_input.rs index 5777ebc..4366d87 100644 --- a/crates/aether-win32/src/window/keyboard_handler/char_input.rs +++ b/crates/aether-win32/src/window/keyboard_handler/char_input.rs @@ -13,7 +13,7 @@ pub(crate) unsafe fn on_char(hwnd: HWND, _msg: u32, wparam: WPARAM, _lparam: LPA // 防止 Alt+Tab / 任务栏切换焦点后键盘输入路由到错误窗口的 EditorState get_and_set_state(hwnd); let ch = (wparam.0 & 0xFFFF) as u16; - tracing::info!(ch_code = ch, "on_char: 收到 WM_CHAR"); + tracing::debug!(ch_code = ch, "on_char: 收到 WM_CHAR"); // P2-9: 处理 UTF-16 代理对以支持 BMP 外字符(emoji、CJK 扩展 B 等) // WM_CHAR 对 BMP 外字符发送两条消息:先高代理(0xD800-0xDBFF),后低代理(0xDC00-0xDFFF) @@ -365,11 +365,22 @@ unsafe fn oc_terminal(hwnd: HWND, c: char) -> Option { .map(|state| state.borrow().terminal_panel.focused) .unwrap_or(false) }); - tracing::info!(active, char = %c, "oc_terminal: 检查终端焦点"); + tracing::debug!(active, char = %c, "oc_terminal: 检查终端焦点"); if active { EDITOR_STATE.with(|s| { if let Some(state) = s.borrow().as_ref() { - state.borrow_mut().terminal_panel.send_char(c); + let mut st = state.borrow_mut(); + st.terminal_panel.send_char(c); + // 标脏底部面板区域:输入回显只需局部重绘终端,避免全窗口重绘导致卡顿 + let bp = st.layout.bottom_panel_region(); + st.dirty_tracker.mark_region( + bp.x, + bp.y, + bp.width, + bp.height, + crate::dirty_rect::DirtyRegionType::BottomPanel, + ); + drop(st); invalidate_window(hwnd); } }); diff --git a/crates/aether-win32/src/window/keyboard_handler/key_down.rs b/crates/aether-win32/src/window/keyboard_handler/key_down.rs index 98cb4c3..b5a5312 100644 --- a/crates/aether-win32/src/window/keyboard_handler/key_down.rs +++ b/crates/aether-win32/src/window/keyboard_handler/key_down.rs @@ -507,33 +507,66 @@ unsafe fn okd_welcome_enter(hwnd: HWND) { match action { crate::welcome::WelcomeAction::OpenFolder => { if let Some(path) = Dialogs::open_folder_dialog(hwnd, "打开文件夹") { + // 信任检查在 borrow_mut 之前(模态框泵消息,避免 RefCell 重入 panic) + if crate::editor::files::check_workspace_trust(hwnd, &path) { + EDITOR_STATE.with(|s| { + if let Some(state) = s.borrow().as_ref() { + state.borrow_mut().open_folder(path); + invalidate_window(hwnd); + } + }); + } else { + EDITOR_STATE.with(|s| { + if let Some(state) = s.borrow().as_ref() { + state.borrow_mut().status_message = + "已取消打开不受信任的工作区".to_string(); + invalidate_window(hwnd); + } + }); + } + } + } + crate::welcome::WelcomeAction::OpenRecentProject(path_str) => { + let path = PathBuf::from(path_str); + // 信任检查在 borrow_mut 之前(模态框泵消息,避免 RefCell 重入 panic) + if crate::editor::files::check_workspace_trust(hwnd, &path) { EDITOR_STATE.with(|s| { if let Some(state) = s.borrow().as_ref() { state.borrow_mut().open_folder(path); invalidate_window(hwnd); } }); - } - } - crate::welcome::WelcomeAction::OpenRecentProject(path_str) => { - let path = PathBuf::from(path_str); - EDITOR_STATE.with(|s| { - if let Some(state) = s.borrow().as_ref() { - state.borrow_mut().open_folder(path); - invalidate_window(hwnd); - } - }); - } - crate::welcome::WelcomeAction::MoreRecentProjects => { - if let Some(path) = Dialogs::open_folder_dialog(hwnd, "打开文件夹") { + } else { EDITOR_STATE.with(|s| { if let Some(state) = s.borrow().as_ref() { - state.borrow_mut().open_folder(path); + state.borrow_mut().status_message = + "已取消打开不受信任的工作区".to_string(); invalidate_window(hwnd); } }); } } + crate::welcome::WelcomeAction::MoreRecentProjects => { + if let Some(path) = Dialogs::open_folder_dialog(hwnd, "打开文件夹") { + // 信任检查在 borrow_mut 之前(模态框泵消息,避免 RefCell 重入 panic) + if crate::editor::files::check_workspace_trust(hwnd, &path) { + EDITOR_STATE.with(|s| { + if let Some(state) = s.borrow().as_ref() { + state.borrow_mut().open_folder(path); + invalidate_window(hwnd); + } + }); + } else { + EDITOR_STATE.with(|s| { + if let Some(state) = s.borrow().as_ref() { + state.borrow_mut().status_message = + "已取消打开不受信任的工作区".to_string(); + invalidate_window(hwnd); + } + }); + } + } + } _ => {} } } diff --git a/crates/aether-win32/src/window/keyboard_handler/key_down_ctrl.rs b/crates/aether-win32/src/window/keyboard_handler/key_down_ctrl.rs index 6deee70..a2089af 100644 --- a/crates/aether-win32/src/window/keyboard_handler/key_down_ctrl.rs +++ b/crates/aether-win32/src/window/keyboard_handler/key_down_ctrl.rs @@ -64,12 +64,23 @@ unsafe fn okd_ctrl_file_ops(hwnd: HWND, vk: VIRTUAL_KEY, shift: bool) { } VK_K => { if let Some(path) = Dialogs::open_folder_dialog(hwnd, "打开文件夹") { - EDITOR_STATE.with(|s| { - if let Some(state) = s.borrow().as_ref() { - state.borrow_mut().open_folder(path); - invalidate_window(hwnd); - } - }); + // 信任检查在 borrow_mut 之前(模态框泵消息,避免 RefCell 重入 panic) + if crate::editor::files::check_workspace_trust(hwnd, &path) { + EDITOR_STATE.with(|s| { + if let Some(state) = s.borrow().as_ref() { + state.borrow_mut().open_folder(path); + invalidate_window(hwnd); + } + }); + } else { + EDITOR_STATE.with(|s| { + if let Some(state) = s.borrow().as_ref() { + state.borrow_mut().status_message = + "已取消打开不受信任的工作区".to_string(); + invalidate_window(hwnd); + } + }); + } } } VK_S => { @@ -264,30 +275,57 @@ unsafe fn okd_ctrl_view_shortcuts(hwnd: HWND, vk: VIRTUAL_KEY, shift: bool) { /// Ctrl+=/-/0/G:字体缩放、命令面板前缀 unsafe fn okd_ctrl_zoom_cmd(hwnd: HWND, vk: VIRTUAL_KEY) { + // 检查是否是图片预览模式 + let is_image = EDITOR_STATE.with(|s| { + s.borrow().as_ref().map(|state| { + state.borrow().content.language == aether_core::lexer::Language::Image + }).unwrap_or(false) + }); + match vk { VK_OEM_PLUS | VK_ADD => { - // P2-3: Ctrl+= 放大字体 EDITOR_STATE.with(|s| { if let Some(state) = s.borrow().as_ref() { - state.borrow_mut().zoom_font(Some(1.0)); + let mut st = state.borrow_mut(); + if is_image { + // 图片预览:Ctrl+= 放大图片 + st.image_zoom = (st.image_zoom + 0.1).min(10.0); + } else { + // P2-3: Ctrl+= 放大字体 + st.zoom_font(Some(1.0)); + } invalidate_window(hwnd); } }); } VK_OEM_MINUS | VK_SUBTRACT => { - // P2-3: Ctrl+- 缩小字体 EDITOR_STATE.with(|s| { if let Some(state) = s.borrow().as_ref() { - state.borrow_mut().zoom_font(Some(-1.0)); + let mut st = state.borrow_mut(); + if is_image { + // 图片预览:Ctrl+- 缩小图片 + st.image_zoom = (st.image_zoom - 0.1).max(0.1); + } else { + // P2-3: Ctrl+- 缩小字体 + st.zoom_font(Some(-1.0)); + } invalidate_window(hwnd); } }); } VK_0 | VK_NUMPAD0 => { - // P2-3: Ctrl+0 重置字体大小 EDITOR_STATE.with(|s| { if let Some(state) = s.borrow().as_ref() { - state.borrow_mut().zoom_font(None); + let mut st = state.borrow_mut(); + if is_image { + // 图片预览:Ctrl+0 重置缩放 + st.image_zoom = 1.0; + st.image_offset_x = 0.0; + st.image_offset_y = 0.0; + } else { + // P2-3: Ctrl+0 重置字体大小 + st.zoom_font(None); + } invalidate_window(hwnd); } }); diff --git a/crates/aether-win32/src/window/keyboard_handler/key_down_edit.rs b/crates/aether-win32/src/window/keyboard_handler/key_down_edit.rs index 0a2a2ba..1224394 100644 --- a/crates/aether-win32/src/window/keyboard_handler/key_down_edit.rs +++ b/crates/aether-win32/src/window/keyboard_handler/key_down_edit.rs @@ -135,6 +135,20 @@ unsafe fn okd_edit_terminal(hwnd: HWND, vk: VIRTUAL_KEY) -> bool { _ => false, }; if handled { + // 标脏底部面板区域:终端按键回显只需局部重绘终端,避免全窗口重绘导致卡顿 + EDITOR_STATE.with(|s| { + if let Some(state) = s.borrow().as_ref() { + let mut st = state.borrow_mut(); + let bp = st.layout.bottom_panel_region(); + st.dirty_tracker.mark_region( + bp.x, + bp.y, + bp.width, + bp.height, + crate::dirty_rect::DirtyRegionType::BottomPanel, + ); + } + }); invalidate_window(hwnd); } handled diff --git a/crates/aether-win32/src/window/mouse_handler.rs b/crates/aether-win32/src/window/mouse_handler.rs index ff49ccf..52065ca 100644 --- a/crates/aether-win32/src/window/mouse_handler.rs +++ b/crates/aether-win32/src/window/mouse_handler.rs @@ -20,6 +20,27 @@ use windows::Win32::UI::WindowsAndMessaging::*; use super::{get_and_set_state, invalidate_window, EDITOR_STATE, LP_TIMER_ID}; +/// WM_MBUTTONUP:鼠标中键释放事件 +pub(crate) unsafe fn on_m_button_up( + _hwnd: HWND, + _msg: u32, + _wparam: WPARAM, + _lparam: LPARAM, +) -> LRESULT { + EDITOR_STATE.with(|s| { + if let Some(state) = s.borrow().as_ref() { + let mut st = state.borrow_mut(); + // 结束图片拖拽 + if st.mouse_press.image_dragging { + st.mouse_press.image_dragging = false; + st.mouse_press.image_drag_start = None; + st.mouse_press.image_drag_offset = None; + } + } + }); + LRESULT(0) +} + /// WM_LBUTTONUP pub(crate) unsafe fn on_l_button_up( hwnd: HWND, @@ -37,8 +58,12 @@ pub(crate) unsafe fn on_l_button_up( // 结束面板拖拽 st.layout.right_panel_resizing = false; st.layout.bottom_panel_resizing = false; + // 拐角手柄拖拽结束(右下拐角仅复位;左下拐角含侧边栏,需收起判断) + st.layout.corner_right_resizing = false; + let corner_left_was = st.layout.corner_left_resizing; + st.layout.corner_left_resizing = false; // 侧边栏拖拽结束:当前宽度低于阈值且仍可见 → 启动平滑收起动画(而非立即跳变) - if st.layout.sidebar_resizing { + if st.layout.sidebar_resizing || corner_left_was { st.layout.sidebar_resizing = false; let collapse_threshold = crate::layout::MIN_SIDEBAR_WIDTH * 0.5; if st.layout.sidebar_visible && st.layout.sidebar_width < collapse_threshold { @@ -178,6 +203,7 @@ pub(crate) unsafe fn on_mouse_wheel( let _ = windows::Win32::Graphics::Gdi::ScreenToClient(hwnd, &mut client_point); // P0-3: Shift + 滚轮 → 横向滚动 let shift = GetKeyState(VK_SHIFT.0 as i32) < 0; + let ctrl = GetKeyState(VK_CONTROL.0 as i32) < 0; EDITOR_STATE.with(|s| { if let Some(state) = s.borrow().as_ref() { let mut state = state.borrow_mut(); @@ -186,6 +212,20 @@ pub(crate) unsafe fn on_mouse_wheel( let cursor_x = client_point.x as f32 / dpi_scale; let cursor_y = client_point.y as f32 / dpi_scale; + // 图片预览:Ctrl+滚轮缩放 + if state.content.language == aether_core::lexer::Language::Image && ctrl { + let editor = state.layout.editor_region(); + if cursor_x >= editor.x && cursor_x < editor.x + editor.width + && cursor_y >= editor.y && cursor_y < editor.y + editor.height + { + // 缩放因子:每 120 单位滚轮 = 10% 缩放 + let zoom_delta = delta / 120.0 * 0.1; + state.image_zoom = (state.image_zoom + zoom_delta).clamp(0.1, 10.0); + invalidate_window(hwnd); + return; + } + } + // SubTask 7.5: 光标在标签栏区域时 → 横向滚动标签栏 let show_tab_bar = state.show_tab_bar(); let tab_region = state.layout.tab_bar_region(show_tab_bar); diff --git a/crates/aether-win32/src/window/mouse_handler/l_button_down.rs b/crates/aether-win32/src/window/mouse_handler/l_button_down.rs index 9ee69c1..15de793 100644 --- a/crates/aether-win32/src/window/mouse_handler/l_button_down.rs +++ b/crates/aether-win32/src/window/mouse_handler/l_button_down.rs @@ -36,6 +36,7 @@ pub(crate) unsafe fn on_l_button_down( st.mouse_press.lbutton_down = true; let mouse_x = raw_x / st.dpi_scale; let mouse_y = raw_y / st.dpi_scale; + st.mouse_press.lbutton_down_pos = Some((mouse_x, mouse_y)); let layout = st.layout.clone(); let activity_region = layout.activity_bar_region(); let titlebar_region = layout.title_bar_region(); diff --git a/crates/aether-win32/src/window/mouse_handler/l_button_down/content_area.rs b/crates/aether-win32/src/window/mouse_handler/l_button_down/content_area.rs index f313f2d..6b881d4 100644 --- a/crates/aether-win32/src/window/mouse_handler/l_button_down/content_area.rs +++ b/crates/aether-win32/src/window/mouse_handler/l_button_down/content_area.rs @@ -83,6 +83,15 @@ pub(super) unsafe fn lbd_panel_resizing( layout: &crate::layout::LayoutManager, ) -> Option { let editor_region = layout.editor_region(); + // 拐角手柄命中(优先于单线):左下 = 侧边栏×底部面板,右下 = 右面板×底部面板 + let corner_left_hit = layout + .corner_left_handle() + .map(|r| r.contains(mouse_x, mouse_y)) + .unwrap_or(false); + let corner_right_hit = layout + .corner_right_handle() + .map(|r| r.contains(mouse_x, mouse_y)) + .unwrap_or(false); let right_panel_resize_zone = layout.right_panel_visible && (mouse_x >= editor_region.right() - 4.0 && mouse_x <= editor_region.right() + 4.0) && mouse_y >= editor_region.y @@ -109,6 +118,20 @@ pub(super) unsafe fn lbd_panel_resizing( && mouse_y < sidebar_region.y + sidebar_region.height }; let mut st = state.borrow_mut(); + // 拐角优先:拖拽交点同时调整两条分割线 + if corner_left_hit { + st.layout.corner_left_resizing = true; + st.layout.cancel_sidebar_anim(); + drop(st); + invalidate_window(hwnd); + return Some(LRESULT(0)); + } + if corner_right_hit { + st.layout.corner_right_resizing = true; + drop(st); + invalidate_window(hwnd); + return Some(LRESULT(0)); + } if right_panel_resize_zone { st.layout.right_panel_resizing = true; drop(st); @@ -822,10 +845,16 @@ unsafe fn lbd_right_panel_ai_controls( if hit { // 弹出系统文件夹选择对话框 if let Some(path) = Dialogs::open_folder_dialog(hwnd, "选择工作区文件夹") { - let mut st = state.borrow_mut(); - st.open_folder(path.clone()); - st.status_message = format!("已打开: {}", path.display()); - drop(st); + // 信任检查在 borrow_mut 之前(模态框泵消息,避免 RefCell 重入 panic) + if crate::editor::files::check_workspace_trust(hwnd, &path) { + let mut st = state.borrow_mut(); + st.open_folder(path.clone()); + st.status_message = format!("已打开: {}", path.display()); + drop(st); + } else { + state.borrow_mut().status_message = + "已取消打开不受信任的工作区".to_string(); + } } invalidate_window(hwnd); return Some(LRESULT(0)); @@ -1788,8 +1817,10 @@ pub(super) unsafe fn lbd_welcome_or_editor( } else { let editor_content = layout.editor_content_region(st.show_tab_bar()); st.set_cursor_from_mouse(mouse_x, mouse_y, editor_content.x, editor_content.y); + // 单击只设置光标位置,不启动选区 + // 选区在鼠标移动时(WM_MOUSEMOVE + is_dragging)启动 st.clear_selection(); - st.start_selection(); + st.is_selecting = false; // 重置光标闪烁状态并启动定时器 st.content.caret_visible = true; let _ = SetTimer(hwnd, crate::window::CARET_TIMER_ID, 530, None); @@ -1826,7 +1857,13 @@ unsafe fn lbd_welcome_action( match action { crate::welcome::WelcomeAction::OpenFolder => { if let Some(path) = Dialogs::open_folder_dialog(hwnd, "打开文件夹") { - state.borrow_mut().open_folder(path); + // 信任检查在 borrow_mut 之前(模态框泵消息,避免 RefCell 重入 panic) + if crate::editor::files::check_workspace_trust(hwnd, &path) { + state.borrow_mut().open_folder(path); + } else { + state.borrow_mut().status_message = + "已取消打开不受信任的工作区".to_string(); + } invalidate_window(hwnd); } } @@ -1846,12 +1883,24 @@ unsafe fn lbd_welcome_action( } crate::welcome::WelcomeAction::OpenRecentProject(path_str) => { let path = PathBuf::from(path_str); - state.borrow_mut().open_folder(path); + // 信任检查在 borrow_mut 之前(模态框泵消息,避免 RefCell 重入 panic) + if crate::editor::files::check_workspace_trust(hwnd, &path) { + state.borrow_mut().open_folder(path); + } else { + state.borrow_mut().status_message = + "已取消打开不受信任的工作区".to_string(); + } invalidate_window(hwnd); } crate::welcome::WelcomeAction::MoreRecentProjects => { if let Some(path) = Dialogs::open_folder_dialog(hwnd, "打开文件夹") { - state.borrow_mut().open_folder(path); + // 信任检查在 borrow_mut 之前(模态框泵消息,避免 RefCell 重入 panic) + if crate::editor::files::check_workspace_trust(hwnd, &path) { + state.borrow_mut().open_folder(path); + } else { + state.borrow_mut().status_message = + "已取消打开不受信任的工作区".to_string(); + } invalidate_window(hwnd); } } diff --git a/crates/aether-win32/src/window/mouse_handler/m_button_down.rs b/crates/aether-win32/src/window/mouse_handler/m_button_down.rs index b22fd45..9b140df 100644 --- a/crates/aether-win32/src/window/mouse_handler/m_button_down.rs +++ b/crates/aether-win32/src/window/mouse_handler/m_button_down.rs @@ -1,7 +1,8 @@ -//! `WM_MBUTTONDOWN` 处理:中键点击关闭标签页。 +//! `WM_MBUTTONDOWN` 处理:中键点击关闭标签页、图片预览中键拖拽。 //! //! SubTask 7.1: 当用户在标签栏中某个标签上按下鼠标中键时,关闭该标签。 //! 复用 `EditorState::close_tab` 的 dirty 检查逻辑,与关闭按钮行为一致。 +//! 图片预览模式下,中键按下开始拖拽平移。 use windows::Win32::Foundation::{HWND, LPARAM, LRESULT, WPARAM}; @@ -12,6 +13,7 @@ use super::super::{get_and_set_state, invalidate_window}; /// 仅响应标签栏区域内的中键点击:命中标签则调用 `close_tab(index)`, /// 由 `close_tab` 内部统一处理 dirty 检查(活动标签走 `close_current_tab_checked`, /// 非活动标签走 dirty 询问对话框)。 +/// 图片预览模式下,中键按下开始拖拽平移。 pub(crate) unsafe fn on_m_button_down( hwnd: HWND, _msg: u32, @@ -32,6 +34,18 @@ pub(crate) unsafe fn on_m_button_down( ) }; let mut st = state.borrow_mut(); + + // 图片预览模式:中键按下开始拖拽 + if st.content.language == aether_core::lexer::Language::Image { + let editor = layout.editor_region(); + if editor.contains(mouse_x, mouse_y) { + st.mouse_press.image_dragging = true; + st.mouse_press.image_drag_start = Some((mouse_x, mouse_y)); + st.mouse_press.image_drag_offset = Some((st.image_offset_x, st.image_offset_y)); + return LRESULT(0); + } + } + let show_tab_bar = st.show_tab_bar(); let tab_region = layout.tab_bar_region(show_tab_bar); if !show_tab_bar || !tab_region.contains(mouse_x, mouse_y) { diff --git a/crates/aether-win32/src/window/mouse_handler/mouse_move.rs b/crates/aether-win32/src/window/mouse_handler/mouse_move.rs index 0121446..31e2234 100644 --- a/crates/aether-win32/src/window/mouse_handler/mouse_move.rs +++ b/crates/aether-win32/src/window/mouse_handler/mouse_move.rs @@ -17,6 +17,25 @@ use super::super::{ HOVER_TIMER_ID, LP_MOVE_TOLERANCE, LP_TIMER_ID, }; +/// 面板拖拽同步重绘节流(~120fps)。 +/// +/// 高回报率鼠标下每条 WM_MOUSEMOVE 都同步重绘会压垮管线产生卡顿感, +/// 故节流至 8ms 最小帧间隔;被跳过的帧由后续 WM_PAINT 合并补齐。 +/// 返回 true 表示本帧应调用 UpdateWindow 立即重绘。 +fn panel_drag_should_sync_paint() -> bool { + const MIN_FRAME: std::time::Duration = std::time::Duration::from_millis(8); + static LAST: std::sync::Mutex> = std::sync::Mutex::new(None); + let now = std::time::Instant::now(); + let mut last = LAST.lock().unwrap(); + match *last { + Some(t) if now.duration_since(t) < MIN_FRAME => false, + _ => { + *last = Some(now); + true + } + } +} + /// WM_MOUSEMOVE:鼠标移动事件调度器。 pub(crate) unsafe fn on_mouse_move( hwnd: HWND, @@ -41,52 +60,106 @@ pub(crate) unsafe fn on_mouse_move( if let Some(r) = omm_early_returns(hwnd, &state, mouse_x, mouse_y, is_dragging, &layout) { return r; } + + // 图片预览拖拽:中键拖拽平移(优先级最高,跳过其他 hover 检测) + let is_mbutton_dragging = wparam.0 & 0x0010 != 0; // MK_MBUTTON + if is_mbutton_dragging { + let mut st = state.borrow_mut(); + if st.mouse_press.image_dragging { + if let (Some((start_x, start_y)), Some((orig_offset_x, orig_offset_y))) = + (st.mouse_press.image_drag_start, st.mouse_press.image_drag_offset) + { + let dx = mouse_x - start_x; + let dy = mouse_y - start_y; + st.image_offset_x = orig_offset_x + dx; + st.image_offset_y = orig_offset_y + dy; + drop(st); + invalidate_window(hwnd); + return LRESULT(0); + } + } + } // 文本拖拽选区:前置为最高优先级(仅次于菜单/对话框)。 // 旧实现放在所有 hover 判定之后的 else 分支,任一 hover 变化都会 // 提前走 invalidate 分支跳过选区更新,导致拖拽时预选中高亮跟不上鼠标; // 拖拽选区期间 hover/tooltip 状态无意义,直接跳过还能省掉逐帧命中开销。 - if is_dragging && state.borrow().is_selecting { + if is_dragging { + // 面板拖拽前置:跳过全部 hover 检测(拖拽中 hover 无意义), + // 直接处理分割线/拐角调整并提前返回,避免热路径上 7 个 hover 命中的逐帧开销。 + let panel_dragging = { + let st = state.borrow(); + st.layout.right_panel_resizing + || st.layout.bottom_panel_resizing + || st.layout.sidebar_resizing + || st.layout.corner_left_resizing + || st.layout.corner_right_resizing + }; + if panel_dragging { + if let Some(r) = omm_resize_drag(hwnd, &state, mouse_x, mouse_y, is_dragging, &layout) + { + return r; + } + return LRESULT(0); + } + let mut st = state.borrow_mut(); let editor_content = layout.editor_content_region(st.show_tab_bar()); - let before = ( - st.content.cursor_line, - st.content.cursor_col, - st.content.selection_end, - ); - st.set_cursor_from_mouse(mouse_x, mouse_y, editor_content.x, editor_content.y); - st.update_selection(); - let changed = ( - st.content.cursor_line, - st.content.cursor_col, - st.content.selection_end, - ) != before; - if changed { - // 标记编辑区+状态栏脏区:避免无脏区退化为全窗口无裁剪重绘 - st.dirty_tracker.mark_region( - editor_content.x, - editor_content.y, - editor_content.width, - editor_content.height, - crate::dirty_rect::DirtyRegionType::EditorContent, - ); - let sb = st.layout.status_bar_region(); - st.dirty_tracker.mark_region( - sb.x, - sb.y, - sb.width, - sb.height, - crate::dirty_rect::DirtyRegionType::StatusBar, + + // 如果尚未进入选区模式,检查鼠标是否移动了足够距离来启动选区 + if !st.is_selecting { + // 记录鼠标按下位置(在 WM_LBUTTONDOWN 时设置) + if let Some((press_x, press_y)) = st.mouse_press.lbutton_down_pos { + let dx = mouse_x - press_x; + let dy = mouse_y - press_y; + // 超过 3px 阈值才启动选区(避免单击时的微小抖动) + if dx * dx + dy * dy > 9.0 { + st.start_selection(); + } + } + } + + if st.is_selecting { + let before = ( + st.content.cursor_line, + st.content.cursor_col, + st.content.selection_end, ); + st.set_cursor_from_mouse(mouse_x, mouse_y, editor_content.x, editor_content.y); + st.update_selection(); + let changed = ( + st.content.cursor_line, + st.content.cursor_col, + st.content.selection_end, + ) != before; + if changed { + // 标记编辑区+状态栏脏区:避免无脏区退化为全窗口无裁剪重绘 + st.dirty_tracker.mark_region( + editor_content.x, + editor_content.y, + editor_content.width, + editor_content.height, + crate::dirty_rect::DirtyRegionType::EditorContent, + ); + let sb = st.layout.status_bar_region(); + st.dirty_tracker.mark_region( + sb.x, + sb.y, + sb.width, + sb.height, + crate::dirty_rect::DirtyRegionType::StatusBar, + ); + } + drop(st); + // 光标未跨过字符边界时跳过重绘,避免鼠标微动刷帧 + if changed { + invalidate_window(hwnd); + // WM_PAINT 优先级低于 WM_MOUSEMOVE,快速拖拽时会被消息洪流饿死, + // 导致选区/光标视觉滞后——UpdateWindow 绕过队列立即重绘 + let _ = windows::Win32::Graphics::Gdi::UpdateWindow(hwnd); + } + return LRESULT(0); } drop(st); - // 光标未跨过字符边界时跳过重绘,避免鼠标微动刷帧 - if changed { - invalidate_window(hwnd); - // WM_PAINT 优先级低于 WM_MOUSEMOVE,快速拖拽时会被消息洪流饿死, - // 导致选区/光标视觉滞后——UpdateWindow 绕过队列立即重绘 - let _ = windows::Win32::Graphics::Gdi::UpdateWindow(hwnd); - } - return LRESULT(0); } // 文件树拖拽:按下候选节点后处理阈值判定与放置目标/浮标更新。 // 进入拖拽后独占本次消息(跳过 hover/tooltip 更新,避免高亮叠加)。 @@ -796,6 +869,15 @@ unsafe fn omm_resize_drag( ) -> Option { let mut st = state.borrow_mut(); let editor_region = layout.editor_region(); + // 拐角手柄 hover(优先于单线):左下 = 侧边栏×底部面板,右下 = 右面板×底部面板 + let corner_left_hit = layout + .corner_left_handle() + .map(|r| r.contains(mouse_x, mouse_y)) + .unwrap_or(false); + let corner_right_hit = layout + .corner_right_handle() + .map(|r| r.contains(mouse_x, mouse_y)) + .unwrap_or(false); let right_panel_resize_zone = layout.right_panel_visible && (mouse_x >= editor_region.right() - 4.0 && mouse_x <= editor_region.right() + 4.0) && mouse_y >= editor_region.y @@ -825,8 +907,14 @@ unsafe fn omm_resize_drag( }; // 更新 hover 状态 st.hover_sidebar_resize = sidebar_resize_zone; - // 设置拖拽光标 - if right_panel_resize_zone + // 设置拖拽光标(拐角优先,斜向光标) + if corner_left_hit || st.layout.corner_left_resizing { + let hcursor = LoadCursorW(None, IDC_SIZENWSE).unwrap_or_default(); + let _ = SetCursor(hcursor); + } else if corner_right_hit || st.layout.corner_right_resizing { + let hcursor = LoadCursorW(None, IDC_SIZENESW).unwrap_or_default(); + let _ = SetCursor(hcursor); + } else if right_panel_resize_zone || st.layout.right_panel_resizing || sidebar_resize_zone || st.layout.sidebar_resizing @@ -840,13 +928,46 @@ unsafe fn omm_resize_drag( let hcursor = LoadCursorW(None, IDC_HAND).unwrap_or_default(); let _ = SetCursor(hcursor); } - // 处理拖拽调整 + // 处理拖拽调整(拐角优先:同时调整两条分割线) if is_dragging { - if st.layout.right_panel_resizing { + if st.layout.corner_left_resizing { + // 左下拐角:水平调侧边栏宽度(绝对值,与单线一致)+ 垂直调底部面板高度(增量) + let sidebar_left = if st.layout.activity_bar_visible { + st.layout.activity_bar_width + } else { + 0.0 + }; + st.layout + .set_sidebar_width_or_collapse(mouse_x - sidebar_left); + let delta_y = mouse_y - bottom_region.y; + st.layout.resize_bottom_panel(-delta_y); + drop(st); + invalidate_window(hwnd); + // 拖拽中 WM_PAINT 被消息洪流饿死,节流 UpdateWindow 立即重绘 + if panel_drag_should_sync_paint() { + let _ = windows::Win32::Graphics::Gdi::UpdateWindow(hwnd); + } + return Some(LRESULT(0)); + } else if st.layout.corner_right_resizing { + // 右下拐角:水平调右面板宽度(增量)+ 垂直调底部面板高度(增量) + let delta_x = mouse_x - editor_region.right(); + st.layout.resize_right_panel(-delta_x); + let delta_y = mouse_y - bottom_region.y; + st.layout.resize_bottom_panel(-delta_y); + drop(st); + invalidate_window(hwnd); + if panel_drag_should_sync_paint() { + let _ = windows::Win32::Graphics::Gdi::UpdateWindow(hwnd); + } + return Some(LRESULT(0)); + } else if st.layout.right_panel_resizing { let delta = mouse_x - editor_region.right(); st.layout.resize_right_panel(-delta); drop(st); invalidate_window(hwnd); + if panel_drag_should_sync_paint() { + let _ = windows::Win32::Graphics::Gdi::UpdateWindow(hwnd); + } return Some(LRESULT(0)); } else if st.layout.sidebar_resizing { // 期望宽度 = 鼠标相对侧边栏左缘;不用 region.right() 做增量, @@ -860,12 +981,18 @@ unsafe fn omm_resize_drag( .set_sidebar_width_or_collapse(mouse_x - sidebar_left); drop(st); invalidate_window(hwnd); + if panel_drag_should_sync_paint() { + let _ = windows::Win32::Graphics::Gdi::UpdateWindow(hwnd); + } return Some(LRESULT(0)); } else if st.layout.bottom_panel_resizing { let delta = mouse_y - bottom_region.y; st.layout.resize_bottom_panel(-delta); drop(st); invalidate_window(hwnd); + if panel_drag_should_sync_paint() { + let _ = windows::Win32::Graphics::Gdi::UpdateWindow(hwnd); + } return Some(LRESULT(0)); } } @@ -1045,6 +1172,12 @@ pub(crate) unsafe fn compute_cursor_for_pos(_hwnd: HWND, x: i32, y: i32) -> Curs } // 5. 面板拖拽中:固定 resize 光标(无论当前位置) + if layout.corner_left_resizing { + return CursorType::SizeNWSE; + } + if layout.corner_right_resizing { + return CursorType::SizeNESW; + } if layout.right_panel_resizing { return CursorType::SizeWE; } @@ -1061,6 +1194,18 @@ pub(crate) unsafe fn compute_cursor_for_pos(_hwnd: HWND, x: i32, y: i32) -> Curs return CursorType::Hand; } + // 6b. 拐角手柄 hover(优先于单线分隔条)→ 斜向光标 + if let Some(r) = layout.corner_left_handle() { + if r.contains(mouse_x, mouse_y) { + return CursorType::SizeNWSE; + } + } + if let Some(r) = layout.corner_right_handle() { + if r.contains(mouse_x, mouse_y) { + return CursorType::SizeNESW; + } + } + // 7. 侧边栏分隔条(sidebar 右边缘 4px 容差) if layout.sidebar_visible { let sidebar_right = layout.sidebar_region().right(); diff --git a/crates/aether-win32/src/window/window_messages.rs b/crates/aether-win32/src/window/window_messages.rs index eec0101..46ca494 100644 --- a/crates/aether-win32/src/window/window_messages.rs +++ b/crates/aether-win32/src/window/window_messages.rs @@ -607,12 +607,23 @@ pub(crate) unsafe fn on_dropfiles( if let Ok(path_str) = String::from_utf16(&path_buf[..path_len as usize]) { let path = PathBuf::from(path_str); if path.is_dir() { - EDITOR_STATE.with(|s| { - if let Some(state) = s.borrow().as_ref() { - state.borrow_mut().open_folder(path); - invalidate_window(hwnd); - } - }); + // 信任检查在 borrow_mut 之前(模态框泵消息,避免 RefCell 重入 panic) + if crate::editor::files::check_workspace_trust(hwnd, &path) { + EDITOR_STATE.with(|s| { + if let Some(state) = s.borrow().as_ref() { + state.borrow_mut().open_folder(path); + invalidate_window(hwnd); + } + }); + } else { + EDITOR_STATE.with(|s| { + if let Some(state) = s.borrow().as_ref() { + state.borrow_mut().status_message = + "已取消打开不受信任的工作区".to_string(); + invalidate_window(hwnd); + } + }); + } break; } else { EDITOR_STATE.with(|s| { diff --git a/crates/aether-win32/src/window/window_setup.rs b/crates/aether-win32/src/window/window_setup.rs index b31df98..383a6ab 100644 --- a/crates/aether-win32/src/window/window_setup.rs +++ b/crates/aether-win32/src/window/window_setup.rs @@ -225,11 +225,20 @@ pub(crate) fn apply_launch_args(state: &mut EditorState, args: &LaunchArgs) { for path in &args.paths { if path.is_dir() { - state.open_folder(path.clone()); + // 信任检查在 open_folder 之前(不持有 RefCell 借用,避免模态框重入 panic) + if crate::editor::files::check_workspace_trust(state.hwnd, path) { + state.open_folder(path.clone()); + } else { + state.status_message = "已取消打开不受信任的工作区".to_string(); + } } else if path.is_file() { // 文件:先打开所在文件夹作为工作区,再加载文件到标签页 if let Some(parent) = path.parent() { - state.open_folder(parent.to_path_buf()); + if crate::editor::files::check_workspace_trust(state.hwnd, parent) { + state.open_folder(parent.to_path_buf()); + } else { + state.status_message = "已取消打开不受信任的工作区".to_string(); + } } state.load_file(path.clone()); if args.goto.is_some() && loaded_file_for_goto.is_none() { diff --git a/tests/repro/find_test.ps1 b/tests/repro/find_test.ps1 new file mode 100644 index 0000000..5747738 --- /dev/null +++ b/tests/repro/find_test.ps1 @@ -0,0 +1,4 @@ +Add-Type 'using System; using System.Runtime.InteropServices; public class FW { [DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern IntPtr FindWindowW(string c, string t); }' +$h = [FW]::FindWindowW('AetherEditor', $null) +Write-Host "FindWindowW => $h" +Get-Process aether-app -ErrorAction SilentlyContinue | Select-Object Id, MainWindowHandle, MainWindowTitle diff --git a/tests/repro/gen_test_images.ps1 b/tests/repro/gen_test_images.ps1 new file mode 100644 index 0000000..afe8754 --- /dev/null +++ b/tests/repro/gen_test_images.ps1 @@ -0,0 +1,47 @@ +# 生成多格式测试图片(JPG/BMP/GIF),PNG 用项目 assets +Add-Type -AssemblyName System.Drawing +$dir = 'C:\Users\songd\AppData\Local\Temp\aether_img_preview' +New-Item -ItemType Directory -Force -Path $dir | Out-Null + +function New-TestBitmap([int]$w, [int]$h, [string]$label) { + $bmp = New-Object System.Drawing.Bitmap $w, $h + $g = [System.Drawing.Graphics]::FromImage($bmp) + # 渐变背景 + for ($i = 0; $i -lt $h; $i++) { + $c = [System.Drawing.Color]::FromArgb(255, [int](120 + 100 * $i / $h), [int](60 + 80 * $i / $h), 200) + $pen = New-Object System.Drawing.Pen $c + $g.DrawLine($pen, 0, $i, $w, $i) + $pen.Dispose() + } + # 圆形 + $brush = New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::FromArgb(255, 255, 180, 0)) + $g.FillEllipse($brush, [int]($w*0.2), [int]($h*0.2), [int]($w*0.4), [int]($h*0.4)) + $brush.Dispose() + # 文字 + $font = New-Object System.Drawing.Font('Arial', [int]($h/8)) + $tb = [System.Drawing.Brushes]::White + $g.DrawString($label, $font, $tb, 20, [int]($h*0.7)) + $font.Dispose() + $g.Dispose() + return $bmp +} + +# JPG +$b1 = New-TestBitmap 640 480 'JPEG Test' +$b1.Save((Join-Path $dir 'test.jpg'), [System.Drawing.Imaging.ImageFormat]::Jpeg) +$b1.Dispose() +# BMP +$b2 = New-TestBitmap 500 400 'BMP Test' +$b2.Save((Join-Path $dir 'test.bmp'), [System.Drawing.Imaging.ImageFormat]::Bmp) +$b2.Dispose() +# GIF +$b3 = New-TestBitmap 400 300 'GIF Test' +$b3.Save((Join-Path $dir 'test.gif'), [System.Drawing.Imaging.ImageFormat]::Gif) +$b3.Dispose() +# PNG(也生成一张,尺寸不同于 assets) +$b4 = New-TestBitmap 800 600 'PNG Test' +$b4.Save((Join-Path $dir 'test.png'), [System.Drawing.Imaging.ImageFormat]::Png) +$b4.Dispose() + +Get-ChildItem $dir | Select-Object Name, Length | Format-Table -AutoSize +Write-Host "DIR=$dir" diff --git a/tests/repro/probe_window.ps1 b/tests/repro/probe_window.ps1 new file mode 100644 index 0000000..ffae77e --- /dev/null +++ b/tests/repro/probe_window.ps1 @@ -0,0 +1,49 @@ +Add-Type @" +using System; +using System.Runtime.InteropServices; +using System.Text; +public class W2 { + [DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern int GetClassName(IntPtr h, StringBuilder s, int n); + [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h, out RECT r); + [StructLayout(LayoutKind.Sequential)] public struct RECT { public int L, T, R, B; } +} +"@ +$p = Get-Process aether-app -ErrorAction SilentlyContinue | Select-Object -First 1 +if (-not $p) { Write-Host "进程不存在"; exit 1 } +$h = $p.MainWindowHandle +Write-Host "PID=$($p.Id) HWND=$h Title='$($p.MainWindowTitle)'" +if ($h -ne [IntPtr]::Zero) { + $sb = New-Object System.Text.StringBuilder 256 + [W2]::GetClassName($h, $sb) | Out-Null + Write-Host "Class=$($sb.ToString())" + $r = New-Object W2+RECT + [W2]::GetWindowRect($h, [ref]$r) | Out-Null + Write-Host "Rect=($($r.L),$($r.T))-($($r.R),$($r.B))" +} +# 列出所有 aether-app 的顶级窗口(枚举) +Add-Type @" +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Text; +public class EnumW { + delegate bool EnumProc(IntPtr h, IntPtr l); + [DllImport("user32.dll")] static extern bool EnumWindows(EnumProc e, IntPtr l); + [DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid); + [DllImport("user32.dll", CharSet=CharSet.Unicode)] static extern int GetWindowText(IntPtr h, StringBuilder s, int n); + [DllImport("user32.dll", CharSet=CharSet.Unicode)] static extern int GetClassName(IntPtr h, StringBuilder s, int n); + [DllImport("user32.dll")] static extern bool IsWindowVisible(IntPtr h); + public static void List(uint pid) { + EnumWindows((h, l) => { + uint p2; GetWindowThreadProcessId(h, out p2); + if (p2 == pid) { + var t = new StringBuilder(256); GetWindowText(h, t, 256); + var c = new StringBuilder(256); GetClassName(h, c, 256); + Console.WriteLine("hwnd={0} visible={1} class={2} title={3}", h, IsWindowVisible(h), c, t); + } + return true; + }, IntPtr.Zero); + } +} +"@ +[EnumW]::List([uint32]$p.Id) diff --git a/tests/repro/repro_click_cursor.ps1 b/tests/repro/repro_click_cursor.ps1 new file mode 100644 index 0000000..7e2aecd --- /dev/null +++ b/tests/repro/repro_click_cursor.ps1 @@ -0,0 +1,133 @@ +# 复现:文件编辑区点击两次才能移动光标 +# 策略:通过 --aether-launch-args JSON 直接让实例打开测试文件(路径用正斜杠, +# 避免 Windows 反斜杠在 JSON/命令行双层转义中的陷阱) +$ErrorActionPreference = 'Stop' + +Add-Type @" +using System; +using System.Runtime.InteropServices; +public class Win { + [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h, out RECT r); + [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h); + [DllImport("user32.dll")] public static extern bool SetCursorPos(int x, int y); + [DllImport("user32.dll")] public static extern void mouse_event(uint f, uint dx, uint dy, uint d, IntPtr i); + [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int n); + [DllImport("user32.dll")] public static extern bool SetWindowPos(IntPtr h, IntPtr a, int x, int y, int cx, int cy, uint f); + [StructLayout(LayoutKind.Sequential)] public struct RECT { public int L, T, R, B; } + public const uint DOWN = 0x0002, UP = 0x0004; + public const uint SWP_NOZORDER = 0x0004, SWP_NOACTIVATE = 0x0010; + public static void Click(int x, int y) { + SetCursorPos(x, y); + System.Threading.Thread.Sleep(200); + mouse_event(DOWN, 0, 0, 0, IntPtr.Zero); + System.Threading.Thread.Sleep(100); + mouse_event(UP, 0, 0, 0, IntPtr.Zero); + } +} +"@ +Add-Type -AssemblyName System.Drawing + +function Capture($hwnd, $path) { + $r = New-Object Win+RECT + [Win]::GetWindowRect($hwnd, [ref]$r) | Out-Null + $w = $r.R - $r.L; $h = $r.B - $r.T + $bmp = New-Object System.Drawing.Bitmap $w, $h + $g = [System.Drawing.Graphics]::FromImage($bmp) + $g.CopyFromScreen($r.L, $r.T, 0, 0, (New-Object System.Drawing.Size $w, $h)) + $bmp.Save($path, [System.Drawing.Imaging.ImageFormat]::Png) + $g.Dispose(); $bmp.Dispose() +} + +$ws = "$env:TEMP\aether_click_repro" +New-Item -ItemType Directory -Force -Path $ws | Out-Null +1..30 | ForEach-Object { "line $_ : hello world aether editor test" } | Set-Content "$ws\test.txt" -Encoding UTF8 + +$exe = "d:\Application\牧羊人编辑器\target\x86_64-pc-windows-msvc\debug\aether-app.exe" +# 清理旧实例(单实例应用会把参数转发给旧进程,导致跑的不是新构建) +Get-Process aether-app -ErrorAction SilentlyContinue | Stop-Process -Force +Start-Sleep -Seconds 1 + +# 预信任临时工作区,避免 open_folder 弹"工作区信任"模态框阻塞复现 +# (模态框在持有 borrow_mut 期间泵消息,会引发 RefCell 重入 panic 风暴) +$trustFile = "$env:APPDATA\Aether\trusted_folders.txt" +$wsLowerFwd = ($ws -replace '\\', '/').ToLower() +$wsLowerBack = $ws.ToLower() +$existing = if (Test-Path $trustFile) { Get-Content $trustFile } else { @() } +$need = @($wsLowerFwd, $wsLowerBack) | Where-Object { $_ -notin $existing } +if ($need) { Add-Content $trustFile ($need -join "`n") } +Write-Host "信任列表已包含: $wsLowerFwd" + +# 构造启动参数 JSON:路径用正斜杠(Windows API 接受,且避免 JSON 反斜杠转义) +$fileFwd = ("$ws\test.txt") -replace '\\', '/' +$json = '{"paths":["' + $fileFwd + '"],"new_window":false,"goto":null,"wait":false}' +# Start-Process 不会自动给含引号的参数加外层引号,需手动转义: +# 内层 " 变成 \",整体再包一层 ",否则 CommandLineToArgvW 会把 JSON 引号当分隔符 +$argStr = '--aether-launch-args "' + ($json -replace '"', '\"') + '"' +Write-Host "启动参数: $argStr" + +# 记录启动前日志大小,便于后面只看新产生的日志 +$logFile = Get-ChildItem "$env:TEMP\Aether\logs\aether.*" -ErrorAction SilentlyContinue | + Sort-Object LastWriteTime -Descending | Select-Object -First 1 +$logOffset = 0 +if ($logFile) { $logOffset = $logFile.Length } + +# 直接带启动参数启动(第一个实例,无转发问题) +Start-Process -FilePath $exe -ArgumentList $argStr | Out-Null + +# 轮询等待窗口句柄出现(debug 构建启动较慢) +$hwnd = [IntPtr]::Zero +for ($i = 0; $i -lt 60; $i++) { + Start-Sleep -Milliseconds 500 + $p = Get-Process aether-app -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($p -and $p.MainWindowHandle -ne [IntPtr]::Zero) { $hwnd = $p.MainWindowHandle; break } +} +if ($hwnd -eq [IntPtr]::Zero) { throw "未找到 aether-app 主窗口" } +[Win]::ShowWindow($hwnd, 9) | Out-Null # SW_RESTORE +Start-Sleep -Seconds 2 +[Win]::SetForegroundWindow($hwnd) | Out-Null +Start-Sleep -Seconds 3 + +$r = New-Object Win+RECT +[Win]::GetWindowRect($hwnd, [ref]$r) | Out-Null +Write-Host "窗口区域: L=$($r.L) T=$($r.T) R=$($r.R) B=$($r.B)" + +$shotDir = "$ws\shots" +New-Item -ItemType Directory -Force -Path $shotDir | Out-Null +Remove-Item "$shotDir\*.png" -Force -ErrorAction SilentlyContinue + +# 基线截图(文件应已打开) +[Win]::SetForegroundWindow($hwnd) | Out-Null +Start-Sleep -Milliseconds 500 +Capture $hwnd "$shotDir\00_baseline.png" + +# 第一次点击:编辑区中部偏上(应落在文件前几行) +$x1 = $r.L + 500; $y1 = $r.T + 180 +Write-Host "第一次点击: ($x1, $y1)" +[Win]::Click($x1, $y1) +Start-Sleep -Milliseconds 900 +Capture $hwnd "$shotDir\01_after_click1.png" + +# 第二次点击:换位置(向右、向下) +$x2 = $r.L + 750; $y2 = $r.T + 260 +Write-Host "第二次点击: ($x2, $y2)" +[Win]::Click($x2, $y2) +Start-Sleep -Milliseconds 900 +Capture $hwnd "$shotDir\02_after_click2.png" + +# 第三次点击:再换位置 +$x3 = $r.L + 420; $y3 = $r.T + 320 +Write-Host "第三次点击: ($x3, $y3)" +[Win]::Click($x3, $y3) +Start-Sleep -Milliseconds 900 +Capture $hwnd "$shotDir\03_after_click3.png" + +Write-Host "`n===== 本次运行新增日志中的 LBD 诊断 =====" +$logFile = Get-ChildItem "$env:TEMP\Aether\logs\aether.*" | Sort-Object LastWriteTime -Descending | Select-Object -First 1 +$fs = [System.IO.File]::Open($logFile.FullName, 'Open', 'Read', 'ReadWrite') +$fs.Seek($logOffset, 'Begin') | Out-Null +$reader = New-Object System.IO.StreamReader($fs, [System.Text.Encoding]::UTF8) +$newLog = $reader.ReadToEnd() +$reader.Dispose(); $fs.Dispose() +$newLog -split "`n" | Where-Object { $_ -match 'LBD|load_file|打开' } | Select-Object -Last 30 + +Write-Host "`n截图目录: $shotDir" diff --git a/tests/repro/verify_corner_handle.ps1 b/tests/repro/verify_corner_handle.ps1 new file mode 100644 index 0000000..d45a475 --- /dev/null +++ b/tests/repro/verify_corner_handle.ps1 @@ -0,0 +1,85 @@ +# Corner Handle 实测:Ctrl+J 开底部面板 → 拖拽左下拐角 → 对比前后截图 +Add-Type -AssemblyName System.Drawing +Add-Type @" +using System; +using System.Runtime.InteropServices; +public class U32E { + [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h, out RECT r); + [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h); + [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int cmd); + [DllImport("user32.dll")] public static extern bool SetCursorPos(int x, int y); + [DllImport("user32.dll")] public static extern void mouse_event(uint f, uint x, uint y, int d, uint e); + [DllImport("user32.dll")] public static extern void keybd_event(byte vk, byte s, uint f, uint e); + [DllImport("user32.dll")] public static extern short GetAsyncKeyState(int vk); + public struct RECT { public int Left, Top, Right, Bottom; } + public const uint LDOWN = 0x0002, LUP = 0x0004; + public const uint KEYUP = 0x0002; + public const byte VK_CONTROL = 0x11, VK_J = 0x4A; +} +"@ +function Shot([string]$path, $r) { + $w = $r.Right - $r.Left; $h = $r.Bottom - $r.Top + $bmp = New-Object System.Drawing.Bitmap $w, $h + $g = [System.Drawing.Graphics]::FromImage($bmp) + $g.CopyFromScreen($r.Left, $r.Top, 0, 0, (New-Object System.Drawing.Size $w, $h)) + $bmp.Save($path, [System.Drawing.Imaging.ImageFormat]::Png) + $g.Dispose(); $bmp.Dispose() +} + +$dir = 'C:\Users\songd\AppData\Local\Temp\aether_img_preview' +$file = Join-Path $dir 'scroll_test.txt' +Get-Process aether-app -ErrorAction SilentlyContinue | Stop-Process -Force +Start-Sleep -Seconds 1 +$trustFile = Join-Path $env:APPDATA 'Aether\trusted_folders.txt' +$existing = @(); if (Test-Path $trustFile) { $existing = Get-Content $trustFile -ErrorAction SilentlyContinue } +$keys = @( ($dir -replace '/', '\').ToLower(), ($dir -replace '\\', '/').ToLower() ) +$need = $keys | Where-Object { $_ -notin $existing }; if ($need) { Add-Content $trustFile ($need -join "`n") } + +$exe = 'd:\Application\牧羊人编辑器\target\x86_64-pc-windows-msvc\debug\aether-app.exe' +$json = '{"paths":["' + ($file -replace '\\', '/') + '"],"new_window":false,"goto":null,"wait":false}' +$argStr = '--aether-launch-args "' + ($json -replace '"', '\"') + '"' +Start-Process -FilePath $exe -ArgumentList $argStr | Out-Null +$proc = $null +for ($i = 0; $i -lt 40; $i++) { Start-Sleep -Milliseconds 500; $proc = Get-Process aether-app -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowHandle -ne 0 } | Select-Object -First 1; if ($proc) { break } } +if (-not $proc) { Write-Host "ERROR: 窗口未出现"; exit 1 } +Start-Sleep -Seconds 4 +$hwnd = $proc.MainWindowHandle +[U32E]::ShowWindow($hwnd, 9) | Out-Null; Start-Sleep -Milliseconds 300 +[U32E]::SetForegroundWindow($hwnd) | Out-Null; Start-Sleep -Milliseconds 800 + +# Ctrl+J 打开底部面板 +[U32E]::keybd_event([U32E]::VK_CONTROL, 0, 0, 0) +[U32E]::keybd_event([U32E]::VK_J, 0, 0, 0) +Start-Sleep -Milliseconds 200 +[U32E]::keybd_event([U32E]::VK_J, 0, [U32E]::KEYUP, 0) +[U32E]::keybd_event([U32E]::VK_CONTROL, 0, [U32E]::KEYUP, 0) +Start-Sleep -Seconds 2 + +$r = New-Object U32E+RECT +[U32E]::GetWindowRect($hwnd, [ref]$r) | Out-Null +Shot (Join-Path $dir 'corner_before.png') $r +Write-Host "拖拽前截图: corner_before.png 窗口: L=$($r.Left) T=$($r.Top) R=$($r.Right) B=$($r.Bottom)" + +# 左下拐角屏幕坐标:从 before 截图实测(1280x800 客户区) +# 侧边栏右缘 ≈ 截图 x=625,底部面板顶缘 ≈ 截图 y=655 +# 屏幕坐标 = 窗口左上 + 客户区坐标(自绘窗口客户区≈窗口矩形) +$cornerX = $r.Left + 625 +$cornerY = $r.Top + 655 +Write-Host "拐角屏幕坐标: ($cornerX, $cornerY)" + +# 拖拽:按下 → 右下移动 60px → 释放 +[U32E]::SetCursorPos($cornerX, $cornerY) | Out-Null +Start-Sleep -Milliseconds 400 +[U32E]::mouse_event([U32E]::LDOWN, 0, 0, 0, 0) +Start-Sleep -Milliseconds 200 +for ($i = 1; $i -le 6; $i++) { + [U32E]::SetCursorPos($cornerX + $i * 10, $cornerY + $i * 10) | Out-Null + Start-Sleep -Milliseconds 60 +} +Start-Sleep -Milliseconds 200 +[U32E]::mouse_event([U32E]::LUP, 0, 0, 0, 0) +Start-Sleep -Seconds 1 + +[U32E]::GetWindowRect($hwnd, [ref]$r) | Out-Null +Shot (Join-Path $dir 'corner_after.png') $r +Write-Host "拖拽后截图: corner_after.png" diff --git a/tests/repro/verify_fake_terminal.ps1 b/tests/repro/verify_fake_terminal.ps1 new file mode 100644 index 0000000..cfe994f --- /dev/null +++ b/tests/repro/verify_fake_terminal.ps1 @@ -0,0 +1,72 @@ +# 假终端输入暂存验证:Ctrl+J 后立即输入字符 → 截假终端(本地回显)→ 等真终端(暂存映射执行) +Add-Type -AssemblyName System.Drawing +Add-Type @" +using System; +using System.Runtime.InteropServices; +public class U32G { + [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h, out RECT r); + [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h); + [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int cmd); + [DllImport("user32.dll")] public static extern void keybd_event(byte vk, byte s, uint f, uint e); + public struct RECT { public int Left, Top, Right, Bottom; } + public const uint KEYUP = 0x0002; + public const byte VK_CONTROL = 0x11, VK_J = 0x4A, VK_RETURN = 0x0D; +} +"@ +function Shot([string]$path, $r) { + $w = $r.Right - $r.Left; $h = $r.Bottom - $r.Top + $bmp = New-Object System.Drawing.Bitmap $w, $h + $g = [System.Drawing.Graphics]::FromImage($bmp) + $g.CopyFromScreen($r.Left, $r.Top, 0, 0, (New-Object System.Drawing.Size $w, $h)) + $bmp.Save($path, [System.Drawing.Imaging.ImageFormat]::Png) + $g.Dispose(); $bmp.Dispose() +} +function Type-Str([string]$s) { + foreach ($ch in $s.ToCharArray()) { + $vk = [byte][char]::ToUpperInvariant($ch) + [U32G]::keybd_event($vk, 0, 0, 0); Start-Sleep -Milliseconds 30 + [U32G]::keybd_event($vk, 0, [U32G]::KEYUP, 0); Start-Sleep -Milliseconds 30 + } +} +$dir = 'C:\Users\songd\AppData\Local\Temp\aether_img_preview' +Get-Process aether-app -ErrorAction SilentlyContinue | Stop-Process -Force +Start-Sleep -Seconds 1 +$trustFile = Join-Path $env:APPDATA 'Aether\trusted_folders.txt' +$existing = @(); if (Test-Path $trustFile) { $existing = Get-Content $trustFile -ErrorAction SilentlyContinue } +$keys = @( ($dir -replace '/', '\').ToLower(), ($dir -replace '\\', '/').ToLower() ) +$need = $keys | Where-Object { $_ -notin $existing }; if ($need) { Add-Content $trustFile ($need -join "`n") } +$exe = 'd:\Application\牧羊人编辑器\target\x86_64-pc-windows-msvc\debug\aether-app.exe' +$json = '{"paths":["' + ($dir -replace '\\', '/') + '"],"new_window":false,"goto":null,"wait":false}' +$argStr = '--aether-launch-args "' + ($json -replace '"', '\"') + '"' +Start-Process -FilePath $exe -ArgumentList $argStr | Out-Null +$proc = $null +for ($i = 0; $i -lt 40; $i++) { Start-Sleep -Milliseconds 500; $proc = Get-Process aether-app -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowHandle -ne 0 } | Select-Object -First 1; if ($proc) { break } } +if (-not $proc) { Write-Host "ERROR: 窗口未出现"; exit 1 } +Start-Sleep -Seconds 3 +$hwnd = $proc.MainWindowHandle +[U32G]::ShowWindow($hwnd, 9) | Out-Null; Start-Sleep -Milliseconds 300 +[U32G]::SetForegroundWindow($hwnd) | Out-Null; Start-Sleep -Milliseconds 500 + +# Ctrl+J 打开终端 +[U32G]::keybd_event([U32G]::VK_CONTROL, 0, 0, 0); [U32G]::keybd_event([U32G]::VK_J, 0, 0, 0) +Start-Sleep -Milliseconds 150 +[U32G]::keybd_event([U32G]::VK_J, 0, [U32G]::KEYUP, 0); [U32G]::keybd_event([U32G]::VK_CONTROL, 0, [U32G]::KEYUP, 0) + +# 立即(假终端阶段)输入 "ls" + 回车 +Start-Sleep -Milliseconds 300 +Type-Str "ls" +[U32G]::keybd_event([U32G]::VK_RETURN, 0, 0, 0); Start-Sleep -Milliseconds 50 +[U32G]::keybd_event([U32G]::VK_RETURN, 0, [U32G]::KEYUP, 0) + +# 截假终端(应显示 PS...> ls 本地回显) +Start-Sleep -Milliseconds 400 +$r = New-Object U32G+RECT +[U32G]::GetWindowRect($hwnd, [ref]$r) | Out-Null +Shot (Join-Path $dir 'term_fake_input.png') $r +Write-Host "假终端输入截图: term_fake_input.png" + +# 等真终端就绪(暂存的 ls 应被映射执行,显示目录列表) +Start-Sleep -Seconds 4 +[U32G]::GetWindowRect($hwnd, [ref]$r) | Out-Null +Shot (Join-Path $dir 'term_real_exec.png') $r +Write-Host "真终端执行截图: term_real_exec.png" diff --git a/tests/repro/verify_image_preview.ps1 b/tests/repro/verify_image_preview.ps1 new file mode 100644 index 0000000..b10cced --- /dev/null +++ b/tests/repro/verify_image_preview.ps1 @@ -0,0 +1,68 @@ +# 图片预览验证:启动应用打开指定图片,激活窗口后截图 +param( + [string]$ImagePath = 'C:/Users/songd/AppData/Local/Temp/aether_img_preview/test.png', + [string]$OutShot = 'C:\Users\songd\AppData\Local\Temp\aether_img_preview\shot.png', + [int]$WaitSeconds = 4 +) +Add-Type -AssemblyName System.Drawing +Add-Type @" +using System; +using System.Runtime.InteropServices; +public class U32B { + [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h, out RECT r); + [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h); + [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int cmd); + [DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr h); + [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow(); + public struct RECT { public int Left, Top, Right, Bottom; } +} +"@ + +Get-Process aether-app -ErrorAction SilentlyContinue | Stop-Process -Force +Start-Sleep -Seconds 1 + +$imgDir = Split-Path $ImagePath -Parent +$trustFile = Join-Path $env:APPDATA 'Aether\trusted_folders.txt' +$trustDir = Split-Path $trustFile -Parent +if (-not (Test-Path $trustDir)) { New-Item -ItemType Directory -Force -Path $trustDir | Out-Null } +$existing = @() +if (Test-Path $trustFile) { $existing = Get-Content $trustFile -ErrorAction SilentlyContinue } +$keys = @( ($imgDir -replace '/', '\').ToLower(), ($imgDir -replace '\\', '/').ToLower() ) +$need = $keys | Where-Object { $_ -notin $existing } +if ($need) { Add-Content $trustFile ($need -join "`n") } + +$exe = 'd:\Application\牧羊人编辑器\target\x86_64-pc-windows-msvc\debug\aether-app.exe' +$json = '{"paths":["' + ($ImagePath -replace '\\', '/') + '"],"new_window":false,"goto":null,"wait":false}' +$argStr = '--aether-launch-args "' + ($json -replace '"', '\"') + '"' +Start-Process -FilePath $exe -ArgumentList $argStr | Out-Null + +$proc = $null +for ($i = 0; $i -lt 40; $i++) { + Start-Sleep -Milliseconds 500 + $proc = Get-Process aether-app -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowHandle -ne 0 } | Select-Object -First 1 + if ($proc) { break } +} +if (-not $proc) { Write-Host "ERROR: 窗口未出现"; exit 1 } + +Start-Sleep -Seconds $WaitSeconds +$hwnd = $proc.MainWindowHandle +# 恢复并置前 +[U32B]::ShowWindow($hwnd, 9) | Out-Null # SW_RESTORE +Start-Sleep -Milliseconds 300 +[U32B]::SetForegroundWindow($hwnd) | Out-Null +Start-Sleep -Milliseconds 800 + +$fg = [U32B]::GetForegroundWindow() +$vis = [U32B]::IsWindowVisible($hwnd) +Write-Host "Aether hwnd=$hwnd visible=$vis 前台=$($fg -eq $hwnd)" + +$r = New-Object U32B+RECT +[U32B]::GetWindowRect($hwnd, [ref]$r) | Out-Null +$w = $r.Right - $r.Left; $h = $r.Bottom - $r.Top +if ($w -le 0 -or $h -le 0) { Write-Host "ERROR: 窗口尺寸异常 $w x $h"; exit 1 } +$bmp = New-Object System.Drawing.Bitmap $w, $h +$g = [System.Drawing.Graphics]::FromImage($bmp) +$g.CopyFromScreen($r.Left, $r.Top, 0, 0, (New-Object System.Drawing.Size $w, $h)) +$bmp.Save($OutShot, [System.Drawing.Imaging.ImageFormat]::Png) +$g.Dispose(); $bmp.Dispose() +Write-Host "截图已保存: $OutShot ($w x $h)" diff --git a/tests/repro/verify_tab_overlap.ps1 b/tests/repro/verify_tab_overlap.ps1 new file mode 100644 index 0000000..a19b7a5 --- /dev/null +++ b/tests/repro/verify_tab_overlap.ps1 @@ -0,0 +1,73 @@ +# 打开文件后模拟鼠标滚轮小幅滚动(触发 scroll_y 非整数倍行高),截图验证首行不越界 +Add-Type -AssemblyName System.Drawing +Add-Type @" +using System; +using System.Runtime.InteropServices; +public class U32D { + [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h, out RECT r); + [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h); + [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int cmd); + [DllImport("user32.dll")] public static extern bool SetCursorPos(int x, int y); + [DllImport("user32.dll")] public static extern void mouse_event(uint flags, uint dx, uint dy, int data, uint extra); + public struct RECT { public int Left, Top, Right, Bottom; } + public const uint WHEEL = 0x0800; +} +"@ + +$dir = 'C:\Users\songd\AppData\Local\Temp\aether_img_preview' +$file = Join-Path $dir 'scroll_test.txt' +# 文件已存在则跳过重建 +if (-not (Test-Path $file)) { + $lines = 1..50 | ForEach-Object { "line $_ : this.canvas = document.getElementById('gameCanvas');" } + Set-Content -Path $file -Value ($lines -join "`n") -Encoding UTF8 +} + +Get-Process aether-app -ErrorAction SilentlyContinue | Stop-Process -Force +Start-Sleep -Seconds 1 +$trustFile = Join-Path $env:APPDATA 'Aether\trusted_folders.txt' +$existing = @() +if (Test-Path $trustFile) { $existing = Get-Content $trustFile -ErrorAction SilentlyContinue } +$keys = @( ($dir -replace '/', '\').ToLower(), ($dir -replace '\\', '/').ToLower() ) +$need = $keys | Where-Object { $_ -notin $existing } +if ($need) { Add-Content $trustFile ($need -join "`n") } + +$exe = 'd:\Application\牧羊人编辑器\target\x86_64-pc-windows-msvc\debug\aether-app.exe' +$json = '{"paths":["' + ($file -replace '\\', '/') + '"],"new_window":false,"goto":null,"wait":false}' +$argStr = '--aether-launch-args "' + ($json -replace '"', '\"') + '"' +Start-Process -FilePath $exe -ArgumentList $argStr | Out-Null + +$proc = $null +for ($i = 0; $i -lt 40; $i++) { + Start-Sleep -Milliseconds 500 + $proc = Get-Process aether-app -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowHandle -ne 0 } | Select-Object -First 1 + if ($proc) { break } +} +if (-not $proc) { Write-Host "ERROR: 窗口未出现"; exit 1 } +Start-Sleep -Seconds 4 +$hwnd = $proc.MainWindowHandle +[U32D]::ShowWindow($hwnd, 9) | Out-Null +Start-Sleep -Milliseconds 300 +[U32D]::SetForegroundWindow($hwnd) | Out-Null +Start-Sleep -Milliseconds 800 + +$r = New-Object U32D+RECT +[U32D]::GetWindowRect($hwnd, [ref]$r) | Out-Null +# 编辑区中心(屏幕坐标):窗口左 + 编辑区偏移。编辑区约从窗口 x+640, y+170 起 +$cx = $r.Left + 900 +$cy = $r.Top + 400 +[U32D]::SetCursorPos($cx, $cy) | Out-Null +Start-Sleep -Milliseconds 300 +# 向下滚动 2 格(每格 120,编辑器通常按行滚动,可能产生非整数倍偏移) +[U32D]::mouse_event([U32D]::WHEEL, 0, 0, -120, 0) +Start-Sleep -Milliseconds 400 +[U32D]::mouse_event([U32D]::WHEEL, 0, 0, -120, 0) +Start-Sleep -Milliseconds 800 + +$w = $r.Right - $r.Left; $h = $r.Bottom - $r.Top +$bmp = New-Object System.Drawing.Bitmap $w, $h +$g = [System.Drawing.Graphics]::FromImage($bmp) +$g.CopyFromScreen($r.Left, $r.Top, 0, 0, (New-Object System.Drawing.Size $w, $h)) +$out = Join-Path $dir 'shot_scrolled.png' +$bmp.Save($out, [System.Drawing.Imaging.ImageFormat]::Png) +$g.Dispose(); $bmp.Dispose() +Write-Host "截图已保存: $out" diff --git a/tests/repro/verify_terminal_manual.ps1 b/tests/repro/verify_terminal_manual.ps1 new file mode 100644 index 0000000..86b9da4 --- /dev/null +++ b/tests/repro/verify_terminal_manual.ps1 @@ -0,0 +1,78 @@ +# 验证:替换后真终端是否正常工作(手动敲 ls 看输出) +Add-Type -AssemblyName System.Drawing +Add-Type @" +using System; +using System.Runtime.InteropServices; +public class U32H { + [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h, out RECT r); + [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h); + [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int cmd); + [DllImport("user32.dll")] public static extern bool SetCursorPos(int x, int y); + [DllImport("user32.dll")] public static extern void mouse_event(uint f, uint x, uint y, int d, uint e); + [DllImport("user32.dll")] public static extern void keybd_event(byte vk, byte s, uint f, uint e); + public struct RECT { public int Left, Top, Right, Bottom; } + public const uint KEYUP = 0x0002; + public const byte VK_CONTROL = 0x11, VK_J = 0x4A, VK_RETURN = 0x0D; +} +"@ +function Shot([string]$path, $r) { + $w = $r.Right - $r.Left; $h = $r.Bottom - $r.Top + $bmp = New-Object System.Drawing.Bitmap $w, $h + $g = [System.Drawing.Graphics]::FromImage($bmp) + $g.CopyFromScreen($r.Left, $r.Top, 0, 0, (New-Object System.Drawing.Size $w, $h)) + $bmp.Save($path, [System.Drawing.Imaging.ImageFormat]::Png) + $g.Dispose(); $bmp.Dispose() +} +function Type-Str([string]$s) { + foreach ($ch in $s.ToCharArray()) { + $vk = [byte][char]::ToUpperInvariant($ch) + [U32H]::keybd_event($vk, 0, 0, 0); Start-Sleep -Milliseconds 40 + [U32H]::keybd_event($vk, 0, [U32H]::KEYUP, 0); Start-Sleep -Milliseconds 40 + } +} +$dir = 'C:\Users\songd\AppData\Local\Temp\aether_img_preview' +Get-Process aether-app -ErrorAction SilentlyContinue | Stop-Process -Force +Start-Sleep -Seconds 1 +$trustFile = Join-Path $env:APPDATA 'Aether\trusted_folders.txt' +$existing = @(); if (Test-Path $trustFile) { $existing = Get-Content $trustFile -ErrorAction SilentlyContinue } +$keys = @( ($dir -replace '/', '\').ToLower(), ($dir -replace '\\', '/').ToLower() ) +$need = $keys | Where-Object { $_ -notin $existing }; if ($need) { Add-Content $trustFile ($need -join "`n") } +$exe = 'd:\Application\牧羊人编辑器\target\x86_64-pc-windows-msvc\debug\aether-app.exe' +$json = '{"paths":["' + ($dir -replace '\\', '/') + '"],"new_window":false,"goto":null,"wait":false}' +$argStr = '--aether-launch-args "' + ($json -replace '"', '\"') + '"' +Start-Process -FilePath $exe -ArgumentList $argStr | Out-Null +$proc = $null +for ($i = 0; $i -lt 40; $i++) { Start-Sleep -Milliseconds 500; $proc = Get-Process aether-app -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowHandle -ne 0 } | Select-Object -First 1; if ($proc) { break } } +if (-not $proc) { Write-Host "ERROR: 窗口未出现"; exit 1 } +Start-Sleep -Seconds 3 +$hwnd = $proc.MainWindowHandle +[U32H]::ShowWindow($hwnd, 9) | Out-Null; Start-Sleep -Milliseconds 300 +[U32H]::SetForegroundWindow($hwnd) | Out-Null; Start-Sleep -Milliseconds 500 + +# Ctrl+J 打开终端(不在假终端阶段输入,等真终端完全就绪) +[U32H]::keybd_event([U32H]::VK_CONTROL, 0, 0, 0); [U32H]::keybd_event([U32H]::VK_J, 0, 0, 0) +Start-Sleep -Milliseconds 150 +[U32H]::keybd_event([U32H]::VK_J, 0, [U32H]::KEYUP, 0); [U32H]::keybd_event([U32H]::VK_CONTROL, 0, [U32H]::KEYUP, 0) + +# 等真终端完全就绪(替换完成) +Start-Sleep -Seconds 4 +# 点击终端面板中心确保聚焦(focused=true) +$r0 = New-Object U32H+RECT +[U32H]::GetWindowRect($hwnd, [ref]$r0) | Out-Null +$tx = $r0.Left + 900; $ty = $r0.Top + 720 +[U32H]::SetCursorPos($tx, $ty) | Out-Null +Start-Sleep -Milliseconds 200 +[U32H]::mouse_event(0x0002, 0, 0, 0, 0) # LDOWN +Start-Sleep -Milliseconds 80 +[U32H]::mouse_event(0x0004, 0, 0, 0, 0) # LUP +Start-Sleep -Milliseconds 500 +# 手动敲 ls + 回车 +Type-Str "ls" +[U32H]::keybd_event([U32H]::VK_RETURN, 0, 0, 0); Start-Sleep -Milliseconds 60 +[U32H]::keybd_event([U32H]::VK_RETURN, 0, [U32H]::KEYUP, 0) +# 等执行 + 输出 +Start-Sleep -Seconds 2 +$r = New-Object U32H+RECT +[U32H]::GetWindowRect($hwnd, [ref]$r) | Out-Null +Shot (Join-Path $dir 'term_manual_ls.png') $r +Write-Host "替换后手动 ls 截图: term_manual_ls.png" From 3404345f2935699d89e226e60842fa9beea11b22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=8B=E7=8B=84=E9=98=B3?= <2128533242@qq.com> Date: Wed, 5 Aug 2026 21:16:47 +0800 Subject: [PATCH 4/4] =?UTF-8?q?style:=20=E8=BF=90=E8=A1=8C=20cargo=20fmt?= =?UTF-8?q?=20=E4=BF=AE=E5=A4=8D=E4=BB=A3=E7=A0=81=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/aether-render/src/gpu/benchmark.rs | 34 +- crates/aether-render/src/gpu/buffer.rs | 9 +- .../aether-render/src/gpu/compute_context.rs | 98 ++- .../aether-render/src/gpu/language_tables.rs | 636 +++++++++++++++--- crates/aether-render/src/gpu/lexer.rs | 88 ++- crates/aether-render/src/gpu/render.rs | 17 +- crates/aether-render/src/gpu/shader.rs | 23 +- crates/aether-render/src/gpu/syntax.rs | 40 +- crates/aether-render/src/gpu/viewport.rs | 12 +- crates/aether-win32/src/editor/events.rs | 59 +- crates/aether-win32/src/editor/tabs.rs | 12 +- crates/aether-win32/src/layout.rs | 4 +- crates/aether-win32/src/render/dialogs.rs | 400 +++++------ crates/aether-win32/src/tabs.rs | 3 +- .../window/keyboard_handler/key_down_ctrl.rs | 7 +- .../aether-win32/src/window/mouse_handler.rs | 6 +- .../l_button_down/content_area.rs | 12 +- .../src/window/mouse_handler/mouse_move.rs | 14 +- .../src/window/window_messages.rs | 8 +- 19 files changed, 972 insertions(+), 510 deletions(-) diff --git a/crates/aether-render/src/gpu/benchmark.rs b/crates/aether-render/src/gpu/benchmark.rs index ca18d9f..6207951 100644 --- a/crates/aether-render/src/gpu/benchmark.rs +++ b/crates/aether-render/src/gpu/benchmark.rs @@ -125,11 +125,9 @@ impl LexerBenchmark { println!("\n========== 加速比 =========="); let baseline = &self.results[0]; for result in &self.results[1..] { - let speedup = baseline.avg_duration.as_secs_f64() / result.avg_duration.as_secs_f64(); - println!( - "{} vs {}: {:.2}x", - result.name, baseline.name, speedup - ); + let speedup = + baseline.avg_duration.as_secs_f64() / result.avg_duration.as_secs_f64(); + println!("{} vs {}: {:.2}x", result.name, baseline.name, speedup); } } } @@ -239,8 +237,8 @@ pub mod test_data { #[cfg(test)] mod tests { - use super::*; use super::test_data::*; + use super::*; #[test] fn test_benchmark_rust() { @@ -248,15 +246,25 @@ mod tests { let mut bench = LexerBenchmark::new(); // 模拟 CPU lexer - bench.run("CPU Lexer", &code, |text| { - let _ = text.split_whitespace().count(); - }, 100); + bench.run( + "CPU Lexer", + &code, + |text| { + let _ = text.split_whitespace().count(); + }, + 100, + ); // 模拟 GPU lexer(更快) - bench.run("GPU Lexer", &code, |_text| { - // 模拟 GPU 处理时间 - std::thread::sleep(Duration::from_micros(10)); - }, 100); + bench.run( + "GPU Lexer", + &code, + |_text| { + // 模拟 GPU 处理时间 + std::thread::sleep(Duration::from_micros(10)); + }, + 100, + ); bench.print_report(); assert!(!bench.results.is_empty()); diff --git a/crates/aether-render/src/gpu/buffer.rs b/crates/aether-render/src/gpu/buffer.rs index 315ac2e..8ddae88 100644 --- a/crates/aether-render/src/gpu/buffer.rs +++ b/crates/aether-render/src/gpu/buffer.rs @@ -27,18 +27,21 @@ impl GpuBufferManager { /// 创建 Token 输出缓冲区 pub fn create_token_buffer(&self, max_tokens: usize) -> Result { let size = max_tokens * std::mem::size_of::(); - self.context.create_buffer(size, super::compute_context::BufferUsage::ReadWrite, None) + self.context + .create_buffer(size, super::compute_context::BufferUsage::ReadWrite, None) } /// 创建字符分类缓冲区 pub fn create_char_class_buffer(&self, text_len: usize) -> Result { let size = text_len * std::mem::size_of::(); - self.context.create_buffer(size, super::compute_context::BufferUsage::ReadWrite, None) + self.context + .create_buffer(size, super::compute_context::BufferUsage::ReadWrite, None) } /// 创建计数器缓冲区 pub fn create_counter_buffer(&self) -> Result { let size = std::mem::size_of::(); - self.context.create_buffer(size, super::compute_context::BufferUsage::ReadWrite, None) + self.context + .create_buffer(size, super::compute_context::BufferUsage::ReadWrite, None) } } diff --git a/crates/aether-render/src/gpu/compute_context.rs b/crates/aether-render/src/gpu/compute_context.rs index 5248f54..f711499 100644 --- a/crates/aether-render/src/gpu/compute_context.rs +++ b/crates/aether-render/src/gpu/compute_context.rs @@ -1,19 +1,18 @@ use windows::core::Result; +use windows::Win32::Graphics::Direct3D::D3D11_SRV_DIMENSION_BUFFER; +use windows::Win32::Graphics::Direct3D::{D3D_DRIVER_TYPE_HARDWARE, D3D_FEATURE_LEVEL_11_0}; +use windows::Win32::Graphics::Direct3D11::D3D11CreateDevice; +use windows::Win32::Graphics::Direct3D11::D3D11_CREATE_DEVICE_BGRA_SUPPORT; use windows::Win32::Graphics::Direct3D11::{ - ID3D11Buffer, ID3D11ComputeShader, ID3D11Device, ID3D11DeviceContext, - ID3D11ShaderResourceView, ID3D11UnorderedAccessView, - D3D11_BIND_CONSTANT_BUFFER, D3D11_BIND_SHADER_RESOURCE, + ID3D11Buffer, ID3D11ComputeShader, ID3D11Device, ID3D11DeviceContext, ID3D11ShaderResourceView, + ID3D11UnorderedAccessView, D3D11_BIND_CONSTANT_BUFFER, D3D11_BIND_SHADER_RESOURCE, D3D11_BIND_UNORDERED_ACCESS, D3D11_BUFFER_DESC, D3D11_BUFFER_SRV, D3D11_BUFFER_UAV, D3D11_CPU_ACCESS_READ, D3D11_CPU_ACCESS_WRITE, D3D11_RESOURCE_MISC_BUFFER_STRUCTURED, - D3D11_SHADER_RESOURCE_VIEW_DESC, D3D11_SHADER_RESOURCE_VIEW_DESC_0, - D3D11_SUBRESOURCE_DATA, D3D11_UNORDERED_ACCESS_VIEW_DESC, - D3D11_UNORDERED_ACCESS_VIEW_DESC_0, D3D11_USAGE_DEFAULT, D3D11_USAGE_STAGING, + D3D11_SHADER_RESOURCE_VIEW_DESC, D3D11_SHADER_RESOURCE_VIEW_DESC_0, D3D11_SUBRESOURCE_DATA, + D3D11_UNORDERED_ACCESS_VIEW_DESC, D3D11_UNORDERED_ACCESS_VIEW_DESC_0, D3D11_USAGE_DEFAULT, + D3D11_USAGE_STAGING, }; -use windows::Win32::Graphics::Direct3D::D3D11_SRV_DIMENSION_BUFFER; use windows::Win32::Graphics::Dxgi::Common::DXGI_FORMAT_R32_UINT; -use windows::Win32::Graphics::Direct3D11::D3D11CreateDevice; -use windows::Win32::Graphics::Direct3D::{D3D_DRIVER_TYPE_HARDWARE, D3D_FEATURE_LEVEL_11_0}; -use windows::Win32::Graphics::Direct3D11::D3D11_CREATE_DEVICE_BGRA_SUPPORT; /// GPU 计算上下文,封装 D3D11 Compute Shader 所需的所有资源 /// @@ -49,7 +48,9 @@ impl GpuComputeContext { /// /// 通过 D3D11CreateDevice 创建独立的 D3D11 设备用于 Compute Shader。 /// 与 D2D 渲染设备分离,避免互相影响。 - pub fn create_from_d2d(_d2d_factory: &super::super::d2d::factory::D2DFactory) -> Result { + pub fn create_from_d2d( + _d2d_factory: &super::super::d2d::factory::D2DFactory, + ) -> Result { unsafe { let mut device = None; let mut context = None; @@ -66,14 +67,18 @@ impl GpuComputeContext { Some(&mut context), ); hr?; - let device = device.ok_or_else(|| windows::core::Error::new( - windows::Win32::Foundation::E_FAIL, - "D3D11CreateDevice returned no device", - ))?; - let context = context.ok_or_else(|| windows::core::Error::new( - windows::Win32::Foundation::E_FAIL, - "D3D11CreateDevice returned no context", - ))?; + let device = device.ok_or_else(|| { + windows::core::Error::new( + windows::Win32::Foundation::E_FAIL, + "D3D11CreateDevice returned no device", + ) + })?; + let context = context.ok_or_else(|| { + windows::core::Error::new( + windows::Win32::Foundation::E_FAIL, + "D3D11CreateDevice returned no context", + ) + })?; Ok(GpuComputeContext { device, context }) } } @@ -159,12 +164,10 @@ impl GpuComputeContext { StructureByteStride: element_size as u32, }; - let subresource = initial_data.map(|data| { - D3D11_SUBRESOURCE_DATA { - pSysMem: data.as_ptr() as *const _, - SysMemPitch: 0, - SysMemSlicePitch: 0, - } + let subresource = initial_data.map(|data| D3D11_SUBRESOURCE_DATA { + pSysMem: data.as_ptr() as *const _, + SysMemPitch: 0, + SysMemSlicePitch: 0, }); let buffer = unsafe { @@ -191,7 +194,8 @@ impl GpuComputeContext { }; let mut uav = None; unsafe { - self.device.CreateUnorderedAccessView(&buffer, Some(&uav_desc), Some(&mut uav))?; + self.device + .CreateUnorderedAccessView(&buffer, Some(&uav_desc), Some(&mut uav))?; } uav } else { @@ -219,7 +223,8 @@ impl GpuComputeContext { }; unsafe { let mut srv = None; - self.device.CreateShaderResourceView(buffer, Some(&srv_desc), Some(&mut srv))?; + self.device + .CreateShaderResourceView(buffer, Some(&srv_desc), Some(&mut srv))?; Ok(srv.unwrap()) } } @@ -229,13 +234,10 @@ impl GpuComputeContext { /// # Arguments /// * `shader` - Compute Shader /// * `thread_groups` - (X, Y, Z) 线程组数量 - pub fn dispatch( - &self, - _shader: &ID3D11ComputeShader, - thread_groups: (u32, u32, u32), - ) { + pub fn dispatch(&self, _shader: &ID3D11ComputeShader, thread_groups: (u32, u32, u32)) { unsafe { - self.context.Dispatch(thread_groups.0, thread_groups.1, thread_groups.2); + self.context + .Dispatch(thread_groups.0, thread_groups.1, thread_groups.2); } } @@ -291,7 +293,8 @@ impl GpuComputeContext { unsafe { self.context.CopyResource(&staging, src); - let mut mapped = windows::Win32::Graphics::Direct3D11::D3D11_MAPPED_SUBRESOURCE::default(); + let mut mapped = + windows::Win32::Graphics::Direct3D11::D3D11_MAPPED_SUBRESOURCE::default(); self.context.Map( &staging, 0, @@ -300,11 +303,7 @@ impl GpuComputeContext { Some(&mut mapped), )?; - std::ptr::copy_nonoverlapping( - mapped.pData as *const u8, - dest.as_mut_ptr(), - dest.len(), - ); + std::ptr::copy_nonoverlapping(mapped.pData as *const u8, dest.as_mut_ptr(), dest.len()); self.context.Unmap(&staging, 0); } @@ -330,7 +329,8 @@ impl GpuComputeContext { }; unsafe { - let mut mapped = windows::Win32::Graphics::Direct3D11::D3D11_MAPPED_SUBRESOURCE::default(); + let mut mapped = + windows::Win32::Graphics::Direct3D11::D3D11_MAPPED_SUBRESOURCE::default(); self.context.Map( &staging, 0, @@ -339,11 +339,7 @@ impl GpuComputeContext { Some(&mut mapped), )?; - std::ptr::copy_nonoverlapping( - data.as_ptr(), - mapped.pData as *mut u8, - data.len(), - ); + std::ptr::copy_nonoverlapping(data.as_ptr(), mapped.pData as *mut u8, data.len()); self.context.Unmap(&staging, 0); self.context.CopyResource(buffer, &staging); @@ -359,16 +355,8 @@ impl GpuComputeContext { data: Option<&[u8]>, ) -> Result<(D3D11_BUFFER_DESC, Option)> { let (usage_type, bind_flags, cpu_access) = match usage { - BufferUsage::Constant => ( - D3D11_USAGE_DEFAULT, - D3D11_BIND_CONSTANT_BUFFER.0, - 0, - ), - BufferUsage::Structured => ( - D3D11_USAGE_DEFAULT, - D3D11_BIND_SHADER_RESOURCE.0, - 0, - ), + BufferUsage::Constant => (D3D11_USAGE_DEFAULT, D3D11_BIND_CONSTANT_BUFFER.0, 0), + BufferUsage::Structured => (D3D11_USAGE_DEFAULT, D3D11_BIND_SHADER_RESOURCE.0, 0), BufferUsage::ReadWrite => ( D3D11_USAGE_DEFAULT, D3D11_BIND_UNORDERED_ACCESS.0 | D3D11_BIND_SHADER_RESOURCE.0, diff --git a/crates/aether-render/src/gpu/language_tables.rs b/crates/aether-render/src/gpu/language_tables.rs index 7e9d70e..343a157 100644 --- a/crates/aether-render/src/gpu/language_tables.rs +++ b/crates/aether-render/src/gpu/language_tables.rs @@ -37,17 +37,14 @@ impl LanguageLexerTables { // === Rust === fn rust_tables() -> (DfaTable, KeywordTable) { let keywords = vec![ - "as", "async", "await", "break", "const", "continue", "crate", "dyn", - "else", "enum", "extern", "false", "fn", "for", "if", "impl", "in", - "let", "loop", "match", "mod", "move", "mut", "pub", "ref", "return", - "self", "Self", "static", "struct", "super", "trait", "true", "type", - "unsafe", "use", "where", "while", "yield", + "as", "async", "await", "break", "const", "continue", "crate", "dyn", "else", "enum", + "extern", "false", "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", + "move", "mut", "pub", "ref", "return", "self", "Self", "static", "struct", "super", + "trait", "true", "type", "unsafe", "use", "where", "while", "yield", // 常用类型 - "i8", "i16", "i32", "i64", "i128", "isize", - "u8", "u16", "u32", "u64", "u128", "usize", - "f32", "f64", "bool", "char", "str", "String", - "Vec", "Option", "Result", "Box", "Rc", "Arc", - // 宏 + "i8", "i16", "i32", "i64", "i128", "isize", "u8", "u16", "u32", "u64", "u128", "usize", + "f32", "f64", "bool", "char", "str", "String", "Vec", "Option", "Result", "Box", "Rc", + "Arc", // 宏 "println!", "format!", "vec!", "assert!", "panic!", ]; @@ -60,26 +57,116 @@ impl LanguageLexerTables { // === C/C++ === fn c_family_tables() -> (DfaTable, KeywordTable) { let keywords = vec![ - "auto", "break", "case", "char", "const", "continue", "default", "do", - "double", "else", "enum", "extern", "float", "for", "goto", "if", - "inline", "int", "long", "register", "restrict", "return", "short", - "signed", "sizeof", "static", "struct", "switch", "typedef", "union", - "unsigned", "void", "volatile", "while", + "auto", + "break", + "case", + "char", + "const", + "continue", + "default", + "do", + "double", + "else", + "enum", + "extern", + "float", + "for", + "goto", + "if", + "inline", + "int", + "long", + "register", + "restrict", + "return", + "short", + "signed", + "sizeof", + "static", + "struct", + "switch", + "typedef", + "union", + "unsigned", + "void", + "volatile", + "while", // C++ 关键字 - "alignas", "alignof", "and", "and_eq", "asm", "bitand", "bitor", - "bool", "catch", "class", "compl", "concept", "consteval", "constexpr", - "constinit", "co_await", "co_return", "co_yield", "decltype", "delete", - "dynamic_cast", "explicit", "export", "false", "friend", "mutable", - "namespace", "new", "noexcept", "not", "not_eq", "nullptr", "operator", - "or", "or_eq", "private", "protected", "public", "requires", - "reinterpret_cast", "static_assert", "static_cast", "template", "this", - "thread_local", "throw", "true", "try", "typename", "using", "virtual", - "wchar_t", "xor", "xor_eq", + "alignas", + "alignof", + "and", + "and_eq", + "asm", + "bitand", + "bitor", + "bool", + "catch", + "class", + "compl", + "concept", + "consteval", + "constexpr", + "constinit", + "co_await", + "co_return", + "co_yield", + "decltype", + "delete", + "dynamic_cast", + "explicit", + "export", + "false", + "friend", + "mutable", + "namespace", + "new", + "noexcept", + "not", + "not_eq", + "nullptr", + "operator", + "or", + "or_eq", + "private", + "protected", + "public", + "requires", + "reinterpret_cast", + "static_assert", + "static_cast", + "template", + "this", + "thread_local", + "throw", + "true", + "try", + "typename", + "using", + "virtual", + "wchar_t", + "xor", + "xor_eq", // 常用类型 - "size_t", "ssize_t", "uint8_t", "uint16_t", "uint32_t", "uint64_t", - "int8_t", "int16_t", "int32_t", "int64_t", "uintptr_t", "intptr_t", + "size_t", + "ssize_t", + "uint8_t", + "uint16_t", + "uint32_t", + "uint64_t", + "int8_t", + "int16_t", + "int32_t", + "int64_t", + "uintptr_t", + "intptr_t", // 预处理 - "define", "ifdef", "ifndef", "endif", "include", "pragma", "undef", + "define", + "ifdef", + "ifndef", + "endif", + "include", + "pragma", + "undef", ]; let dfa = Self::build_generic_dfa(); @@ -91,21 +178,83 @@ impl LanguageLexerTables { // === JavaScript / TypeScript === fn js_tables() -> (DfaTable, KeywordTable) { let keywords = vec![ - "break", "case", "catch", "class", "const", "continue", "debugger", - "default", "delete", "do", "else", "export", "extends", "false", - "finally", "for", "function", "if", "import", "in", "instanceof", - "new", "null", "return", "super", "switch", "this", "throw", "true", - "try", "typeof", "var", "void", "while", "with", "yield", + "break", + "case", + "catch", + "class", + "const", + "continue", + "debugger", + "default", + "delete", + "do", + "else", + "export", + "extends", + "false", + "finally", + "for", + "function", + "if", + "import", + "in", + "instanceof", + "new", + "null", + "return", + "super", + "switch", + "this", + "throw", + "true", + "try", + "typeof", + "var", + "void", + "while", + "with", + "yield", // ES6+ - "let", "static", "await", "async", "of", + "let", + "static", + "await", + "async", + "of", // 常用全局 - "undefined", "NaN", "Infinity", "console", "window", "document", - "require", "module", "exports", "global", "process", + "undefined", + "NaN", + "Infinity", + "console", + "window", + "document", + "require", + "module", + "exports", + "global", + "process", // TypeScript - "interface", "type", "namespace", "declare", "abstract", "readonly", - "any", "number", "string", "boolean", "symbol", "object", "never", - "unknown", "enum", "implements", "private", "protected", "public", - "constructor", "get", "set", + "interface", + "type", + "namespace", + "declare", + "abstract", + "readonly", + "any", + "number", + "string", + "boolean", + "symbol", + "object", + "never", + "unknown", + "enum", + "implements", + "private", + "protected", + "public", + "constructor", + "get", + "set", ]; let dfa = Self::build_generic_dfa(); @@ -117,16 +266,61 @@ impl LanguageLexerTables { // === Python === fn python_tables() -> (DfaTable, KeywordTable) { let keywords = vec![ - "and", "as", "assert", "async", "await", "break", "class", "continue", - "def", "del", "elif", "else", "except", "False", "finally", "for", - "from", "global", "if", "import", "in", "is", "lambda", "None", - "nonlocal", "not", "or", "pass", "raise", "return", "True", "try", - "while", "with", "yield", + "and", + "as", + "assert", + "async", + "await", + "break", + "class", + "continue", + "def", + "del", + "elif", + "else", + "except", + "False", + "finally", + "for", + "from", + "global", + "if", + "import", + "in", + "is", + "lambda", + "None", + "nonlocal", + "not", + "or", + "pass", + "raise", + "return", + "True", + "try", + "while", + "with", + "yield", // 常用内置 - "print", "len", "range", "list", "dict", "set", "tuple", "str", - "int", "float", "bool", "type", "isinstance", "hasattr", "getattr", + "print", + "len", + "range", + "list", + "dict", + "set", + "tuple", + "str", + "int", + "float", + "bool", + "type", + "isinstance", + "hasattr", + "getattr", // 常用模块 - "self", "cls", "super", + "self", + "cls", + "super", ]; let dfa = Self::build_generic_dfa(); @@ -138,17 +332,68 @@ impl LanguageLexerTables { // === Go === fn go_tables() -> (DfaTable, KeywordTable) { let keywords = vec![ - "break", "case", "chan", "const", "continue", "default", "defer", - "else", "fallthrough", "for", "func", "go", "goto", "if", "import", - "interface", "map", "package", "range", "return", "select", "struct", - "switch", "type", "var", + "break", + "case", + "chan", + "const", + "continue", + "default", + "defer", + "else", + "fallthrough", + "for", + "func", + "go", + "goto", + "if", + "import", + "interface", + "map", + "package", + "range", + "return", + "select", + "struct", + "switch", + "type", + "var", // 常用类型 - "bool", "byte", "complex64", "complex128", "error", "float32", "float64", - "int", "int8", "int16", "int32", "int64", "rune", "string", - "uint", "uint8", "uint16", "uint32", "uint64", "uintptr", + "bool", + "byte", + "complex64", + "complex128", + "error", + "float32", + "float64", + "int", + "int8", + "int16", + "int32", + "int64", + "rune", + "string", + "uint", + "uint8", + "uint16", + "uint32", + "uint64", + "uintptr", // 内置函数 - "append", "cap", "close", "complex", "copy", "delete", "imag", "len", - "make", "new", "panic", "print", "println", "real", "recover", + "append", + "cap", + "close", + "complex", + "copy", + "delete", + "imag", + "len", + "make", + "new", + "panic", + "print", + "println", + "real", + "recover", ]; let dfa = Self::build_generic_dfa(); @@ -160,17 +405,71 @@ impl LanguageLexerTables { // === Java === fn java_tables() -> (DfaTable, KeywordTable) { let keywords = vec![ - "abstract", "assert", "boolean", "break", "byte", "case", "catch", - "char", "class", "const", "continue", "default", "do", "double", - "else", "enum", "extends", "final", "finally", "float", "for", - "goto", "if", "implements", "import", "instanceof", "int", - "interface", "long", "native", "new", "package", "private", - "protected", "public", "return", "short", "static", "strictfp", - "super", "switch", "synchronized", "this", "throw", "throws", - "transient", "try", "void", "volatile", "while", + "abstract", + "assert", + "boolean", + "break", + "byte", + "case", + "catch", + "char", + "class", + "const", + "continue", + "default", + "do", + "double", + "else", + "enum", + "extends", + "final", + "finally", + "float", + "for", + "goto", + "if", + "implements", + "import", + "instanceof", + "int", + "interface", + "long", + "native", + "new", + "package", + "private", + "protected", + "public", + "return", + "short", + "static", + "strictfp", + "super", + "switch", + "synchronized", + "this", + "throw", + "throws", + "transient", + "try", + "void", + "volatile", + "while", // 常用类型 - "String", "Object", "Integer", "Double", "Boolean", "List", "Map", - "Set", "ArrayList", "HashMap", "HashSet", "System", "out", "println", + "String", + "Object", + "Integer", + "Double", + "Boolean", + "List", + "Map", + "Set", + "ArrayList", + "HashMap", + "HashSet", + "System", + "out", + "println", ]; let dfa = Self::build_generic_dfa(); @@ -212,21 +511,121 @@ impl LanguageLexerTables { // === HTML === fn html_tables() -> (DfaTable, KeywordTable) { let keywords = vec![ - "!DOCTYPE", "a", "abbr", "address", "area", "article", "aside", "audio", - "b", "base", "bdi", "bdo", "blockquote", "body", "br", "button", - "canvas", "caption", "cite", "code", "col", "colgroup", "data", - "datalist", "dd", "del", "details", "dfn", "dialog", "div", "dl", - "dt", "em", "embed", "fieldset", "figcaption", "figure", "footer", - "form", "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", - "hgroup", "hr", "html", "i", "iframe", "img", "input", "ins", - "kbd", "label", "legend", "li", "link", "main", "map", "mark", - "math", "menu", "meta", "meter", "nav", "noscript", "object", "ol", - "optgroup", "option", "output", "p", "picture", "pre", "progress", - "q", "rp", "rt", "ruby", "s", "samp", "script", "search", "section", - "select", "slot", "small", "source", "span", "strong", "style", - "sub", "summary", "sup", "svg", "table", "tbody", "td", "template", - "textarea", "tfoot", "th", "thead", "time", "title", "tr", "track", - "u", "ul", "var", "video", "wbr", + "!DOCTYPE", + "a", + "abbr", + "address", + "area", + "article", + "aside", + "audio", + "b", + "base", + "bdi", + "bdo", + "blockquote", + "body", + "br", + "button", + "canvas", + "caption", + "cite", + "code", + "col", + "colgroup", + "data", + "datalist", + "dd", + "del", + "details", + "dfn", + "dialog", + "div", + "dl", + "dt", + "em", + "embed", + "fieldset", + "figcaption", + "figure", + "footer", + "form", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "head", + "header", + "hgroup", + "hr", + "html", + "i", + "iframe", + "img", + "input", + "ins", + "kbd", + "label", + "legend", + "li", + "link", + "main", + "map", + "mark", + "math", + "menu", + "meta", + "meter", + "nav", + "noscript", + "object", + "ol", + "optgroup", + "option", + "output", + "p", + "picture", + "pre", + "progress", + "q", + "rp", + "rt", + "ruby", + "s", + "samp", + "script", + "search", + "section", + "select", + "slot", + "small", + "source", + "span", + "strong", + "style", + "sub", + "summary", + "sup", + "svg", + "table", + "tbody", + "td", + "template", + "textarea", + "tfoot", + "th", + "thead", + "time", + "title", + "tr", + "track", + "u", + "ul", + "var", + "video", + "wbr", ]; let dfa = Self::build_generic_dfa(); @@ -238,18 +637,66 @@ impl LanguageLexerTables { // === CSS === fn css_tables() -> (DfaTable, KeywordTable) { let keywords = vec![ - "align-content", "align-items", "align-self", "all", "animation", - "background", "border", "bottom", "box-shadow", "color", "display", - "flex", "flex-direction", "font", "font-family", "font-size", - "font-weight", "grid", "height", "justify-content", "left", - "margin", "max-height", "max-width", "min-height", "min-width", - "opacity", "overflow", "padding", "position", "right", "top", - "transform", "transition", "visibility", "width", "z-index", - "@media", "@import", "@keyframes", "@font-face", + "align-content", + "align-items", + "align-self", + "all", + "animation", + "background", + "border", + "bottom", + "box-shadow", + "color", + "display", + "flex", + "flex-direction", + "font", + "font-family", + "font-size", + "font-weight", + "grid", + "height", + "justify-content", + "left", + "margin", + "max-height", + "max-width", + "min-height", + "min-width", + "opacity", + "overflow", + "padding", + "position", + "right", + "top", + "transform", + "transition", + "visibility", + "width", + "z-index", + "@media", + "@import", + "@keyframes", + "@font-face", // 常用值 - "absolute", "auto", "block", "center", "column", "fixed", "flex", - "grid", "hidden", "inline", "inline-block", "none", "relative", - "row", "static", "sticky", "transparent", "unset", + "absolute", + "auto", + "block", + "center", + "column", + "fixed", + "flex", + "grid", + "hidden", + "inline", + "inline-block", + "none", + "relative", + "row", + "static", + "sticky", + "transparent", + "unset", ]; let dfa = Self::build_generic_dfa(); @@ -303,7 +750,10 @@ impl LanguageLexerTables { table[b'\n' as usize] = 7; table[b'\r' as usize] = 7; // 标点/运算符 - for &c in &[b'+', b'-', b'*', b'%', b'=', b'!', b'<', b'>', b'&', b'|', b'^', b'~', b'?', b':', b';', b',', b'.', b'(', b')', b'[', b']', b'{', b'}', b'@', b'#', b'$', b'`'] { + for &c in &[ + b'+', b'-', b'*', b'%', b'=', b'!', b'<', b'>', b'&', b'|', b'^', b'~', b'?', b':', + b';', b',', b'.', b'(', b')', b'[', b']', b'{', b'}', b'@', b'#', b'$', b'`', + ] { table[c as usize] = 8; } diff --git a/crates/aether-render/src/gpu/lexer.rs b/crates/aether-render/src/gpu/lexer.rs index 4866156..0802f5f 100644 --- a/crates/aether-render/src/gpu/lexer.rs +++ b/crates/aether-render/src/gpu/lexer.rs @@ -1,12 +1,11 @@ use windows::core::Result; use windows::Win32::Graphics::Direct3D11::{ - ID3D11Buffer, ID3D11ComputeShader, ID3D11ShaderResourceView, - ID3D11UnorderedAccessView, + ID3D11Buffer, ID3D11ComputeShader, ID3D11ShaderResourceView, ID3D11UnorderedAccessView, D3D11_BUFFER_UAV, D3D11_UNORDERED_ACCESS_VIEW_DESC, D3D11_UNORDERED_ACCESS_VIEW_DESC_0, }; use windows::Win32::Graphics::Dxgi::Common::DXGI_FORMAT_R32_UINT; -use super::compute_context::{GpuComputeContext, BufferUsage}; +use super::compute_context::{BufferUsage, GpuComputeContext}; use super::shader::ShaderCompiler; /// GPU Token 结构,与 Shader 中的结构体对齐 @@ -80,11 +79,7 @@ impl GpuLexer { /// * `context` - GPU 计算上下文 /// * `dfa_table` - DFA 状态转换表(256 * num_states 字节) /// * `keyword_hash` - 关键字完美哈希表 - pub fn new( - context: GpuComputeContext, - dfa_table: &[u8], - keyword_hash: &[u32], - ) -> Result { + pub fn new(context: GpuComputeContext, dfa_table: &[u8], keyword_hash: &[u32]) -> Result { // 创建 DFA 表缓冲区 let (dfa_buf, dfa_srv) = Self::create_dfa_buffer(&context, dfa_table)?; @@ -281,7 +276,9 @@ impl GpuLexer { // 检查并重新分配 token 计数缓冲区 if self.token_count_buffer.is_none() { let counter_size = std::mem::size_of::(); - let buf = self.context.create_buffer(counter_size, BufferUsage::ReadWrite, None)?; + let buf = self + .context + .create_buffer(counter_size, BufferUsage::ReadWrite, None)?; // 创建 UAV let uav_desc = D3D11_UNORDERED_ACCESS_VIEW_DESC { @@ -291,13 +288,18 @@ impl GpuLexer { Buffer: D3D11_BUFFER_UAV { FirstElement: 0, NumElements: 1, - Flags: windows::Win32::Graphics::Direct3D11::D3D11_BUFFER_UAV_FLAG_COUNTER.0 as u32, + Flags: windows::Win32::Graphics::Direct3D11::D3D11_BUFFER_UAV_FLAG_COUNTER.0 + as u32, }, }, }; let mut uav = None; unsafe { - self.context.device().CreateUnorderedAccessView(&buf, Some(&uav_desc), Some(&mut uav))?; + self.context.device().CreateUnorderedAccessView( + &buf, + Some(&uav_desc), + Some(&mut uav), + )?; } self.token_count_buffer = Some(buf); @@ -308,61 +310,57 @@ impl GpuLexer { } fn upload_text(&self, text: &[u8]) -> Result { - self.context.create_buffer(text.len(), BufferUsage::Structured, Some(text)) + self.context + .create_buffer(text.len(), BufferUsage::Structured, Some(text)) } fn run_char_classify(&self, text_buffer: &ID3D11Buffer, text_len: usize) -> Result<()> { let srv = self.context.create_srv(text_buffer)?; - let char_classes_uav = self.create_uav_from_buffer( - self.char_classes_buffer.as_ref().unwrap(), - text_len as u32, - )?; + let char_classes_uav = self + .create_uav_from_buffer(self.char_classes_buffer.as_ref().unwrap(), text_len as u32)?; self.context.set_compute_shader(&self.char_classify_shader); self.context.set_shader_resources(0, &[Some(srv)]); - self.context.set_unordered_access_views(0, &[Some(char_classes_uav)]); + self.context + .set_unordered_access_views(0, &[Some(char_classes_uav)]); let groups = ((text_len + 255) / 256) as u32; - self.context.dispatch(&self.char_classify_shader, (groups, 1, 1)); + self.context + .dispatch(&self.char_classify_shader, (groups, 1, 1)); Ok(()) } fn run_token_scan(&self, text_len: usize, _max_tokens: usize) -> Result<()> { - let srv = self.context.create_srv(self.char_classes_buffer.as_ref().unwrap())?; + let srv = self + .context + .create_srv(self.char_classes_buffer.as_ref().unwrap())?; self.context.set_compute_shader(&self.token_scan_shader); self.context.set_shader_resources(0, &[Some(srv)]); self.context.set_unordered_access_views( 0, - &[ - self.tokens_uav.clone(), - self.token_count_uav.clone(), - ], + &[self.tokens_uav.clone(), self.token_count_uav.clone()], ); let groups = ((text_len + 255) / 256) as u32; - self.context.dispatch(&self.token_scan_shader, (groups, 1, 1)); + self.context + .dispatch(&self.token_scan_shader, (groups, 1, 1)); Ok(()) } fn run_keyword_lookup(&self, max_tokens: usize) -> Result<()> { self.context.set_compute_shader(&self.keyword_lookup_shader); - self.context.set_shader_resources( - 0, - &[ - Some(self.keyword_srv.clone()), - ], - ); - self.context.set_unordered_access_views( - 0, - &[self.tokens_uav.clone()], - ); + self.context + .set_shader_resources(0, &[Some(self.keyword_srv.clone())]); + self.context + .set_unordered_access_views(0, &[self.tokens_uav.clone()]); let groups = ((max_tokens + 255) / 256) as u32; - self.context.dispatch(&self.keyword_lookup_shader, (groups, 1, 1)); + self.context + .dispatch(&self.keyword_lookup_shader, (groups, 1, 1)); Ok(()) } @@ -370,15 +368,13 @@ impl GpuLexer { fn readback_tokens(&self, max_tokens: usize) -> Result> { // 读取 token 数量 let mut count = 0u32; - self.context.read_buffer( - self.token_count_buffer.as_ref().unwrap(), - unsafe { + self.context + .read_buffer(self.token_count_buffer.as_ref().unwrap(), unsafe { std::slice::from_raw_parts_mut( &mut count as *mut u32 as *mut u8, std::mem::size_of::(), ) - }, - )?; + })?; let token_count = count.min(max_tokens as u32) as usize; if token_count == 0 { @@ -394,10 +390,8 @@ impl GpuLexer { ) }; - self.context.read_buffer( - self.tokens_buffer.as_ref().unwrap(), - token_bytes, - )?; + self.context + .read_buffer(self.tokens_buffer.as_ref().unwrap(), token_bytes)?; Ok(tokens) } @@ -420,7 +414,11 @@ impl GpuLexer { }; unsafe { let mut uav = None; - self.context.device().CreateUnorderedAccessView(buffer, Some(&uav_desc), Some(&mut uav))?; + self.context.device().CreateUnorderedAccessView( + buffer, + Some(&uav_desc), + Some(&mut uav), + )?; Ok(uav.unwrap()) } } diff --git a/crates/aether-render/src/gpu/render.rs b/crates/aether-render/src/gpu/render.rs index dd5a828..2cc2719 100644 --- a/crates/aether-render/src/gpu/render.rs +++ b/crates/aether-render/src/gpu/render.rs @@ -1,8 +1,8 @@ use aether_core::lexer::{LexemeSpan, TokenKind}; use windows::Win32::Graphics::Direct2D::Common::D2D1_COLOR_F; -use super::lexer::{GpuToken, token_types}; -use super::syntax::{SyntaxClass, syntax_classes}; +use super::lexer::{token_types, GpuToken}; +use super::syntax::{syntax_classes, SyntaxClass}; /// GPU Token 到 LexemeSpan 的转换 /// @@ -36,11 +36,13 @@ fn resolve_token_kind(token: &GpuToken, syntax: &SyntaxClass) -> TokenKind { // 优先使用语法分类(如果置信度足够高) if syntax.confidence >= 70 { match syntax.class_id { - syntax_classes::SYNTAX_FUNCTION_DECL | - syntax_classes::SYNTAX_FUNCTION_CALL => TokenKind::Function, + syntax_classes::SYNTAX_FUNCTION_DECL | syntax_classes::SYNTAX_FUNCTION_CALL => { + TokenKind::Function + } syntax_classes::SYNTAX_TYPE_NAME => TokenKind::TypeName, - syntax_classes::SYNTAX_VARIABLE_DECL | - syntax_classes::SYNTAX_VARIABLE_REF => TokenKind::Identifier, + syntax_classes::SYNTAX_VARIABLE_DECL | syntax_classes::SYNTAX_VARIABLE_REF => { + TokenKind::Identifier + } syntax_classes::SYNTAX_PARAMETER => TokenKind::Identifier, syntax_classes::SYNTAX_FIELD_ACCESS => TokenKind::Attribute, syntax_classes::SYNTAX_MACRO => TokenKind::Macro, @@ -190,7 +192,8 @@ impl GpuBufferPool { } // 创建新缓冲区 - let buf = context.create_buffer(size, super::compute_context::BufferUsage::ReadWrite, None)?; + let buf = + context.create_buffer(size, super::compute_context::BufferUsage::ReadWrite, None)?; self.in_use.push(buf.clone()); Ok(buf) } diff --git a/crates/aether-render/src/gpu/shader.rs b/crates/aether-render/src/gpu/shader.rs index 6d292d4..5e98340 100644 --- a/crates/aether-render/src/gpu/shader.rs +++ b/crates/aether-render/src/gpu/shader.rs @@ -1,6 +1,6 @@ use windows::core::Result; use windows::Win32::Graphics::Direct3D::Fxc::{ - D3DCompile, D3DCOMPILE_OPTIMIZATION_LEVEL3, D3DCOMPILE_ENABLE_STRICTNESS, + D3DCompile, D3DCOMPILE_ENABLE_STRICTNESS, D3DCOMPILE_OPTIMIZATION_LEVEL3, }; use windows::Win32::Graphics::Direct3D::ID3DBlob; @@ -19,11 +19,7 @@ impl ShaderCompiler { /// /// # Returns /// 编译后的字节码 - pub fn compile_compute_shader( - hlsl: &str, - entry_point: &str, - target: &str, - ) -> Result> { + pub fn compile_compute_shader(hlsl: &str, entry_point: &str, target: &str) -> Result> { if hlsl.is_empty() { return Err(windows::core::Error::new( windows::Win32::Foundation::E_FAIL, @@ -143,12 +139,11 @@ pub fn create_constant_buffer( data: &T, ) -> Result { let size = std::mem::size_of::(); - let bytes = unsafe { - std::slice::from_raw_parts( - data as *const T as *const u8, - size, - ) - }; - - context.create_buffer(size, super::compute_context::BufferUsage::Constant, Some(bytes)) + let bytes = unsafe { std::slice::from_raw_parts(data as *const T as *const u8, size) }; + + context.create_buffer( + size, + super::compute_context::BufferUsage::Constant, + Some(bytes), + ) } diff --git a/crates/aether-render/src/gpu/syntax.rs b/crates/aether-render/src/gpu/syntax.rs index 0d5f0ef..8830715 100644 --- a/crates/aether-render/src/gpu/syntax.rs +++ b/crates/aether-render/src/gpu/syntax.rs @@ -1,7 +1,6 @@ use windows::core::Result; use windows::Win32::Graphics::Direct3D11::{ - ID3D11Buffer, ID3D11ComputeShader, ID3D11ShaderResourceView, - ID3D11UnorderedAccessView, + ID3D11Buffer, ID3D11ComputeShader, ID3D11ShaderResourceView, ID3D11UnorderedAccessView, D3D11_BUFFER_UAV, D3D11_UNORDERED_ACCESS_VIEW_DESC, D3D11_UNORDERED_ACCESS_VIEW_DESC_0, }; use windows::Win32::Graphics::Dxgi::Common::DXGI_FORMAT_R32_UINT; @@ -72,10 +71,7 @@ pub struct SyntaxPattern { impl GpuSyntaxClassifier { /// 创建语法分类器 - pub fn new( - context: GpuComputeContext, - language: &str, - ) -> Result { + pub fn new(context: GpuComputeContext, language: &str) -> Result { let patterns = Self::build_patterns(language); let (patterns_buf, patterns_srv) = Self::create_patterns_buffer(&context, &patterns)?; @@ -99,11 +95,7 @@ impl GpuSyntaxClassifier { /// /// # Returns /// 语法分类结果(GPU 缓冲区) - pub fn classify( - &self, - tokens: &ID3D11Buffer, - token_count: usize, - ) -> Result { + pub fn classify(&self, tokens: &ID3D11Buffer, token_count: usize) -> Result { // 创建输出缓冲区 let output_size = token_count * std::mem::size_of::(); let output = self.context.create_buffer( @@ -117,17 +109,10 @@ impl GpuSyntaxClassifier { // 设置 Shader 资源 self.context.set_compute_shader(&self.classify_shader); - self.context.set_shader_resources( - 0, - &[ - Some(tokens_srv), - Some(self.patterns_srv.clone()), - ], - ); - self.context.set_unordered_access_views( - 0, - &[Some(output_uav)], - ); + self.context + .set_shader_resources(0, &[Some(tokens_srv), Some(self.patterns_srv.clone())]); + self.context + .set_unordered_access_views(0, &[Some(output_uav)]); // 分派 let groups = ((token_count + 255) / 256) as u32; @@ -268,10 +253,7 @@ impl GpuSyntaxClassifier { Ok((buffer, srv)) } - fn load_shader( - context: &GpuComputeContext, - bytecode: &[u8], - ) -> Result { + fn load_shader(context: &GpuComputeContext, bytecode: &[u8]) -> Result { context.create_compute_shader(bytecode) } @@ -293,7 +275,11 @@ impl GpuSyntaxClassifier { }; unsafe { let mut uav = None; - self.context.device().CreateUnorderedAccessView(buffer, Some(&uav_desc), Some(&mut uav))?; + self.context.device().CreateUnorderedAccessView( + buffer, + Some(&uav_desc), + Some(&mut uav), + )?; Ok(uav.unwrap()) } } diff --git a/crates/aether-render/src/gpu/viewport.rs b/crates/aether-render/src/gpu/viewport.rs index 04c0762..6457eae 100644 --- a/crates/aether-render/src/gpu/viewport.rs +++ b/crates/aether-render/src/gpu/viewport.rs @@ -64,7 +64,10 @@ impl ViewportHighlightCache { /// /// 重叠部分保留,新进入窗口的行标记为脏。 pub fn resize_window(&mut self, new_start: usize, new_len: usize, version: u64) { - if self.window_start == new_start && self.window_len == new_len && version == self.current_version { + if self.window_start == new_start + && self.window_len == new_len + && version == self.current_version + { return; } @@ -105,12 +108,7 @@ impl ViewportHighlightCache { /// 使用编辑距离检测增量更新 /// /// 比较新旧文本,只标记真正发生变化的行为脏。 - pub fn update_with_edit_distance( - &mut self, - lines: &[String], - version: u64, - threshold: f32, - ) { + pub fn update_with_edit_distance(&mut self, lines: &[String], version: u64, threshold: f32) { self.current_version = version; for (slot, new_text) in lines.iter().enumerate() { diff --git a/crates/aether-win32/src/editor/events.rs b/crates/aether-win32/src/editor/events.rs index d7a66c3..541dcb8 100644 --- a/crates/aether-win32/src/editor/events.rs +++ b/crates/aether-win32/src/editor/events.rs @@ -373,11 +373,9 @@ impl EditorState { total_lines, ); // 检查可见区域是否已有高亮缓存 - let has_highlight = (visible_start..visible_end.min(total_lines)) - .all(|i| { - i < self.content.cached_tokens.len() - && !self.content.cached_tokens[i].is_empty() - }); + let has_highlight = (visible_start..visible_end.min(total_lines)).all(|i| { + i < self.content.cached_tokens.len() && !self.content.cached_tokens[i].is_empty() + }); if has_highlight { return; // 缓存完整,0延迟渲染 } @@ -482,8 +480,8 @@ impl EditorState { // 检查是否需要重新运行 GPU 分析: // 1. 视口范围变化 2. buffer_version 变化(内容编辑) - let vp_changed = vp_cache.window_start() != cache_start - || vp_cache.window_len() != window_len; + let vp_changed = + vp_cache.window_start() != cache_start || vp_cache.window_len() != window_len; let content_changed = vp_cache.buffer_version() != self.content.buffer_version; let need_gpu_rebuild = vp_changed || content_changed || vp_cache.is_empty(); @@ -506,7 +504,9 @@ impl EditorState { if let Ok(tokens) = gpu_lexer.lex(text.as_bytes()) { if !tokens.is_empty() { let gpu_spans = - aether_render::gpu::render::gpu_tokens_to_lexeme_spans(&tokens, None); + aether_render::gpu::render::gpu_tokens_to_lexeme_spans( + &tokens, None, + ); gpu_highlighted = true; for line_idx in cache_start..cache_end { @@ -523,13 +523,14 @@ impl EditorState { .map(|(_, e)| e as u32) .unwrap_or(text.len() as u32); - let line_tokens: Vec = gpu_spans - .iter() - .filter(|span| { - span.start >= line_start && span.start < line_end - }) - .cloned() - .collect(); + let line_tokens: Vec = + gpu_spans + .iter() + .filter(|span| { + span.start >= line_start && span.start < line_end + }) + .cloned() + .collect(); vp_cache.set_line_tokens( line_idx, @@ -578,8 +579,12 @@ impl EditorState { .as_ref() .map(|p| p.to_string_lossy().to_string()) .unwrap_or_else(|| "untitled".to_string()); - self.bg_highlighter - .request(&doc_id, lang, self.content.buffer_version, snapshot); + self.bg_highlighter.request( + &doc_id, + lang, + self.content.buffer_version, + snapshot, + ); self.hl_request_version = self.content.buffer_version; self.content.tokens_trimmed = false; } @@ -606,7 +611,14 @@ impl EditorState { if i >= cache_start && i < cache_end { let slot = i - cache_start; if self.content.line_cache_versions[slot] != self.content.buffer_version { - self.highlight_line(i, slot, gpu_highlighted, use_sync_lexer, ts_lang, &mut lexer); + self.highlight_line( + i, + slot, + gpu_highlighted, + use_sync_lexer, + ts_lang, + &mut lexer, + ); } } } @@ -616,7 +628,14 @@ impl EditorState { if i < viewport_start || i >= viewport_end { let slot = i - cache_start; if self.content.line_cache_versions[slot] != self.content.buffer_version { - self.highlight_line(i, slot, gpu_highlighted, use_sync_lexer, ts_lang, &mut lexer); + self.highlight_line( + i, + slot, + gpu_highlighted, + use_sync_lexer, + ts_lang, + &mut lexer, + ); } } } @@ -670,4 +689,4 @@ impl EditorState { self.content.line_cache_versions[slot] = self.content.buffer_version; } } -} \ No newline at end of file +} diff --git a/crates/aether-win32/src/editor/tabs.rs b/crates/aether-win32/src/editor/tabs.rs index 379a339..e053c68 100644 --- a/crates/aether-win32/src/editor/tabs.rs +++ b/crates/aether-win32/src/editor/tabs.rs @@ -103,7 +103,7 @@ impl EditorState { self.tab_bar.active_tab = index; self.swap_tab_content(self.tab_bar.active_tab); self.is_selecting = false; - + // 无感切换优化:预先计算新标签页的可见范围并更新缓存签名, // 避免切换后第一帧因签名不匹配而强制重建缓存。 // 这样切换回之前打开的标签页时,如果滚动位置没变,缓存立即命中。 @@ -126,7 +126,9 @@ impl EditorState { ); // 确保 cached_tokens 长度与当前文件匹配 if self.content.cached_tokens.len() != total_lines { - self.content.cached_tokens.resize_with(total_lines, Vec::new); + self.content + .cached_tokens + .resize_with(total_lines, Vec::new); } // 如果行文本缓存窗口不匹配,需要重建(但保留已有缓存数据) if self.content.cache_window_start != cache_start @@ -136,14 +138,14 @@ impl EditorState { self.content.slide_cache_window(cache_start, window_len); } } - + // 异步延迟同步文件树选择,避免阻塞切换 let need_sync = self.active_tab_is_file() && self.content.file_path.is_some(); if need_sync { // 立即执行(文件树遍历通常很快),但如果项目极大可改为异步 self.sync_file_tree_selection(); } - + // 0延迟切换:标记刚切换过来的标签页 // 这样 rebuild_cache 首帧会跳过所有工作,直接渲染已有缓存 self.content.just_switched = true; @@ -154,7 +156,7 @@ impl EditorState { self.image_zoom = 1.0; self.image_offset_x = 0.0; self.image_offset_y = 0.0; - + let title = self.tab_bar.tabs[self.tab_bar.active_tab].title(); self.status_message = format!("切换到: {}", title); self.emit_event(crate::events::EditorEvent::TabChanged); diff --git a/crates/aether-win32/src/layout.rs b/crates/aether-win32/src/layout.rs index 732be0a..bcb9f2d 100644 --- a/crates/aether-win32/src/layout.rs +++ b/crates/aether-win32/src/layout.rs @@ -839,7 +839,9 @@ mod tests { // 打开底部面板:左下拐角出现(侧边栏可见),右下拐角仍无(右面板隐藏) layout.toggle_bottom_panel(); - let left = layout.corner_left_handle().expect("侧边栏+底部面板可见时应有左下拐角"); + let left = layout + .corner_left_handle() + .expect("侧边栏+底部面板可见时应有左下拐角"); assert!(layout.corner_right_handle().is_none()); let editor = layout.editor_region(); diff --git a/crates/aether-win32/src/render/dialogs.rs b/crates/aether-win32/src/render/dialogs.rs index 9c0e5ef..86aa56a 100644 --- a/crates/aether-win32/src/render/dialogs.rs +++ b/crates/aether-win32/src/render/dialogs.rs @@ -430,113 +430,117 @@ impl EditorState { height: f32, ) { unsafe { - const INFO_BAR_H: f32 = 40.0; - const MARGIN: f32 = 20.0; - - // 惰性创建位图缓存(设备相关) - if self.image_bitmap.is_none() { - if let Some(img) = &self.content.image_data { - match crate::bitmap_loader::create_bitmap_from_rgba( - target, - img.width, - img.height, - &img.rgba, - ) { - Ok(bmp) => self.image_bitmap = Some(bmp), - Err(e) => { - tracing::warn!(error = %e, "创建图片预览位图失败"); + const INFO_BAR_H: f32 = 40.0; + const MARGIN: f32 = 20.0; + + // 惰性创建位图缓存(设备相关) + if self.image_bitmap.is_none() { + if let Some(img) = &self.content.image_data { + match crate::bitmap_loader::create_bitmap_from_rgba( + target, img.width, img.height, &img.rgba, + ) { + Ok(bmp) => self.image_bitmap = Some(bmp), + Err(e) => { + tracing::warn!(error = %e, "创建图片预览位图失败"); + } } } } - } - // 顶部信息栏:文件名 + 尺寸/格式(左对齐,垂直居中) - let (img_w, img_h, fmt) = self - .content - .image_data - .as_ref() - .map(|i| (i.width, i.height, i.format_name)) - .unwrap_or((0, 0, "?")); - let file_name = self - .content - .file_path - .as_ref() - .and_then(|p| p.file_name()) - .map(|n| n.to_string_lossy().to_string()) - .unwrap_or_else(|| "图片".to_string()); - let zoom_percent = (self.image_zoom * 100.0).round() as i32; - let info_text = format!("{} | {} x {} | {} | {}%", file_name, img_w, img_h, fmt, zoom_percent); - let info_format = self - .render_ctx - .text_format_cache - .get_format( - 13.0, - DWRITE_FONT_WEIGHT_NORMAL.0 as u32, - windows::Win32::Graphics::DirectWrite::DWRITE_TEXT_ALIGNMENT_LEADING.0 as u32, - windows::Win32::Graphics::DirectWrite::DWRITE_PARAGRAPH_ALIGNMENT_CENTER.0 as u32, - ) - .unwrap(); - let info_color = color_f(0.6, 0.6, 0.6, 1.0); - let info_brush = self - .render_ctx - .brush_cache - .get_brush(target, &info_color) - .unwrap(); - let info_rect = D2D_RECT_F { - left: x + MARGIN, - top: y, - right: x + width - MARGIN, - bottom: y + INFO_BAR_H, - }; - let info_wide: Vec = info_text.encode_utf16().chain(Some(0)).collect(); - target.DrawText( - &info_wide, - &info_format, - &info_rect, - &info_brush, - D2D1_DRAW_TEXT_OPTIONS_NONE, - DWRITE_MEASURING_MODE_NATURAL, - ); - - // 图片显示区域(信息栏下方,四周边距) - let area_x = x + MARGIN; - let area_y = y + INFO_BAR_H + MARGIN; - let area_w = (width - MARGIN * 2.0).max(1.0); - let area_h = (height - INFO_BAR_H - MARGIN * 2.0).max(1.0); - - if let Some(ref bitmap) = self.image_bitmap { - // 计算基础缩放(适应窗口,保持宽高比) - let fit_scale = (area_w / img_w as f32).min(area_h / img_h as f32); - // 应用用户缩放 - let scale = fit_scale * self.image_zoom; - let draw_w = img_w as f32 * scale; - let draw_h = img_h as f32 * scale; - // 居中 + 用户偏移 - let draw_x = area_x + (area_w - draw_w) / 2.0 + self.image_offset_x; - let draw_y = area_y + (area_h - draw_h) / 2.0 + self.image_offset_y; - let dest_rect = D2D_RECT_F { - left: draw_x, - top: draw_y, - right: draw_x + draw_w, - bottom: draw_y + draw_h, - }; - // 裁剪到图片显示区域,防止溢出 - let clip_rect = D2D_RECT_F { - left: area_x, - top: area_y, - right: area_x + area_w, - bottom: area_y + area_h, + // 顶部信息栏:文件名 + 尺寸/格式(左对齐,垂直居中) + let (img_w, img_h, fmt) = self + .content + .image_data + .as_ref() + .map(|i| (i.width, i.height, i.format_name)) + .unwrap_or((0, 0, "?")); + let file_name = self + .content + .file_path + .as_ref() + .and_then(|p| p.file_name()) + .map(|n| n.to_string_lossy().to_string()) + .unwrap_or_else(|| "图片".to_string()); + let zoom_percent = (self.image_zoom * 100.0).round() as i32; + let info_text = format!( + "{} | {} x {} | {} | {}%", + file_name, img_w, img_h, fmt, zoom_percent + ); + let info_format = self + .render_ctx + .text_format_cache + .get_format( + 13.0, + DWRITE_FONT_WEIGHT_NORMAL.0 as u32, + windows::Win32::Graphics::DirectWrite::DWRITE_TEXT_ALIGNMENT_LEADING.0 as u32, + windows::Win32::Graphics::DirectWrite::DWRITE_PARAGRAPH_ALIGNMENT_CENTER.0 + as u32, + ) + .unwrap(); + let info_color = color_f(0.6, 0.6, 0.6, 1.0); + let info_brush = self + .render_ctx + .brush_cache + .get_brush(target, &info_color) + .unwrap(); + let info_rect = D2D_RECT_F { + left: x + MARGIN, + top: y, + right: x + width - MARGIN, + bottom: y + INFO_BAR_H, }; - target.PushAxisAlignedClip(&clip_rect, windows::Win32::Graphics::Direct2D::D2D1_ANTIALIAS_MODE_PER_PRIMITIVE); - target.DrawBitmap( - bitmap, - Some(&dest_rect), - 1.0, - windows::Win32::Graphics::Direct2D::D2D1_BITMAP_INTERPOLATION_MODE_LINEAR, - None, + let info_wide: Vec = info_text.encode_utf16().chain(Some(0)).collect(); + target.DrawText( + &info_wide, + &info_format, + &info_rect, + &info_brush, + D2D1_DRAW_TEXT_OPTIONS_NONE, + DWRITE_MEASURING_MODE_NATURAL, ); - target.PopAxisAlignedClip(); - } + + // 图片显示区域(信息栏下方,四周边距) + let area_x = x + MARGIN; + let area_y = y + INFO_BAR_H + MARGIN; + let area_w = (width - MARGIN * 2.0).max(1.0); + let area_h = (height - INFO_BAR_H - MARGIN * 2.0).max(1.0); + + if let Some(ref bitmap) = self.image_bitmap { + // 计算基础缩放(适应窗口,保持宽高比) + let fit_scale = (area_w / img_w as f32).min(area_h / img_h as f32); + // 应用用户缩放 + let scale = fit_scale * self.image_zoom; + let draw_w = img_w as f32 * scale; + let draw_h = img_h as f32 * scale; + // 居中 + 用户偏移 + let draw_x = area_x + (area_w - draw_w) / 2.0 + self.image_offset_x; + let draw_y = area_y + (area_h - draw_h) / 2.0 + self.image_offset_y; + let dest_rect = D2D_RECT_F { + left: draw_x, + top: draw_y, + right: draw_x + draw_w, + bottom: draw_y + draw_h, + }; + // 裁剪到图片显示区域,防止溢出 + let clip_rect = D2D_RECT_F { + left: area_x, + top: area_y, + right: area_x + area_w, + bottom: area_y + area_h, + }; + target.PushAxisAlignedClip( + &clip_rect, + windows::Win32::Graphics::Direct2D::D2D1_ANTIALIAS_MODE_PER_PRIMITIVE, + ); + target.DrawBitmap( + bitmap, + Some(&dest_rect), + 1.0, + windows::Win32::Graphics::Direct2D::D2D1_BITMAP_INTERPOLATION_MODE_LINEAR, + None, + ); + target.PopAxisAlignedClip(); + } } } @@ -550,110 +554,110 @@ impl EditorState { height: f32, ) { unsafe { - let title_format = self - .render_ctx - .text_format_cache - .get_center_format(20.0, DWRITE_FONT_WEIGHT_BOLD.0 as u32) - .unwrap(); - let info_format = self - .render_ctx - .text_format_cache - .get_center_format(14.0, DWRITE_FONT_WEIGHT_NORMAL.0 as u32) - .unwrap(); - - let title_color = color_f(0.83, 0.83, 0.83, 1.0); - let title_brush = self - .render_ctx - .brush_cache - .get_brush(target, &title_color) - .unwrap(); - let info_color = color_f(0.5, 0.5, 0.5, 1.0); - let info_brush = self - .render_ctx - .brush_cache - .get_brush(target, &info_color) - .unwrap(); - let icon_color = color_f(0.3, 0.7, 1.0, 1.0); - let icon_brush = self - .render_ctx - .brush_cache - .get_brush(target, &icon_color) - .unwrap(); - - let center_y = y + height / 2.0; - - // 图片图标 - let icon_text: Vec = "🖼️".encode_utf16().chain(Some(0)).collect(); - let icon_rect = D2D_RECT_F { - left: x, - top: center_y - 70.0, - right: x + width, - bottom: center_y - 30.0, - }; - target.DrawText( - &icon_text, - &title_format, - &icon_rect, - &icon_brush, - D2D1_DRAW_TEXT_OPTIONS_NONE, - DWRITE_MEASURING_MODE_NATURAL, - ); - - // 标题 - let title = "无法预览此图片"; - let title_wide: Vec = title.encode_utf16().chain(Some(0)).collect(); - let title_rect = D2D_RECT_F { - left: x, - top: center_y - 30.0, - right: x + width, - bottom: center_y, - }; - target.DrawText( - &title_wide, - &title_format, - &title_rect, - &title_brush, - D2D1_DRAW_TEXT_OPTIONS_NONE, - DWRITE_MEASURING_MODE_NATURAL, - ); - - // 提示(格式不支持或文件损坏) - let hint = "该格式暂不支持预览,或文件已损坏"; - let hint_wide: Vec = hint.encode_utf16().chain(Some(0)).collect(); - let hint_rect = D2D_RECT_F { - left: x, - top: center_y + 4.0, - right: x + width, - bottom: center_y + 30.0, - }; - target.DrawText( - &hint_wide, - &info_format, - &hint_rect, - &info_brush, - D2D1_DRAW_TEXT_OPTIONS_NONE, - DWRITE_MEASURING_MODE_NATURAL, - ); - - // 文件路径 - if let Some(path) = &self.content.file_path { - let path_text = format!("{}", path.display()); - let path_wide: Vec = path_text.encode_utf16().chain(Some(0)).collect(); - let path_rect = D2D_RECT_F { - left: x + 20.0, - top: center_y + 34.0, - right: x + width - 20.0, - bottom: center_y + 64.0, + let title_format = self + .render_ctx + .text_format_cache + .get_center_format(20.0, DWRITE_FONT_WEIGHT_BOLD.0 as u32) + .unwrap(); + let info_format = self + .render_ctx + .text_format_cache + .get_center_format(14.0, DWRITE_FONT_WEIGHT_NORMAL.0 as u32) + .unwrap(); + + let title_color = color_f(0.83, 0.83, 0.83, 1.0); + let title_brush = self + .render_ctx + .brush_cache + .get_brush(target, &title_color) + .unwrap(); + let info_color = color_f(0.5, 0.5, 0.5, 1.0); + let info_brush = self + .render_ctx + .brush_cache + .get_brush(target, &info_color) + .unwrap(); + let icon_color = color_f(0.3, 0.7, 1.0, 1.0); + let icon_brush = self + .render_ctx + .brush_cache + .get_brush(target, &icon_color) + .unwrap(); + + let center_y = y + height / 2.0; + + // 图片图标 + let icon_text: Vec = "🖼️".encode_utf16().chain(Some(0)).collect(); + let icon_rect = D2D_RECT_F { + left: x, + top: center_y - 70.0, + right: x + width, + bottom: center_y - 30.0, + }; + target.DrawText( + &icon_text, + &title_format, + &icon_rect, + &icon_brush, + D2D1_DRAW_TEXT_OPTIONS_NONE, + DWRITE_MEASURING_MODE_NATURAL, + ); + + // 标题 + let title = "无法预览此图片"; + let title_wide: Vec = title.encode_utf16().chain(Some(0)).collect(); + let title_rect = D2D_RECT_F { + left: x, + top: center_y - 30.0, + right: x + width, + bottom: center_y, }; target.DrawText( - &path_wide, + &title_wide, + &title_format, + &title_rect, + &title_brush, + D2D1_DRAW_TEXT_OPTIONS_NONE, + DWRITE_MEASURING_MODE_NATURAL, + ); + + // 提示(格式不支持或文件损坏) + let hint = "该格式暂不支持预览,或文件已损坏"; + let hint_wide: Vec = hint.encode_utf16().chain(Some(0)).collect(); + let hint_rect = D2D_RECT_F { + left: x, + top: center_y + 4.0, + right: x + width, + bottom: center_y + 30.0, + }; + target.DrawText( + &hint_wide, &info_format, - &path_rect, + &hint_rect, &info_brush, D2D1_DRAW_TEXT_OPTIONS_NONE, DWRITE_MEASURING_MODE_NATURAL, ); - } + + // 文件路径 + if let Some(path) = &self.content.file_path { + let path_text = format!("{}", path.display()); + let path_wide: Vec = path_text.encode_utf16().chain(Some(0)).collect(); + let path_rect = D2D_RECT_F { + left: x + 20.0, + top: center_y + 34.0, + right: x + width - 20.0, + bottom: center_y + 64.0, + }; + target.DrawText( + &path_wide, + &info_format, + &path_rect, + &info_brush, + D2D1_DRAW_TEXT_OPTIONS_NONE, + DWRITE_MEASURING_MODE_NATURAL, + ); + } } } } diff --git a/crates/aether-win32/src/tabs.rs b/crates/aether-win32/src/tabs.rs index 7613fb0..e0ccfeb 100644 --- a/crates/aether-win32/src/tabs.rs +++ b/crates/aether-win32/src/tabs.rs @@ -57,7 +57,8 @@ pub struct TabContent { // 语言类型 pub(crate) language: Language, /// GPU 视口高亮缓存(增量更新,仅缓存可见行) - pub(crate) viewport_highlight_cache: Option, + pub(crate) viewport_highlight_cache: + Option, /// 0延迟切换:标记刚切换过来的标签页,跳过首帧 rebuild_cache pub(crate) just_switched: bool, /// 图片预览:解码后的图像数据(仅 language == Image 时有值),随标签 swap 恢复 diff --git a/crates/aether-win32/src/window/keyboard_handler/key_down_ctrl.rs b/crates/aether-win32/src/window/keyboard_handler/key_down_ctrl.rs index a2089af..aa05532 100644 --- a/crates/aether-win32/src/window/keyboard_handler/key_down_ctrl.rs +++ b/crates/aether-win32/src/window/keyboard_handler/key_down_ctrl.rs @@ -277,9 +277,10 @@ unsafe fn okd_ctrl_view_shortcuts(hwnd: HWND, vk: VIRTUAL_KEY, shift: bool) { unsafe fn okd_ctrl_zoom_cmd(hwnd: HWND, vk: VIRTUAL_KEY) { // 检查是否是图片预览模式 let is_image = EDITOR_STATE.with(|s| { - s.borrow().as_ref().map(|state| { - state.borrow().content.language == aether_core::lexer::Language::Image - }).unwrap_or(false) + s.borrow() + .as_ref() + .map(|state| state.borrow().content.language == aether_core::lexer::Language::Image) + .unwrap_or(false) }); match vk { diff --git a/crates/aether-win32/src/window/mouse_handler.rs b/crates/aether-win32/src/window/mouse_handler.rs index 52065ca..6724777 100644 --- a/crates/aether-win32/src/window/mouse_handler.rs +++ b/crates/aether-win32/src/window/mouse_handler.rs @@ -215,8 +215,10 @@ pub(crate) unsafe fn on_mouse_wheel( // 图片预览:Ctrl+滚轮缩放 if state.content.language == aether_core::lexer::Language::Image && ctrl { let editor = state.layout.editor_region(); - if cursor_x >= editor.x && cursor_x < editor.x + editor.width - && cursor_y >= editor.y && cursor_y < editor.y + editor.height + if cursor_x >= editor.x + && cursor_x < editor.x + editor.width + && cursor_y >= editor.y + && cursor_y < editor.y + editor.height { // 缩放因子:每 120 单位滚轮 = 10% 缩放 let zoom_delta = delta / 120.0 * 0.1; diff --git a/crates/aether-win32/src/window/mouse_handler/l_button_down/content_area.rs b/crates/aether-win32/src/window/mouse_handler/l_button_down/content_area.rs index 6b881d4..23f81ee 100644 --- a/crates/aether-win32/src/window/mouse_handler/l_button_down/content_area.rs +++ b/crates/aether-win32/src/window/mouse_handler/l_button_down/content_area.rs @@ -852,8 +852,7 @@ unsafe fn lbd_right_panel_ai_controls( st.status_message = format!("已打开: {}", path.display()); drop(st); } else { - state.borrow_mut().status_message = - "已取消打开不受信任的工作区".to_string(); + state.borrow_mut().status_message = "已取消打开不受信任的工作区".to_string(); } } invalidate_window(hwnd); @@ -1861,8 +1860,7 @@ unsafe fn lbd_welcome_action( if crate::editor::files::check_workspace_trust(hwnd, &path) { state.borrow_mut().open_folder(path); } else { - state.borrow_mut().status_message = - "已取消打开不受信任的工作区".to_string(); + state.borrow_mut().status_message = "已取消打开不受信任的工作区".to_string(); } invalidate_window(hwnd); } @@ -1887,8 +1885,7 @@ unsafe fn lbd_welcome_action( if crate::editor::files::check_workspace_trust(hwnd, &path) { state.borrow_mut().open_folder(path); } else { - state.borrow_mut().status_message = - "已取消打开不受信任的工作区".to_string(); + state.borrow_mut().status_message = "已取消打开不受信任的工作区".to_string(); } invalidate_window(hwnd); } @@ -1898,8 +1895,7 @@ unsafe fn lbd_welcome_action( if crate::editor::files::check_workspace_trust(hwnd, &path) { state.borrow_mut().open_folder(path); } else { - state.borrow_mut().status_message = - "已取消打开不受信任的工作区".to_string(); + state.borrow_mut().status_message = "已取消打开不受信任的工作区".to_string(); } invalidate_window(hwnd); } diff --git a/crates/aether-win32/src/window/mouse_handler/mouse_move.rs b/crates/aether-win32/src/window/mouse_handler/mouse_move.rs index 31e2234..00daa09 100644 --- a/crates/aether-win32/src/window/mouse_handler/mouse_move.rs +++ b/crates/aether-win32/src/window/mouse_handler/mouse_move.rs @@ -66,9 +66,10 @@ pub(crate) unsafe fn on_mouse_move( if is_mbutton_dragging { let mut st = state.borrow_mut(); if st.mouse_press.image_dragging { - if let (Some((start_x, start_y)), Some((orig_offset_x, orig_offset_y))) = - (st.mouse_press.image_drag_start, st.mouse_press.image_drag_offset) - { + if let (Some((start_x, start_y)), Some((orig_offset_x, orig_offset_y))) = ( + st.mouse_press.image_drag_start, + st.mouse_press.image_drag_offset, + ) { let dx = mouse_x - start_x; let dy = mouse_y - start_y; st.image_offset_x = orig_offset_x + dx; @@ -95,8 +96,7 @@ pub(crate) unsafe fn on_mouse_move( || st.layout.corner_right_resizing }; if panel_dragging { - if let Some(r) = omm_resize_drag(hwnd, &state, mouse_x, mouse_y, is_dragging, &layout) - { + if let Some(r) = omm_resize_drag(hwnd, &state, mouse_x, mouse_y, is_dragging, &layout) { return r; } return LRESULT(0); @@ -104,7 +104,7 @@ pub(crate) unsafe fn on_mouse_move( let mut st = state.borrow_mut(); let editor_content = layout.editor_content_region(st.show_tab_bar()); - + // 如果尚未进入选区模式,检查鼠标是否移动了足够距离来启动选区 if !st.is_selecting { // 记录鼠标按下位置(在 WM_LBUTTONDOWN 时设置) @@ -117,7 +117,7 @@ pub(crate) unsafe fn on_mouse_move( } } } - + if st.is_selecting { let before = ( st.content.cursor_line, diff --git a/crates/aether-win32/src/window/window_messages.rs b/crates/aether-win32/src/window/window_messages.rs index 46ca494..b31caf4 100644 --- a/crates/aether-win32/src/window/window_messages.rs +++ b/crates/aether-win32/src/window/window_messages.rs @@ -322,7 +322,13 @@ unsafe fn on_timer_caret(hwnd: HWND) -> LRESULT { any_active = true; } // 编辑器内容区光标闪烁(文件编辑状态) - if st.tab_bar.tabs.get(st.tab_bar.active_tab).map(|t| t.is_file()).unwrap_or(false) { + if st + .tab_bar + .tabs + .get(st.tab_bar.active_tab) + .map(|t| t.is_file()) + .unwrap_or(false) + { st.content.caret_visible = !st.content.caret_visible; let er = st.layout.editor_region().clone(); st.dirty_tracker.mark_region(