Skip to content

Commit 3d8c23a

Browse files
authored
Merge pull request #866 from posit-dev/feature/optional-section-workspace-symbols
Exclude comment sections from workspace symbols by default
2 parents 8336b32 + 40e9c0f commit 3d8c23a

File tree

9 files changed

+287
-21
lines changed

9 files changed

+287
-21
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ark/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ tracing-error = "0.2.0"
6666
insta = { version = "1.39.0" }
6767
stdext = { path = "../stdext", features = ["testing"] }
6868
tempfile = "3.13.0"
69+
assert_matches = "1.5.0"
6970

7071
[build-dependencies]
7172
cc = "1.1.22"

crates/ark/src/lsp/config.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@ pub static GLOBAL_SETTINGS: &[Setting<LspConfig>] = &[
3333
.unwrap_or_else(|| SymbolsConfig::default().include_assignments_in_blocks)
3434
},
3535
},
36+
Setting {
37+
key: "positron.r.workspaceSymbols.includeCommentSections",
38+
set: |cfg, v| {
39+
cfg.workspace_symbols.include_comment_sections = v
40+
.as_bool()
41+
.unwrap_or_else(|| WorkspaceSymbolsConfig::default().include_comment_sections)
42+
},
43+
},
3644
];
3745

3846
/// These document settings are updated on a URI basis. Each document has its
@@ -77,6 +85,7 @@ pub static DOCUMENT_SETTINGS: &[Setting<DocumentConfig>] = &[
7785
pub(crate) struct LspConfig {
7886
pub(crate) diagnostics: DiagnosticsConfig,
7987
pub(crate) symbols: SymbolsConfig,
88+
pub(crate) workspace_symbols: WorkspaceSymbolsConfig,
8089
}
8190

8291
#[derive(Serialize, Deserialize, Clone, Debug)]
@@ -85,6 +94,12 @@ pub struct SymbolsConfig {
8594
pub include_assignments_in_blocks: bool,
8695
}
8796

97+
#[derive(Serialize, Deserialize, Clone, Debug)]
98+
pub struct WorkspaceSymbolsConfig {
99+
/// Whether to include sections like `# My section ---` in workspace symbols.
100+
pub include_comment_sections: bool,
101+
}
102+
88103
/// Configuration of a document.
89104
///
90105
/// The naming follows <https://editorconfig.org/> where possible.
@@ -120,6 +135,14 @@ impl Default for SymbolsConfig {
120135
}
121136
}
122137

