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
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions assets/settings/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -1114,6 +1114,13 @@
// - "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",

// Controls the source of document symbols used for outlines and breadcrumbs.
//
// Options:
// - "off": Use tree-sitter queries to compute document symbols (default).
// - "on": Use the language server's `textDocument/documentSymbol` LSP response. When enabled, tree-sitter is not used for document symbols.
"document_symbols": "off",

// When to automatically save edited buffers. This setting can
// take four values.
//
Expand Down
178 changes: 176 additions & 2 deletions crates/collab/tests/integration/editor_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ use recent_projects::disconnected_overlay::DisconnectedOverlay;
use rpc::RECEIVE_TIMEOUT;
use serde_json::json;
use settings::{
DocumentFoldingRanges, InlayHintSettingsContent, InlineBlameSettings, SemanticTokens,
SettingsStore,
DocumentFoldingRanges, DocumentSymbols, InlayHintSettingsContent, InlineBlameSettings,
SemanticTokens, SettingsStore,
};
use std::{
collections::BTreeSet,
Expand All @@ -51,6 +51,7 @@ use std::{
};
use text::Point;
use util::{path, rel_path::rel_path, uri};
use workspace::item::Item as _;
use workspace::{CloseIntent, Workspace};

#[gpui::test(iterations = 10)]
Expand Down Expand Up @@ -5503,6 +5504,179 @@ async fn test_remote_project_worktree_trust(cx_a: &mut TestAppContext, cx_b: &mu
);
}

#[gpui::test]
async fn test_document_symbols(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 {
document_symbol_provider: Some(lsp::OneOf::Left(true)),
..lsp::ServerCapabilities::default()
};
client_a.language_registry().add(rust_lang());
#[allow(deprecated)]
let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
"Rust",
FakeLspAdapter {
capabilities: capabilities.clone(),
initializer: Some(Box::new(|fake_language_server| {
#[allow(deprecated)]
fake_language_server
.set_request_handler::<lsp::request::DocumentSymbolRequest, _, _>(
move |_, _| async move {
Ok(Some(lsp::DocumentSymbolResponse::Nested(vec![
lsp::DocumentSymbol {
name: "Foo".to_string(),
detail: None,
kind: lsp::SymbolKind::STRUCT,
tags: None,
deprecated: None,
range: lsp::Range::new(
lsp::Position::new(0, 0),
lsp::Position::new(2, 1),
),
selection_range: lsp::Range::new(
lsp::Position::new(0, 7),
lsp::Position::new(0, 10),
),
children: Some(vec![lsp::DocumentSymbol {
name: "bar".to_string(),
detail: None,
kind: lsp::SymbolKind::FIELD,
tags: None,
deprecated: None,
range: lsp::Range::new(
lsp::Position::new(1, 4),
lsp::Position::new(1, 13),
),
selection_range: lsp::Range::new(
lsp::Position::new(1, 4),
lsp::Position::new(1, 7),
),
children: None,
}]),
},
])))
},
);
})),
..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": "struct Foo {\n bar: u32,\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 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();
executor.run_until_parked();

cx_a.update(|_, cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings(cx, |settings| {
settings.project.all_languages.defaults.document_symbols =
Some(DocumentSymbols::On);
});
});
});
executor.advance_clock(LSP_REQUEST_DEBOUNCE_TIMEOUT + Duration::from_millis(100));
executor.run_until_parked();

editor_a.update(cx_a, |editor, cx| {
let breadcrumbs = editor
.breadcrumbs(cx)
.expect("Host should have breadcrumbs");
let texts: Vec<_> = breadcrumbs.iter().map(|b| b.text.as_str()).collect();
assert_eq!(
texts,
vec!["main.rs", "Foo"],
"Host should see file path and LSP symbol 'Foo' in breadcrumbs"
);
});

cx_b.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings(cx, |settings| {
settings.project.all_languages.defaults.document_symbols =
Some(DocumentSymbols::On);
});
});
});
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.advance_clock(LSP_REQUEST_DEBOUNCE_TIMEOUT + Duration::from_millis(100));
executor.run_until_parked();

editor_b.update(cx_b, |editor, cx| {
let breadcrumbs = editor
.breadcrumbs(cx)
.expect("Client B should have breadcrumbs");
let texts: Vec<_> = breadcrumbs.iter().map(|b| b.text.as_str()).collect();
assert_eq!(
texts,
vec!["main.rs", "Foo"],
"Client B should see file path and LSP symbol 'Foo' via remote project"
);
});
}

