rgpui 1.3.0

GUI UI framework
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
//! 代码编辑器状态(`editor` feature 门控,P3 分拆)。
//!
//! 编排外壳:唯一真相源是内部 `Entity<InputState>`(配置为代码编辑);
//! `EditorState` 只做编辑器级编排(大纲缓存/符号跳转/高亮接入),不复制文本逻辑。
//! 文本核复用仍在(`InputState` 经 P1a 持有 `TextCore`),渲染走 `Editor` 组件。

use std::ops::Range;

use crate::{App, AppContext as _, Context, Entity, Pixels, SharedString, Window, px};

use super::super::InputState;
use super::codelens::CodelensState;
use super::extensions::ExtensionsState;
use super::inlay_hints::InlayState;
use super::lsp_attach::LspAttach;
use super::minimap::MinimapState;
use super::snippets::SnippetSession;
use crate::highlight::{DocumentSymbol, Highlighter};

/// 代码编辑器状态(`cx.new` 持有,`Editor` 组件消费)。
pub struct EditorState {
    /// 内部输入实体(唯一真相源,代码编辑配置)。
    pub(super) input: Entity<InputState>,
    /// 大纲缓存(文本变更时刷新,`outline()` 读取)。
    pub(super) outline: Vec<DocumentSymbol>,
    /// LSP 接入状态(provider + 补全/诊断/悬停,见 `lsp_attach.rs`)。
    pub(super) lsp: LspAttach,
    /// 片段会话(进行中为 `Some`,见 `snippets.rs`)。
    pub(super) snippet: Option<SnippetSession>,
    /// inlay 接入状态(provider,见 `inlay_hints.rs`)。
    pub(super) inlay: InlayState,
    /// 缩略图开关状态(见 `minimap.rs`,O2,默认关)。
    pub(super) minimap: MinimapState,
    /// 透镜接入状态(provider + 已落位透镜,见 `codelens.rs`,O4)。
    pub(super) codelens: CodelensState,
    /// 扩展表 + 变更订阅表运行时状态(见 `extensions.rs`,O7)。
    pub(super) extensions: ExtensionsState,
    /// 粘性滚动开关(默认开,见 `sticky_scroll.rs`)。
    pub(super) sticky_scroll: bool,
    /// 面包屑位置(默认顶部顶栏,见 `sticky_scroll.rs`)。
    pub(super) sticky_position: super::sticky_scroll::StickyPosition,
    /// 当前语言(`set_language` 维护;未设置/已降级为 `None`)。
    language: Option<SharedString>,
    /// 编辑器字号覆盖(`None` 使用窗口默认;`Editor::font_size` 透传)。
    font_size: Option<Pixels>,
}

impl EditorState {
    /// 创建编辑器状态(内部输入配好多行 + 行号 + 折叠 + 引导线 + 键入体验)。
    ///
    /// 初始内容走 `set_value`:不进撤销栈(打开即 undo 不清空),光标落末尾。
    pub fn new(window: &mut Window, cx: &mut Context<Self>, initial: &str) -> Self {
        let input = cx.new(|cx| {
            let mut state = InputState::new(window, cx)
                .multi_line(true)
                .rows(2)
                .line_number(true)
                .folding(true)
                .indent_guides(true)
                .auto_close_pairs(true)
                .bracket_match(true)
                .current_line_highlight(true);
            let end = initial.len();
            state.set_value(initial, window, cx);
            state.set_selected_range(end..end, cx);
            state
        });
        let mut this = Self {
            input,
            outline: Vec::new(),
            lsp: LspAttach::new(cx.new(|_| crate::lsp::CompletionPopupState::default())),
            snippet: None,
            inlay: InlayState::new(),
            minimap: MinimapState::new(),
            codelens: CodelensState::new(),
            extensions: ExtensionsState::new(),
            sticky_scroll: true,
            sticky_position: super::sticky_scroll::StickyPosition::Top,
            language: None,
            font_size: None,
        };
        this.refresh_outline(cx);
        // 文本一改:刷新大纲 → 自动补全(开关开时)→ 透镜(有 provider 即刷新)→
        // 分发变更事件(扩展表/订阅表,O7)。
        //(`subscribe_in` 带 window,`request_*` 与钩子要 window 才能调。)
        cx.subscribe_in(&this.input, window, move |this, _, event, window, cx| {
            if !matches!(event, crate::input_ui::InputEvent::Change) {
                return;
            }
            this.refresh_outline(cx);
            this.maybe_auto_complete(window, cx);
            this.request_codelenses(window, cx);
            let edit = this.edit_event(cx);
            this.fire_edit_event(&edit, window, cx);
        })
        .detach();
        this
    }