138+
impl Default for WorkspaceSymbolsConfig {
139+
fn default() -> Self {
140+
Self {
141+
include_comment_sections: false,
142+
}
143+
}
144+
}
145+
123146
impl Default for IndentationConfig {
124147
fn default() -> Self {
125148
Self {

crates/ark/src/lsp/definitions.rs

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use crate::lsp::traits::node::NodeExt;
2020
use crate::lsp::traits::rope::RopeExt;
2121
use crate::treesitter::NodeTypeExt;
2222

23-
pub unsafe fn goto_definition<'a>(
23+
pub fn goto_definition<'a>(
2424
document: &'a Document,
2525
params: GotoDefinitionParams,
2626
) -> Result<Option<GotoDefinitionResponse>> {
@@ -75,3 +75,88 @@ pub unsafe fn goto_definition<'a>(
7575
let response = GotoDefinitionResponse::Link(vec![link]);
7676
Ok(Some(response))
7777
}
78+
79+
#[cfg(test)]
80+
mod tests {
81+
use assert_matches::assert_matches;
82+
use tower_lsp::lsp_types;
83+
84+
use super::*;
85+
use crate::lsp::documents::Document;
86+
use crate::lsp::indexer;
87+
use crate::lsp::util::test_path;
88+
89+
#[test]
90+
fn test_goto_definition() {
91+
let _guard = indexer::ResetIndexerGuard;
92+
93+
let code = r#"
94+
foo <- 42
95+
print(foo)
96+
"#;
97+
let doc = Document::new(code, None);
98+
let (path, uri) = test_path();
99+
100+
indexer::update(&doc, &path).unwrap();
101+
102+
let params = GotoDefinitionParams {
103+
text_document_position_params: lsp_types::TextDocumentPositionParams {
104+
text_document: lsp_types::TextDocumentIdentifier { uri },
105+
position: lsp_types::Position::new(2, 7),
106+
},
107+
work_done_progress_params: Default::default(),
108+
partial_result_params: Default::default(),
109+
};
110+
111+
assert_matches!(
112+
goto_definition(&doc, params).unwrap(),
113+
Some(GotoDefinitionResponse::Link(ref links)) => {
114+
assert_eq!(
115+
links[0].target_range,
116+
lsp_types::Range {
117+
start: lsp_types::Position::new(1, 0),
118+
end: lsp_types::Position::new(1, 3),
119+
}
120+
);
121+
}
122+
);
123+
}
124+
125+
#[test]
126+
fn test_goto_definition_comment_section() {
127+
let _guard = indexer::ResetIndexerGuard;
128+
129+
let code = r#"
130+
# foo ----
131+
foo <- 1
132+
print(foo)
133+
"#;
134+
let doc = Document::new(code, None);
135+
let (path, uri) = test_path();
136+
137+
indexer::update(&doc, &path).unwrap();
138+
139+
let params = lsp_types::GotoDefinitionParams {
140+
text_document_position_params: lsp_types::TextDocumentPositionParams {
141+
text_document: lsp_types::TextDocumentIdentifier { uri },
142+
position: lsp_types::Position::new(3, 7),
143+
},
144+
work_done_progress_params: Default::default(),
145+
partial_result_params: Default::default(),
146+
};
147+
148+
assert_matches!(
149+
goto_definition(&doc, params).unwrap(),
150+
Some(lsp_types::GotoDefinitionResponse::Link(ref links)) => {
151+
// The section should is not the target, the variable has priority
152+
assert_eq!(
153+
links[0].target_range,
154+
lsp_types::Range {
155+
start: lsp_types::Position::new(2, 0),
156+
end: lsp_types::Position::new(2, 3),
157+
}
158+
);
159+
}
160+
);
161+
}
162+
}

crates/ark/src/lsp/handlers.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,9 @@ pub(crate) async fn handle_initialized(
129129
#[tracing::instrument(level = "info", skip_all)]
130130
pub(crate) fn handle_symbol(
131131
params: WorkspaceSymbolParams,
132+
state: &WorldState,
132133
) -> anyhow::Result<Option<Vec<SymbolInformation>>> {
133-
symbols::symbols(&params)
134+
symbols::symbols(&params, state)
134135
.map(|res| Some(res))
135136
.or_else(|err| {
136137
// Missing doc: Why are we not propagating errors to the frontend?
@@ -288,7 +289,7 @@ pub(crate) fn handle_goto_definition(
288289
let document = state.get_document(uri)?;
289290

290291
// build goto definition context
291-
let result = unwrap!(unsafe { goto_definition(&document, params) }, Err(err) => {
292+
let result = unwrap!(goto_definition(&document, params), Err(err) => {
292293
lsp::log_error!("{err:?}");
293294
return Ok(None);
294295
});

crates/ark/src/lsp/indexer.rs

Lines changed: 98 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -125,15 +125,25 @@ fn insert(path: &Path, entry: IndexEntry) -> anyhow::Result<()> {
125125
let path = str_from_path(path)?;
126126

127127
let index = index.entry(path.to_string()).or_default();
128+
index_insert(index, entry);
128129

129-
// Retain the first occurrence in the index. In the future we'll track every occurrences and
130-
// their scopes but for now we only track the first definition of an object (in a way, its
130+
Ok(())
131+
}
132+
133+
fn index_insert(index: &mut HashMap<String, IndexEntry>, entry: IndexEntry) {
134+
// We generally retain only the first occurrence in the index. In the
135+
// future we'll track every occurrences and their scopes but for now we
136+
// only track the first definition of an object (in a way, its
131137
// declaration).
132-
if !index.contains_key(&entry.key) {
138+
if let Some(existing_entry) = index.get(&entry.key) {
139+
// Give priority to non-section entries.
140+
if matches!(existing_entry.data, IndexEntryData::Section { .. }) {
141+
index.insert(entry.key.clone(), entry);
142+
}
143+
// Else, ignore.
144+
} else {
133145
index.insert(entry.key.clone(), entry);
134146
}
135-
136-
Ok(())
137147
}
138148

139149
fn clear(path: &Path) -> anyhow::Result<()> {
@@ -148,6 +158,24 @@ fn clear(path: &Path) -> anyhow::Result<()> {
148158
Ok(())
149159
}
150160

161+
#[cfg(test)]
162+
pub(crate) fn indexer_clear() {
163+
let mut index = WORKSPACE_INDEX.lock().unwrap();
164+
index.clear();
165+
}
166+
167+
/// RAII guard that clears `WORKSPACE_INDEX` when dropped.
168+
/// Useful for ensuring a clean index state in tests.
169+
#[cfg(test)]
170+
pub(crate) struct ResetIndexerGuard;
171+
172+
#[cfg(test)]
173+
impl Drop for ResetIndexerGuard {
174+
fn drop(&mut self) {
175+
indexer_clear();
176+
}
177+
}
178+
151179
fn str_from_path(path: &Path) -> anyhow::Result<&str> {
152180
path.to_str().ok_or(anyhow!(
153181
"Couldn't convert path {} to string",
@@ -401,7 +429,9 @@ fn index_comment(
401429
mod tests {
402430
use std::path::PathBuf;
403431

432+
use assert_matches::assert_matches;
404433
use insta::assert_debug_snapshot;
434+
use tower_lsp::lsp_types;
405435

406436
use super::*;
407437
use crate::lsp::documents::Document;
@@ -532,4 +562,67 @@ class <- R6::R6Class(
532562
"#
533563
);
534564
}
565+
566+
#[test]
567+
fn test_index_insert_priority() {
568+
let mut index = HashMap::new();
569+
570+
let section_entry = IndexEntry {
571+
key: "foo".to_string(),
572+
range: Range::new(
573+
lsp_types::Position::new(0, 0),
574+
lsp_types::Position::new(0, 3),
575+
),
576+
data: IndexEntryData::Section {
577+
level: 1,
578+
title: "foo".to_string(),
579+
},
580+
};
581+
582+
let variable_entry = IndexEntry {
583+
key: "foo".to_string(),
584+
range: Range::new(
585+
lsp_types::Position::new(1, 0),
586+
lsp_types::Position::new(1, 3),
587+
),
588+
data: IndexEntryData::Variable {
589+
name: "foo".to_string(),
590+
},
591+
};
592+
593+
// The Variable has priority and should replace the Section
594+
index_insert(&mut index, section_entry.clone());
595+
index_insert(&mut index, variable_entry.clone());
596+
assert_matches!(
597+
&index.get("foo").unwrap().data,
598+
IndexEntryData::Variable { name } => assert_eq!(name, "foo")
599+
);
600+
601+
// Inserting a Section again with the same key does not override the Variable
602+
index_insert(&mut index, section_entry.clone());
603+
assert_matches!(
604+
&index.get("foo").unwrap().data,
605+
IndexEntryData::Variable { name } => assert_eq!(name, "foo")
606+
);
607+
608+
let function_entry = IndexEntry {
609+
key: "foo".to_string(),
610+
range: Range::new(
611+
lsp_types::Position::new(2, 0),
612+
lsp_types::Position::new(2, 3),
613+
),
614+
data: IndexEntryData::Function {
615+
name: "foo".to_string(),
616+
arguments: vec!["a".to_string()],
617+
},
618+
};
619+
620+
// Inserting another kind of variable (e.g., Function) with the same key
621+
// does not override it either. The first occurrence is generally retained.
622+
index_insert(&mut index, function_entry.clone());
623+
assert_matches!(
624+
&index.get("foo").unwrap().data,
625+
IndexEntryData::Variable { name } => assert_eq!(name, "foo")
626+
);
627+
}
535628
}

crates/ark/src/lsp/main_loop.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ impl GlobalState {
281281
respond(tx, || state_handlers::initialize(params, &mut self.lsp_state, &mut self.world), LspResponse::Initialize)?;
282282
},
283283
LspRequest::WorkspaceSymbol(params) => {
284-
respond(tx, || handlers::handle_symbol(params), LspResponse::WorkspaceSymbol)?;
284+
respond(tx, || handlers::handle_symbol(params, &self.world), LspResponse::WorkspaceSymbol)?;
285285
},
286286
LspRequest::DocumentSymbol(params) => {
287287
respond(tx, || handlers::handle_document_symbol(params, &self.world), LspResponse::DocumentSymbol)?;

0 commit comments

Comments
 (0)