Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions assets/settings/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -1102,6 +1102,14 @@
// May require language server restart to properly apply.
"semantic_tokens": "off",

// Controls whether folding ranges from language servers are used instead of
// tree-sitter and indent-based folding.
//
// Options:
// - "off": Use tree-sitter and indent-based folding (default).
// - "on": Use LSP folding wherever possible, falling back to tree-sitter and indent-based folding when no results were returned by the server.
"document_folding_ranges": "off",

// When to automatically save edited buffers. This setting can
// take four values.
//
Expand Down
194 changes: 189 additions & 5 deletions crates/collab/tests/integration/editor_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use call::ActiveCall;
use collab::rpc::RECONNECT_TIMEOUT;
use collections::{HashMap, HashSet};
use editor::{
DocumentColorsRenderMode, Editor, FETCH_COLORS_DEBOUNCE_TIMEOUT, MultiBufferOffset, RowInfo,
DocumentColorsRenderMode, Editor, LSP_REQUEST_DEBOUNCE_TIMEOUT, MultiBufferOffset, RowInfo,
SelectionEffects,
actions::{
ConfirmCodeAction, ConfirmCompletion, ConfirmRename, ContextMenuFirst, CopyFileLocation,
Expand All @@ -24,7 +24,7 @@ use gpui::{
use indoc::indoc;
use language::{FakeLspAdapter, language_settings::language_settings, rust_lang};
use lsp::LSP_REQUEST_TIMEOUT;
use multi_buffer::AnchorRangeExt as _;
use multi_buffer::{AnchorRangeExt as _, MultiBufferRow};
use pretty_assertions::assert_eq;
use project::{
ProgressToken, ProjectPath, SERVER_PROGRESS_THROTTLE_TIMEOUT,
Expand All @@ -34,7 +34,10 @@ use project::{
use recent_projects::disconnected_overlay::DisconnectedOverlay;
use rpc::RECEIVE_TIMEOUT;
use serde_json::json;
use settings::{InlayHintSettingsContent, InlineBlameSettings, SemanticTokens, SettingsStore};
use settings::{
DocumentFoldingRanges, InlayHintSettingsContent, InlineBlameSettings, SemanticTokens,
SettingsStore,
};
use std::{
collections::BTreeSet,
num::NonZeroU32,
Expand Down Expand Up @@ -2557,7 +2560,7 @@ async fn test_lsp_document_color(cx_a: &mut TestAppContext, cx_b: &mut TestAppCo
.unwrap();

color_request_handle.next().await.unwrap();
executor.advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
executor.advance_clock(LSP_REQUEST_DEBOUNCE_TIMEOUT);
executor.run_until_parked();

assert_eq!(
Expand Down Expand Up @@ -5187,7 +5190,7 @@ async fn test_semantic_token_refresh_is_forwarded(
.into_response()
.expect("semantic tokens refresh request failed");
// wait out the debounce timeout
executor.advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
executor.advance_clock(LSP_REQUEST_DEBOUNCE_TIMEOUT);
executor.run_until_parked();
editor_a.update(cx_a, |editor, cx| {
assert!(
Expand All @@ -5206,6 +5209,187 @@ async fn test_semantic_token_refresh_is_forwarded(
});
}

#[gpui::test]
async fn test_document_folding_ranges(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
let mut server = TestServer::start(cx_a.executor()).await;
let executor = cx_a.executor();
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.await;
let active_call_a = cx_a.read(ActiveCall::global);
let active_call_b = cx_b.read(ActiveCall::global);

cx_a.update(editor::init);
cx_b.update(editor::init);

let capabilities = lsp::ServerCapabilities {
folding_range_provider: Some(lsp::FoldingRangeProviderCapability::Simple(true)),
..lsp::ServerCapabilities::default()
};
client_a.language_registry().add(rust_lang());
let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
"Rust",
FakeLspAdapter {
capabilities: capabilities.clone(),
..FakeLspAdapter::default()
},
);
client_b.language_registry().add(rust_lang());
client_b.language_registry().register_fake_lsp_adapter(
"Rust",
FakeLspAdapter {
capabilities,
..FakeLspAdapter::default()
},
);

client_a
.fs()
.insert_tree(
path!("/a"),
json!({
"main.rs": "fn main() {\n if true {\n println!(\"hello\");\n }\n}\n",
}),
)
.await;
let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
active_call_a
.update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
.await
.unwrap();
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();

let project_b = client_b.join_remote_project(project_id, cx_b).await;
active_call_b
.update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
.await
.unwrap();

let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);

let _buffer_a = project_a
.update(cx_a, |project, cx| {
project.open_local_buffer(path!("/a/main.rs"), cx)
})
.await
.unwrap();
let editor_a = workspace_a
.update_in(cx_a, |workspace, window, cx| {
workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
})
.await
.unwrap()
.downcast::<Editor>()
.unwrap();

let fake_language_server = fake_language_servers.next().await.unwrap();

let folding_request_count = Arc::new(AtomicUsize::new(0));
let closure_count = Arc::clone(&folding_request_count);
let mut folding_request_handle = fake_language_server
.set_request_handler::<lsp::request::FoldingRangeRequest, _, _>(move |_, _| {
let count = Arc::clone(&closure_count);
async move {
count.fetch_add(1, atomic::Ordering::Release);
Ok(Some(vec![lsp::FoldingRange {
start_line: 0,
start_character: Some(10),
end_line: 4,
end_character: Some(1),
kind: None,
collapsed_text: None,
}]))
}
});

executor.run_until_parked();

assert_eq!(
0,
folding_request_count.load(atomic::Ordering::Acquire),
"LSP folding ranges are off by default, no request should have been made"
);
editor_a.update(cx_a, |editor, cx| {
assert!(
!editor.document_folding_ranges_enabled(cx),
"Host should not have LSP folding ranges enabled"
);
});

let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
let editor_b = workspace_b
.update_in(cx_b, |workspace, window, cx| {
workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
})
.await
.unwrap()
.downcast::<Editor>()
.unwrap();
executor.run_until_parked();

editor_b.update(cx_b, |editor, cx| {
assert!(
!editor.document_folding_ranges_enabled(cx),
"Client should not have LSP folding ranges enabled by default"
);
});

cx_b.update(|_, cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings(cx, |settings| {
settings
.project
.all_languages
.defaults
.document_folding_ranges = Some(DocumentFoldingRanges::On);
});
});
});
executor.advance_clock(LSP_REQUEST_DEBOUNCE_TIMEOUT);
folding_request_handle.next().await.unwrap();
executor.run_until_parked();

assert!(
folding_request_count.load(atomic::Ordering::Acquire) > 0,
"After the client enables LSP folding ranges, a request should be made"
);
editor_b.update(cx_b, |editor, cx| {
assert!(
editor.document_folding_ranges_enabled(cx),
"Client should have LSP folding ranges enabled after toggling the setting on"
);
});
editor_a.update(cx_a, |editor, cx| {
assert!(
!editor.document_folding_ranges_enabled(cx),
"Host should remain unaffected by the client's setting change"
);
});

editor_b.update_in(cx_b, |editor, window, cx| {
let snapshot = editor.display_snapshot(cx);
assert!(
!snapshot.is_line_folded(MultiBufferRow(0)),
"Line 0 should not be folded before fold_at"
);
editor.fold_at(MultiBufferRow(0), window, cx);
});
executor.run_until_parked();

editor_b.update(cx_b, |editor, cx| {
let snapshot = editor.display_snapshot(cx);
assert!(
snapshot.is_line_folded(MultiBufferRow(0)),
"Line 0 should be folded after fold_at using LSP folding range"
);
});
}

#[gpui::test]
async fn test_remote_project_worktree_trust(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
let has_restricted_worktrees = |project: &gpui::Entity<project::Project>,
Expand Down
69 changes: 67 additions & 2 deletions crates/editor/src/display_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ pub use inlay_map::{InlayOffset, InlayPoint};
pub use invisibles::{is_invisible, replacement};
pub use wrap_map::{WrapPoint, WrapRow, WrapSnapshot};

use collections::{HashMap, HashSet, IndexSet};
use collections::{HashMap, HashSet, IndexSet, hash_map};
use gpui::{
App, Context, Entity, EntityId, Font, HighlightStyle, LineLayout, Pixels, UnderlineStyle,
WeakEntity,
Expand Down Expand Up @@ -225,6 +225,7 @@ pub struct DisplayMap {
pub(crate) masked: bool,
pub(crate) diagnostics_max_severity: DiagnosticSeverity,
pub(crate) companion: Option<(WeakEntity<DisplayMap>, Entity<Companion>)>,
lsp_folding_crease_ids: HashMap<BufferId, Vec<CreaseId>>,
}

// test change
Expand Down Expand Up @@ -463,6 +464,7 @@ impl DisplayMap {
clip_at_line_ends: false,
masked: false,
companion: None,
lsp_folding_crease_ids: HashMap::default(),
}
}

Expand Down Expand Up @@ -671,6 +673,7 @@ impl DisplayMap {
semantic_token_highlights: self.semantic_token_highlights.clone(),
clip_at_line_ends: self.clip_at_line_ends,
masked: self.masked,
use_lsp_folding_ranges: !self.lsp_folding_crease_ids.is_empty(),
fold_placeholder: self.fold_placeholder.clone(),
}
}
Expand All @@ -694,6 +697,7 @@ impl DisplayMap {
semantic_token_highlights: self.semantic_token_highlights.clone(),
clip_at_line_ends: self.clip_at_line_ends,
masked: self.masked,
use_lsp_folding_ranges: !self.lsp_folding_crease_ids.is_empty(),
fold_placeholder: self.fold_placeholder.clone(),
}
}
Expand Down Expand Up @@ -1332,6 +1336,63 @@ impl DisplayMap {
self.crease_map.remove(crease_ids, &snapshot)
}

/// Replaces the LSP folding-range creases for a single buffer.
/// Converts the supplied buffer-anchor ranges into multi-buffer creases
/// by mapping them through the appropriate excerpts.
pub(super) fn set_lsp_folding_ranges(
&mut self,
buffer_id: BufferId,
ranges: Vec<Range<text::Anchor>>,
cx: &mut Context<Self>,
) {
let snapshot = self.buffer.read(cx).snapshot(cx);

let old_ids = self
.lsp_folding_crease_ids
.remove(&buffer_id)
.unwrap_or_default();
if !old_ids.is_empty() {
self.crease_map.remove(old_ids, &snapshot);
}

if ranges.is_empty() {
return;
}

let excerpt_ids = snapshot
.excerpts()
.filter(|(_, buf, _)| buf.remote_id() == buffer_id)
.map(|(id, _, _)| id)
.collect::<Vec<_>>();

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 new_ids = self.crease_map.insert(creases, &snapshot);
if !new_ids.is_empty() {
self.lsp_folding_crease_ids.insert(buffer_id, new_ids);
}
}

/// Removes all LSP folding-range creases for a single buffer.
pub(super) fn clear_lsp_folding_ranges(&mut self, buffer_id: BufferId, cx: &mut Context<Self>) {
if let hash_map::Entry::Occupied(entry) = self.lsp_folding_crease_ids.entry(buffer_id) {
let old_ids = entry.remove();
let snapshot = self.buffer.read(cx).snapshot(cx);
self.crease_map.remove(old_ids, &snapshot);
}
}

/// Returns `true` when at least one buffer has LSP folding-range creases.
pub(super) fn has_lsp_folding_ranges(&self) -> bool {
!self.lsp_folding_crease_ids.is_empty()
}

#[instrument(skip_all)]
pub fn insert_blocks(
&mut self,
Expand Down Expand Up @@ -2000,6 +2061,9 @@ pub struct DisplaySnapshot {
masked: bool,
diagnostics_max_severity: DiagnosticSeverity,
pub(crate) fold_placeholder: FoldPlaceholder,
/// When true, LSP folding ranges are used via the crease map and the
/// indent-based fallback in `crease_for_buffer_row` is skipped.
pub(crate) use_lsp_folding_ranges: bool,
}

impl DisplaySnapshot {
Expand Down Expand Up @@ -2615,7 +2679,8 @@ impl DisplaySnapshot {
render_toggle: render_toggle.clone(),
}),
}
} else if self.starts_indent(MultiBufferRow(start.row))
} else if !self.use_lsp_folding_ranges
&& self.starts_indent(MultiBufferRow(start.row))
&& !self.is_line_folded(MultiBufferRow(start.row))
{
let start_line_indent = self.line_indent_for_buffer_row(buffer_row);
Expand Down
Loading