1pub mod editor_lsp_test_context;
2pub mod editor_test_context;
3
4use std::{rc::Rc, sync::LazyLock};
5
6pub use crate::rust_analyzer_ext::expand_macro_recursively;
7use crate::{
8 DisplayPoint, Editor, EditorMode, FoldPlaceholder, MultiBuffer, SelectionEffects, Size,
9 display_map::{
10 Block, BlockPlacement, CustomBlockId, DisplayMap, DisplayRow, DisplaySnapshot,
11 ToDisplayPoint,
12 },
13};
14use collections::HashMap;
15use gpui::{
16 AppContext as _, Context, Entity, EntityId, Font, FontFeatures, FontStyle, FontWeight, Pixels,
17 VisualTestContext, Window, font, size,
18};
19use multi_buffer::{MultiBufferOffset, ToPoint};
20use pretty_assertions::assert_eq;
21use project::{Project, project_settings::DiagnosticSeverity};
22use ui::{App, BorrowAppContext, IntoElement, px};
23use util::test::{generate_marked_text, marked_text_offsets, marked_text_ranges};
24
25#[cfg(test)]
26#[ctor::ctor]
27fn init_logger() {
28 zlog::init_test();
29}
30
31pub fn test_font() -> Font {
32 static TEST_FONT: LazyLock<Font> = LazyLock::new(|| {
33 #[cfg(not(target_os = "windows"))]
34 {
35 font("Helvetica")
36 }
37
38 #[cfg(target_os = "windows")]
39 {
40 font("Courier New")
41 }
42 });
43
44 TEST_FONT.clone()
45}
46
47// Returns a snapshot from text containing '|' character markers with the markers removed, and DisplayPoints for each one.
48#[track_caller]
49pub fn marked_display_snapshot(
50 text: &str,
51 cx: &mut gpui::App,
52) -> (DisplaySnapshot, Vec<DisplayPoint>) {
53 let (unmarked_text, markers) = marked_text_offsets(text);
54
55 let font = Font {
56 family: ".ZedMono".into(),
57 features: FontFeatures::default(),
58 fallbacks: None,
59 weight: FontWeight::default(),
60 style: FontStyle::default(),
61 };
62 let font_size: Pixels = 14usize.into();
63
64 let buffer = MultiBuffer::build_simple(&unmarked_text, cx);
65 let display_map = cx.new(|cx| {
66 DisplayMap::new(
67 buffer,
68 font,
69 font_size,
70 None,
71 1,
72 1,
73 FoldPlaceholder::test(),
74 DiagnosticSeverity::Warning,
75 cx,
76 )
77 });
78 let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
79 let markers = markers
80 .into_iter()
81 .map(|offset| MultiBufferOffset(offset).to_display_point(&snapshot))
82 .collect();
83
84 (snapshot, markers)
85}
86
87#[track_caller]
88pub fn select_ranges(
89 editor: &mut Editor,
90 marked_text: &str,
91 window: &mut Window,
92 cx: &mut Context<Editor>,
93) {
94 let (unmarked_text, text_ranges) = marked_text_ranges(marked_text, true);
95 assert_eq!(editor.text(cx), unmarked_text);
96 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
97 s.select_ranges(
98 text_ranges
99 .into_iter()
100 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end)),
101 )
102 });
103}
104
105#[track_caller]
106pub fn assert_text_with_selections(
107 editor: &mut Editor,
108 marked_text: &str,
109 cx: &mut Context<Editor>,
110) {
111 let (unmarked_text, _text_ranges) = marked_text_ranges(marked_text, true);
112 assert_eq!(editor.text(cx), unmarked_text, "text doesn't match");
113 let actual = generate_marked_text(
114 &editor.text(cx),
115 &editor
116 .selections
117 .ranges::<MultiBufferOffset>(&editor.display_snapshot(cx))
118 .into_iter()
119 .map(|range| range.start.0..range.end.0)
120 .collect::<Vec<_>>(),
121 marked_text.contains("«"),
122 );
123 assert_eq!(actual, marked_text, "Selections don't match");
124}
125
126#[cfg(any(test, feature = "test-support"))]
127pub(crate) fn build_editor(
128 buffer: Entity<MultiBuffer>,
129 window: &mut Window,
130 cx: &mut Context<Editor>,
131) -> Editor {
132 Editor::new(EditorMode::full(), buffer, None, window, cx)
133}
134
135pub(crate) fn build_editor_with_project(
136 project: Entity<Project>,
137 buffer: Entity<MultiBuffer>,
138 window: &mut Window,
139 cx: &mut Context<Editor>,
140) -> Editor {
141 Editor::new(EditorMode::full(), buffer, Some(project), window, cx)
142}
143
144#[derive(Default)]
145struct TestBlockContent(
146 HashMap<(EntityId, CustomBlockId), Rc<dyn Fn(&mut VisualTestContext) -> String>>,
147);
148
149impl gpui::Global for TestBlockContent {}
150
151pub fn set_block_content_for_tests(
152 editor: &Entity<Editor>,
153 id: CustomBlockId,
154 cx: &mut App,
155 f: impl Fn(&mut VisualTestContext) -> String + 'static,
156) {
157 cx.update_default_global::<TestBlockContent, _>(|bc, _| {
158 bc.0.insert((editor.entity_id(), id), Rc::new(f))
159 });
160}
161
162pub fn block_content_for_tests(
163 editor: &Entity<Editor>,
164 id: CustomBlockId,
165 cx: &mut VisualTestContext,
166) -> Option<String> {
167 let f = cx.update(|_, cx| {
168 cx.default_global::<TestBlockContent>()
169 .0
170 .get(&(editor.entity_id(), id))
171 .cloned()
172 })?;
173 Some(f(cx))
174}
175
176pub fn editor_content_with_blocks(editor: &Entity<Editor>, cx: &mut VisualTestContext) -> String {
177 editor_content_with_blocks_and_width(editor, px(3000.), cx)
178}
179
180pub fn editor_content_with_blocks_and_width(
181 editor: &Entity<Editor>,
182 width: Pixels,
183 cx: &mut VisualTestContext,
184) -> String {
185 editor_content_with_blocks_and_size(editor, size(width, px(3000.0)), cx)
186}
187
188pub fn editor_content_with_blocks_and_size(
189 editor: &Entity<Editor>,
190 draw_size: Size<Pixels>,
191 cx: &mut VisualTestContext,
192) -> String {
193 cx.simulate_resize(draw_size);
194 cx.draw(gpui::Point::default(), draw_size, |_, _| {
195 editor.clone().into_any_element()
196 });
197 let (snapshot, mut lines, blocks) = editor.update_in(cx, |editor, window, cx| {
198 let snapshot = editor.snapshot(window, cx);
199 let text = editor.display_text(cx);
200 let lines = text.lines().map(|s| s.to_string()).collect::<Vec<String>>();
201 let blocks = snapshot
202 .blocks_in_range(DisplayRow(0)..snapshot.max_point().row())
203 .map(|(row, block)| (row, block.clone()))
204 .collect::<Vec<_>>();
205 (snapshot, lines, blocks)
206 });
207 for (row, block) in blocks {
208 match block {
209 Block::Custom(custom_block) => {
210 if let BlockPlacement::Near(x) = &custom_block.placement
211 && snapshot.intersects_fold(x.to_point(&snapshot.buffer_snapshot()))
212 {
213 continue;
214 };
215 let content = block_content_for_tests(editor, custom_block.id, cx)
216 .expect("block content not found");
217 // 2: "related info 1 for diagnostic 0"
218 if let Some(height) = custom_block.height {
219 if height == 0 {
220 lines[row.0 as usize - 1].push_str(" § ");
221 lines[row.0 as usize - 1].push_str(&content);
222 } else {
223 let block_lines = content.lines().collect::<Vec<_>>();
224 assert_eq!(block_lines.len(), height as usize);
225 lines[row.0 as usize].push_str("§ ");
226 lines[row.0 as usize].push_str(block_lines[0].trim_end());
227 for i in 1..height as usize {
228 if row.0 as usize + i >= lines.len() {
229 lines.push("".to_string());
230 };
231 lines[row.0 as usize + i].push_str("§ ");
232 lines[row.0 as usize + i].push_str(block_lines[i].trim_end());
233 }
234 }
235 }
236 }
237 Block::FoldedBuffer {
238 first_excerpt,
239 height,
240 } => {
241 while lines.len() <= row.0 as usize {
242 lines.push(String::new());
243 }
244 lines[row.0 as usize].push_str(&cx.update(|_, cx| {
245 format!(
246 "§ {}",
247 first_excerpt
248 .buffer
249 .file()
250 .map(|file| file.file_name(cx))
251 .unwrap_or("<no file>")
252 )
253 }));
254 for row in row.0 + 1..row.0 + height {
255 while lines.len() <= row as usize {
256 lines.push(String::new());
257 }
258 lines[row as usize].push_str("§ -----");
259 }
260 }
261 Block::ExcerptBoundary { height, .. } => {
262 for row in row.0..row.0 + height {
263 while lines.len() <= row as usize {
264 lines.push(String::new());
265 }
266 lines[row as usize].push_str("§ -----");
267 }
268 }
269 Block::BufferHeader { excerpt, height } => {
270 while lines.len() <= row.0 as usize {
271 lines.push(String::new());
272 }
273 lines[row.0 as usize].push_str(&cx.update(|_, cx| {
274 format!(
275 "§ {}",
276 excerpt
277 .buffer
278 .file()
279 .map(|file| file.file_name(cx))
280 .unwrap_or("<no file>")
281 )
282 }));
283 for row in row.0 + 1..row.0 + height {
284 while lines.len() <= row as usize {
285 lines.push(String::new());
286 }
287 lines[row as usize].push_str("§ -----");
288 }
289 }
290 Block::Spacer { height, .. } => {
291 for row in row.0..row.0 + height {
292 while lines.len() <= row as usize {
293 lines.push(String::new());
294 }
295 lines[row as usize].push_str("§ spacer");
296 }
297 }
298 }
299 }
300 lines.join("\n")
301}