From 0704a05986ceb3e227dfb8dc6b6f0e484c170229 Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Fri, 27 Jun 2025 14:30:48 +0200 Subject: [PATCH 1/6] Add `CollectContect` arguments --- crates/ark/src/lsp/symbols.rs | 48 +++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/crates/ark/src/lsp/symbols.rs b/crates/ark/src/lsp/symbols.rs index 5c7e01869..c1cf1c852 100644 --- a/crates/ark/src/lsp/symbols.rs +++ b/crates/ark/src/lsp/symbols.rs @@ -122,6 +122,8 @@ struct Section { children: Vec, } +struct CollectContext; + pub(crate) fn document_symbols( state: &WorldState, params: &DocumentSymbolParams, @@ -136,7 +138,8 @@ pub(crate) fn document_symbols( let mut result = Vec::new(); // Extract and process all symbols from the AST - if let Err(err) = collect_symbols(&root_node, contents, 0, &mut result) { + let mut ctx = CollectContext; + if let Err(err) = collect_symbols(&mut ctx, &root_node, contents, 0, &mut result) { log::error!("Failed to collect symbols: {err:?}"); return Ok(Vec::new()); } @@ -146,6 +149,7 @@ pub(crate) fn document_symbols( /// Collect all document symbols from a node recursively fn collect_symbols( + ctx: &mut CollectContext, node: &Node, contents: &Rope, current_level: usize, @@ -153,26 +157,26 @@ fn collect_symbols( ) -> anyhow::Result<()> { match node.node_type() { NodeType::Program | NodeType::BracedExpression => { - collect_sections(node, contents, current_level, symbols)?; + collect_sections(ctx, node, contents, current_level, symbols)?; }, NodeType::Call => { - collect_call(node, contents, symbols)?; + collect_call(ctx, node, contents, symbols)?; }, NodeType::BinaryOperator(BinaryOperatorType::LeftAssignment) | NodeType::BinaryOperator(BinaryOperatorType::EqualsAssignment) => { - collect_assignment(node, contents, symbols)?; + collect_assignment(ctx, node, contents, symbols)?; }, // For all other node types, no symbols need to be added _ => {}, } - Ok(()) } fn collect_sections( + ctx: &mut CollectContext, node: &Node, contents: &Rope, current_level: usize, @@ -224,11 +228,11 @@ fn collect_sections( if active_sections.is_empty() { // If no active section, extend current vector of symbols - collect_symbols(&child, contents, current_level, symbols)?; + collect_symbols(ctx, &child, contents, current_level, symbols)?; } else { // Otherwise create new store of symbols for the current section let mut child_symbols = Vec::new(); - collect_symbols(&child, contents, current_level, &mut child_symbols)?; + collect_symbols(ctx, &child, contents, current_level, &mut child_symbols)?; // Nest them inside last section if !child_symbols.is_empty() { @@ -258,6 +262,7 @@ fn collect_sections( } fn collect_call( + ctx: &mut CollectContext, node: &Node, contents: &Rope, symbols: &mut Vec, @@ -268,19 +273,19 @@ fn collect_call( if callee.is_identifier() { let fun_symbol = contents.node_slice(&callee)?.to_string(); - match fun_symbol.as_str() { - "test_that" => return collect_call_test_that(node, contents, symbols), + "test_that" => return collect_call_test_that(ctx, node, contents, symbols), _ => {}, // fallthrough } } - collect_call_arguments(node, contents, symbols)?; + collect_call_arguments(ctx, node, contents, symbols)?; Ok(()) } fn collect_call_arguments( + ctx: &mut CollectContext, node: &Node, contents: &Rope, symbols: &mut Vec, @@ -299,17 +304,17 @@ fn collect_call_arguments( "function_definition" => { if let Some(arg_fun) = arg.child_by_field_name("name") { // If this is a named function, collect it as a method - collect_method(&arg_fun, &arg_value, contents, symbols)?; + collect_method(ctx, &arg_fun, &arg_value, contents, symbols)?; } else { // Otherwise, just recurse into the function let body = arg_value.child_by_field_name("body").into_result()?; - collect_symbols(&body, contents, 0, symbols)?; + collect_symbols(ctx, &body, contents, 0, symbols)?; }; }, _ => { // Recurse into arguments. They might be a braced list, another call // that might contain functions, etc. - collect_symbols(&arg_value, contents, 0, symbols)?; + collect_symbols(ctx, &arg_value, contents, 0, symbols)?; }, } } @@ -318,6 +323,7 @@ fn collect_call_arguments( } fn collect_method( + ctx: &mut CollectContext, arg_fun: &Node, arg_value: &Node, contents: &Rope, @@ -333,7 +339,7 @@ fn collect_method( let body = arg_value.child_by_field_name("body").into_result()?; let mut children = vec![]; - collect_symbols(&body, contents, 0, &mut children)?; + collect_symbols(ctx, &body, contents, 0, &mut children)?; let mut symbol = new_symbol_node( arg_name_str, @@ -354,6 +360,7 @@ fn collect_method( // https://github.com/posit-dev/positron/issues/1428 fn collect_call_test_that( + ctx: &mut CollectContext, node: &Node, contents: &Rope, symbols: &mut Vec, @@ -380,7 +387,7 @@ fn collect_call_test_that( let mut cursor = arguments.walk(); for child in arguments.children_by_field_name("argument", &mut cursor) { if let Some(value) = child.child_by_field_name("value") { - collect_symbols(&value, contents, 0, &mut children)?; + collect_symbols(ctx, &value, contents, 0, &mut children)?; } } @@ -397,6 +404,7 @@ fn collect_call_test_that( } fn collect_assignment( + ctx: &mut CollectContext, node: &Node, contents: &Rope, symbols: &mut Vec, @@ -417,7 +425,7 @@ fn collect_assignment( // If a function, collect symbol as function let function = lhs.is_identifier_or_string() && rhs.is_function_definition(); if function { - return collect_assignment_with_function(node, contents, symbols); + return collect_assignment_with_function(ctx, node, contents, symbols); } // Otherwise, collect as generic object @@ -428,7 +436,7 @@ fn collect_assignment( // Now recurse into RHS let mut children = Vec::new(); - collect_symbols(&rhs, contents, 0, &mut children)?; + collect_symbols(ctx, &rhs, contents, 0, &mut children)?; let symbol = new_symbol_node(name, SymbolKind::VARIABLE, Range { start, end }, children); symbols.push(symbol); @@ -437,6 +445,7 @@ fn collect_assignment( } fn collect_assignment_with_function( + ctx: &mut CollectContext, node: &Node, contents: &Rope, symbols: &mut Vec, @@ -468,7 +477,7 @@ fn collect_assignment_with_function( // Process the function body to extract child symbols let mut children = Vec::new(); - collect_symbols(&body, contents, 0, &mut children)?; + collect_symbols(ctx, &body, contents, 0, &mut children)?; let mut symbol = new_symbol_node(name, SymbolKind::FUNCTION, range, children); symbol.detail = Some(detail); @@ -535,7 +544,8 @@ mod tests { let node = doc.ast.root_node(); let mut symbols = Vec::new(); - collect_symbols(&node, &doc.contents, 0, &mut symbols).unwrap(); + let mut ctx = CollectContext; + collect_symbols(&mut ctx, &node, &doc.contents, 0, &mut symbols).unwrap(); symbols } From 32059407cab39ce540e134237079be64a42a18e9 Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Fri, 27 Jun 2025 15:03:21 +0200 Subject: [PATCH 2/6] Don't emit assigned objects in nested contexts as document symbols --- ...ts__symbol_assignment_function_nested.snap | 71 ++++++++++ ...ols__tests__symbol_nested_assignments.snap | 71 ++++++++++ crates/ark/src/lsp/symbols.rs | 123 +++++++++--------- 3 files changed, 206 insertions(+), 59 deletions(-) create mode 100644 crates/ark/src/lsp/snapshots/ark__lsp__symbols__tests__symbol_assignment_function_nested.snap create mode 100644 crates/ark/src/lsp/snapshots/ark__lsp__symbols__tests__symbol_nested_assignments.snap diff --git a/crates/ark/src/lsp/snapshots/ark__lsp__symbols__tests__symbol_assignment_function_nested.snap b/crates/ark/src/lsp/snapshots/ark__lsp__symbols__tests__symbol_assignment_function_nested.snap new file mode 100644 index 000000000..c12ca2b32 --- /dev/null +++ b/crates/ark/src/lsp/snapshots/ark__lsp__symbols__tests__symbol_assignment_function_nested.snap @@ -0,0 +1,71 @@ +--- +source: crates/ark/src/lsp/symbols.rs +expression: "test_symbol(\"foo <- function() { bar <- function() 1 }\")" +--- +[ + DocumentSymbol { + name: "foo", + detail: Some( + "function()", + ), + kind: Function, + tags: None, + deprecated: None, + range: Range { + start: Position { + line: 0, + character: 0, + }, + end: Position { + line: 0, + character: 41, + }, + }, + selection_range: Range { + start: Position { + line: 0, + character: 0, + }, + end: Position { + line: 0, + character: 41, + }, + }, + children: Some( + [ + DocumentSymbol { + name: "bar", + detail: Some( + "function()", + ), + kind: Function, + tags: None, + deprecated: None, + range: Range { + start: Position { + line: 0, + character: 20, + }, + end: Position { + line: 0, + character: 39, + }, + }, + selection_range: Range { + start: Position { + line: 0, + character: 20, + }, + end: Position { + line: 0, + character: 39, + }, + }, + children: Some( + [], + ), + }, + ], + ), + }, +] diff --git a/crates/ark/src/lsp/snapshots/ark__lsp__symbols__tests__symbol_nested_assignments.snap b/crates/ark/src/lsp/snapshots/ark__lsp__symbols__tests__symbol_nested_assignments.snap new file mode 100644 index 000000000..4cc8ab1c1 --- /dev/null +++ b/crates/ark/src/lsp/snapshots/ark__lsp__symbols__tests__symbol_nested_assignments.snap @@ -0,0 +1,71 @@ +--- +source: crates/ark/src/lsp/symbols.rs +expression: "test_symbol(\"\nlocal({\n inner1 <- 1 # Not a symbol\n})\na <- function() {\n inner2 <- 2 # Not a symbol\n inner3 <- function() 3 # Symbol\n}\n\")" +--- +[ + DocumentSymbol { + name: "a", + detail: Some( + "function()", + ), + kind: Function, + tags: None, + deprecated: None, + range: Range { + start: Position { + line: 4, + character: 0, + }, + end: Position { + line: 7, + character: 1, + }, + }, + selection_range: Range { + start: Position { + line: 4, + character: 0, + }, + end: Position { + line: 7, + character: 1, + }, + }, + children: Some( + [ + DocumentSymbol { + name: "inner3", + detail: Some( + "function()", + ), + kind: Function, + tags: None, + deprecated: None, + range: Range { + start: Position { + line: 6, + character: 2, + }, + end: Position { + line: 6, + character: 24, + }, + }, + selection_range: Range { + start: Position { + line: 6, + character: 2, + }, + end: Position { + line: 6, + character: 24, + }, + }, + children: Some( + [], + ), + }, + ], + ), + }, +] diff --git a/crates/ark/src/lsp/symbols.rs b/crates/ark/src/lsp/symbols.rs index c1cf1c852..48f0cf825 100644 --- a/crates/ark/src/lsp/symbols.rs +++ b/crates/ark/src/lsp/symbols.rs @@ -122,7 +122,15 @@ struct Section { children: Vec, } -struct CollectContext; +struct CollectContext { + top_level: bool, +} + +impl CollectContext { + fn new() -> Self { + Self { top_level: true } + } +} pub(crate) fn document_symbols( state: &WorldState, @@ -138,8 +146,13 @@ pub(crate) fn document_symbols( let mut result = Vec::new(); // Extract and process all symbols from the AST - let mut ctx = CollectContext; - if let Err(err) = collect_symbols(&mut ctx, &root_node, contents, 0, &mut result) { + if let Err(err) = collect_symbols( + &mut CollectContext::new(), + &root_node, + contents, + 0, + &mut result, + ) { log::error!("Failed to collect symbols: {err:?}"); return Ok(Vec::new()); } @@ -156,7 +169,12 @@ fn collect_symbols( symbols: &mut Vec, ) -> anyhow::Result<()> { match node.node_type() { - NodeType::Program | NodeType::BracedExpression => { + NodeType::Program => { + collect_sections(ctx, node, contents, current_level, symbols)?; + }, + + NodeType::BracedExpression => { + ctx.top_level = false; collect_sections(ctx, node, contents, current_level, symbols)?; }, @@ -428,18 +446,25 @@ fn collect_assignment( return collect_assignment_with_function(ctx, node, contents, symbols); } - // Otherwise, collect as generic object - let name = contents.node_slice(&lhs)?.to_string(); + if ctx.top_level { + // Collect as generic object, but only if we're at top-level. Assigned + // objects in nested functions and blocks cause the outline to become + // too busy. + let name = contents.node_slice(&lhs)?.to_string(); - let start = convert_point_to_position(contents, lhs.start_position()); - let end = convert_point_to_position(contents, lhs.end_position()); + let start = convert_point_to_position(contents, lhs.start_position()); + let end = convert_point_to_position(contents, lhs.end_position()); - // Now recurse into RHS - let mut children = Vec::new(); - collect_symbols(ctx, &rhs, contents, 0, &mut children)?; + // Now recurse into RHS + let mut children = Vec::new(); + collect_symbols(ctx, &rhs, contents, 0, &mut children)?; - let symbol = new_symbol_node(name, SymbolKind::VARIABLE, Range { start, end }, children); - symbols.push(symbol); + let symbol = new_symbol_node(name, SymbolKind::VARIABLE, Range { start, end }, children); + symbols.push(symbol); + } else { + // Recurse into RHS + collect_symbols(ctx, &rhs, contents, 0, symbols)?; + } Ok(()) } @@ -544,8 +569,14 @@ mod tests { let node = doc.ast.root_node(); let mut symbols = Vec::new(); - let mut ctx = CollectContext; - collect_symbols(&mut ctx, &node, &doc.contents, 0, &mut symbols).unwrap(); + collect_symbols( + &mut CollectContext::new(), + &node, + &doc.contents, + 0, + &mut symbols, + ) + .unwrap(); symbols } @@ -623,33 +654,7 @@ mod tests { #[test] fn test_symbol_assignment_function_nested() { - let range = Range { - start: Position { - line: 0, - character: 20, - }, - end: Position { - line: 0, - character: 23, - }, - }; - let bar = new_symbol(String::from("bar"), SymbolKind::VARIABLE, range); - - let range = Range { - start: Position { - line: 0, - character: 0, - }, - end: Position { - line: 0, - character: 30, - }, - }; - let mut foo = new_symbol(String::from("foo"), SymbolKind::FUNCTION, range); - foo.children = Some(vec![bar]); - foo.detail = Some(String::from("function()")); - - assert_eq!(test_symbol("foo <- function() { bar <- 1 }"), vec![foo]); + insta::assert_debug_snapshot!(test_symbol("foo <- function() { bar <- function() 1 }")); } #[test] @@ -667,23 +672,6 @@ foo <- function() { )); } - #[test] - fn test_symbol_braced_list() { - let range = Range { - start: Position { - line: 0, - character: 2, - }, - end: Position { - line: 0, - character: 5, - }, - }; - let foo = new_symbol(String::from("foo"), SymbolKind::VARIABLE, range); - - assert_eq!(test_symbol("{ foo <- 1 }"), vec![foo]); - } - #[test] fn test_symbol_section_ranges_extend() { let symbols = test_symbol( @@ -843,4 +831,21 @@ class <- r6::r6class( " )); } + + #[test] + // Assigned variables in nested contexts are not emitted as symbols + fn test_symbol_nested_assignments() { + insta::assert_debug_snapshot!(test_symbol( + " +local({ + inner1 <- 1 # Not a symbol +}) +a <- function() { + inner2 <- 2 # Not a symbol + inner3 <- function() 3 # Symbol +} +" + )); + assert_eq!(test_symbol("{ foo <- 1 }"), vec![]); + } } From be4621342d20228cef31ec375618842ee88e9f24 Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Fri, 4 Jul 2025 14:47:05 +0200 Subject: [PATCH 3/6] Add symbols config --- crates/ark/src/lsp/config.rs | 36 +++++++++++++++++++++++++--- crates/ark/src/lsp/handlers.rs | 6 +++++ crates/ark/src/lsp/state_handlers.rs | 31 +++++++++++++++++++++++- 3 files changed, 69 insertions(+), 4 deletions(-) diff --git a/crates/ark/src/lsp/config.rs b/crates/ark/src/lsp/config.rs index d9f51ed90..e7041b048 100644 --- a/crates/ark/src/lsp/config.rs +++ b/crates/ark/src/lsp/config.rs @@ -6,9 +6,16 @@ use crate::lsp; use crate::lsp::diagnostics::DiagnosticsConfig; /// Configuration of the LSP -#[derive(Clone, Debug)] +#[derive(Clone, Default, Debug)] pub(crate) struct LspConfig { pub(crate) diagnostics: DiagnosticsConfig, + pub(crate) symbols: SymbolsConfig, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct SymbolsConfig { + /// Whether to emit assignments in `{` bloks as document symbols. + pub include_assignments_in_blocks: bool, } /// Configuration of a document. @@ -53,6 +60,12 @@ pub(crate) struct VscDiagnosticsConfig { pub enable: bool, } +#[derive(Serialize, Deserialize, FieldNamesAsArray, Clone, Debug)] +pub(crate) struct VscSymbolsConfig { + // DEV NOTE: Update `section_from_key()` method after adding a field + pub include_assignments_in_blocks: bool, +} + #[derive(Serialize, Deserialize, Clone, Debug)] #[serde(untagged)] pub(crate) enum VscIndentSize { @@ -60,10 +73,10 @@ pub(crate) enum VscIndentSize { Size(usize), } -impl Default for LspConfig { +impl Default for SymbolsConfig { fn default() -> Self { Self { - diagnostics: Default::default(), + include_assignments_in_blocks: false, } } } @@ -134,6 +147,23 @@ impl From for DiagnosticsConfig { } } +impl VscSymbolsConfig { + pub(crate) fn section_from_key(key: &str) -> &str { + match key { + "include_assignments_in_blocks" => "positron.r.symbols.includeAssignmentsInBlocks", + _ => "unknown", // To be caught via downstream errors + } + } +} + +impl From for SymbolsConfig { + fn from(value: VscSymbolsConfig) -> Self { + Self { + include_assignments_in_blocks: value.include_assignments_in_blocks, + } + } +} + pub(crate) fn indent_style_from_lsp(insert_spaces: bool) -> IndentStyle { if insert_spaces { IndentStyle::Space diff --git a/crates/ark/src/lsp/handlers.rs b/crates/ark/src/lsp/handlers.rs index d6a6ea118..7cb115e7c 100644 --- a/crates/ark/src/lsp/handlers.rs +++ b/crates/ark/src/lsp/handlers.rs @@ -48,6 +48,7 @@ use crate::lsp::completions::provide_completions; use crate::lsp::completions::resolve_completion; use crate::lsp::config::VscDiagnosticsConfig; use crate::lsp::config::VscDocumentConfig; +use crate::lsp::config::VscSymbolsConfig; use crate::lsp::definitions::goto_definition; use crate::lsp::document_context::DocumentContext; use crate::lsp::encoding::convert_lsp_range_to_tree_sitter_range; @@ -109,12 +110,17 @@ pub(crate) async fn handle_initialized( VscDocumentConfig::FIELD_NAMES_AS_ARRAY.to_vec(), VscDocumentConfig::section_from_key, ); + let mut config_symbols_regs: Vec = collect_regs( + VscSymbolsConfig::FIELD_NAMES_AS_ARRAY.to_vec(), + VscSymbolsConfig::section_from_key, + ); let mut config_diagnostics_regs: Vec = collect_regs( VscDiagnosticsConfig::FIELD_NAMES_AS_ARRAY.to_vec(), VscDiagnosticsConfig::section_from_key, ); regs.append(&mut config_document_regs); + regs.append(&mut config_symbols_regs); regs.append(&mut config_diagnostics_regs); } diff --git a/crates/ark/src/lsp/state_handlers.rs b/crates/ark/src/lsp/state_handlers.rs index c5f2a20e0..1bca6fcc0 100644 --- a/crates/ark/src/lsp/state_handlers.rs +++ b/crates/ark/src/lsp/state_handlers.rs @@ -43,8 +43,10 @@ use crate::lsp; use crate::lsp::capabilities::Capabilities; use crate::lsp::config::indent_style_from_lsp; use crate::lsp::config::DocumentConfig; +use crate::lsp::config::SymbolsConfig; use crate::lsp::config::VscDiagnosticsConfig; use crate::lsp::config::VscDocumentConfig; +use crate::lsp::config::VscSymbolsConfig; use crate::lsp::diagnostics::DiagnosticsConfig; use crate::lsp::documents::Document; use crate::lsp::encoding::get_position_encoding_kind; @@ -244,6 +246,9 @@ pub(crate) async fn did_change_configuration( // we should just ignore it. Instead we need to pull the settings again for // all URI of interest. + // Note that the client sends notifications for settings for which we have + // declared interest in. This registration is done in `handle_initialized()`. + update_config(workspace_uris(state), client, state) .instrument(tracing::info_span!("did_change_configuration")) .await @@ -296,6 +301,16 @@ async fn update_config( .collect(); items.append(&mut diagnostics_items); + let symbols_keys = VscSymbolsConfig::FIELD_NAMES_AS_ARRAY; + let mut symbols_items: Vec = symbols_keys + .iter() + .map(|key| ConfigurationItem { + scope_uri: None, + section: Some(VscSymbolsConfig::section_from_key(key).into()), + }) + .collect(); + items.append(&mut symbols_items); + // For document configs we collect all pairs of URIs and config keys of // interest in a flat vector let document_keys = VscDocumentConfig::FIELD_NAMES_AS_ARRAY; @@ -316,7 +331,8 @@ async fn update_config( // by chunk let n_document_items = document_keys.len(); let n_diagnostics_items = diagnostics_keys.len(); - let n_items = n_diagnostics_items + (n_document_items * uris.len()); + let n_symbols_items = symbols_keys.len(); + let n_items = n_diagnostics_items + n_symbols_items + (n_document_items * uris.len()); if configs.len() != n_items { return Err(anyhow!( @@ -351,6 +367,19 @@ async fn update_config( lsp::spawn_diagnostics_refresh_all(state.clone()); } + // --- Symbols + let keys = symbols_keys.into_iter(); + let items: Vec = configs.by_ref().take(n_symbols_items).collect(); + + let mut map = serde_json::Map::new(); + std::iter::zip(keys, items).for_each(|(key, item)| { + map.insert(key.into(), item); + }); + + let config: VscSymbolsConfig = serde_json::from_value(serde_json::Value::Object(map))?; + let config: SymbolsConfig = config.into(); + state.config.symbols = config; + // --- Documents // For each document, deserialise the vector of JSON values into a typed config for uri in uris.into_iter() { From d54cd6d9fcfa6fbbfdfc4d2f8c96e914eb943fd1 Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Sat, 5 Jul 2025 09:35:52 +0200 Subject: [PATCH 4/6] Make inclusion in outline of assignments in blocks optional --- crates/ark/src/lsp/symbols.rs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/crates/ark/src/lsp/symbols.rs b/crates/ark/src/lsp/symbols.rs index 48f0cf825..beb62b748 100644 --- a/crates/ark/src/lsp/symbols.rs +++ b/crates/ark/src/lsp/symbols.rs @@ -124,11 +124,15 @@ struct Section { struct CollectContext { top_level: bool, + include_assignments_in_blocks: bool, } impl CollectContext { fn new() -> Self { - Self { top_level: true } + Self { + top_level: true, + include_assignments_in_blocks: false, + } } } @@ -145,14 +149,11 @@ pub(crate) fn document_symbols( let root_node = ast.root_node(); let mut result = Vec::new(); + let mut ctx = CollectContext::new(); + ctx.include_assignments_in_blocks = state.config.symbols.include_assignments_in_blocks; + // Extract and process all symbols from the AST - if let Err(err) = collect_symbols( - &mut CollectContext::new(), - &root_node, - contents, - 0, - &mut result, - ) { + if let Err(err) = collect_symbols(&mut ctx, &root_node, contents, 0, &mut result) { log::error!("Failed to collect symbols: {err:?}"); return Ok(Vec::new()); } @@ -446,7 +447,7 @@ fn collect_assignment( return collect_assignment_with_function(ctx, node, contents, symbols); } - if ctx.top_level { + if ctx.top_level || ctx.include_assignments_in_blocks { // Collect as generic object, but only if we're at top-level. Assigned // objects in nested functions and blocks cause the outline to become // too busy. From 12443edacc471ea8e3137da9be5ca5f29eb81873 Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Mon, 21 Jul 2025 12:03:19 +0200 Subject: [PATCH 5/6] Tweak comment --- crates/ark/src/lsp/symbols.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ark/src/lsp/symbols.rs b/crates/ark/src/lsp/symbols.rs index beb62b748..69d234b81 100644 --- a/crates/ark/src/lsp/symbols.rs +++ b/crates/ark/src/lsp/symbols.rs @@ -448,7 +448,7 @@ fn collect_assignment( } if ctx.top_level || ctx.include_assignments_in_blocks { - // Collect as generic object, but only if we're at top-level. Assigned + // Collect as generic object, but typically only if we're at top-level. Assigned // objects in nested functions and blocks cause the outline to become // too busy. let name = contents.node_slice(&lhs)?.to_string(); From db257d2db7ac2d1d5d01468412bc1edc2c0f5984 Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Mon, 21 Jul 2025 12:26:45 +0200 Subject: [PATCH 6/6] Add test with argument enabled --- ...ts__symbol_nested_assignments_enabled.snap | 131 ++++++++++++++++++ crates/ark/src/lsp/symbols.rs | 25 ++++ 2 files changed, 156 insertions(+) create mode 100644 crates/ark/src/lsp/snapshots/ark__lsp__symbols__tests__symbol_nested_assignments_enabled.snap diff --git a/crates/ark/src/lsp/snapshots/ark__lsp__symbols__tests__symbol_nested_assignments_enabled.snap b/crates/ark/src/lsp/snapshots/ark__lsp__symbols__tests__symbol_nested_assignments_enabled.snap new file mode 100644 index 000000000..a2d4fed52 --- /dev/null +++ b/crates/ark/src/lsp/snapshots/ark__lsp__symbols__tests__symbol_nested_assignments_enabled.snap @@ -0,0 +1,131 @@ +--- +source: crates/ark/src/lsp/symbols.rs +expression: symbols +--- +[ + DocumentSymbol { + name: "inner1", + detail: None, + kind: Variable, + tags: None, + deprecated: None, + range: Range { + start: Position { + line: 2, + character: 2, + }, + end: Position { + line: 2, + character: 8, + }, + }, + selection_range: Range { + start: Position { + line: 2, + character: 2, + }, + end: Position { + line: 2, + character: 8, + }, + }, + children: Some( + [], + ), + }, + DocumentSymbol { + name: "a", + detail: Some( + "function()", + ), + kind: Function, + tags: None, + deprecated: None, + range: Range { + start: Position { + line: 4, + character: 0, + }, + end: Position { + line: 7, + character: 1, + }, + }, + selection_range: Range { + start: Position { + line: 4, + character: 0, + }, + end: Position { + line: 7, + character: 1, + }, + }, + children: Some( + [ + DocumentSymbol { + name: "inner2", + detail: None, + kind: Variable, + tags: None, + deprecated: None, + range: Range { + start: Position { + line: 5, + character: 2, + }, + end: Position { + line: 5, + character: 8, + }, + }, + selection_range: Range { + start: Position { + line: 5, + character: 2, + }, + end: Position { + line: 5, + character: 8, + }, + }, + children: Some( + [], + ), + }, + DocumentSymbol { + name: "inner3", + detail: Some( + "function()", + ), + kind: Function, + tags: None, + deprecated: None, + range: Range { + start: Position { + line: 6, + character: 2, + }, + end: Position { + line: 6, + character: 24, + }, + }, + selection_range: Range { + start: Position { + line: 6, + character: 2, + }, + end: Position { + line: 6, + character: 24, + }, + }, + children: Some( + [], + ), + }, + ], + ), + }, +] diff --git a/crates/ark/src/lsp/symbols.rs b/crates/ark/src/lsp/symbols.rs index 69d234b81..e72d6b1a2 100644 --- a/crates/ark/src/lsp/symbols.rs +++ b/crates/ark/src/lsp/symbols.rs @@ -849,4 +849,29 @@ a <- function() { )); assert_eq!(test_symbol("{ foo <- 1 }"), vec![]); } + + #[test] + fn test_symbol_nested_assignments_enabled() { + let doc = Document::new( + " +local({ + inner1 <- 1 +}) +a <- function() { + inner2 <- 2 + inner3 <- function() 3 +} +", + None, + ); + let node = doc.ast.root_node(); + + let ctx = &mut CollectContext::new(); + ctx.include_assignments_in_blocks = true; + + let mut symbols = Vec::new(); + collect_symbols(ctx, &node, &doc.contents, 0, &mut symbols).unwrap(); + + insta::assert_debug_snapshot!(symbols); + } }