    /// 内部输入实体(接搜索面板等需要 `Entity<InputState>` 的 API)。
    pub fn input(&self) -> &Entity<InputState> {
        &self.input
    }

    /// 全文只读视图。
    pub fn text(&self, cx: &App) -> String {
        self.input
            .read_with(cx, |state, _| state.text().to_string())
    }

    /// 程序化写入全文(透传内部输入)。
    ///
    /// 不进撤销栈(打开即 undo 不清空);多行时光标重置为起始,调用方按需再调
    /// [`set_selected_range`](Self::set_selected_range);大纲同步刷新(内部
    /// `set_value` 为性能压住了 `Change` 事件,外壳在此补刷)。
    pub fn set_value(
        &mut self,
        value: impl Into<SharedString>,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        self.input.update(cx, |state, cx| {
            state.set_value(value, window, cx);
        });
        self.refresh_outline(cx);
    }

    /// 设置选中区(外部驱动选中/跳转;透传内部输入)。
    pub fn set_selected_range(&self, range: Range<usize>, cx: &mut App) {
        let _ = self.input.update(cx, |state, cx| {
            state.set_selected_range(range, cx);
        });
    }

    /// 滚动使给定字节偏移可见,不改变光标与选区(透传内部输入)。
    ///
    /// 首次布局前调用无效果(内部尚无 layout 时直接返回)。
    pub fn reveal_offset(&self, offset: usize, cx: &mut App) {
        let _ = self.input.update(cx, |state, cx| {
            state.reveal_offset(offset, cx);
        });
    }

    /// 滚动使给定区间可见(以区间起点为准),不改变光标与选区(透传内部输入)。
    pub fn reveal_range(&self, range: Range<usize>, cx: &mut App) {
        let _ = self.input.update(cx, |state, cx| {
            state.reveal_range(range, cx);
        });
    }

    /// 设置折叠开关(透传内部输入;关闭后折叠图标与折叠区消失)。
    pub fn set_folding(&mut self, folding: bool, window: &mut Window, cx: &mut Context<Self>) {
        self.input.update(cx, |state, cx| {
            state.set_folding(folding, window, cx);
        });
    }

    /// 设置只读模式(创建后修改;透传内部输入)。
    ///
    /// 只读保持正常样式,允许移动光标/选择/复制;程序化写入不受影响。
    pub fn set_read_only(&self, read_only: bool, cx: &mut App) {
        let _ = self.input.update(cx, |state, cx| {
            state.set_read_only(read_only, cx);
        });
    }

    /// 设置标尺列(字符数;空即关,O5;透传内部输入)。
    pub fn set_rulers(&self, columns: Vec<usize>, cx: &mut App) {
        let _ = self.input.update(cx, |state, cx| {
            state.set_rulers(columns, cx);
        });
    }

    /// 设置标尺颜色(`None` 跟主题边框色;透传内部输入)。
    pub fn set_ruler_color(&self, color: Option<crate::Hsla>, cx: &mut App) {
        let _ = self.input.update(cx, |state, cx| {
            state.set_ruler_color(color, cx);
        });
    }

    /// 设置括号彩虹开关(O5;透传内部输入)。
    pub fn set_bracket_rainbow_enabled(&self, enabled: bool, cx: &mut App) {
        let _ = self.input.update(cx, |state, cx| {
            state.set_bracket_rainbow_enabled(enabled, cx);
        });
    }

