Skip to content

Commit

Permalink
Improve behavior of folding feature (#918)
Browse files Browse the repository at this point in the history
Do not fold the entire structure but the contents inside.

See #915.
pfoerster authored Aug 14, 2023
1 parent ef6e5f1 commit da96c8e
Showing 12 changed files with 268 additions and 190 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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

12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions crates/folding/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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
107 changes: 107 additions & 0 deletions crates/folding/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<FoldingRange> {
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(&section);
} 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<FoldingRange>,
}

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;
88 changes: 88 additions & 0 deletions crates/folding/src/tests.rs
Original file line number Diff line number Diff line change
@@ -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,
},
]
"#]],
);
}
1 change: 1 addition & 0 deletions crates/texlab/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
90 changes: 36 additions & 54 deletions crates/texlab/src/features/folding.rs
Original file line number Diff line number Diff line change
@@ -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<Vec<FoldingRange>> {
pub fn find_all(
workspace: &Workspace,
uri: &Url,
capabilities: &ClientCapabilities,
) -> Option<Vec<serde_json::Value>> {
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())
}
3 changes: 2 additions & 1 deletion crates/texlab/src/server.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
1 change: 0 additions & 1 deletion crates/texlab/tests/lsp/text_document.rs
Original file line number Diff line number Diff line change
@@ -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;
66 changes: 0 additions & 66 deletions crates/texlab/tests/lsp/text_document/folding_range.rs

This file was deleted.

This file was deleted.

This file was deleted.

0 comments on commit da96c8e

Please sign in to comment.