summaryrefslogtreecommitdiff
path: root/support/texlab/src/features/formatting/bibtex_internal.rs
diff options
context:
space:
mode:
Diffstat (limited to 'support/texlab/src/features/formatting/bibtex_internal.rs')
-rw-r--r--support/texlab/src/features/formatting/bibtex_internal.rs381
1 files changed, 381 insertions, 0 deletions
diff --git a/support/texlab/src/features/formatting/bibtex_internal.rs b/support/texlab/src/features/formatting/bibtex_internal.rs
new file mode 100644
index 0000000000..8481f561e7
--- /dev/null
+++ b/support/texlab/src/features/formatting/bibtex_internal.rs
@@ -0,0 +1,381 @@
+use cancellation::CancellationToken;
+use cstree::NodeOrToken;
+use lsp_types::{DocumentFormattingParams, TextEdit};
+
+use crate::{
+ features::FeatureRequest,
+ syntax::{
+ bibtex::{self, HasType},
+ CstNode,
+ },
+ LineIndex, LineIndexExt,
+};
+
+pub fn format_bibtex_internal(
+ request: &FeatureRequest<DocumentFormattingParams>,
+ _cancellation_token: &CancellationToken,
+) -> Option<Vec<TextEdit>> {
+ let mut indent = String::new();
+ if request.params.options.insert_spaces {
+ for _ in 0..request.params.options.tab_size {
+ indent.push(' ');
+ }
+ } else {
+ indent.push('\t');
+ }
+
+ let line_length = {
+ request
+ .context
+ .options
+ .read()
+ .unwrap()
+ .formatter_line_length
+ .map(|value| {
+ if value <= 0 {
+ usize::MAX
+ } else {
+ value as usize
+ }
+ })
+ .unwrap_or(80)
+ };
+
+ let document = request.main_document();
+ let data = document.data.as_bibtex()?;
+ let mut edits = Vec::new();
+
+ for node in data.root.children() {
+ let range = if let Some(entry) = bibtex::Entry::cast(node) {
+ entry.small_range()
+ } else if let Some(string) = bibtex::String::cast(node) {
+ string.small_range()
+ } else if let Some(preamble) = bibtex::Preamble::cast(node) {
+ preamble.small_range()
+ } else {
+ continue;
+ };
+
+ let mut formatter = Formatter::new(
+ indent.clone(),
+ request.params.options.tab_size,
+ line_length,
+ &document.line_index,
+ );
+
+ formatter.visit_node(node);
+ edits.push(TextEdit {
+ range: document.line_index.line_col_lsp_range(range),
+ new_text: formatter.output,
+ });
+ }
+
+ Some(edits)
+}
+
+struct Formatter<'a> {
+ indent: String,
+ tab_size: u32,
+ line_length: usize,
+ output: String,
+ align: Vec<usize>,
+ line_index: &'a LineIndex,
+}
+
+impl<'a> Formatter<'a> {
+ fn new(indent: String, tab_size: u32, line_length: usize, line_index: &'a LineIndex) -> Self {
+ Self {
+ indent,
+ tab_size,
+ line_length,
+ output: String::new(),
+ align: Vec::new(),
+ line_index,
+ }
+ }
+
+ fn visit_token_lowercase(&mut self, token: &bibtex::SyntaxToken) {
+ self.output.push_str(&token.text().to_lowercase());
+ }
+
+ fn should_insert_space(
+ &self,
+ previous: &bibtex::SyntaxToken,
+ current: &bibtex::SyntaxToken,
+ ) -> bool {
+ let previous_range = self.line_index.line_col_lsp_range(previous.text_range());
+ let current_range = self.line_index.line_col_lsp_range(current.text_range());
+ previous_range.start.line != current_range.start.line
+ || previous_range.end.character < current_range.start.character
+ }
+
+ fn base_align(&self) -> usize {
+ self.output[self.output.rfind('\n').unwrap_or(0)..]
+ .chars()
+ .count()
+ }
+
+ fn visit_node(&mut self, parent: &bibtex::SyntaxNode) {
+ match parent.kind() {
+ bibtex::PREAMBLE => {
+ let preamble = bibtex::Preamble::cast(parent).unwrap();
+ self.visit_token_lowercase(preamble.ty().unwrap());
+ self.output.push('{');
+ if preamble.syntax().arity() > 0 {
+ self.align.push(self.base_align());
+ for node in preamble.syntax().children() {
+ self.visit_node(node);
+ }
+ self.output.push('}');
+ }
+ }
+ bibtex::STRING => {
+ let string = bibtex::String::cast(parent).unwrap();
+ self.visit_token_lowercase(string.ty().unwrap());
+ self.output.push('{');
+ if let Some(name) = string.name() {
+ self.output.push_str(name.text());
+ self.output.push_str(" = ");
+ if let Some(value) = string.value() {
+ self.align.push(self.base_align());
+ self.visit_node(value.syntax());
+ self.output.push('}');
+ }
+ }
+ }
+ bibtex::ENTRY => {
+ let entry = bibtex::Entry::cast(parent).unwrap();
+ self.visit_token_lowercase(entry.ty().unwrap());
+ self.output.push('{');
+ if let Some(key) = entry.key() {
+ self.output.push_str(key.text());
+ self.output.push(',');
+ self.output.push('\n');
+ for field in entry.fields() {
+ self.visit_node(field.syntax());
+ }
+ self.output.push('}');
+ }
+ }
+ bibtex::FIELD => {
+ let field = bibtex::Field::cast(parent).unwrap();
+ self.output.push_str(&self.indent);
+ let name = field.name().unwrap();
+ self.output.push_str(name.text());
+ self.output.push_str(" = ");
+ if let Some(value) = field.value() {
+ let count = name.text().chars().count();
+ self.align.push(self.tab_size as usize + count + 3);
+ self.visit_node(value.syntax());
+ self.output.push(',');
+ self.output.push('\n');
+ }
+ }
+ bibtex::VALUE => {
+ let tokens: Vec<_> = parent
+ .descendants_with_tokens()
+ .filter_map(|element| element.into_token())
+ .filter(|token| token.kind() != bibtex::WHITESPACE)
+ .collect();
+
+ self.output.push_str(tokens[0].text());
+
+ let align = self.align.pop().unwrap_or_default();
+ let mut length = align + tokens[0].text().chars().count();
+ for i in 1..tokens.len() {
+ let previous = tokens[i - 1];
+ let current = tokens[i];
+ let current_length = current.text().chars().count();
+
+ let insert_space = self.should_insert_space(previous, current);
+ let space_length = if insert_space { 1 } else { 0 };
+
+ if length + current_length + space_length > self.line_length {
+ self.output.push('\n');
+ self.output.push_str(self.indent.as_ref());
+ for _ in 0..=align - self.tab_size as usize {
+ self.output.push(' ');
+ }
+ length = align;
+ } else if insert_space {
+ self.output.push(' ');
+ length += 1;
+ }
+ self.output.push_str(current.text());
+ length += current_length;
+ }
+ }
+ bibtex::ROOT | bibtex::JUNK | bibtex::COMMENT => {
+ for element in parent.children_with_tokens() {
+ match element {
+ NodeOrToken::Token(token) => {
+ self.output.push_str(token.text());
+ }
+ NodeOrToken::Node(node) => {
+ self.visit_node(node);
+ }
+ }
+ }
+ }
+ _ => unreachable!(),
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use insta::{assert_debug_snapshot, assert_display_snapshot};
+
+ use crate::features::testing::FeatureTester;
+
+ use super::*;
+
+ #[test]
+ fn test_wrap_long_lines() {
+ let request = FeatureTester::builder()
+ .files(vec![(
+ "main.bib",
+ "@article{foo, bar = {Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit.},}",
+ )])
+ .main("main.bib")
+ .build()
+ .formatting();
+
+ let edit = format_bibtex_internal(&request, CancellationToken::none())
+ .unwrap()
+ .pop()
+ .unwrap();
+
+ assert_display_snapshot!(edit.new_text);
+ }
+
+ #[test]
+ fn test_multiple_entries() {
+ let request = FeatureTester::builder()
+ .files(vec![(
+ "main.bib",
+ "@article{foo, bar = {Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit.},}\n\n@article{foo, bar = {Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit.},}",
+ )])
+ .main("main.bib")
+ .build()
+ .formatting();
+
+ let mut edits = format_bibtex_internal(&request, CancellationToken::none()).unwrap();
+ let edit2 = edits.pop().unwrap();
+ let edit1 = edits.pop().unwrap();
+
+ assert_debug_snapshot!((edit1.new_text, edit2.new_text));
+ }
+
+ #[test]
+ fn test_trailing_comma() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "@article{foo, bar = baz}")])
+ .main("main.bib")
+ .build()
+ .formatting();
+
+ let edit = format_bibtex_internal(&request, CancellationToken::none())
+ .unwrap()
+ .pop()
+ .unwrap();
+
+ assert_display_snapshot!(edit.new_text);
+ }
+
+ #[test]
+ fn test_insert_braces() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "@article{foo, bar = baz,")])
+ .main("main.bib")
+ .build()
+ .formatting();
+
+ let edit = format_bibtex_internal(&request, CancellationToken::none())
+ .unwrap()
+ .pop()
+ .unwrap();
+
+ assert_display_snapshot!(edit.new_text);
+ }
+
+ #[test]
+ fn test_command() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "@article{foo, bar = \"\\baz\",}")])
+ .main("main.bib")
+ .build()
+ .formatting();
+
+ let edit = format_bibtex_internal(&request, CancellationToken::none())
+ .unwrap()
+ .pop()
+ .unwrap();
+
+ assert_display_snapshot!(edit.new_text);
+ }
+
+ #[test]
+ fn test_concatenation() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "@article{foo, bar = \"baz\" # \"qux\"}")])
+ .main("main.bib")
+ .build()
+ .formatting();
+
+ let edit = format_bibtex_internal(&request, CancellationToken::none())
+ .unwrap()
+ .pop()
+ .unwrap();
+
+ assert_display_snapshot!(edit.new_text);
+ }
+
+ #[test]
+ fn test_parens() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "@article(foo,)")])
+ .main("main.bib")
+ .build()
+ .formatting();
+
+ let edit = format_bibtex_internal(&request, CancellationToken::none())
+ .unwrap()
+ .pop()
+ .unwrap();
+
+ assert_display_snapshot!(edit.new_text);
+ }
+
+ #[test]
+ fn test_string() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "@string{foo=\"bar\"}")])
+ .main("main.bib")
+ .build()
+ .formatting();
+
+ let edit = format_bibtex_internal(&request, CancellationToken::none())
+ .unwrap()
+ .pop()
+ .unwrap();
+
+ assert_display_snapshot!(edit.new_text);
+ }
+
+ #[test]
+ fn test_preamble() {
+ let request = FeatureTester::builder()
+ .files(vec![("main.bib", "@preamble{\n\"foo bar baz\"}")])
+ .main("main.bib")
+ .build()
+ .formatting();
+
+ let edit = format_bibtex_internal(&request, CancellationToken::none())
+ .unwrap()
+ .pop()
+ .unwrap();
+
+ assert_display_snapshot!(edit.new_text);
+ }
+}