    /// 设置 Vim 模式开关(O3;默认关,透传内部输入)。
    ///
    /// 打开即进 normal、坍缩选区;关闭清前缀/锚点。vim 上下文进内部输入的
    /// key_context(与 `Input` 同节点,后注册优先覆盖同键默认行为)。
    pub fn set_vim_enabled(&mut self, enabled: bool, cx: &mut Context<Self>) {
        self.input.update(cx, |state, cx| {
            state.vim.enabled = enabled;
            state.vim.mode = super::vim::VimMode::Normal;
            state.vim.pending = None;
            state.vim.anchor = None;
            if enabled {
                let cursor = state.cursor();
                state.move_to(cursor, None, cx);
            }
            cx.notify();
        });
        cx.notify();
    }

    /// Vim 是否启用。
    pub fn vim_enabled(&self, cx: &App) -> bool {
        self.input.read_with(cx, |state, _| state.vim.enabled)
    }

    /// Vim 当前模式(未启用返回 `None`,状态行指示用)。
    pub fn vim_mode(&self, cx: &App) -> Option<super::vim::VimMode> {
        self.input
            .read_with(cx, |state, _| state.vim.enabled.then_some(state.vim.mode))
    }
}

/// 布局调试几何(`chrome_geometry` 返回;窗口坐标系像素值)。
#[derive(Debug, Clone, Copy)]
pub struct ChromeGeometry {
    /// gutter 列右缘(列关为左缘)。
    pub gutter_end: Pixels,
    /// 行号区右缘(含 gutter/折叠/边距)。
    pub numbers_end: Pixels,
    /// 折叠区右缘。
    pub fold_end: Pixels,
    /// 文本首 x(行首字形理论起始)。
    pub text_start: Pixels,
}

impl EditorState {
    /// Vim 按键分发(`Editor` 的 `VimKey` 捕获调用;绑定命中即激活态,调用方吞传播)。
    pub(crate) fn vim_key(&mut self, key: &str, window: &mut Window, cx: &mut Context<Self>) {
        self.input.update(cx, |state, cx| {
            super::vim::handle_key(state, key, window, cx);
        });
    }

    /// 布局调试几何(布局调试开关用;窗口坐标系像素值)。
    ///
    /// 由上帧落盘的 `last_bounds`/`last_layout` 换算(与绘制同口径):
    /// gutter 右缘 / 行号区右缘 / 折叠区右缘 / 文本首 x。
    /// 首绘前返回 `None`。
    pub fn chrome_geometry(&self, cx: &App) -> Option<ChromeGeometry> {
        self.input.read_with(cx, |state, _| {
            let bounds = state.last_bounds?;
            let layout = state.last_layout.as_ref()?;
            let gutter = super::gutter::gutter_column_width(state);
            let folding = state.mode.is_folding();
            let fold_part = if folding {
                super::super::input::FOLD_ICON_HITBOX_WIDTH
            } else {
                px(0.)
            };
            let full = layout.line_number_width;
            let margin = super::super::input::LINE_NUMBER_RIGHT_MARGIN;
            Some(ChromeGeometry {
                gutter_end: bounds.origin.x + gutter,
                numbers_end: bounds.origin.x + full - margin - fold_part,
                fold_end: bounds.origin.x + full - margin,
                text_start: bounds.origin.x + full,
            })
        })
    }

    /// Vim Esc 模式切换(`capture_key_down` 调用;`Input` 自身 Esc 照跑,不吞)。
    pub(crate) fn vim_escape(&mut self, window: &mut Window, cx: &mut Context<Self>) {
        self.input.update(cx, |state, cx| {
            super::vim::escape_pressed(state, window, cx);
        });
    }

    /// 设置行注释符(创建后修改;透传内部输入,默认 `//`)。
    ///
    /// 按语言设置,如 Python 传 `"#"`、Lua 传 `"--"`。
    pub fn set_line_comment_prefix(&self, prefix: impl Into<SharedString>, cx: &mut App) {
        let _ = self.input.update(cx, |state, cx| {
            state.set_line_comment_prefix(prefix, cx);
        });
    }

    /// 当前光标偏移(UTF-8 字节)。
    pub fn cursor(&self, cx: &App) -> usize {
        self.input.read_with(cx, |state, _| state.cursor())
    }

