diff --git a/tooling/lsp/src/lib.rs b/tooling/lsp/src/lib.rs index 6cd9dccbf8f..1e4ab126644 100644 --- a/tooling/lsp/src/lib.rs +++ b/tooling/lsp/src/lib.rs @@ -14,8 +14,9 @@ use std::{ use acvm::{BlackBoxFunctionSolver, FieldElement}; use async_lsp::lsp_types::request::{ - CodeActionRequest, Completion, DocumentSymbolRequest, HoverRequest, InlayHintRequest, - PrepareRenameRequest, References, Rename, SignatureHelpRequest, WorkspaceSymbolRequest, + CodeActionRequest, Completion, DocumentSymbolRequest, FoldingRangeRequest, HoverRequest, + InlayHintRequest, PrepareRenameRequest, References, Rename, SignatureHelpRequest, + WorkspaceSymbolRequest, }; use async_lsp::{ AnyEvent, AnyNotification, AnyRequest, ClientSocket, Error, LspService, ResponseError, @@ -79,7 +80,7 @@ use types::{NargoTest, NargoTestId, Position, Range, Url, notification, request} use with_file::parsed_module_with_file; use crate::{ - requests::{on_expand_request, on_std_source_code_request}, + requests::{on_expand_request, on_folding_range_request, on_std_source_code_request}, types::request::{NargoExpand, NargoStdSourceCode}, }; @@ -176,6 +177,7 @@ impl NargoLspService { .request::(on_signature_help_request) .request::(on_code_action_request) .request::(on_workspace_symbol_request) + .request::(on_folding_range_request) .request::(on_expand_request) .request::(on_std_source_code_request) .notification::(on_initialized) diff --git a/tooling/lsp/src/requests/folding_range.rs b/tooling/lsp/src/requests/folding_range.rs new file mode 100644 index 00000000000..0dc107e785f --- /dev/null +++ b/tooling/lsp/src/requests/folding_range.rs @@ -0,0 +1,47 @@ +use std::future; + +use async_lsp::{ + ResponseError, + lsp_types::{FoldingRange, FoldingRangeParams, Position, TextDocumentPositionParams}, +}; + +use crate::{ + LspState, + requests::{ + folding_range::{comments_collector::CommentsCollector, nodes_collector::NodesCollector}, + process_request, + }, +}; + +mod comments_collector; +mod nodes_collector; +#[cfg(test)] +mod tests; + +pub(crate) fn on_folding_range_request( + state: &mut LspState, + params: FoldingRangeParams, +) -> impl Future>, ResponseError>> + use<> { + let text_document_position_params = TextDocumentPositionParams { + text_document: params.text_document.clone(), + position: Position { line: 0, character: 0 }, + }; + + let result = process_request(state, text_document_position_params, |args| { + let file_id = args.location.file; + let file = args.files.get_file(file_id).unwrap(); + let source = file.source(); + + let comments_collector = CommentsCollector::new(file_id, args.files); + let mut ranges = comments_collector.collect(source); + + let nodes_collector = NodesCollector::new(file_id, args.files); + let node_ranges = nodes_collector.collect(source); + + ranges.extend(node_ranges); + + Some(ranges) + }); + + future::ready(result) +} diff --git a/tooling/lsp/src/requests/folding_range/comments_collector.rs b/tooling/lsp/src/requests/folding_range/comments_collector.rs new file mode 100644 index 00000000000..d669a472bc3 --- /dev/null +++ b/tooling/lsp/src/requests/folding_range/comments_collector.rs @@ -0,0 +1,113 @@ +use async_lsp::lsp_types::{self, FoldingRange, FoldingRangeKind}; +use fm::{FileId, FileMap}; +use noirc_frontend::{ + lexer::Lexer, + token::{DocStyle, Token}, +}; + +use crate::requests::to_lsp_location; + +pub(super) struct CommentsCollector<'files> { + file_id: FileId, + files: &'files FileMap, + ranges: Vec, + current_line_comment_group: Option, +} + +struct LineCommentGroup { + start: lsp_types::Location, + end: lsp_types::Location, + doc_style: Option, +} + +impl<'files> CommentsCollector<'files> { + pub(super) fn new(file_id: FileId, files: &'files FileMap) -> Self { + Self { file_id, files, ranges: Vec::new(), current_line_comment_group: None } + } + + pub(super) fn collect(mut self, source: &str) -> Vec { + let lexer = Lexer::new(source, self.file_id).skip_comments(false); + + for token in lexer { + let Ok(token) = token else { + continue; + }; + + let location = token.location(); + + match token.into_token() { + Token::BlockComment(..) => { + self.push_current_line_comment_group(); + + let Some(location) = to_lsp_location(self.files, self.file_id, location.span) + else { + continue; + }; + + // Block comments are never grouped with other comments + self.ranges.push(FoldingRange { + start_line: location.range.start.line, + start_character: None, + end_line: location.range.end.line, + end_character: None, + kind: Some(FoldingRangeKind::Comment), + collapsed_text: None, + }); + } + Token::LineComment(_, doc_style) => { + let Some(location) = to_lsp_location(self.files, self.file_id, location.span) + else { + continue; + }; + + if let Some(group) = &mut self.current_line_comment_group { + // Keep grouping while the line comment style is the same and they are consecutive lines + if group.doc_style == doc_style + && location.range.start.line - group.end.range.end.line <= 1 + { + group.end = location; + continue; + } + + // A new group starts, so push the current one + Self::push_line_comment_group(group, &mut self.ranges); + } + + let start = location.clone(); + self.current_line_comment_group = + Some(LineCommentGroup { start, end: location, doc_style }); + } + _ => { + self.push_current_line_comment_group(); + } + } + } + + self.push_current_line_comment_group(); + + self.ranges + } + + fn push_current_line_comment_group(&mut self) { + if let Some(group) = self.current_line_comment_group.take() { + Self::push_line_comment_group(&group, &mut self.ranges); + } + } + + fn push_line_comment_group(group: &LineCommentGroup, ranges: &mut Vec) { + let start_line = group.start.range.start.line; + let end_line = group.end.range.end.line; + if start_line == end_line { + return; + } + + ranges.push(FoldingRange { + start_line, + start_character: None, + end_line, + end_character: None, + kind: Some(FoldingRangeKind::Comment), + collapsed_text: None, + }); + } +} diff --git a/tooling/lsp/src/requests/folding_range/nodes_collector.rs b/tooling/lsp/src/requests/folding_range/nodes_collector.rs new file mode 100644 index 00000000000..8ebf0bff933 --- /dev/null +++ b/tooling/lsp/src/requests/folding_range/nodes_collector.rs @@ -0,0 +1,123 @@ +use async_lsp::lsp_types::{self, FoldingRange, FoldingRangeKind}; +use fm::{FileId, FileMap}; +use noirc_errors::Span; +use noirc_frontend::ast::{ItemVisibility, ModuleDeclaration, UseTree, Visitor}; + +use crate::requests::to_lsp_location; + +pub(super) struct NodesCollector<'files> { + file_id: FileId, + files: &'files FileMap, + ranges: Vec, + module_group: Option, + use_group: Option, +} + +struct Group { + start: lsp_types::Location, + end: lsp_types::Location, + count: usize, +} + +impl<'files> NodesCollector<'files> { + pub(super) fn new(file_id: FileId, files: &'files FileMap) -> Self { + Self { file_id, files, ranges: Vec::new(), module_group: None, use_group: None } + } + + pub(super) fn collect(mut self, source: &str) -> Vec { + let (parsed_module, _errors) = noirc_frontend::parse_program(source, self.file_id); + parsed_module.accept(&mut self); + + if let Some(group) = &self.module_group { + Self::push_group(group, None, &mut self.ranges); + } + + if let Some(group) = &self.use_group { + Self::push_group(group, Some(FoldingRangeKind::Imports), &mut self.ranges); + } + + self.ranges + } + + fn push_group(group: &Group, kind: Option, ranges: &mut Vec) { + if group.count == 1 { + return; + } + + let start_line = group.start.range.start.line; + let end_line = group.end.range.end.line; + if start_line == end_line { + return; + } + + ranges.push(FoldingRange { + start_line, + start_character: None, + end_line, + end_character: None, + kind, + collapsed_text: None, + }); + } +} + +impl Visitor for NodesCollector<'_> { + fn visit_module_declaration(&mut self, _: &ModuleDeclaration, span: Span) { + let Some(location) = to_lsp_location(self.files, self.file_id, span) else { + return; + }; + + if let Some(group) = &mut self.module_group { + if location.range.start.line - group.end.range.end.line <= 1 { + group.end = location; + group.count += 1; + return; + } + + Self::push_group(group, None, &mut self.ranges); + } + + self.module_group = Some(Group { start: location.clone(), end: location, count: 1 }); + } + + fn visit_import(&mut self, _: &UseTree, span: Span, _visibility: ItemVisibility) -> bool { + let Some(location) = to_lsp_location(self.files, self.file_id, span) else { + return true; + }; + + if let Some(group) = &mut self.use_group { + if location.range.start.line - group.end.range.end.line <= 1 { + group.end = location; + group.count += 1; + return true; + } + + Self::push_group(group, Some(FoldingRangeKind::Imports), &mut self.ranges); + } + + self.use_group = Some(Group { start: location.clone(), end: location, count: 1 }); + + true + } + + fn visit_use_tree(&mut self, use_tree: &UseTree) -> bool { + let Some(location) = to_lsp_location(self.files, self.file_id, use_tree.location.span) + else { + return true; + }; + + // If the use tree spans multiple lines, we can fold it + if location.range.start.line != location.range.end.line { + self.ranges.push(FoldingRange { + start_line: location.range.start.line, + start_character: None, + end_line: location.range.end.line, + end_character: None, + kind: Some(FoldingRangeKind::Imports), + collapsed_text: None, + }); + } + + true + } +} diff --git a/tooling/lsp/src/requests/folding_range/tests.rs b/tooling/lsp/src/requests/folding_range/tests.rs new file mode 100644 index 00000000000..3b105c4f7a0 --- /dev/null +++ b/tooling/lsp/src/requests/folding_range/tests.rs @@ -0,0 +1,202 @@ +use crate::{notifications::on_did_open_text_document, test_utils}; + +use super::*; +use async_lsp::lsp_types::{ + DidOpenTextDocumentParams, FoldingRangeKind, PartialResultParams, TextDocumentIdentifier, + TextDocumentItem, WorkDoneProgressParams, +}; +use tokio::test; + +async fn get_folding_ranges(src: &str) -> Vec { + let (mut state, noir_text_document) = test_utils::init_lsp_server("document_symbol").await; + + let _ = on_did_open_text_document( + &mut state, + DidOpenTextDocumentParams { + text_document: TextDocumentItem { + uri: noir_text_document.clone(), + language_id: "noir".to_string(), + version: 0, + text: src.to_string(), + }, + }, + ); + + on_folding_range_request( + &mut state, + FoldingRangeParams { + text_document: TextDocumentIdentifier { uri: noir_text_document }, + work_done_progress_params: WorkDoneProgressParams { work_done_token: None }, + partial_result_params: PartialResultParams { partial_result_token: None }, + }, + ) + .await + .expect("Could not execute on_folding_range_request") + .unwrap() +} + +#[test] +async fn test_block_comment() { + let src = " + fn foo() {} + + /* This is a + block + comment */ + + fn bar() {} + "; + let ranges = get_folding_ranges(src).await; + assert_eq!(ranges.len(), 1); + + let range = &ranges[0]; + assert_eq!(range.start_line, 3); + assert_eq!(range.end_line, 5); + assert_eq!(range.kind, Some(FoldingRangeKind::Comment)); +} + +#[test] +async fn test_line_comment() { + let src = " + fn foo() {} + + // This is a + // series of + // consecutive comments + + // And this + // is another one + + fn bar() {} + "; + let ranges = get_folding_ranges(src).await; + assert_eq!(ranges.len(), 2); + + let range = &ranges[0]; + assert_eq!(range.start_line, 3); + assert_eq!(range.end_line, 5); + assert_eq!(range.kind, Some(FoldingRangeKind::Comment)); + + let range = &ranges[1]; + assert_eq!(range.start_line, 7); + assert_eq!(range.end_line, 8); + assert_eq!(range.kind, Some(FoldingRangeKind::Comment)); +} + +#[test] +async fn test_does_not_mix_different_styles() { + let src = " + //! This should not + //! be mixed with the next comment + // This is a + // series of + // consecutive comments + "; + let ranges = get_folding_ranges(src).await; + assert_eq!(ranges.len(), 2); + + let range = &ranges[0]; + assert_eq!(range.start_line, 1); + assert_eq!(range.end_line, 2); + assert_eq!(range.kind, Some(FoldingRangeKind::Comment)); + + let range = &ranges[1]; + assert_eq!(range.start_line, 3); + assert_eq!(range.end_line, 5); + assert_eq!(range.kind, Some(FoldingRangeKind::Comment)); +} + +#[test] +async fn test_series_of_mod() { + let src = " + mod one; + mod two; + + mod three; + mod four; + mod five; + "; + let ranges = get_folding_ranges(src).await; + assert_eq!(ranges.len(), 2); + + let range = &ranges[0]; + assert_eq!(range.start_line, 1); + assert_eq!(range.end_line, 2); + assert_eq!(range.kind, None); + + let range = &ranges[1]; + assert_eq!(range.start_line, 4); + assert_eq!(range.end_line, 6); + assert_eq!(range.kind, None); +} + +#[test] +async fn test_series_of_use() { + let src = " + use one; + use two; + + use three; + use four; + use five; + "; + let ranges = get_folding_ranges(src).await; + assert_eq!(ranges.len(), 2); + + let range = &ranges[0]; + assert_eq!(range.start_line, 1); + assert_eq!(range.end_line, 2); + assert_eq!(range.kind, Some(FoldingRangeKind::Imports)); + + let range = &ranges[1]; + assert_eq!(range.start_line, 4); + assert_eq!(range.end_line, 6); + assert_eq!(range.kind, Some(FoldingRangeKind::Imports)); +} + +#[test] +async fn test_use_list() { + let src = " + use one::{ + two::{ + three, + four + }, + }; + "; + let ranges = get_folding_ranges(src).await; + + assert_eq!(ranges.len(), 2); + + let range = &ranges[0]; + assert_eq!(range.start_line, 1); + assert_eq!(range.end_line, 6); + assert_eq!(range.kind, Some(FoldingRangeKind::Imports)); + + let range = &ranges[1]; + assert_eq!(range.start_line, 2); + assert_eq!(range.end_line, 5); + assert_eq!(range.kind, Some(FoldingRangeKind::Imports)); +} + +#[test] +async fn test_series_of_use_when_there_is_a_list() { + let src = " + use one; + use two::{ + three, + }; + "; + let ranges = get_folding_ranges(src).await; + assert_eq!(ranges.len(), 2); + + let range = &ranges[0]; + assert_eq!(range.start_line, 2); + assert_eq!(range.end_line, 4); + assert_eq!(range.kind, Some(FoldingRangeKind::Imports)); + + let range = &ranges[1]; + assert_eq!(range.start_line, 1); + assert_eq!(range.end_line, 4); + assert_eq!(range.kind, Some(FoldingRangeKind::Imports)); +} diff --git a/tooling/lsp/src/requests/mod.rs b/tooling/lsp/src/requests/mod.rs index 5bf47c163bc..21974e60046 100644 --- a/tooling/lsp/src/requests/mod.rs +++ b/tooling/lsp/src/requests/mod.rs @@ -55,6 +55,7 @@ mod code_lens_request; mod completion; mod document_symbol; mod expand; +mod folding_range; mod goto_declaration; mod goto_definition; mod hover; @@ -70,9 +71,10 @@ mod workspace_symbol; pub(crate) use { code_action::on_code_action_request, code_lens_request::on_code_lens_request, completion::on_completion_request, document_symbol::on_document_symbol_request, - expand::on_expand_request, goto_declaration::on_goto_declaration_request, - goto_definition::on_goto_definition_request, goto_definition::on_goto_type_definition_request, - hover::on_hover_request, inlay_hint::on_inlay_hint_request, references::on_references_request, + expand::on_expand_request, folding_range::on_folding_range_request, + goto_declaration::on_goto_declaration_request, goto_definition::on_goto_definition_request, + goto_definition::on_goto_type_definition_request, hover::on_hover_request, + inlay_hint::on_inlay_hint_request, references::on_references_request, rename::on_prepare_rename_request, rename::on_rename_request, signature_help::on_signature_help_request, std_source_code::on_std_source_code_request, test_run::on_test_run_request, tests::on_tests_request, @@ -308,6 +310,7 @@ pub(crate) fn on_initialize( resolve_provider: None, }, )), + folding_range_provider: Some(true), }, server_info: None, }) diff --git a/tooling/lsp/src/types.rs b/tooling/lsp/src/types.rs index b21e228e61a..adfc2367f26 100644 --- a/tooling/lsp/src/types.rs +++ b/tooling/lsp/src/types.rs @@ -181,6 +181,10 @@ pub(crate) struct ServerCapabilities { /// The server provides workspace symbol support. #[serde(skip_serializing_if = "Option::is_none")] pub(crate) workspace_symbol_provider: Option>, + + /// The server provides folding range support. + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) folding_range_provider: Option, } #[derive(Debug, PartialEq, Clone, Default, Deserialize, Serialize)]