1use std::ops::Range;
2use std::sync::Arc;
3use std::time::Duration;
4
5use anyhow::Context as _;
6use clock::Global;
7use collections::HashMap;
8use futures::FutureExt as _;
9use futures::future::{Shared, join_all};
10use gpui::{AppContext as _, Context, Entity, Task};
11use itertools::Itertools;
12use language::{Buffer, BufferSnapshot, OutlineItem};
13use lsp::LanguageServerId;
14use settings::Settings as _;
15use text::{Anchor, Bias, PointUtf16};
16use util::ResultExt;
17
18use crate::DocumentSymbol;
19use crate::lsp_command::{GetDocumentSymbols, LspCommand as _};
20use crate::lsp_store::LspStore;
21use crate::project_settings::ProjectSettings;
22
23pub(super) type DocumentSymbolsTask =
24 Shared<Task<std::result::Result<Vec<OutlineItem<Anchor>>, Arc<anyhow::Error>>>>;
25
26#[derive(Debug, Default)]
27pub(super) struct DocumentSymbolsData {
28 symbols: HashMap<LanguageServerId, Vec<OutlineItem<Anchor>>>,
29 symbols_update: Option<(Global, DocumentSymbolsTask)>,
30}
31
32impl DocumentSymbolsData {
33 pub(super) fn remove_server_data(&mut self, for_server: LanguageServerId) {
34 self.symbols.remove(&for_server);
35 }
36}
37
38impl LspStore {
39 /// Returns a task that resolves to the document symbol outline items for
40 /// the given buffer.
41 ///
42 /// Caches results per buffer version so repeated calls for the same version
43 /// return immediately. Deduplicates concurrent in-flight requests.
44 ///
45 /// The returned items contain text and ranges but no syntax highlights.
46 /// Callers (e.g. the editor) are responsible for applying highlights
47 /// via the buffer's tree-sitter data and the active theme.
48 pub fn fetch_document_symbols(
49 &mut self,
50 buffer: &Entity<Buffer>,
51 cx: &mut Context<Self>,
52 ) -> Task<Vec<OutlineItem<Anchor>>> {
53 let version_queried_for = buffer.read(cx).version();
54 let buffer_id = buffer.read(cx).remote_id();
55
56 let current_language_servers = self.as_local().map(|local| {
57 local
58 .buffers_opened_in_servers
59 .get(&buffer_id)
60 .cloned()
61 .unwrap_or_default()
62 });
63
64 if let Some(lsp_data) = self.current_lsp_data(buffer_id) {
65 if let Some(cached) = &lsp_data.document_symbols {
66 if !version_queried_for.changed_since(&lsp_data.buffer_version) {
67 let has_different_servers =
68 current_language_servers.is_some_and(|current_language_servers| {
69 current_language_servers != cached.symbols.keys().copied().collect()
70 });
71 if !has_different_servers {
72 let snapshot = buffer.read(cx).snapshot();
73 return Task::ready(
74 cached
75 .symbols
76 .values()
77 .flatten()
78 .unique()
79 .cloned()
80 .sorted_by(|a, b| a.range.start.cmp(&b.range.start, &snapshot))
81 .collect(),
82 );
83 }
84 }
85 }
86 }
87
88 let doc_symbols_data = self
89 .latest_lsp_data(buffer, cx)
90 .document_symbols
91 .get_or_insert_default();
92 if let Some((updating_for, running_update)) = &doc_symbols_data.symbols_update {
93 if !version_queried_for.changed_since(updating_for) {
94 let running = running_update.clone();
95 return cx
96 .background_spawn(async move { running.await.log_err().unwrap_or_default() });
97 }
98 }
99
100 let buffer = buffer.clone();
101 let query_version = version_queried_for.clone();
102 let new_task = cx
103 .spawn(async move |lsp_store, cx| {
104 cx.background_executor()
105 .timer(Duration::from_millis(30))
106 .await;
107
108 let fetched = lsp_store
109 .update(cx, |lsp_store, cx| {
110 lsp_store.fetch_document_symbols_for_buffer(&buffer, cx)
111 })
112 .map_err(Arc::new)?
113 .await
114 .context("fetching document symbols")
115 .map_err(Arc::new);
116
117 let fetched = match fetched {
118 Ok(fetched) => fetched,
119 Err(e) => {
120 lsp_store
121 .update(cx, |lsp_store, _| {
122 if let Some(lsp_data) = lsp_store.lsp_data.get_mut(&buffer_id) {
123 if let Some(document_symbols) = &mut lsp_data.document_symbols {
124 document_symbols.symbols_update = None;
125 }
126 }
127 })
128 .ok();
129 return Err(e);
130 }
131 };
132
133 lsp_store
134 .update(cx, |lsp_store, cx| {
135 let snapshot = buffer.read(cx).snapshot();
136 let lsp_data = lsp_store.latest_lsp_data(&buffer, cx);
137 let doc_symbols = lsp_data.document_symbols.get_or_insert_default();
138
139 if let Some(fetched_symbols) = fetched {
140 let converted = fetched_symbols
141 .iter()
142 .map(|(&server_id, symbols)| {
143 let mut items = Vec::new();
144 flatten_document_symbols(symbols, &snapshot, 0, &mut items);
145 (server_id, items)
146 })
147 .collect();
148 if lsp_data.buffer_version == query_version {
149 doc_symbols.symbols.extend(converted);
150 } else if !lsp_data.buffer_version.changed_since(&query_version) {
151 lsp_data.buffer_version = query_version;
152 doc_symbols.symbols = converted;
153 }
154 }
155 doc_symbols.symbols_update = None;
156 doc_symbols
157 .symbols
158 .values()
159 .flatten()
160 .unique()
161 .cloned()
162 .sorted_by(|a, b| a.range.start.cmp(&b.range.start, &snapshot))
163 .collect()
164 })
165 .map_err(Arc::new)
166 })
167 .shared();
168
169 doc_symbols_data.symbols_update = Some((version_queried_for, new_task.clone()));
170
171 cx.background_spawn(async move { new_task.await.log_err().unwrap_or_default() })
172 }
173
174 fn fetch_document_symbols_for_buffer(
175 &mut self,
176 buffer: &Entity<Buffer>,
177 cx: &mut Context<Self>,
178 ) -> Task<anyhow::Result<Option<HashMap<LanguageServerId, Vec<DocumentSymbol>>>>> {
179 if let Some((client, project_id)) = self.upstream_client() {
180 let request = GetDocumentSymbols;
181 if !self.is_capable_for_proto_request(buffer, &request, cx) {
182 return Task::ready(Ok(None));
183 }
184
185 let request_timeout = ProjectSettings::get_global(cx)
186 .global_lsp_settings
187 .get_request_timeout();
188 let request_task = client.request_lsp(
189 project_id,
190 None,
191 request_timeout,
192 cx.background_executor().clone(),
193 request.to_proto(project_id, buffer.read(cx)),
194 );
195 let buffer = buffer.clone();
196 cx.spawn(async move |weak_lsp_store, cx| {
197 let Some(lsp_store) = weak_lsp_store.upgrade() else {
198 return Ok(None);
199 };
200 let Some(responses) = request_task.await? else {
201 return Ok(None);
202 };
203
204 let document_symbols = join_all(responses.payload.into_iter().map(|response| {
205 let lsp_store = lsp_store.clone();
206 let buffer = buffer.clone();
207 let cx = cx.clone();
208 async move {
209 (
210 LanguageServerId::from_proto(response.server_id),
211 GetDocumentSymbols
212 .response_from_proto(response.response, lsp_store, buffer, cx)
213 .await,
214 )
215 }
216 }))
217 .await;
218
219 let mut has_errors = false;
220 let result = document_symbols
221 .into_iter()
222 .filter_map(|(server_id, symbols)| match symbols {
223 Ok(symbols) => Some((server_id, symbols)),
224 Err(e) => {
225 has_errors = true;
226 log::error!("Failed to fetch document symbols: {e:#}");
227 None
228 }
229 })
230 .collect::<HashMap<_, _>>();
231 anyhow::ensure!(
232 !has_errors || !result.is_empty(),
233 "Failed to fetch document symbols"
234 );
235 Ok(Some(result))
236 })
237 } else {
238 let symbols_task =
239 self.request_multiple_lsp_locally(buffer, None::<usize>, GetDocumentSymbols, cx);
240 cx.background_spawn(async move { Ok(Some(symbols_task.await.into_iter().collect())) })
241 }
242 }
243}
244
245fn flatten_document_symbols(
246 symbols: &[DocumentSymbol],
247 snapshot: &BufferSnapshot,
248 depth: usize,
249 output: &mut Vec<OutlineItem<Anchor>>,
250) {
251 for symbol in symbols {
252 let name = super::collapse_newlines(&symbol.name, " ");
253
254 let start = snapshot.clip_point_utf16(symbol.range.start, Bias::Right);
255 let end = snapshot.clip_point_utf16(symbol.range.end, Bias::Left);
256 let selection_start = snapshot.clip_point_utf16(symbol.selection_range.start, Bias::Right);
257 let selection_end = snapshot.clip_point_utf16(symbol.selection_range.end, Bias::Left);
258
259 let range = snapshot.anchor_after(start)..snapshot.anchor_before(end);
260 let selection_range =
261 snapshot.anchor_after(selection_start)..snapshot.anchor_before(selection_end);
262
263 let (text, name_ranges, source_range_for_text) =
264 enriched_symbol_text(&name, start, selection_start, selection_end, snapshot)
265 .unwrap_or_else(|| {
266 let name_len = name.len();
267 (name.clone(), vec![0..name_len], selection_range.clone())
268 });
269
270 output.push(OutlineItem {
271 depth,
272 range,
273 source_range_for_text,
274 text,
275 highlight_ranges: Vec::new(),
276 name_ranges,
277 body_range: None,
278 annotation_range: None,
279 });
280
281 if !symbol.children.is_empty() {
282 flatten_document_symbols(&symbol.children, snapshot, depth + 1, output);
283 }
284 }
285}
286
287/// Tries to build an enriched label by including buffer text from the symbol
288/// range start to the selection range end (e.g., "struct Foo" instead of just "Foo").
289/// Only uses same-line prefix to avoid pulling in attributes/decorators.
290fn enriched_symbol_text(
291 name: &str,
292 range_start: PointUtf16,
293 selection_start: PointUtf16,
294 selection_end: PointUtf16,
295 snapshot: &BufferSnapshot,
296) -> Option<(String, Vec<Range<usize>>, Range<Anchor>)> {
297 let text_start = if range_start.row == selection_start.row {
298 range_start
299 } else {
300 PointUtf16::new(selection_start.row, 0)
301 };
302
303 let start_offset = snapshot.point_utf16_to_offset(text_start);
304 let end_offset = snapshot.point_utf16_to_offset(selection_end);
305 if start_offset >= end_offset {
306 return None;
307 }
308
309 let raw: String = snapshot.text_for_range(start_offset..end_offset).collect();
310 let trimmed = raw.trim_start();
311 if trimmed.len() <= name.len() || !trimmed.ends_with(name) {
312 return None;
313 }
314
315 let name_start = trimmed.len() - name.len();
316 let leading_ws = raw.len() - trimmed.len();
317 let adjusted_start = start_offset + leading_ws;
318
319 Some((
320 trimmed.to_string(),
321 vec![name_start..trimmed.len()],
322 snapshot.anchor_after(adjusted_start)..snapshot.anchor_before(end_offset),
323 ))
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329 use gpui::TestAppContext;
330 use text::Unclipped;
331
332 fn make_symbol(
333 name: &str,
334 kind: lsp::SymbolKind,
335 range: std::ops::Range<(u32, u32)>,
336 selection_range: std::ops::Range<(u32, u32)>,
337 children: Vec<DocumentSymbol>,
338 ) -> DocumentSymbol {
339 use text::PointUtf16;
340 DocumentSymbol {
341 name: name.to_string(),
342 kind,
343 range: Unclipped(PointUtf16::new(range.start.0, range.start.1))
344 ..Unclipped(PointUtf16::new(range.end.0, range.end.1)),
345 selection_range: Unclipped(PointUtf16::new(
346 selection_range.start.0,
347 selection_range.start.1,
348 ))
349 ..Unclipped(PointUtf16::new(
350 selection_range.end.0,
351 selection_range.end.1,
352 )),
353 children,
354 }
355 }
356
357 #[gpui::test]
358 async fn test_flatten_document_symbols(cx: &mut TestAppContext) {
359 let buffer = cx.new(|cx| {
360 Buffer::local(
361 concat!(
362 "struct Foo {\n",
363 " bar: u32,\n",
364 " baz: String,\n",
365 "}\n",
366 "\n",
367 "impl Foo {\n",
368 " fn new() -> Self {\n",
369 " Foo { bar: 0, baz: String::new() }\n",
370 " }\n",
371 "}\n",
372 ),
373 cx,
374 )
375 });
376
377 let symbols = vec![
378 make_symbol(
379 "Foo",
380 lsp::SymbolKind::STRUCT,
381 (0, 0)..(3, 1),
382 (0, 7)..(0, 10),
383 vec![
384 make_symbol(
385 "bar",
386 lsp::SymbolKind::FIELD,
387 (1, 4)..(1, 13),
388 (1, 4)..(1, 7),
389 vec![],
390 ),
391 make_symbol(
392 "baz",
393 lsp::SymbolKind::FIELD,
394 (2, 4)..(2, 15),
395 (2, 4)..(2, 7),
396 vec![],
397 ),
398 ],
399 ),
400 make_symbol(
401 "Foo",
402 lsp::SymbolKind::STRUCT,
403 (5, 0)..(9, 1),
404 (5, 5)..(5, 8),
405 vec![make_symbol(
406 "new",
407 lsp::SymbolKind::FUNCTION,
408 (6, 4)..(8, 5),
409 (6, 7)..(6, 10),
410 vec![],
411 )],
412 ),
413 ];
414
415 let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
416
417 let mut items = Vec::new();
418 flatten_document_symbols(&symbols, &snapshot, 0, &mut items);
419
420 assert_eq!(items.len(), 5);
421
422 assert_eq!(items[0].depth, 0);
423 assert_eq!(items[0].text, "struct Foo");
424 assert_eq!(items[0].name_ranges, vec![7..10]);
425
426 assert_eq!(items[1].depth, 1);
427 assert_eq!(items[1].text, "bar");
428 assert_eq!(items[1].name_ranges, vec![0..3]);
429
430 assert_eq!(items[2].depth, 1);
431 assert_eq!(items[2].text, "baz");
432 assert_eq!(items[2].name_ranges, vec![0..3]);
433
434 assert_eq!(items[3].depth, 0);
435 assert_eq!(items[3].text, "impl Foo");
436 assert_eq!(items[3].name_ranges, vec![5..8]);
437
438 assert_eq!(items[4].depth, 1);
439 assert_eq!(items[4].text, "fn new");
440 assert_eq!(items[4].name_ranges, vec![3..6]);
441 }
442
443 #[gpui::test]
444 async fn test_empty_symbols(cx: &mut TestAppContext) {
445 let buffer = cx.new(|cx| Buffer::local("", cx));
446 let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
447
448 let symbols: Vec<DocumentSymbol> = Vec::new();
449 let mut items = Vec::new();
450 flatten_document_symbols(&symbols, &snapshot, 0, &mut items);
451 assert!(items.is_empty());
452 }
453
454 #[gpui::test]
455 async fn test_newlines_collapsed_in_name(cx: &mut TestAppContext) {
456 let buffer = cx.new(|cx| Buffer::local("x = 1\ny = 2\n", cx));
457
458 let symbols = vec![
459 make_symbol(
460 "line1\nline2",
461 lsp::SymbolKind::VARIABLE,
462 (0, 0)..(0, 5),
463 (0, 0)..(0, 1),
464 vec![],
465 ),
466 make_symbol(
467 " a \n b ",
468 lsp::SymbolKind::VARIABLE,
469 (1, 0)..(1, 5),
470 (1, 0)..(1, 1),
471 vec![],
472 ),
473 make_symbol(
474 "a\r\nb",
475 lsp::SymbolKind::VARIABLE,
476 (0, 0)..(1, 5),
477 (0, 0)..(0, 1),
478 vec![],
479 ),
480 make_symbol(
481 "a\n\nb",
482 lsp::SymbolKind::VARIABLE,
483 (0, 0)..(1, 5),
484 (0, 0)..(0, 1),
485 vec![],
486 ),
487 ];
488
489 let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
490 let mut items = Vec::new();
491 flatten_document_symbols(&symbols, &snapshot, 0, &mut items);
492
493 assert_eq!(items.len(), 4);
494 assert_eq!(items[0].text, "line1 line2");
495 assert_eq!(items[1].text, "a b");
496 assert_eq!(items[2].text, "a b");
497 assert_eq!(items[3].text, "a b");
498 }
499}