    /// 设置语法高亮器(如 `highlight::rust_highlighter()`),透传内部输入。
    ///
    /// 高亮器变化不经过 `Change` 事件,外壳在此同步刷新大纲(否则大纲停留在
    /// 旧高亮器的结果上,直到下一次文本编辑)。
    pub fn set_highlighter(
        &mut self,
        highlighter: Option<Box<dyn Highlighter>>,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        self.input.update(cx, |state, cx| {
            state.set_highlighter(highlighter, window, cx);
        });
        self.refresh_outline(cx);
    }

    /// 设置语言(查注册表 + 高亮/大纲联动;未注册静默降级为纯文本)。
    ///
    /// 注册表见 `highlight::register_highlighter`;Rust 内置需 `--features
    /// tree-sitter`(默认关,wasm 不可用);1.2 不加 Rust 之外的 grammar。
    pub fn set_language(&mut self, language: &str, window: &mut Window, cx: &mut Context<Self>) {
        let highlighter = crate::highlight::highlighter_for(language);
        self.language = if highlighter.is_some() {
            Some(language.into())
        } else {
            None
        };
        self.set_highlighter(highlighter, window, cx);
    }

    /// 当前语言(未设置/已降级为 `None`,状态行展示用)。
    pub fn language(&self) -> Option<SharedString> {
        self.language.clone()
    }

    /// 编辑器字号(`None` 使用窗口默认;`set_font_size` 写入)。
    pub fn font_size(&self) -> Option<Pixels> {
        self.font_size
    }

    /// 设置编辑器字号(覆盖窗口默认,仅影响本编辑器)。
    pub fn set_font_size(&mut self, font_size: Option<Pixels>, cx: &mut Context<Self>) {
        self.font_size = font_size;
        self.input.update(cx, |state, cx| {
            state.set_font_size_override(font_size, cx);
        });
    }

    /// 大纲缓存(`refresh_outline` 维护)。
    pub fn outline(&self) -> &[DocumentSymbol] {
        &self.outline
    }

    /// 跳转到符号(光标落符号头 + 滚动可见)。
    pub fn goto_symbol(&mut self, symbol: &DocumentSymbol, cx: &mut Context<Self>) {
        let symbol = symbol.clone();
        self.input.update(cx, |state, cx| {
            state.goto_symbol(&symbol, cx);
        });
    }

