From a2970f92fbd69f3ffe628c2472b68612f3b344af Mon Sep 17 00:00:00 2001 From: Dianyi Yang <105025148+kv9898@users.noreply.github.com> Date: Sun, 12 Jan 2025 19:23:09 +0800 Subject: [PATCH 1/3] Get example folding range working --- crates/lsp/src/folding_range.rs | 17 +++++++++++++++++ crates/lsp/src/handlers.rs | 20 ++++++++++++++++++++ crates/lsp/src/handlers_state.rs | 2 ++ crates/lsp/src/lib.rs | 1 + crates/lsp/src/main_loop.rs | 3 +++ crates/lsp/src/tower_lsp.rs | 9 +++++++++ 6 files changed, 52 insertions(+) create mode 100644 crates/lsp/src/folding_range.rs diff --git a/crates/lsp/src/folding_range.rs b/crates/lsp/src/folding_range.rs new file mode 100644 index 00000000..7f6abca5 --- /dev/null +++ b/crates/lsp/src/folding_range.rs @@ -0,0 +1,17 @@ +use tower_lsp::lsp_types::FoldingRange; +use tower_lsp::lsp_types::FoldingRangeKind; + +use crate::documents::Document; + +pub fn folding_range(_document: &Document) -> anyhow::Result> { + // sample + let example_range = FoldingRange { + start_line: 0, + start_character: None, + end_line: 1, + end_character: None, + kind: Some(FoldingRangeKind::Region), + collapsed_text: None, + }; + Ok(vec![example_range]) +} diff --git a/crates/lsp/src/handlers.rs b/crates/lsp/src/handlers.rs index 53cb5ebc..e5c35cc2 100644 --- a/crates/lsp/src/handlers.rs +++ b/crates/lsp/src/handlers.rs @@ -7,12 +7,16 @@ use struct_field_names_as_array::FieldNamesAsArray; use tower_lsp::lsp_types; +use tower_lsp::lsp_types::FoldingRange; +use tower_lsp::lsp_types::FoldingRangeParams; use tower_lsp::Client; use tracing::Instrument; use crate::config::VscDiagnosticsConfig; use crate::config::VscDocumentConfig; +use crate::folding_range::folding_range; use crate::main_loop::LspState; +use crate::state::WorldState; // Handlers that do not mutate the world state. They take a sharing reference or // a clone of the state. @@ -66,3 +70,19 @@ fn collect_regs( }) .collect() } + +#[tracing::instrument(level = "info", skip_all)] +pub(crate) fn handle_folding_range( + params: FoldingRangeParams, + state: &WorldState, +) -> anyhow::Result>> { + let uri = params.text_document.uri; + let document = state.get_document(&uri)?; + match folding_range(document) { + Ok(foldings) => Ok(Some(foldings)), + Err(err) => { + tracing::error!("{err:?}"); + Ok(None) + } + } +} diff --git a/crates/lsp/src/handlers_state.rs b/crates/lsp/src/handlers_state.rs index a570cf31..ba6176e5 100644 --- a/crates/lsp/src/handlers_state.rs +++ b/crates/lsp/src/handlers_state.rs @@ -15,6 +15,7 @@ use tower_lsp::lsp_types::DidChangeConfigurationParams; use tower_lsp::lsp_types::DidChangeTextDocumentParams; use tower_lsp::lsp_types::DidCloseTextDocumentParams; use tower_lsp::lsp_types::DidOpenTextDocumentParams; +use tower_lsp::lsp_types::FoldingRangeProviderCapability; use tower_lsp::lsp_types::FormattingOptions; use tower_lsp::lsp_types::InitializeParams; use tower_lsp::lsp_types::InitializeResult; @@ -134,6 +135,7 @@ pub(crate) fn initialize( }), document_formatting_provider: Some(OneOf::Left(true)), document_range_formatting_provider: Some(OneOf::Left(true)), + folding_range_provider: Some(FoldingRangeProviderCapability::Simple(true)), ..ServerCapabilities::default() }, }) diff --git a/crates/lsp/src/lib.rs b/crates/lsp/src/lib.rs index 65451b9f..e4af1b7c 100644 --- a/crates/lsp/src/lib.rs +++ b/crates/lsp/src/lib.rs @@ -7,6 +7,7 @@ pub mod config; pub mod crates; pub mod documents; pub mod encoding; +pub mod folding_range; pub mod from_proto; pub mod handlers; pub mod handlers_ext; diff --git a/crates/lsp/src/main_loop.rs b/crates/lsp/src/main_loop.rs index 780a635b..31e3b2a5 100644 --- a/crates/lsp/src/main_loop.rs +++ b/crates/lsp/src/main_loop.rs @@ -341,6 +341,9 @@ impl GlobalState { LspRequest::DocumentRangeFormatting(params) => { respond(tx, handlers_format::document_range_formatting(params, &self.world), LspResponse::DocumentRangeFormatting)?; }, + LspRequest::FoldingRange(params) => { + respond(tx, handlers::handle_folding_range(params, &self.world), LspResponse::FoldingRange)?; + }, LspRequest::AirViewFile(params) => { respond(tx, handlers_ext::view_file(params, &self.world), LspResponse::AirViewFile)?; }, diff --git a/crates/lsp/src/tower_lsp.rs b/crates/lsp/src/tower_lsp.rs index ba114988..a845f18d 100644 --- a/crates/lsp/src/tower_lsp.rs +++ b/crates/lsp/src/tower_lsp.rs @@ -63,6 +63,7 @@ pub(crate) enum LspRequest { DocumentFormatting(DocumentFormattingParams), Shutdown, DocumentRangeFormatting(DocumentRangeFormattingParams), + FoldingRange(FoldingRangeParams), AirViewFile(ViewFileParams), } @@ -72,6 +73,7 @@ pub(crate) enum LspResponse { Initialize(InitializeResult), DocumentFormatting(Option>), DocumentRangeFormatting(Option>), + FoldingRange(Option>), Shutdown(()), AirViewFile(String), } @@ -263,6 +265,13 @@ impl LanguageServer for Backend { LspResponse::DocumentRangeFormatting ) } + + async fn folding_range(&self, params: FoldingRangeParams) -> Result>> { + cast_response!( + self.request(LspRequest::FoldingRange(params)).await, + LspResponse::FoldingRange + ) + } } pub async fn start_lsp(read: I, write: O) From d71d1351f9a9c4316434f5315fdb45e64948d591 Mon Sep 17 00:00:00 2001 From: Dianyi Yang <105025148+kv9898@users.noreply.github.com> Date: Mon, 13 Jan 2025 01:19:20 +0800 Subject: [PATCH 2/3] Add bracket, nested comment, region and cell handling --- Cargo.lock | 1 + crates/lsp/Cargo.toml | 1 + crates/lsp/src/folding_range.rs | 367 +++++++++++++++++++++++++++++++- 3 files changed, 363 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2985a699..6a3aa60d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1330,6 +1330,7 @@ dependencies = [ "line_ending", "lsp_test", "memchr", + "regex", "serde", "serde_json", "struct-field-names-as-array", diff --git a/crates/lsp/Cargo.toml b/crates/lsp/Cargo.toml index 3c726341..e913665a 100644 --- a/crates/lsp/Cargo.toml +++ b/crates/lsp/Cargo.toml @@ -28,6 +28,7 @@ futures.workspace = true itertools.workspace = true line_ending.workspace = true memchr.workspace = true +regex = "1.11.1" serde.workspace = true serde_json.workspace = true struct-field-names-as-array.workspace = true diff --git a/crates/lsp/src/folding_range.rs b/crates/lsp/src/folding_range.rs index 7f6abca5..0f61ad9f 100644 --- a/crates/lsp/src/folding_range.rs +++ b/crates/lsp/src/folding_range.rs @@ -1,17 +1,372 @@ +use regex::Regex; +use std::sync::LazyLock; + use tower_lsp::lsp_types::FoldingRange; use tower_lsp::lsp_types::FoldingRangeKind; use crate::documents::Document; -pub fn folding_range(_document: &Document) -> anyhow::Result> { - // sample - let example_range = FoldingRange { - start_line: 0, +pub fn folding_range(document: &Document) -> anyhow::Result> { + let mut folding_ranges: Vec = Vec::new(); + + // Activate the parser + let mut parser = tree_sitter::Parser::new(); + parser + .set_language(&tree_sitter_r::LANGUAGE.into()) + .unwrap(); + + let ast = parser.parse(&document.contents, None).unwrap(); + + if ast.root_node().has_error() { + tracing::error!("Folding range service: Parse error"); + return Err(anyhow::anyhow!("Parse error")); + } + + // Traverse the AST + let mut cursor = ast.root_node().walk(); + parse_ts_node( + &mut cursor, + 0, + &mut folding_ranges, + document, + &mut vec![Vec::new()], + &mut None, + &mut None, + ); + + Ok(folding_ranges) +} + +fn parse_ts_node( + cursor: &mut tree_sitter::TreeCursor, + _depth: usize, + folding_ranges: &mut Vec, + document: &Document, + comment_stack: &mut Vec>, + region_marker: &mut Option, + cell_marker: &mut Option, +) { + let node = cursor.node(); + let _field_name = match cursor.field_name() { + Some(name) => format!("{name}: "), + None => String::new(), + }; + + let start = node.start_position(); + let end = node.end_position(); + let node_type = node.kind(); + + match node_type { + "parameters" | "arguments" | "braced_expression" => { + // ignore same line folding + if start.row == end.row { + return; + } + let folding_range = bracket_range( + start.row, + start.column, + end.row, + end.column - 1, + count_leading_whitespaces(document, end.row), + ); + folding_ranges.push(folding_range); + } + "comment" => { + // Only process standalone comment + if count_leading_whitespaces(document, start.row) != start.column { + return; + } + + // Nested comment section handling + let comment_line = get_line_text(document, start.row, None, None); + + nested_processor(comment_stack, folding_ranges, start.row, &comment_line); + region_processor(folding_ranges, region_marker, start.row, &comment_line); + cell_processor(folding_ranges, cell_marker, start.row, &comment_line); + } + _ => (), + } + + if cursor.goto_first_child() { + // create node child stacks + // This is a stack of stacks for each bracket level, within each stack is a vector of (level, start_line) tuples + let mut child_comment_stack: Vec> = vec![Vec::new()]; + let mut child_region_marker: Option = None; + let mut child_cell_marker: Option = None; + + // recursive loop + loop { + parse_ts_node( + cursor, + _depth + 1, + folding_ranges, + document, + &mut child_comment_stack, + &mut child_region_marker, + &mut child_cell_marker, + ); + if !cursor.goto_next_sibling() { + break; + } + } + // End of node handling + end_node_handler( + folding_ranges, + end.row, + &mut child_comment_stack, + &mut child_region_marker, + &mut child_cell_marker, + ); + + cursor.goto_parent(); + } +} + +// Function to create a folding range that specifically deals with bracket ending +fn bracket_range( + start_line: usize, + start_char: usize, + end_line: usize, + end_char: usize, + white_space_count: usize, +) -> FoldingRange { + let mut end_line: u32 = end_line.try_into().unwrap(); + let mut end_char: Option = Some(end_char.try_into().unwrap()); + + let adjusted_end_char = end_char.and_then(|val| val.checked_sub(white_space_count as u32)); + + match adjusted_end_char { + Some(0) => { + end_line -= 1; + end_char = None; + } + Some(_) => { + if let Some(ref mut value) = end_char { + *value -= 1; + } + } + None => { + tracing::error!( + "Folding Range (bracket_range): adjusted_end_char should not be None here" + ); + } + } + + FoldingRange { + start_line: start_line.try_into().unwrap(), + start_character: Some(start_char as u32), + end_line, + end_character: end_char, + kind: Some(FoldingRangeKind::Region), + collapsed_text: None, + } +} + +fn comment_range(start_line: usize, end_line: usize) -> FoldingRange { + FoldingRange { + start_line: start_line.try_into().unwrap(), start_character: None, - end_line: 1, + end_line: end_line.try_into().unwrap(), end_character: None, kind: Some(FoldingRangeKind::Region), collapsed_text: None, + } +} + +fn get_line_text( + document: &Document, + line_num: usize, + start_char: Option, + end_char: Option, +) -> String { + let text = &document.contents; + // Split the text into lines + let lines: Vec<&str> = text.lines().collect(); + + // Ensure the start_line is within bounds + if line_num >= lines.len() { + return String::new(); // Return an empty string if out of bounds + } + + // Get the line corresponding to start_line + let line = lines[line_num]; + + // Determine the start and end character indices + let start_idx = start_char.unwrap_or(0); // Default to 0 if None + let end_idx = end_char.unwrap_or(line.len()); // Default to the line's length if None + + // Ensure indices are within bounds for the line + let start_idx = start_idx.min(line.len()); + let end_idx = end_idx.min(line.len()); + + // Extract the substring and return it + line[start_idx..end_idx].to_string() // TODO +} + +fn count_leading_whitespaces(document: &Document, line_num: usize) -> usize { + let line_text = get_line_text(document, line_num, None, None); + line_text.chars().take_while(|c| c.is_whitespace()).count() +} + +pub static RE_COMMENT_SECTION: LazyLock = + LazyLock::new(|| Regex::new(r"^\s*(#+)\s*(.*?)\s*[#=-]{4,}\s*$").unwrap()); + +fn parse_comment_as_section(comment: &str) -> Option<(usize, String)> { + // Match lines starting with one or more '#' followed by some non-empty content and must end with 4 or more '-', '#', or `=` + // Ensure that there's actual content between the start and the trailing symbols. + if let Some(caps) = RE_COMMENT_SECTION.captures(comment) { + let hashes = caps.get(1)?.as_str().len(); // Count the number of '#' + let title = caps.get(2)?.as_str().trim().to_string(); // Extract the title text without trailing punctuations + if title.is_empty() { + return None; // Return None for lines with only hashtags + } + return Some((hashes, title)); // Return the level based on the number of '#' and the title + } + + None +} + +fn nested_processor( + comment_stack: &mut Vec>, + folding_ranges: &mut Vec, + line_num: usize, + comment_line: &str, +) { + let Some((level, _title)) = parse_comment_as_section(comment_line) else { + return; // return if the line is not a comment section + }; + if comment_stack.is_empty() { + tracing::error!( + "Folding Range: comment_stack should always contain at least one element here" + ); + return; + } + loop { + if comment_stack.last().unwrap().is_empty() { + comment_stack.last_mut().unwrap().push((level, line_num)); + return; // return if the stack is empty + } + + let Some((last_level, _)) = comment_stack.last().unwrap().last() else { + tracing::error!("Folding Range: comment_stacks should not be empty here"); + return; + }; + if *last_level < level { + comment_stack.last_mut().unwrap().push((level, line_num)); + break; + } else if *last_level == level { + folding_ranges.push(comment_range( + comment_stack.last().unwrap().last().unwrap().1, + line_num - 1, + )); + comment_stack.last_mut().unwrap().pop(); + comment_stack.last_mut().unwrap().push((level, line_num)); + break; + } else { + folding_ranges.push(comment_range( + comment_stack.last().unwrap().last().unwrap().1, + line_num - 1, + )); + comment_stack.last_mut().unwrap().pop(); // TODO: Handle case where comment_stack is empty + } + } +} + +fn region_processor( + folding_ranges: &mut Vec, + region_marker: &mut Option, + line_idx: usize, + line_text: &str, +) { + let Some(region_type) = parse_region_type(line_text) else { + return; // return if the line is not a region section }; - Ok(vec![example_range]) + match region_type.as_str() { + "start" => { + region_marker.replace(line_idx); + } + "end" => { + if let Some(region_start) = region_marker { + let folding_range = comment_range(*region_start, line_idx); + folding_ranges.push(folding_range); + *region_marker = None; + } + } + _ => {} + } +} + +fn parse_region_type(line_text: &str) -> Option { + // return the region type + // "start": "^\\s*#\\s*region\\b" + // "end": "^\\s*#\\s*endregion\\b" + // None: otherwise + let region_start = Regex::new(r"^\s*#\s*region\b").unwrap(); + let region_end = Regex::new(r"^\s*#\s*endregion\b").unwrap(); + + if region_start.is_match(line_text) { + Some("start".to_string()) + } else if region_end.is_match(line_text) { + Some("end".to_string()) + } else { + None + } +} + +fn cell_processor( + // Almost identical to region_processor + folding_ranges: &mut Vec, + cell_marker: &mut Option, + line_idx: usize, + line_text: &str, +) { + let cell_pattern: Regex = Regex::new(r"^#\s*(%%|\+)(.*)").unwrap(); + + if !cell_pattern.is_match(line_text) { + } else { + let Some(start_line) = cell_marker else { + cell_marker.replace(line_idx); + return; + }; + + let folding_range = comment_range(*start_line, line_idx - 1); + folding_ranges.push(folding_range); + cell_marker.replace(line_idx); + } +} + +fn end_node_handler( + folding_ranges: &mut Vec, + line_idx: usize, + comment_stack: &mut Vec>, + region_marker: &mut Option, + cell_marker: &mut Option, +) { + // Nested comment handling + // Iterate over the last element of the comment stack and add it to the folding ranges by using the comment_range function + if let Some(last_section) = comment_stack.last() { + // Iterate over each (start level, start line) in the last section + for &(_level, start_line) in last_section.iter() { + // Add a new folding range for each range in the last section + let folding_range = comment_range(start_line, line_idx - 1); + + folding_ranges.push(folding_range); + } + } + // Remove the last element from the comment stack after processing + comment_stack.pop(); + + // Unclosed region handling + if let Some(region_start) = region_marker { + let folding_range = comment_range(*region_start, line_idx - 1); + folding_ranges.push(folding_range); + *region_marker = None; + } + + // End cell Handling + if let Some(cell_start) = cell_marker { + let folding_range = comment_range(*cell_start, line_idx - 1); + folding_ranges.push(folding_range); + *cell_marker = None; + } } From b3f207d99d4db01de0187067f482200bdc865063 Mon Sep 17 00:00:00 2001 From: Dianyi Yang <105025148+kv9898@users.noreply.github.com> Date: Mon, 13 Jan 2025 01:31:44 +0800 Subject: [PATCH 3/3] Add copyright header --- crates/lsp/src/folding_range.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/lsp/src/folding_range.rs b/crates/lsp/src/folding_range.rs index 0f61ad9f..8a04208b 100644 --- a/crates/lsp/src/folding_range.rs +++ b/crates/lsp/src/folding_range.rs @@ -1,3 +1,10 @@ +// +// folding_range.rs +// +// Copyright (C) 2025 Posit Software, PBC. All rights reserved. +// +// + use regex::Regex; use std::sync::LazyLock;