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 start = snapshot.clip_point_utf16(symbol.range.start, Bias::Right);
253 let end = snapshot.clip_point_utf16(symbol.range.end, Bias::Left);
254 let selection_start = snapshot.clip_point_utf16(symbol.selection_range.start, Bias::Right);
255 let selection_end = snapshot.clip_point_utf16(symbol.selection_range.end, Bias::Left);
256
257 let range = snapshot.anchor_after(start)..snapshot.anchor_before(end);
258 let selection_range =
259 snapshot.anchor_after(selection_start)..snapshot.anchor_before(selection_end);
260
261 let (text, name_ranges, source_range_for_text) = enriched_symbol_text(
262 &symbol.name,
263 start,
264 selection_start,
265 selection_end,
266 snapshot,
267 )
268 .unwrap_or_else(|| {
269 let name = symbol.name.clone();
270 let name_len = name.len();
271 (name, vec![0..name_len], selection_range.clone())
272 });
273
274 output.push(OutlineItem {
275 depth,
276 range,
277 source_range_for_text,
278 text,
279 highlight_ranges: Vec::new(),
280 name_ranges,
281 body_range: None,
282 annotation_range: None,
283 });
284
285 if !symbol.children.is_empty() {
286 flatten_document_symbols(&symbol.children, snapshot, depth + 1, output);
287 }
288 }
289}
290
291/// Tries to build an enriched label by including buffer text from the symbol
292/// range start to the selection range end (e.g., "struct Foo" instead of just "Foo").
293/// Only uses same-line prefix to avoid pulling in attributes/decorators.
294fn enriched_symbol_text(
295 name: &str,
296 range_start: PointUtf16,
297 selection_start: PointUtf16,
298 selection_end: PointUtf16,
299 snapshot: &BufferSnapshot,
300) -> Option<(String, Vec<Range<usize>>, Range<Anchor>)> {
301 let text_start = if range_start.row == selection_start.row {
302 range_start
303 } else {
304 PointUtf16::new(selection_start.row, 0)
305 };
306
307 let start_offset = snapshot.point_utf16_to_offset(text_start);
308 let end_offset = snapshot.point_utf16_to_offset(selection_end);
309 if start_offset >= end_offset {
310 return None;
311 }
312
313 let raw: String = snapshot.text_for_range(start_offset..end_offset).collect();
314 let trimmed = raw.trim_start();
315 if trimmed.len() <= name.len() || !trimmed.ends_with(name) {
316 return None;
317 }
318
319 let name_start = trimmed.len() - name.len();
320 let leading_ws = raw.len() - trimmed.len();
321 let adjusted_start = start_offset + leading_ws;
322
323 Some((
324 trimmed.to_string(),
325 vec![name_start..trimmed.len()],
326 snapshot.anchor_after(adjusted_start)..snapshot.anchor_before(end_offset),
327 ))
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333 use gpui::TestAppContext;
334 use text::Unclipped;
335
336 fn make_symbol(
337 name: &str,
338 kind: lsp::SymbolKind,
339 range: std::ops::Range<(u32, u32)>,
340 selection_range: std::ops::Range<(u32, u32)>,
341 children: Vec<DocumentSymbol>,
342 ) -> DocumentSymbol {
343 use text::PointUtf16;
344 DocumentSymbol {
345 name: name.to_string(),
346 kind,
347 range: Unclipped(PointUtf16::new(range.start.0, range.start.1))
348 ..Unclipped(PointUtf16::new(range.end.0, range.end.1)),
349 selection_range: Unclipped(PointUtf16::new(
350 selection_range.start.0,
351 selection_range.start.1,
352 ))
353 ..Unclipped(PointUtf16::new(
354 selection_range.end.0,
355 selection_range.end.1,
356 )),
357 children,
358 }
359 }
360
361 #[gpui::test]
362 async fn test_flatten_document_symbols(cx: &mut TestAppContext) {
363 let buffer = cx.new(|cx| {
364 Buffer::local(
365 concat!(
366 "struct Foo {\n",
367 " bar: u32,\n",
368 " baz: String,\n",
369 "}\n",
370 "\n",
371 "impl Foo {\n",
372 " fn new() -> Self {\n",
373 " Foo { bar: 0, baz: String::new() }\n",
374 " }\n",
375 "}\n",
376 ),
377 cx,
378 )
379 });
380
381 let symbols = vec![
382 make_symbol(
383 "Foo",
384 lsp::SymbolKind::STRUCT,
385 (0, 0)..(3, 1),
386 (0, 7)..(0, 10),
387 vec![
388 make_symbol(
389 "bar",
390 lsp::SymbolKind::FIELD,
391 (1, 4)..(1, 13),
392 (1, 4)..(1, 7),
393 vec![],
394 ),
395 make_symbol(
396 "baz",
397 lsp::SymbolKind::FIELD,
398 (2, 4)..(2, 15),
399 (2, 4)..(2, 7),
400 vec![],
401 ),
402 ],
403 ),
404 make_symbol(
405 "Foo",
406 lsp::SymbolKind::STRUCT,
407 (5, 0)..(9, 1),
408 (5, 5)..(5, 8),
409 vec![make_symbol(
410 "new",
411 lsp::SymbolKind::FUNCTION,
412 (6, 4)..(8, 5),
413 (6, 7)..(6, 10),
414 vec![],
415 )],
416 ),
417 ];
418
419 let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
420
421 let mut items = Vec::new();
422 flatten_document_symbols(&symbols, &snapshot, 0, &mut items);
423
424 assert_eq!(items.len(), 5);
425
426 assert_eq!(items[0].depth, 0);
427 assert_eq!(items[0].text, "struct Foo");
428 assert_eq!(items[0].name_ranges, vec![7..10]);
429
430 assert_eq!(items[1].depth, 1);
431 assert_eq!(items[1].text, "bar");
432 assert_eq!(items[1].name_ranges, vec![0..3]);
433
434 assert_eq!(items[2].depth, 1);
435 assert_eq!(items[2].text, "baz");
436 assert_eq!(items[2].name_ranges, vec![0..3]);
437
438 assert_eq!(items[3].depth, 0);
439 assert_eq!(items[3].text, "impl Foo");
440 assert_eq!(items[3].name_ranges, vec![5..8]);
441
442 assert_eq!(items[4].depth, 1);
443 assert_eq!(items[4].text, "fn new");
444 assert_eq!(items[4].name_ranges, vec![3..6]);
445 }
446
447 #[gpui::test]
448 async fn test_empty_symbols(cx: &mut TestAppContext) {
449 let buffer = cx.new(|cx| Buffer::local("", cx));
450 let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
451
452 let symbols: Vec<DocumentSymbol> = Vec::new();
453 let mut items = Vec::new();
454 flatten_document_symbols(&symbols, &snapshot, 0, &mut items);
455 assert!(items.is_empty());
456 }
457}