    /// 按内部高亮器重算大纲(文本变更后调用)。
    fn refresh_outline(&mut self, cx: &mut Context<Self>) {
        let symbols = self
            .input
            .read_with(cx, |state, _| state.document_symbols());
        self.outline = symbols;
        cx.notify();
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::Entity;

    /// 持有编辑器状态的测试宿主视图。
    struct Probe {
        state: Entity<EditorState>,
    }

    impl crate::Render for Probe {
        fn render(
            &mut self,
            _window: &mut Window,
            _cx: &mut Context<Self>,
        ) -> impl crate::IntoElement {
            crate::div()
        }
    }

    /// 编辑器文本往返(实体创建→读回→光标落点)。
    #[rgpui::test]
    fn editor_state_text_roundtrip(cx: &mut crate::TestAppContext) {
        let (probe, cx) = cx.add_window_view(|window, cx| {
            let editor = cx.new(|cx| EditorState::new(window, cx, "fn main() {}\n"));
            Probe { state: editor }
        });
        let editor = probe.read_with(cx, |probe, _| probe.state.clone());
        let text = editor.read_with(cx, |state, cx| state.text(cx));
        assert_eq!(text, "fn main() {}\n");
        let cursor = editor.read_with(cx, |state, cx| state.cursor(cx));
        assert_eq!(cursor, "fn main() {}\n".len());
    }

    /// 未注册语言静默降级(语言记 `None`,大纲为空,不 panic)。
    #[rgpui::test]
    fn set_language_unknown_downgrades(cx: &mut crate::TestAppContext) {
        let (probe, cx) = cx.add_window_view(|window, cx| {
            let editor = cx.new(|cx| EditorState::new(window, cx, "fn main() {}\n"));
            Probe { state: editor }
        });
        let editor = probe.read_with(cx, |probe, _| probe.state.clone());
        cx.update(|window, cx| {
            editor.update(cx, |state, cx| {
                state.set_language("cobol-xyz-not-registered", window, cx);
            });
        });
        assert!(editor.read_with(cx, |state, _| state.language().is_none()));
        assert!(editor.read_with(cx, |state, _| state.outline().is_empty()));
    }

    /// Rust 语言可用(tree-sitter feature 门控)。
    #[cfg(all(not(target_family = "wasm"), feature = "tree-sitter"))]
    #[rgpui::test]
    fn set_language_rust_available(cx: &mut crate::TestAppContext) {
        let (probe, cx) = cx.add_window_view(|window, cx| {
            let editor = cx.new(|cx| EditorState::new(window, cx, "fn main() {}\n"));
            Probe { state: editor }
        });
        let editor = probe.read_with(cx, |probe, _| probe.state.clone());
        cx.update(|window, cx| {
            editor.update(cx, |state, cx| {
                state.set_language("rust", window, cx);
            });
        });
        assert_eq!(
            editor
                .read_with(cx, |state, _| state.language())
                .map(|s| s.to_string()),
            Some("rust".to_string())
        );
        // Rust 大纲非空(tree-sitter 真解析)。
        assert!(!editor.read_with(cx, |state, _| state.outline().is_empty()));
    }

    /// 无高亮器时大纲为空(不崩溃)。
    #[rgpui::test]
    fn document_symbols_empty_without_highlighter(cx: &mut crate::TestAppContext) {
        let (probe, cx) = cx.add_window_view(|window, cx| {
            let editor = cx.new(|cx| EditorState::new(window, cx, "fn main() {}\n"));
            Probe { state: editor }
        });
        let editor = probe.read_with(cx, |probe, _| probe.state.clone());
        let symbols = editor.read_with(cx, |state, _| state.outline().to_vec());
        assert!(symbols.is_empty());
    }

    /// goto_symbol 跳转到符号头(经内部输入)。
    #[rgpui::test]
    fn goto_symbol_moves_cursor(cx: &mut crate::TestAppContext) {
        let (probe, cx) = cx.add_window_view(|window, cx| {
            let editor = cx.new(|cx| EditorState::new(window, cx, "fn main() {}\n"));
            Probe { state: editor }
        });
        let editor = probe.read_with(cx, |probe, _| probe.state.clone());
        let symbol = crate::highlight::DocumentSymbol {
            kind: crate::highlight::SymbolKind::Function,
            name: "main".into(),
            range: 0..2,
            start_row: 0,
        };
        editor.update(cx, |state, cx| {
            state.goto_symbol(&symbol, cx);
        });
        let cursor = editor.read_with(cx, |state, cx| state.cursor(cx));
        assert_eq!(cursor, 0);
    }

    /// 初始内容不进撤销栈:打开即 undo 不得清空内容;
    /// 同一输入 burst(1 秒分组窗内)合并为一个撤销单元。
    #[rgpui::test]
    fn initial_content_not_undoable(cx: &mut crate::TestAppContext) {
        const INITIAL: &str = "fn main() {}\n";
        let (probe, cx) = cx.add_window_view(|window, cx| {
            let editor = cx.new(|cx| EditorState::new(window, cx, INITIAL));
            Probe { state: editor }
        });
        let editor = probe.read_with(cx, |probe, _| probe.state.clone());
        let input = editor.read_with(cx, |state, _| state.input().clone());
        // 快速连输两个字符(同一分组窗)。
        cx.update(|window, cx| {
            input.update(cx, |state, cx| {
                crate::EntityInputHandler::replace_text_in_range(state, None, "a", window, cx);
                crate::EntityInputHandler::replace_text_in_range(state, None, "b", window, cx);
            });
        });
        assert_eq!(
            input.read_with(cx, |state, _| state.value().to_string()),
            "fn main() {}\nab".to_string()
        );
        // 一次 undo 回退整个 burst,初始内容保留。
        cx.update(|window, cx| {
            input.update(cx, |state, cx| {
                state.undo(&crate::input_ui::Undo, window, cx)
            });
        });
        assert_eq!(
            input.read_with(cx, |state, _| state.value().to_string()),
            INITIAL.to_string()
        );
        // 栈已空:再 undo 无变化。
        cx.update(|window, cx| {
            input.update(cx, |state, cx| {
                state.undo(&crate::input_ui::Undo, window, cx)
            });
        });
        assert_eq!(
            input.read_with(cx, |state, _| state.value().to_string()),
            INITIAL.to_string()
        );
    }

    /// 测试桩高亮器(固定返回一个 `main` 符号,无需 tree-sitter feature)。
    struct StubHighlighter;

    impl crate::highlight::Highlighter for StubHighlighter {
        fn language(&self) -> SharedString {
            "stub".into()
        }

        fn update(
            &mut self,
            _edit: Option<crate::highlight::TextEdit>,
            _text: &ropey::Rope,
            _folding: bool,
            _window: &mut Window,
            _cx: &mut App,
        ) {
        }

        fn styles(
            &self,
            range: &Range<usize>,
            _resolver: &dyn crate::highlight::HighlightStyleResolver,
        ) -> Vec<(Range<usize>, crate::HighlightStyle)> {
            vec![(range.clone(), crate::HighlightStyle::default())]
        }

        fn fold_ranges(&self, _text: &ropey::Rope) -> Vec<crate::highlight::FoldRange> {
            Vec::new()
        }

        fn document_symbols(&self, _text: &ropey::Rope) -> Vec<crate::highlight::DocumentSymbol> {
            vec![crate::highlight::DocumentSymbol {
                kind: crate::highlight::SymbolKind::Function,
                name: "main".into(),
                range: 0..2,
                start_row: 0,
            }]
        }
    }

    /// 设置高亮器后大纲同步刷新(高亮器变化不经过 Change,外壳补刷)。
    #[rgpui::test]
    fn set_highlighter_refreshes_outline(cx: &mut crate::TestAppContext) {
        let (probe, cx) = cx.add_window_view(|window, cx| {
            let editor = cx.new(|cx| EditorState::new(window, cx, "fn main() {}\n"));
            Probe { state: editor }
        });
        let editor = probe.read_with(cx, |probe, _| probe.state.clone());
        assert!(editor.read_with(cx, |state, _| state.outline().is_empty()));
        cx.update(|window, cx| {
            editor.update(cx, |state, cx| {
                state.set_highlighter(Some(Box::new(StubHighlighter)), window, cx);
            });
        });
        let outline = editor.read_with(cx, |state, _| state.outline().to_vec());
        assert_eq!(outline.len(), 1);
        assert_eq!(outline[0].name.to_string(), "main");
    }

    /// `set_value` 程序化写入:内容替换 + 光标重置 + 大纲同步 + 不进撤销栈。
    #[rgpui::test]
    fn set_value_replaces_without_history(cx: &mut crate::TestAppContext) {
        let (probe, cx) = cx.add_window_view(|window, cx| {
            let editor = cx.new(|cx| EditorState::new(window, cx, "old\n"));
            Probe { state: editor }
        });
        let editor = probe.read_with(cx, |probe, _| probe.state.clone());
        cx.update(|window, cx| {
            editor.update(cx, |state, cx| {
                state.set_highlighter(Some(Box::new(StubHighlighter)), window, cx);
                state.set_value("new\n", window, cx);
            });
        });
        assert_eq!(editor.read_with(cx, |state, cx| state.text(cx)), "new\n");
        // 多行 `set_value` 光标重置为起始。
        assert_eq!(editor.read_with(cx, |state, cx| state.cursor(cx)), 0);
        // 大纲随写入同步(桩返回固定符号,断言刷新通路不断言内容)。
        assert_eq!(editor.read_with(cx, |state, _| state.outline().len()), 1);
        // 不进撤销栈:undo 无变化。
        let input = editor.read_with(cx, |state, _| state.input().clone());
        cx.update(|window, cx| {
            input.update(cx, |state, cx| {
                state.undo(&crate::input_ui::Undo, window, cx)
            });
        });
        assert_eq!(
            input.read_with(cx, |state, _| state.value().to_string()),
            "new\n"
        );
    }
}