From 30ec2ca370859a5f06e8c0369cdd9b9e9e9ef35b Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 6 Feb 2026 22:24:38 +0200 Subject: [PATCH] Support custom fold text for LSP folds (#48624) Follow-up of https://github.com/zed-industries/zed/pull/48611 Release Notes: - N/A --- crates/agent_ui/src/text_thread_editor.rs | 1 + crates/editor/src/display_map.rs | 35 +- crates/editor/src/display_map/fold_map.rs | 55 ++- crates/editor/src/editor.rs | 9 +- crates/editor/src/folding_ranges.rs | 412 ++++++++++++++++++ crates/lsp/src/lsp.rs | 2 +- crates/project/src/lsp_command.rs | 67 ++- crates/project/src/lsp_store.rs | 1 + .../project/src/lsp_store/folding_ranges.rs | 20 +- crates/proto/proto/lsp.proto | 1 + 10 files changed, 557 insertions(+), 46 deletions(-) diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index cb7861564b8a545083561f86ad2900217b6c09d4..005032ddb40b9ffdba3a144aa8cfd895bfc07c1d 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -2999,6 +2999,7 @@ fn invoked_slash_command_fold_placeholder( text_thread: WeakEntity, ) -> FoldPlaceholder { FoldPlaceholder { + collapsed_text: None, constrain_width: false, merge_adjacent: false, render: Arc::new(move |fold_id, _, cx| { diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 5892c0a28e3adacbf37c68979fc1724aa1bf41b8..b785469bff20f405c181355a84d7d067e7d6f0fa 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -103,7 +103,7 @@ use multi_buffer::{ MultiBufferPoint, MultiBufferRow, MultiBufferSnapshot, RowInfo, ToOffset, ToPoint, }; use project::project_settings::DiagnosticSeverity; -use project::{InlayId, lsp_store::TokenType}; +use project::{InlayId, lsp_store::LspFoldingRange, lsp_store::TokenType}; use serde::Deserialize; use sum_tree::{Bias, TreeMap}; use text::{BufferId, LineIndent, Patch}; @@ -1342,7 +1342,7 @@ impl DisplayMap { pub(super) fn set_lsp_folding_ranges( &mut self, buffer_id: BufferId, - ranges: Vec>, + ranges: Vec, cx: &mut Context, ) { let snapshot = self.buffer.read(cx).snapshot(cx); @@ -1365,12 +1365,31 @@ impl DisplayMap { .map(|(id, _, _)| id) .collect::>(); - let placeholder = self.fold_placeholder.clone(); - let creases = ranges.into_iter().filter_map(|range| { - let mb_range = excerpt_ids - .iter() - .find_map(|&id| snapshot.anchor_range_in_excerpt(id, range.clone()))?; - Some(Crease::simple(mb_range, placeholder.clone())) + let base_placeholder = self.fold_placeholder.clone(); + let creases = ranges.into_iter().filter_map(|folding_range| { + let mb_range = excerpt_ids.iter().find_map(|&id| { + snapshot.anchor_range_in_excerpt(id, folding_range.range.clone()) + })?; + let placeholder = if let Some(collapsed_text) = folding_range.collapsed_text { + FoldPlaceholder { + render: Arc::new({ + let collapsed_text = collapsed_text.clone(); + move |fold_id, _fold_range, cx: &mut gpui::App| { + use gpui::{Element as _, ParentElement as _}; + FoldPlaceholder::fold_element(fold_id, cx) + .child(collapsed_text.clone()) + .into_any() + } + }), + constrain_width: false, + merge_adjacent: base_placeholder.merge_adjacent, + type_tag: base_placeholder.type_tag, + collapsed_text: Some(collapsed_text), + } + } else { + base_placeholder.clone() + }; + Some(Crease::simple(mb_range, placeholder)) }); let new_ids = self.crease_map.insert(creases, &snapshot); diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index 8010dd5187d374bfcbe9b7549f72509ad3257e5d..99667bf9892f9a9509c58a95bf4d39ffb44e52e2 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -4,7 +4,7 @@ use super::{ Highlights, inlay_map::{InlayBufferRows, InlayChunks, InlayEdit, InlayOffset, InlayPoint, InlaySnapshot}, }; -use gpui::{AnyElement, App, ElementId, HighlightStyle, Pixels, Window}; +use gpui::{AnyElement, App, ElementId, HighlightStyle, Pixels, SharedString, Stateful, Window}; use language::{Edit, HighlightId, Point}; use multi_buffer::{ Anchor, AnchorRangeExt, MBTextSummary, MultiBufferOffset, MultiBufferRow, MultiBufferSnapshot, @@ -33,6 +33,9 @@ pub struct FoldPlaceholder { pub merge_adjacent: bool, /// Category of the fold. Useful for carefully removing from overlapping folds. pub type_tag: Option, + /// Text provided by the language server to display in place of the folded range. + /// When set, this is used instead of the default "⋯" ellipsis. + pub collapsed_text: Option, } impl Default for FoldPlaceholder { @@ -42,11 +45,27 @@ impl Default for FoldPlaceholder { constrain_width: true, merge_adjacent: true, type_tag: None, + collapsed_text: None, } } } impl FoldPlaceholder { + /// Returns a styled `Div` container with the standard fold‐placeholder + /// look (background, hover, active, rounded corners, full size). + /// Callers add children and event handlers on top. + pub fn fold_element(fold_id: FoldId, cx: &App) -> Stateful { + use gpui::{InteractiveElement as _, StatefulInteractiveElement as _, Styled as _}; + use theme::ActiveTheme as _; + gpui::div() + .id(fold_id) + .bg(cx.theme().colors().ghost_element_background) + .hover(|style| style.bg(cx.theme().colors().ghost_element_hover)) + .active(|style| style.bg(cx.theme().colors().ghost_element_active)) + .rounded_xs() + .size_full() + } + #[cfg(any(test, feature = "test-support"))] pub fn test() -> Self { Self { @@ -54,6 +73,7 @@ impl FoldPlaceholder { constrain_width: true, merge_adjacent: true, type_tag: None, + collapsed_text: None, } } } @@ -62,6 +82,7 @@ impl fmt::Debug for FoldPlaceholder { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("FoldPlaceholder") .field("constrain_width", &self.constrain_width) + .field("collapsed_text", &self.collapsed_text) .finish() } } @@ -70,7 +91,9 @@ impl Eq for FoldPlaceholder {} impl PartialEq for FoldPlaceholder { fn eq(&self, other: &Self) -> bool { - Arc::ptr_eq(&self.render, &other.render) && self.constrain_width == other.constrain_width + Arc::ptr_eq(&self.render, &other.render) + && self.constrain_width == other.constrain_width + && self.collapsed_text == other.collapsed_text } } @@ -532,17 +555,28 @@ impl FoldMap { if fold_range.end > fold_range.start { const ELLIPSIS: &str = "⋯"; + let placeholder_text: SharedString = fold + .placeholder + .collapsed_text + .clone() + .unwrap_or_else(|| ELLIPSIS.into()); + let chars_bitmap = placeholder_text + .char_indices() + .fold(0u128, |bitmap, (idx, _)| { + bitmap | 1u128.unbounded_shl(idx as u32) + }); + let fold_id = fold.id; new_transforms.push( Transform { summary: TransformSummary { - output: MBTextSummary::from(ELLIPSIS), + output: MBTextSummary::from(placeholder_text.as_ref()), input: inlay_snapshot .text_summary_for_range(fold_range.start..fold_range.end), }, placeholder: Some(TransformPlaceholder { - text: ELLIPSIS, - chars: 1, + text: placeholder_text, + chars: chars_bitmap, renderer: ChunkRenderer { id: ChunkRendererId::Fold(fold.id), render: Arc::new(move |cx| { @@ -691,7 +725,7 @@ impl FoldSnapshot { let end_in_transform = cmp::min(range.end, cursor.end().0).0 - cursor.start().0.0; if let Some(placeholder) = transform.placeholder.as_ref() { summary = MBTextSummary::from( - &placeholder.text + &placeholder.text.as_ref() [start_in_transform.column as usize..end_in_transform.column as usize], ); } else { @@ -715,8 +749,9 @@ impl FoldSnapshot { if let Some(transform) = cursor.item() { let end_in_transform = range.end.0 - cursor.start().0.0; if let Some(placeholder) = transform.placeholder.as_ref() { - summary += - MBTextSummary::from(&placeholder.text[..end_in_transform.column as usize]); + summary += MBTextSummary::from( + &placeholder.text.as_ref()[..end_in_transform.column as usize], + ); } else { let inlay_start = self.inlay_snapshot.to_offset(cursor.start().1); let inlay_end = self @@ -1115,7 +1150,7 @@ struct Transform { #[derive(Clone, Debug)] struct TransformPlaceholder { - text: &'static str, + text: SharedString, chars: u128, renderer: ChunkRenderer, } @@ -1479,7 +1514,7 @@ impl<'a> Iterator for FoldChunks<'a> { self.output_offset.0 += placeholder.text.len(); return Some(Chunk { - text: placeholder.text, + text: &placeholder.text, chars: placeholder.chars, renderer: Some(placeholder.renderer.clone()), ..Default::default() diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index afda579bcff13bb96d0ad94695745724a2ca4deb..fc608a614c383d4cf937ee20c8bb540e6337e4fe 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2067,13 +2067,7 @@ impl Editor { constrain_width: false, render: Arc::new(move |fold_id, fold_range, cx| { let editor = editor.clone(); - div() - .id(fold_id) - .bg(cx.theme().colors().ghost_element_background) - .hover(|style| style.bg(cx.theme().colors().ghost_element_hover)) - .active(|style| style.bg(cx.theme().colors().ghost_element_active)) - .rounded_xs() - .size_full() + FoldPlaceholder::fold_element(fold_id, cx) .cursor_pointer() .child("⋯") .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) @@ -7583,6 +7577,7 @@ impl Editor { constrain_width: false, merge_adjacent: false, type_tag: Some(type_id), + collapsed_text: None, }; let creases = new_newlines .into_iter() diff --git a/crates/editor/src/folding_ranges.rs b/crates/editor/src/folding_ranges.rs index 46578f85e5d86e72f65ba160ec27beb87fc9a149..ad5e0e694d015d1935984e804b8455e8adae7dff 100644 --- a/crates/editor/src/folding_ranges.rs +++ b/crates/editor/src/folding_ranges.rs @@ -130,6 +130,7 @@ mod tests { use gpui::TestAppContext; use lsp::FoldingRange; use multi_buffer::MultiBufferRow; + use pretty_assertions::assert_eq; use settings::DocumentFoldingRanges; use crate::{ @@ -668,4 +669,415 @@ mod tests { assert_eq!(editor.display_text(cx), "fn main() {⋯\n}\n",); }); } + + #[gpui::test] + async fn test_lsp_folding_ranges_collapsed_text(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + update_test_language_settings(cx, |settings| { + settings.defaults.document_folding_ranges = Some(DocumentFoldingRanges::On); + }); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + folding_range_provider: Some(lsp::FoldingRangeProviderCapability::Simple(true)), + ..lsp::ServerCapabilities::default() + }, + cx, + ) + .await; + + let mut folding_request = cx + .set_request_handler::( + move |_, _, _| async move { + Ok(Some(vec![ + // main: custom collapsed text + FoldingRange { + start_line: 0, + start_character: Some(10), + end_line: 4, + end_character: Some(1), + kind: None, + collapsed_text: Some("{ fn body }".to_string()), + }, + // other: collapsed text longer than the original folded content + FoldingRange { + start_line: 6, + start_character: Some(11), + end_line: 8, + end_character: Some(1), + kind: None, + collapsed_text: Some("{ this collapsed text is intentionally much longer than the original function body it replaces }".to_string()), + }, + // emoji: collapsed text WITH emoji and multi-byte chars + FoldingRange { + start_line: 10, + start_character: Some(11), + end_line: 13, + end_character: Some(1), + kind: None, + collapsed_text: Some("{ 🦀…café }".to_string()), + }, + // outer: collapsed text on the outer fn + FoldingRange { + start_line: 15, + start_character: Some(11), + end_line: 22, + end_character: Some(1), + kind: None, + collapsed_text: Some("{ outer… }".to_string()), + }, + // inner_a: nested inside outer, with collapsed text + FoldingRange { + start_line: 16, + start_character: Some(17), + end_line: 18, + end_character: Some(5), + kind: None, + collapsed_text: Some("{ a }".to_string()), + }, + // inner_b: nested inside outer, no collapsed text + FoldingRange { + start_line: 19, + start_character: Some(17), + end_line: 21, + end_character: Some(5), + kind: None, + collapsed_text: None, + }, + // newline: collapsed text containing \n + FoldingRange { + start_line: 24, + start_character: Some(13), + end_line: 27, + end_character: Some(1), + kind: None, + collapsed_text: Some("{\n …\n}".to_string()), + }, + ])) + }, + ); + + cx.set_state( + &[ + "ˇfn main() {\n", + " if true {\n", + " println!(\"hello\");\n", + " }\n", + "}\n", + "\n", + "fn other() {\n", + " let x = 1;\n", + "}\n", + "\n", + "fn emoji() {\n", + " let a = \"🦀🔥\";\n", + " let b = \"café\";\n", + "}\n", + "\n", + "fn outer() {\n", + " fn inner_a() {\n", + " let x = 1;\n", + " }\n", + " fn inner_b() {\n", + " let y = 2;\n", + " }\n", + "}\n", + "\n", + "fn newline() {\n", + " let a = 1;\n", + " let b = 2;\n", + "}\n", + ] + .concat(), + ); + assert!(folding_request.next().await.is_some()); + cx.run_until_parked(); + + let unfolded_text = [ + "fn main() {\n", + " if true {\n", + " println!(\"hello\");\n", + " }\n", + "}\n", + "\n", + "fn other() {\n", + " let x = 1;\n", + "}\n", + "\n", + "fn emoji() {\n", + " let a = \"🦀🔥\";\n", + " let b = \"café\";\n", + "}\n", + "\n", + "fn outer() {\n", + " fn inner_a() {\n", + " let x = 1;\n", + " }\n", + " fn inner_b() {\n", + " let y = 2;\n", + " }\n", + "}\n", + "\n", + "fn newline() {\n", + " let a = 1;\n", + " let b = 2;\n", + "}\n", + ] + .concat(); + + // Fold newline fn — collapsed text that itself contains \n + // (newlines are sanitized to spaces to keep folds single-line). + cx.update_editor(|editor, window, cx| { + editor.fold_at(MultiBufferRow(24), window, cx); + }); + cx.update_editor(|editor, _window, cx| { + assert_eq!( + editor.display_text(cx), + [ + "fn main() {\n", + " if true {\n", + " println!(\"hello\");\n", + " }\n", + "}\n", + "\n", + "fn other() {\n", + " let x = 1;\n", + "}\n", + "\n", + "fn emoji() {\n", + " let a = \"🦀🔥\";\n", + " let b = \"café\";\n", + "}\n", + "\n", + "fn outer() {\n", + " fn inner_a() {\n", + " let x = 1;\n", + " }\n", + " fn inner_b() {\n", + " let y = 2;\n", + " }\n", + "}\n", + "\n", + "fn newline() { … }\n", + ] + .concat(), + ); + }); + + cx.update_editor(|editor, window, cx| { + editor.unfold_all(&crate::actions::UnfoldAll, window, cx); + }); + + // Fold main — custom collapsed text. + cx.update_editor(|editor, window, cx| { + editor.fold_at(MultiBufferRow(0), window, cx); + }); + cx.update_editor(|editor, _window, cx| { + assert_eq!( + editor.display_text(cx), + [ + "fn main() { fn body }\n", + "\n", + "fn other() {\n", + " let x = 1;\n", + "}\n", + "\n", + "fn emoji() {\n", + " let a = \"🦀🔥\";\n", + " let b = \"café\";\n", + "}\n", + "\n", + "fn outer() {\n", + " fn inner_a() {\n", + " let x = 1;\n", + " }\n", + " fn inner_b() {\n", + " let y = 2;\n", + " }\n", + "}\n", + "\n", + "fn newline() {\n", + " let a = 1;\n", + " let b = 2;\n", + "}\n", + ] + .concat(), + ); + }); + + // Fold emoji fn — multi-byte / emoji collapsed text (main still folded). + cx.update_editor(|editor, window, cx| { + editor.fold_at(MultiBufferRow(10), window, cx); + }); + cx.update_editor(|editor, _window, cx| { + assert_eq!( + editor.display_text(cx), + [ + "fn main() { fn body }\n", + "\n", + "fn other() {\n", + " let x = 1;\n", + "}\n", + "\n", + "fn emoji() { 🦀…café }\n", + "\n", + "fn outer() {\n", + " fn inner_a() {\n", + " let x = 1;\n", + " }\n", + " fn inner_b() {\n", + " let y = 2;\n", + " }\n", + "}\n", + "\n", + "fn newline() {\n", + " let a = 1;\n", + " let b = 2;\n", + "}\n", + ] + .concat(), + ); + }); + + // Fold a nested range (inner_a) while outer is still unfolded. + cx.update_editor(|editor, window, cx| { + editor.fold_at(MultiBufferRow(16), window, cx); + }); + cx.update_editor(|editor, _window, cx| { + assert_eq!( + editor.display_text(cx), + [ + "fn main() { fn body }\n", + "\n", + "fn other() {\n", + " let x = 1;\n", + "}\n", + "\n", + "fn emoji() { 🦀…café }\n", + "\n", + "fn outer() {\n", + " fn inner_a() { a }\n", + " fn inner_b() {\n", + " let y = 2;\n", + " }\n", + "}\n", + "\n", + "fn newline() {\n", + " let a = 1;\n", + " let b = 2;\n", + "}\n", + ] + .concat(), + ); + }); + + // Unfold everything to reset. + cx.update_editor(|editor, window, cx| { + editor.unfold_all(&crate::actions::UnfoldAll, window, cx); + }); + cx.update_editor(|editor, _window, cx| { + assert_eq!(editor.display_text(cx), unfolded_text); + }); + + // Fold ALL at once and verify every fold. + cx.update_editor(|editor, window, cx| { + editor.fold_all(&crate::actions::FoldAll, window, cx); + }); + cx.update_editor(|editor, _window, cx| { + assert_eq!( + editor.display_text(cx), + [ + "fn main() { fn body }\n", + "\n", + "fn other() { this collapsed text is intentionally much longer than the original function body it replaces }\n", + "\n", + "fn emoji() { 🦀…café }\n", + "\n", + "fn outer() { outer… }\n", + "\n", + "fn newline() { … }\n", + ] + .concat(), + ); + }); + + // Unfold all again, then fold only the outer, which should swallow inner folds. + cx.update_editor(|editor, window, cx| { + editor.unfold_all(&crate::actions::UnfoldAll, window, cx); + }); + cx.update_editor(|editor, window, cx| { + editor.fold_at(MultiBufferRow(15), window, cx); + }); + cx.update_editor(|editor, _window, cx| { + assert_eq!( + editor.display_text(cx), + [ + "fn main() {\n", + " if true {\n", + " println!(\"hello\");\n", + " }\n", + "}\n", + "\n", + "fn other() {\n", + " let x = 1;\n", + "}\n", + "\n", + "fn emoji() {\n", + " let a = \"🦀🔥\";\n", + " let b = \"café\";\n", + "}\n", + "\n", + "fn outer() { outer… }\n", + "\n", + "fn newline() {\n", + " let a = 1;\n", + " let b = 2;\n", + "}\n", + ] + .concat(), + ); + }); + + // Unfold the outer, then fold both inners independently. + cx.update_editor(|editor, window, cx| { + editor.unfold_all(&crate::actions::UnfoldAll, window, cx); + }); + cx.update_editor(|editor, window, cx| { + editor.fold_at(MultiBufferRow(16), window, cx); + editor.fold_at(MultiBufferRow(19), window, cx); + }); + cx.update_editor(|editor, _window, cx| { + assert_eq!( + editor.display_text(cx), + [ + "fn main() {\n", + " if true {\n", + " println!(\"hello\");\n", + " }\n", + "}\n", + "\n", + "fn other() {\n", + " let x = 1;\n", + "}\n", + "\n", + "fn emoji() {\n", + " let a = \"🦀🔥\";\n", + " let b = \"café\";\n", + "}\n", + "\n", + "fn outer() {\n", + " fn inner_a() { a }\n", + " fn inner_b() ⋯\n", + "}\n", + "\n", + "fn newline() {\n", + " let a = 1;\n", + " let b = 2;\n", + "}\n", + ] + .concat(), + ); + }); + } } diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 17833768160ad43ef8b7f814d89b2460f4ada7e4..e211eb1d444e33b98eb5e274a998ee4914d80f6d 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -966,7 +966,7 @@ impl LanguageServer { line_folding_only: Some(false), range_limit: None, folding_range: Some(FoldingRangeCapability { - collapsed_text: Some(false), + collapsed_text: Some(true), }), folding_range_kind: Some(FoldingRangeKindCapability { value_set: Some(vec![ diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 7f472eefb6b52f81456dd55d48aa7529d77d99e8..e63ce87743dcd191e4aefeea01b6f7be0c5a36be 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -6,7 +6,7 @@ use crate::{ InlayHintLabel, InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Location, LocationLink, LspAction, LspPullDiagnostics, MarkupContent, PrepareRenameResponse, ProjectTransaction, PulledDiagnostics, ResolveState, - lsp_store::{LocalLspStore, LspStore}, + lsp_store::{LocalLspStore, LspFoldingRange, LspStore}, }; use anyhow::{Context as _, Result}; use async_trait::async_trait; @@ -4737,7 +4737,7 @@ impl LspCommand for GetDocumentColor { #[async_trait(?Send)] impl LspCommand for GetFoldingRanges { - type Response = Vec>; + type Response = Vec; type LspRequest = lsp::request::FoldingRangeRequest; type ProtoRequest = proto::GetFoldingRanges; @@ -4786,17 +4786,32 @@ impl LspCommand for GetFoldingRanges { .into_iter() .filter(|range| range.start_line < range.end_line) .filter(|range| range.start_line <= max_point.row && range.end_line <= max_point.row) - .map(|range| { - let start_col = range + .map(|folding_range| { + let start_col = folding_range .start_character - .unwrap_or(snapshot.line_len(range.start_line)); - let end_col = range + .unwrap_or(snapshot.line_len(folding_range.start_line)); + let end_col = folding_range .end_character - .unwrap_or(snapshot.line_len(range.end_line)); - let start = - snapshot.anchor_after(language::Point::new(range.start_line, start_col)); - let end = snapshot.anchor_before(language::Point::new(range.end_line, end_col)); - start..end + .unwrap_or(snapshot.line_len(folding_range.end_line)); + let start = snapshot + .anchor_after(language::Point::new(folding_range.start_line, start_col)); + let end = + snapshot.anchor_before(language::Point::new(folding_range.end_line, end_col)); + let collapsed_text = + folding_range + .collapsed_text + .filter(|t| !t.is_empty()) + .map(|t| { + if t.contains('\n') { + SharedString::from(t.replace('\n', " ")) + } else { + SharedString::from(t) + } + }); + LspFoldingRange { + range: start..end, + collapsed_text, + } }) .collect()) } @@ -4825,8 +4840,20 @@ impl LspCommand for GetFoldingRanges { buffer_version: &clock::Global, _: &mut App, ) -> proto::GetFoldingRangesResponse { + let mut ranges = Vec::with_capacity(response.len()); + let mut collapsed_texts = Vec::with_capacity(response.len()); + for folding_range in response { + ranges.push(serialize_anchor_range(folding_range.range)); + collapsed_texts.push( + folding_range + .collapsed_text + .map(|t| t.to_string()) + .unwrap_or_default(), + ); + } proto::GetFoldingRangesResponse { - ranges: response.into_iter().map(serialize_anchor_range).collect(), + ranges, + collapsed_texts, version: serialize_version(buffer_version), } } @@ -4841,7 +4868,21 @@ impl LspCommand for GetFoldingRanges { message .ranges .into_iter() - .map(deserialize_anchor_range) + .zip( + message + .collapsed_texts + .into_iter() + .map(Some) + .chain(std::iter::repeat(None)), + ) + .map(|(range, collapsed_text)| { + Ok(LspFoldingRange { + range: deserialize_anchor_range(range)?, + collapsed_text: collapsed_text + .filter(|t| !t.is_empty()) + .map(SharedString::from), + }) + }) .collect() } diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index e3cd59a5e85e78fc43f5da7a1a69c5caab05066f..bfea28006a4ece0512caeb3a4e90f9b490c54a49 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -142,6 +142,7 @@ use util::{ }; pub use document_colors::DocumentColors; +pub use folding_ranges::LspFoldingRange; pub use fs::*; pub use language::Location; pub use lsp_store::inlay_hints::{CacheInlayHints, InvalidationStrategy}; diff --git a/crates/project/src/lsp_store/folding_ranges.rs b/crates/project/src/lsp_store/folding_ranges.rs index fc62824f9e4e8e5330516558a6ac28d99de826e3..e5df7c85948ed5551be5b2e956534f67d7d45062 100644 --- a/crates/project/src/lsp_store/folding_ranges.rs +++ b/crates/project/src/lsp_store/folding_ranges.rs @@ -7,7 +7,7 @@ use clock::Global; use collections::HashMap; use futures::FutureExt as _; use futures::future::{Shared, join_all}; -use gpui::{AppContext as _, Context, Entity, Task}; +use gpui::{AppContext as _, Context, Entity, SharedString, Task}; use itertools::Itertools; use language::Buffer; use lsp::{LSP_REQUEST_TIMEOUT, LanguageServerId}; @@ -16,12 +16,18 @@ use text::Anchor; use crate::lsp_command::{GetFoldingRanges, LspCommand as _}; use crate::lsp_store::LspStore; +#[derive(Clone, Debug)] +pub struct LspFoldingRange { + pub range: Range, + pub collapsed_text: Option, +} + pub(super) type FoldingRangeTask = - Shared>, Arc>>>; + Shared, Arc>>>; #[derive(Debug, Default)] pub(super) struct FoldingRangeData { - pub(super) ranges: HashMap>>, + pub(super) ranges: HashMap>, ranges_update: Option<(Global, FoldingRangeTask)>, } @@ -34,7 +40,7 @@ impl LspStore { &mut self, buffer: &Entity, cx: &mut Context, - ) -> Task>> { + ) -> Task> { let version_queried_for = buffer.read(cx).version(); let buffer_id = buffer.read(cx).remote_id(); @@ -61,7 +67,7 @@ impl LspStore { .values() .flatten() .cloned() - .sorted_by(|a, b| a.start.cmp(&b.start, &snapshot)) + .sorted_by(|a, b| a.range.start.cmp(&b.range.start, &snapshot)) .collect(), ); } @@ -133,7 +139,7 @@ impl LspStore { .values() .flatten() .cloned() - .sorted_by(|a, b| a.start.cmp(&b.start, &snapshot)) + .sorted_by(|a, b| a.range.start.cmp(&b.range.start, &snapshot)) .collect() }) .map_err(Arc::new) @@ -149,7 +155,7 @@ impl LspStore { &mut self, buffer: &Entity, cx: &mut Context, - ) -> Task>>>>> { + ) -> Task>>>> { if let Some((client, project_id)) = self.upstream_client() { let request = GetFoldingRanges; if !self.is_capable_for_proto_request(buffer, &request, cx) { diff --git a/crates/proto/proto/lsp.proto b/crates/proto/proto/lsp.proto index 1ec0d9fc338c13f0b7771c5b4a5ee2710e4d3b39..99b29ac224549d9371f5a71bf54cd918090863f1 100644 --- a/crates/proto/proto/lsp.proto +++ b/crates/proto/proto/lsp.proto @@ -1015,4 +1015,5 @@ message GetFoldingRanges { message GetFoldingRangesResponse { repeated AnchorRange ranges = 1; repeated VectorClockEntry version = 2; + repeated string collapsed_texts = 3; }