document_symbols.rs

  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}