diff --git a/CHANGELOG.md b/CHANGELOG.md index 55d87da8..e3db341e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Don't crash when using comments inside `\include`-like commands ([#919](https://github.com/latex-lsp/texlab/issues/919)) +- Folding ranges include only the contents instead of the entire range of the structure. + For example, the folding range of an environment will start after the `\begin` and stop before the `\end` + ([#915](https://github.com/latex-lsp/texlab/issues/915)) ## [5.9.1] - 2023-08-11 diff --git a/Cargo.lock b/Cargo.lock index d0dfd6e7..064cee1d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -653,6 +653,17 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "folding" +version = "0.0.0" +dependencies = [ + "base-db", + "expect-test", + "rowan", + "syntax", + "test-utils", +] + [[package]] name = "form_urlencoded" version = "1.2.0" @@ -1640,6 +1651,7 @@ dependencies = [ "encoding_rs", "encoding_rs_io", "fern", + "folding", "fuzzy-matcher", "hover", "insta", diff --git a/crates/folding/Cargo.toml b/crates/folding/Cargo.toml new file mode 100644 index 00000000..2cf82699 --- /dev/null +++ b/crates/folding/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "folding" +version = "0.0.0" +license.workspace = true +authors.workspace = true +edition.workspace = true +rust-version.workspace = true + +[dependencies] +base-db = { path = "../base-db" } +rowan = "0.15.11" +syntax = { path = "../syntax" } + +[dev-dependencies] +expect-test = "1.4.1" +test-utils = { path = "../test-utils" } + +[lib] +doctest = false diff --git a/crates/folding/src/lib.rs b/crates/folding/src/lib.rs new file mode 100644 index 00000000..c8702101 --- /dev/null +++ b/crates/folding/src/lib.rs @@ -0,0 +1,107 @@ +use base_db::{Document, DocumentData}; +use rowan::{ast::AstNode, TextRange}; +use syntax::{ + bibtex::{self, HasDelims, HasName}, + latex, +}; + +#[derive(Debug)] +pub struct FoldingRange { + pub range: TextRange, + pub kind: FoldingRangeKind, +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)] +pub enum FoldingRangeKind { + Section, + Environment, + Entry, +} + +pub fn find_all(document: &Document) -> Vec { + let mut builder = FoldingBuilder::default(); + + if let DocumentData::Tex(data) = &document.data { + for node in data.root_node().descendants() { + if let Some(section) = latex::Section::cast(node.clone()) { + builder.fold_section(§ion); + } else if let Some(item) = latex::EnumItem::cast(node.clone()) { + builder.fold_enum_item(&item); + } else if let Some(env) = latex::Environment::cast(node) { + builder.fold_environment(env); + } + } + } else if let DocumentData::Bib(data) = &document.data { + for node in data.root_node().descendants() { + if let Some(entry) = bibtex::Entry::cast(node.clone()) { + builder.fold_entry(&entry); + } else if let Some(string) = bibtex::StringDef::cast(node) { + builder.fold_entry(&string); + } + } + } + + builder.ranges +} + +#[derive(Debug, Default)] +struct FoldingBuilder { + ranges: Vec, +} + +impl FoldingBuilder { + fn fold_section(&mut self, section: &latex::Section) -> Option<()> { + let start = section + .name() + .map(|name| latex::small_range(&name).end()) + .or_else(|| section.command().map(|cmd| cmd.text_range().end()))?; + let end = section.syntax().text_range().end(); + + self.ranges.push(FoldingRange { + range: TextRange::new(start, end), + kind: FoldingRangeKind::Section, + }); + + Some(()) + } + + fn fold_enum_item(&mut self, item: &latex::EnumItem) -> Option<()> { + let start = item + .label() + .map(|label| latex::small_range(&label).end()) + .or_else(|| item.command().map(|cmd| cmd.text_range().end()))?; + + let end = item.syntax().text_range().end(); + self.ranges.push(FoldingRange { + range: TextRange::new(start, end), + kind: FoldingRangeKind::Section, + }); + + Some(()) + } + + fn fold_environment(&mut self, env: latex::Environment) -> Option<()> { + let start = latex::small_range(&env.begin()?).end(); + let end = latex::small_range(&env.end()?).start(); + self.ranges.push(FoldingRange { + range: TextRange::new(start, end), + kind: FoldingRangeKind::Environment, + }); + + Some(()) + } + + fn fold_entry(&mut self, entry: &(impl HasName + HasDelims)) -> Option<()> { + let start = entry.name_token()?.text_range().end(); + let end = entry.right_delim_token()?.text_range().start(); + self.ranges.push(FoldingRange { + range: TextRange::new(start, end), + kind: FoldingRangeKind::Entry, + }); + + Some(()) + } +} + +#[cfg(test)] +mod tests; diff --git a/crates/folding/src/tests.rs b/crates/folding/src/tests.rs new file mode 100644 index 00000000..405b56bd --- /dev/null +++ b/crates/folding/src/tests.rs @@ -0,0 +1,88 @@ +use expect_test::{expect, Expect}; + +fn check(input: &str, expect: Expect) { + let fixture = test_utils::fixture::Fixture::parse(input); + let workspace = &fixture.workspace; + let document = workspace.lookup(&fixture.documents[0].uri).unwrap(); + let data = crate::find_all(document); + expect.assert_debug_eq(&data); +} + +#[test] +fn test_latex() { + check( + r#" +%! main.tex +\begin{document} + \section{Foo} + foo + \subsection{Bar} + bar + \section{Baz} + baz + \section{Qux} +\end{document} +|"#, + expect![[r#" + [ + FoldingRange { + range: 16..116, + kind: Environment, + }, + FoldingRange { + range: 34..76, + kind: Section, + }, + FoldingRange { + range: 63..76, + kind: Section, + }, + FoldingRange { + range: 89..102, + kind: Section, + }, + FoldingRange { + range: 115..116, + kind: Section, + }, + ] + "#]], + ); +} + +#[test] +fn test_bibtex() { + check( + r#" +%! main.bib +some junk +here + +@article{foo, + author = {bar}, + title = {baz} +} + +@string{foo = "bar"} + +@comment{foo, + author = {bar}, + title = {baz} +} + +@preamble{"foo"} +|"#, + expect![[r#" + [ + FoldingRange { + range: 28..68, + kind: Entry, + }, + FoldingRange { + range: 82..90, + kind: Entry, + }, + ] + "#]], + ); +} diff --git a/crates/texlab/Cargo.toml b/crates/texlab/Cargo.toml index 98771a39..ee5fb371 100644 --- a/crates/texlab/Cargo.toml +++ b/crates/texlab/Cargo.toml @@ -36,6 +36,7 @@ distro = { path = "../distro" } encoding_rs = "0.8.32" encoding_rs_io = "0.1.7" fern = "0.6.2" +folding = { path = "../folding" } fuzzy-matcher = { version = "0.3.7", features = ["compact"] } hover = { path = "../hover" } itertools = "0.11.0" diff --git a/crates/texlab/src/features/folding.rs b/crates/texlab/src/features/folding.rs index 5d987361..6de4ef0d 100644 --- a/crates/texlab/src/features/folding.rs +++ b/crates/texlab/src/features/folding.rs @@ -1,62 +1,44 @@ -use base_db::{DocumentData, Workspace}; -use lsp_types::{FoldingRange, FoldingRangeKind, Range, Url}; -use rowan::ast::AstNode; -use syntax::{bibtex, latex}; +use base_db::Workspace; +use folding::FoldingRangeKind; +use lsp_types::{ClientCapabilities, Url}; use crate::util::line_index_ext::LineIndexExt; -pub fn find_all(workspace: &Workspace, uri: &Url) -> Option> { +pub fn find_all( + workspace: &Workspace, + uri: &Url, + capabilities: &ClientCapabilities, +) -> Option> { + let custom_kinds = capabilities + .text_document + .as_ref() + .and_then(|cap| cap.folding_range.as_ref()) + .and_then(|cap| cap.folding_range_kind.as_ref()) + .and_then(|cap| cap.value_set.as_ref()) + .is_some(); + let document = workspace.lookup(uri)?; - let line_index = &document.line_index; - let foldings = match &document.data { - DocumentData::Tex(data) => { - let mut results = Vec::new(); - for node in data.root_node().descendants() { - if let Some(folding) = latex::Environment::cast(node.clone()) - .map(|node| latex::small_range(&node)) - .or_else(|| { - latex::Section::cast(node.clone()).map(|node| latex::small_range(&node)) - }) - .or_else(|| latex::EnumItem::cast(node).map(|node| latex::small_range(&node))) - .map(|node| line_index.line_col_lsp_range(node)) - .map(create_range) - { - results.push(folding); - } - } + let foldings = folding::find_all(document).into_iter().map(|folding| { + let range = document.line_index.line_col_lsp_range(folding.range); - results - } - DocumentData::Bib(data) => { - let root = data.root_node(); - root.descendants() - .filter(|node| { - matches!( - node.kind(), - bibtex::PREAMBLE | bibtex::STRING | bibtex::ENTRY - ) - }) - .map(|node| create_range(line_index.line_col_lsp_range(node.text_range()))) - .collect() - } - DocumentData::Aux(_) - | DocumentData::Log(_) - | DocumentData::Root - | DocumentData::Tectonic => { - return None; - } - }; + let kind = if custom_kinds { + Some(match folding.kind { + FoldingRangeKind::Section => "section", + FoldingRangeKind::Environment => "environment", + FoldingRangeKind::Entry => "entry", + }) + } else { + None + }; - Some(foldings) -} + serde_json::json!({ + "startLine": range.start.line, + "startCharacter": range.start.character, + "endLine": range.end.line, + "endCharacter": range.end.character, + "kind": kind, + }) + }); -fn create_range(range: Range) -> FoldingRange { - FoldingRange { - start_line: range.start.line, - start_character: Some(range.start.character), - end_line: range.end.line, - end_character: Some(range.end.character), - collapsed_text: None, - kind: Some(FoldingRangeKind::Region), - } + Some(foldings.collect()) } diff --git a/crates/texlab/src/server.rs b/crates/texlab/src/server.rs index 5d3fc4a2..8f58bb38 100644 --- a/crates/texlab/src/server.rs +++ b/crates/texlab/src/server.rs @@ -605,8 +605,9 @@ impl Server { fn folding_range(&self, id: RequestId, params: FoldingRangeParams) -> Result<()> { let mut uri = params.text_document.uri; normalize_uri(&mut uri); + let client_capabilities = Arc::clone(&self.client_capabilities); self.run_query(id, move |db| { - folding::find_all(db, &uri).unwrap_or_default() + folding::find_all(db, &uri, &client_capabilities).unwrap_or_default() }); Ok(()) } diff --git a/crates/texlab/tests/lsp/text_document.rs b/crates/texlab/tests/lsp/text_document.rs index 88b63338..729ab428 100644 --- a/crates/texlab/tests/lsp/text_document.rs +++ b/crates/texlab/tests/lsp/text_document.rs @@ -2,7 +2,6 @@ mod completion; mod document_highlight; mod document_link; mod document_symbol; -mod folding_range; mod formatting; mod inlay_hint; mod rename; diff --git a/crates/texlab/tests/lsp/text_document/folding_range.rs b/crates/texlab/tests/lsp/text_document/folding_range.rs deleted file mode 100644 index 063ca753..00000000 --- a/crates/texlab/tests/lsp/text_document/folding_range.rs +++ /dev/null @@ -1,66 +0,0 @@ -use insta::assert_json_snapshot; -use lsp_types::{ - request::FoldingRangeRequest, ClientCapabilities, FoldingRange, FoldingRangeParams, -}; - -use crate::fixture::TestBed; - -fn find_foldings(fixture: &str) -> Vec { - let test_bed = TestBed::new(fixture).unwrap(); - - test_bed.initialize(ClientCapabilities::default()).unwrap(); - - let text_document = test_bed.cursor().unwrap().text_document; - test_bed - .client() - .send_request::(FoldingRangeParams { - text_document, - work_done_progress_params: Default::default(), - partial_result_params: Default::default(), - }) - .unwrap() - .unwrap_or_default() -} - -#[test] -fn latex() { - assert_json_snapshot!(find_foldings( - r#" -%! main.tex -\begin{document} - \section{Foo} - foo - \subsection{Bar} - bar - \section{Baz} - baz - \section{Qux} -\end{document} -|"# - )); -} - -#[test] -fn bibtex() { - assert_json_snapshot!(find_foldings( - r#" -%! main.bib -some junk -here - -@article{foo, - author = {bar}, - title = {baz} -} - -@string{foo = "bar"} - -@comment{foo, - author = {bar}, - title = {baz} -} - -@preamble{"foo"} -|"# - )); -} diff --git a/crates/texlab/tests/lsp/text_document/snapshots/lsp__text_document__folding_range__bibtex.snap b/crates/texlab/tests/lsp/text_document/snapshots/lsp__text_document__folding_range__bibtex.snap deleted file mode 100644 index 2063cc39..00000000 --- a/crates/texlab/tests/lsp/text_document/snapshots/lsp__text_document__folding_range__bibtex.snap +++ /dev/null @@ -1,27 +0,0 @@ ---- -source: tests/lsp/text_document/folding_range.rs -expression: "find_foldings(r#\"\n%! main.bib\nsome junk\nhere\n\n@article{foo,\n author = {bar},\n title = {baz}\n}\n\n@string{foo = \"bar\"}\n\n@comment{foo,\n author = {bar},\n title = {baz}\n}\n\n@preamble{\"foo\"}\n|\"#)" ---- -[ - { - "startLine": 3, - "startCharacter": 0, - "endLine": 6, - "endCharacter": 1, - "kind": "region" - }, - { - "startLine": 8, - "startCharacter": 0, - "endLine": 8, - "endCharacter": 20, - "kind": "region" - }, - { - "startLine": 15, - "startCharacter": 0, - "endLine": 15, - "endCharacter": 16, - "kind": "region" - } -] diff --git a/crates/texlab/tests/lsp/text_document/snapshots/lsp__text_document__folding_range__latex.snap b/crates/texlab/tests/lsp/text_document/snapshots/lsp__text_document__folding_range__latex.snap deleted file mode 100644 index 74632eda..00000000 --- a/crates/texlab/tests/lsp/text_document/snapshots/lsp__text_document__folding_range__latex.snap +++ /dev/null @@ -1,41 +0,0 @@ ---- -source: tests/lsp/text_document/folding_range.rs -expression: "find_foldings(r#\"\n%! main.tex\n\\begin{document}\n \\section{Foo}\n foo\n \\subsection{Bar}\n bar\n \\section{Baz}\n baz\n \\section{Qux}\n\\end{document}\n|\"#)" ---- -[ - { - "startLine": 0, - "startCharacter": 0, - "endLine": 8, - "endCharacter": 14, - "kind": "region" - }, - { - "startLine": 1, - "startCharacter": 4, - "endLine": 4, - "endCharacter": 7, - "kind": "region" - }, - { - "startLine": 3, - "startCharacter": 4, - "endLine": 4, - "endCharacter": 7, - "kind": "region" - }, - { - "startLine": 5, - "startCharacter": 4, - "endLine": 6, - "endCharacter": 7, - "kind": "region" - }, - { - "startLine": 7, - "startCharacter": 4, - "endLine": 7, - "endCharacter": 17, - "kind": "region" - } -]