fn blame_entry(sha: &str, range: Range<u32>) -> git::blame::BlameEntry {
git::blame::BlameEntry {
sha: sha.parse().unwrap(),
Expand Down
98 changes: 94 additions & 4 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, hash_map};
use collections::{HashMap, HashSet, IndexSet};
use gpui::{
App, Context, Entity, EntityId, Font, HighlightStyle, LineLayout, Pixels, UnderlineStyle,
WeakEntity,
Expand All @@ -106,7 +106,7 @@ use project::project_settings::DiagnosticSeverity;
use project::{InlayId, lsp_store::LspFoldingRange, lsp_store::TokenType};
use serde::Deserialize;
use sum_tree::{Bias, TreeMap};
use text::{BufferId, LineIndent, Patch};
use text::{BufferId, LineIndent, Patch, ToOffset as _};
use ui::{SharedString, px};
use unicode_segmentation::UnicodeSegmentation;
use ztracing::instrument;
Expand Down Expand Up @@ -1040,8 +1040,7 @@ impl DisplayMap {

/// 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();
if let Some(old_ids) = self.lsp_folding_crease_ids.remove(&buffer_id) {
let snapshot = self.buffer.read(cx).snapshot(cx);
self.crease_map.remove(old_ids, &snapshot);
}
Expand Down Expand Up @@ -1881,6 +1880,97 @@ impl DisplaySnapshot {
})
}

/// Returns combined highlight styles (tree-sitter syntax + semantic tokens)
/// for a byte range within the specified buffer.
/// Returned ranges are 0-based relative to `buffer_range.start`.
pub(super) fn combined_highlights(
&self,
buffer_id: BufferId,
buffer_range: Range<usize>,
syntax_theme: &theme::SyntaxTheme,
) -> Vec<(Range<usize>, HighlightStyle)> {
let multibuffer = self.buffer_snapshot();

let multibuffer_range = multibuffer
.excerpts()
.find_map(|(excerpt_id, buffer, range)| {
if buffer.remote_id() != buffer_id {
return None;
}
let context_start = range.context.start.to_offset(buffer);
let context_end = range.context.end.to_offset(buffer);
if buffer_range.start < context_start || buffer_range.end > context_end {
return None;
}
let start_anchor = buffer.anchor_before(buffer_range.start);
let end_anchor = buffer.anchor_after(buffer_range.end);
let mb_range =
multibuffer.anchor_range_in_excerpt(excerpt_id, start_anchor..end_anchor)?;
Some(mb_range.start.to_offset(multibuffer)..mb_range.end.to_offset(multibuffer))
});

let Some(multibuffer_range) = multibuffer_range else {
// Range is outside all excerpts (e.g. symbol name not in a
// multi-buffer excerpt). Fall back to buffer-level syntax highlights.
let buffer_snapshot = multibuffer.excerpts().find_map(|(_, buffer, _)| {
(buffer.remote_id() == buffer_id).then(|| buffer.clone())
});
let Some(buffer_snapshot) = buffer_snapshot else {
return Vec::new();
};
let mut highlights = Vec::new();
let mut offset = 0usize;
for chunk in buffer_snapshot.chunks(buffer_range, true) {
let chunk_len = chunk.text.len();
if chunk_len == 0 {
continue;
}
if let Some(style) = chunk
.syntax_highlight_id
.and_then(|id| id.style(syntax_theme))
{
highlights.push((offset..offset + chunk_len, style));
}
offset += chunk_len;
}
return highlights;
};

let chunks = custom_highlights::CustomHighlightsChunks::new(
multibuffer_range,
true,
None,
Some(&self.semantic_token_highlights),
multibuffer,
);

let mut highlights = Vec::new();
let mut offset = 0usize;
for chunk in chunks {
let chunk_len = chunk.text.len();
if chunk_len == 0 {
continue;
}

let syntax_style = chunk
.syntax_highlight_id
.and_then(|id| id.style(syntax_theme));
let overlay_style = chunk.highlight_style;

let combined = match (syntax_style, overlay_style) {
(Some(syntax), Some(overlay)) => Some(syntax.highlight(overlay)),
(some @ Some(_), None) | (None, some @ Some(_)) => some,
(None, None) => None,
};

if let Some(style) = combined {
highlights.push((offset..offset + chunk_len, style));
}
offset += chunk_len;
}
highlights
}

#[instrument(skip_all)]
pub fn layout_row(
&self,
Expand Down
2 changes: 1 addition & 1 deletion crates/editor/src/document_colors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ impl LspColorData {
}

impl Editor {
pub(super) fn refresh_colors_for_visible_range(
pub(super) fn refresh_document_colors(
&mut self,
buffer_id: Option<BufferId>,
_: &Window,
Expand Down
Loading