From 623f01ea3dba34bc1b5d33d4d0806be9fb6299ed Mon Sep 17 00:00:00 2001 From: Matthis Kruse Date: Thu, 13 Jun 2024 23:13:36 +0200 Subject: [PATCH] Add configurable prefixes for custom label commands. (#1139) This commit extends the configs with a map from label command name to the respective prefix: * Prepends user-defined prefixes to labels in the semantics pass. Note that this yields to a mismatch between the actual length of a span and its range, but there are no (anticipated) bad side-effects due to this. * Adds two tests that ensure completion for custom labels with custom prefix works as expected. The `\ref` command should list the prefix, while a custom reference command with a configured prefix should not. * Implements key functionality for renaming labels with prefixes. A sane assumption it does is that all labels found as candidate for renaming share a common prefix, up to looking it up first and prepending it, e.g., for `\goal{foo}`. Instead of storing a renaming candidate for each entry, it only keeps track of the prefixes and prepends them accordingly, depending on the renaming candidate, by swapping the `TextRange` with `RenameInformation` inside the `RenameResult`. Unfortunately, this pollutes a bit the other renaming ops, such as commands or citations, which don't have prefixes. Nevertheless, changes there have been incremental and `RenameInformation` implements a `From` to easily map an existing `TextRange` into it, simply assuming an empty prefix. In terms of tests, the `prefix` should be ignored. --- Cargo.lock | 2 + crates/base-db/src/document.rs | 5 +- crates/base-db/src/semantics/tex.rs | 68 ++++-- crates/completion/Cargo.toml | 1 + crates/completion/src/lib.rs | 2 +- crates/completion/src/providers/label_ref.rs | 46 +++- crates/completion/src/tests.rs | 213 ++++++++++++++++++- crates/parser/src/config.rs | 18 ++ crates/rename/Cargo.toml | 1 + crates/rename/src/command.rs | 5 +- crates/rename/src/entry.rs | 2 +- crates/rename/src/label.rs | 47 +++- crates/rename/src/lib.rs | 22 +- crates/rename/src/tests.rs | 111 +++++++++- crates/texlab/src/server/options.rs | 5 +- crates/texlab/src/util/from_proto.rs | 10 + crates/texlab/src/util/to_proto.rs | 11 +- 17 files changed, 529 insertions(+), 40 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 140bd097..b677bd40 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -334,6 +334,7 @@ dependencies = [ "expect-test", "fuzzy-matcher", "line-index", + "parser", "rayon", "rowan", "rustc-hash", @@ -1337,6 +1338,7 @@ name = "rename" version = "0.0.0" dependencies = [ "base-db", + "parser", "rowan", "rustc-hash", "syntax", diff --git a/crates/base-db/src/document.rs b/crates/base-db/src/document.rs index 7a388129..afb5869d 100644 --- a/crates/base-db/src/document.rs +++ b/crates/base-db/src/document.rs @@ -56,7 +56,10 @@ impl Document { Language::Tex => { let green = parser::parse_latex(&text, ¶ms.config.syntax); let mut semantics = semantics::tex::Semantics::default(); - semantics.process_root(&latex::SyntaxNode::new_root(green.clone())); + semantics.process_root( + ¶ms.config.syntax, + &latex::SyntaxNode::new_root(green.clone()), + ); DocumentData::Tex(TexDocumentData { green, semantics }) } Language::Bib => { diff --git a/crates/base-db/src/semantics/tex.rs b/crates/base-db/src/semantics/tex.rs index 7e4c926f..a1301091 100644 --- a/crates/base-db/src/semantics/tex.rs +++ b/crates/base-db/src/semantics/tex.rs @@ -3,7 +3,28 @@ use rustc_hash::FxHashSet; use syntax::latex::{self, HasBrack, HasCurly}; use titlecase::titlecase; +use parser::SyntaxConfig; + use super::Span; +use crate::semantics::tex::latex::SyntaxToken; + +fn maybe_prepend_prefix( + map: &Vec<(String, String)>, + command: &Option, + name: &Span, +) -> Span { + match command { + Some(x) => Span::new( + map.iter() + .find_map(|(k, v)| if k == &x.text()[1..] { Some(v) } else { None }) + .unwrap_or(&String::new()) + .to_owned() + + &name.text, + name.range, + ), + None => name.clone(), + } +} #[derive(Debug, Clone, Default)] pub struct Semantics { @@ -19,11 +40,11 @@ pub struct Semantics { } impl Semantics { - pub fn process_root(&mut self, root: &latex::SyntaxNode) { + pub fn process_root(&mut self, conf: &SyntaxConfig, root: &latex::SyntaxNode) { for node in root.descendants_with_tokens() { match node { latex::SyntaxElement::Node(node) => { - self.process_node(&node); + self.process_node(conf, &node); } latex::SyntaxElement::Token(token) => { if token.kind() == latex::COMMAND_NAME { @@ -40,17 +61,17 @@ impl Semantics { .any(|link| link.kind == LinkKind::Cls && link.path.text == "subfiles"); } - fn process_node(&mut self, node: &latex::SyntaxNode) { + fn process_node(&mut self, conf: &SyntaxConfig, node: &latex::SyntaxNode) { if let Some(include) = latex::Include::cast(node.clone()) { self.process_include(include); } else if let Some(import) = latex::Import::cast(node.clone()) { self.process_import(import); } else if let Some(label) = latex::LabelDefinition::cast(node.clone()) { - self.process_label_definition(label); + self.process_label_definition(conf, label); } else if let Some(label) = latex::LabelReference::cast(node.clone()) { - self.process_label_reference(label); + self.process_label_reference(conf, label); } else if let Some(label) = latex::LabelReferenceRange::cast(node.clone()) { - self.process_label_reference_range(label); + self.process_label_reference_range(conf, label); } else if let Some(citation) = latex::Citation::cast(node.clone()) { self.process_citation(citation); } else if let Some(environment) = latex::Environment::cast(node.clone()) { @@ -111,7 +132,7 @@ impl Semantics { }); } - fn process_label_definition(&mut self, label: latex::LabelDefinition) { + fn process_label_definition(&mut self, conf: &SyntaxConfig, label: latex::LabelDefinition) { let Some(name) = label.name().and_then(|group| group.key()) else { return; }; @@ -190,13 +211,14 @@ impl Semantics { self.labels.push(Label { kind: LabelKind::Definition, - name, + cmd: label.command().map(|x| x.text()[1..].to_string()), + name: maybe_prepend_prefix(&conf.label_definition_prefixes, &label.command(), &name), targets: objects, full_range, }); } - fn process_label_reference(&mut self, label: latex::LabelReference) { + fn process_label_reference(&mut self, conf: &SyntaxConfig, label: latex::LabelReference) { let Some(name_list) = label.name_list() else { return; }; @@ -207,7 +229,12 @@ impl Semantics { if !name.text.contains('#') { self.labels.push(Label { kind: LabelKind::Reference, - name, + cmd: label.command().map(|x| x.text()[1..].to_string()), + name: maybe_prepend_prefix( + &conf.label_reference_prefixes, + &label.command(), + &name, + ), targets: Vec::new(), full_range, }); @@ -215,14 +242,23 @@ impl Semantics { } } - fn process_label_reference_range(&mut self, label: latex::LabelReferenceRange) { + fn process_label_reference_range( + &mut self, + conf: &SyntaxConfig, + label: latex::LabelReferenceRange, + ) { let full_range = latex::small_range(&label); if let Some(from) = label.from().and_then(|group| group.key()) { let name = Span::from(&from); if !name.text.contains('#') { self.labels.push(Label { kind: LabelKind::ReferenceRange, - name, + cmd: label.command().map(|x| x.text()[1..].to_string()), + name: maybe_prepend_prefix( + &conf.label_reference_prefixes, + &label.command(), + &name, + ), targets: Vec::new(), full_range, }); @@ -234,7 +270,12 @@ impl Semantics { if !name.text.contains('#') { self.labels.push(Label { kind: LabelKind::ReferenceRange, - name, + cmd: label.command().map(|x| x.text()[1..].to_string()), + name: maybe_prepend_prefix( + &conf.label_reference_prefixes, + &label.command(), + &name, + ), targets: Vec::new(), full_range, }); @@ -336,6 +377,7 @@ pub enum LabelKind { #[derive(Debug, Clone)] pub struct Label { pub kind: LabelKind, + pub cmd: Option, pub name: Span, pub targets: Vec, pub full_range: TextRange, diff --git a/crates/completion/Cargo.toml b/crates/completion/Cargo.toml index e28e04df..6469b5f0 100644 --- a/crates/completion/Cargo.toml +++ b/crates/completion/Cargo.toml @@ -22,6 +22,7 @@ criterion = "0.5.1" distro = { path = "../distro" } expect-test = "1.5.0" test-utils = { path = "../test-utils" } +parser = { path = "../parser" } [lib] doctest = false diff --git a/crates/completion/src/lib.rs b/crates/completion/src/lib.rs index b1050052..5bfd94d9 100644 --- a/crates/completion/src/lib.rs +++ b/crates/completion/src/lib.rs @@ -72,7 +72,7 @@ impl<'a> CompletionItemData<'a> { Self::Citation(data) => &data.entry.name.text, Self::Environment(data) => data.name, Self::GlossaryEntry(data) => &data.name, - Self::Label(data) => data.name, + Self::Label(data) => &data.name, Self::Color(name) => name, Self::ColorModel(name) => name, Self::File(name) => name, diff --git a/crates/completion/src/providers/label_ref.rs b/crates/completion/src/providers/label_ref.rs index 6f71ae32..8b262f13 100644 --- a/crates/completion/src/providers/label_ref.rs +++ b/crates/completion/src/providers/label_ref.rs @@ -11,12 +11,30 @@ use crate::{ CompletionItem, CompletionItemData, CompletionParams, }; +fn trim_prefix<'a>(prefix: Option<&'a str>, text: &'a str) -> &'a str { + prefix + .and_then(|pref| text.strip_prefix(pref)) + .unwrap_or(text) +} + pub fn complete_label_references<'a>( params: &'a CompletionParams<'a>, builder: &mut CompletionBuilder<'a>, ) -> Option<()> { - let FindResult { cursor, is_math } = - find_reference(params).or_else(|| find_reference_range(params))?; + let FindResult { + cursor, + is_math, + command, + } = find_reference(params).or_else(|| find_reference_range(params))?; + let ref_pref = params + .feature + .workspace + .config() + .syntax + .label_reference_prefixes + .iter() + .find_map(|(k, v)| if *k == command { Some(v) } else { None }) + .map(|x| x.as_str()); for document in ¶ms.feature.project.documents { let DocumentData::Tex(data) = &document.data else { @@ -29,6 +47,10 @@ pub fn complete_label_references<'a>( .iter() .filter(|label| label.kind == LabelKind::Definition) { + if ref_pref.map_or(false, |pref| !label.name.text.starts_with(pref)) { + continue; + } + let labeltext = trim_prefix(ref_pref, &label.name.text); match render_label(params.feature.workspace, ¶ms.feature.project, label) { Some(rendered_label) => { if is_math && !matches!(rendered_label.object, RenderedObject::Equation) { @@ -41,11 +63,12 @@ pub fn complete_label_references<'a>( _ => None, }; - let keywords = format!("{} {}", label.name.text, rendered_label.reference()); + let keywords = format!("{} {}", labeltext, rendered_label.reference()); if let Some(score) = builder.matcher.score(&keywords, &cursor.text) { + let name = trim_prefix(ref_pref, &label.name.text); let data = CompletionItemData::Label(crate::LabelData { - name: &label.name.text, + name, header, footer, object: Some(rendered_label.object), @@ -59,12 +82,13 @@ pub fn complete_label_references<'a>( } None => { if let Some(score) = builder.matcher.score(&label.name.text, &cursor.text) { + let name = trim_prefix(ref_pref, &label.name.text); let data = CompletionItemData::Label(crate::LabelData { - name: &label.name.text, + name, header: None, footer: None, object: None, - keywords: label.name.text.clone(), + keywords: labeltext.to_string(), }); builder @@ -82,20 +106,26 @@ pub fn complete_label_references<'a>( struct FindResult { cursor: Span, is_math: bool, + command: String, } fn find_reference(params: &CompletionParams) -> Option { let (cursor, group) = find_curly_group_word_list(params)?; let reference = latex::LabelReference::cast(group.syntax().parent()?)?; let is_math = reference.command()?.text() == "\\eqref"; - Some(FindResult { cursor, is_math }) + Some(FindResult { + cursor, + is_math, + command: reference.command()?.text()[1..].to_string(), + }) } fn find_reference_range(params: &CompletionParams) -> Option { let (cursor, group) = find_curly_group_word(params)?; - latex::LabelReferenceRange::cast(group.syntax().parent()?)?; + let refrange = latex::LabelReferenceRange::cast(group.syntax().parent()?)?; Some(FindResult { cursor, is_math: false, + command: refrange.command()?.text()[1..].to_string(), }) } diff --git a/crates/completion/src/tests.rs b/crates/completion/src/tests.rs index c6bed6e0..8c68b71d 100644 --- a/crates/completion/src/tests.rs +++ b/crates/completion/src/tests.rs @@ -1,11 +1,17 @@ -use base_db::FeatureParams; +use base_db::{Config, FeatureParams}; use expect_test::{expect, Expect}; +use parser::SyntaxConfig; use rowan::TextRange; use crate::CompletionParams; -fn check(input: &str, expect: Expect) { - let fixture = test_utils::fixture::Fixture::parse(input); +fn check_with_syntax_config(config: SyntaxConfig, input: &str, expect: Expect) { + let mut fixture = test_utils::fixture::Fixture::parse(input); + fixture.workspace.set_config(Config { + syntax: config, + ..Config::default() + }); + let fixture = fixture; let (offset, spec) = fixture .documents @@ -37,6 +43,10 @@ fn check(input: &str, expect: Expect) { expect.assert_debug_eq(&items); } +fn check(input: &str, expect: Expect) { + check_with_syntax_config(SyntaxConfig::default(), input, expect) +} + #[test] fn acronym_ref_simple() { check( @@ -2134,3 +2144,200 @@ fn issue_885() { "#]], ); } + +#[test] +fn test_custom_label_prefix_ref() { + let mut config = SyntaxConfig::default(); + config.label_definition_commands.insert("asm".to_string()); + config + .label_definition_prefixes + .push(("asm".to_string(), "asm:".to_string())); + + check_with_syntax_config( + config, + r#" +%! main.tex + \begin{enumerate}\label{baz} + \asm{foo}{what} + \end{enumerate} + + \ref{} + | +% Comment"#, + expect![[r#" + [ + Label( + LabelData { + name: "asm:foo", + header: None, + footer: None, + object: None, + keywords: "asm:foo", + }, + ), + Label( + LabelData { + name: "baz", + header: None, + footer: None, + object: None, + keywords: "baz", + }, + ), + ] + "#]], + ); +} + +#[test] +fn test_custom_label_prefix_custom_ref() { + let mut config = SyntaxConfig::default(); + config.label_definition_commands.insert("asm".to_string()); + config + .label_definition_prefixes + .push(("asm".to_string(), "asm:".to_string())); + config.label_reference_commands.insert("asmref".to_string()); + config + .label_reference_prefixes + .push(("asmref".to_string(), "asm:".to_string())); + + check_with_syntax_config( + config, + r#" +%! main.tex + \begin{enumerate}\label{baz} + \asm{foo}{what} + \end{enumerate} + + \asmref{} + | +% Comment"#, + expect![[r#" + [ + Label( + LabelData { + name: "foo", + header: None, + footer: None, + object: None, + keywords: "foo", + }, + ), + ] + "#]], + ); +} + +#[test] +fn test_custom_label_multiple_prefix_custom_ref() { + let mut config = SyntaxConfig::default(); + config + .label_definition_commands + .extend(vec!["asm", "goal"].into_iter().map(String::from)); + config.label_definition_prefixes.extend( + vec![("asm", "asm:"), ("goal", "goal:")] + .into_iter() + .map(|(x, y)| (String::from(x), String::from(y))), + ); + config + .label_reference_commands + .extend(vec!["asmref", "goalref"].into_iter().map(String::from)); + config.label_reference_prefixes.extend( + vec![("asmref", "asm:"), ("goalref", "goal:")] + .into_iter() + .map(|(x, y)| (String::from(x), String::from(y))), + ); + + check_with_syntax_config( + config, + r#" +%! main.tex + \begin{enumerate}\label{baz} + \asm{foo}{what} + \goal{foo}{what} + \end{enumerate} + + \goalref{} + | +% Comment"#, + expect![[r#" + [ + Label( + LabelData { + name: "foo", + header: None, + footer: None, + object: None, + keywords: "foo", + }, + ), + ] + "#]], + ); +} + +#[test] +fn test_custom_label_multiple_prefix_ref() { + let mut config = SyntaxConfig::default(); + config + .label_definition_commands + .extend(vec!["asm", "goal"].into_iter().map(String::from)); + config.label_definition_prefixes.extend( + vec![("asm", "asm:"), ("goal", "goal:")] + .into_iter() + .map(|(x, y)| (String::from(x), String::from(y))), + ); + config + .label_reference_commands + .extend(vec!["asmref", "goalref"].into_iter().map(String::from)); + config.label_reference_prefixes.extend( + vec![("asmref", "asm:"), ("goalref", "goal:")] + .into_iter() + .map(|(x, y)| (String::from(x), String::from(y))), + ); + + check_with_syntax_config( + config, + r#" +%! main.tex + \begin{enumerate}\label{baz} + \asm{foo}{what} + \goal{foo}{what} + \end{enumerate} + + \ref{} + | +% Comment"#, + expect![[r#" + [ + Label( + LabelData { + name: "asm:foo", + header: None, + footer: None, + object: None, + keywords: "asm:foo", + }, + ), + Label( + LabelData { + name: "baz", + header: None, + footer: None, + object: None, + keywords: "baz", + }, + ), + Label( + LabelData { + name: "goal:foo", + header: None, + footer: None, + object: None, + keywords: "goal:foo", + }, + ), + ] + "#]], + ); +} diff --git a/crates/parser/src/config.rs b/crates/parser/src/config.rs index 2fb3cb86..95b3e4eb 100644 --- a/crates/parser/src/config.rs +++ b/crates/parser/src/config.rs @@ -8,7 +8,9 @@ pub struct SyntaxConfig { pub verbatim_environments: FxHashSet, pub citation_commands: FxHashSet, pub label_definition_commands: FxHashSet, + pub label_definition_prefixes: Vec<(String, String)>, pub label_reference_commands: FxHashSet, + pub label_reference_prefixes: Vec<(String, String)>, } impl Default for SyntaxConfig { @@ -38,11 +40,21 @@ impl Default for SyntaxConfig { .map(ToString::to_string) .collect(); + let label_definition_prefixes = DEFAULT_LABEL_DEFINITION_PREFIXES + .iter() + .map(|(x, y)| (ToString::to_string(x), ToString::to_string(y))) + .collect(); + let label_reference_commands = DEFAULT_LABEL_REFERENCE_COMMANDS .iter() .map(ToString::to_string) .collect(); + let label_reference_prefixes = DEFAULT_LABEL_REFERENCE_PREFIXES + .iter() + .map(|(x, y)| (ToString::to_string(x), ToString::to_string(y))) + .collect(); + Self { follow_package_links: false, math_environments, @@ -50,7 +62,9 @@ impl Default for SyntaxConfig { verbatim_environments, citation_commands, label_definition_commands, + label_definition_prefixes, label_reference_commands, + label_reference_prefixes, } } } @@ -172,6 +186,8 @@ static DEFAULT_CITATION_COMMANDS: &[&str] = &[ static DEFAULT_LABEL_DEFINITION_COMMANDS: &[&str] = &["label", "zlabel"]; +static DEFAULT_LABEL_DEFINITION_PREFIXES: &[(&str, &str)] = &[]; + static DEFAULT_LABEL_REFERENCE_COMMANDS: &[&str] = &[ "ref", "vref", @@ -196,3 +212,5 @@ static DEFAULT_LABEL_REFERENCE_COMMANDS: &[&str] = &[ "labelcpageref", "eqref", ]; + +static DEFAULT_LABEL_REFERENCE_PREFIXES: &[(&str, &str)] = &[]; diff --git a/crates/rename/Cargo.toml b/crates/rename/Cargo.toml index 1b2b91ee..dc755851 100644 --- a/crates/rename/Cargo.toml +++ b/crates/rename/Cargo.toml @@ -14,6 +14,7 @@ syntax = { path = "../syntax" } [dev-dependencies] test-utils = { path = "../test-utils" } +parser = { path = "../parser" } [lib] doctest = false diff --git a/crates/rename/src/command.rs b/crates/rename/src/command.rs index 7b17d427..037e09e4 100644 --- a/crates/rename/src/command.rs +++ b/crates/rename/src/command.rs @@ -35,7 +35,10 @@ pub(super) fn rename(builder: &mut RenameBuilder) -> Option<()> { } } - builder.result.changes.insert(*document, edits); + builder + .result + .changes + .insert(*document, edits.iter().map(|&x| x.into()).collect()); } Some(()) diff --git a/crates/rename/src/entry.rs b/crates/rename/src/entry.rs index 5bed4ae6..d35d9e9e 100644 --- a/crates/rename/src/entry.rs +++ b/crates/rename/src/entry.rs @@ -42,7 +42,7 @@ pub(super) fn rename(builder: &mut RenameBuilder) -> Option<()> { for (document, range) in citations.chain(entries) { let entry = builder.result.changes.entry(document); - entry.or_default().push(range); + entry.or_default().push(range.into()); } Some(()) diff --git a/crates/rename/src/label.rs b/crates/rename/src/label.rs index b21e1b79..1d82f5e2 100644 --- a/crates/rename/src/label.rs +++ b/crates/rename/src/label.rs @@ -3,22 +3,65 @@ use base_db::{ util::queries::{self, Object}, }; -use crate::{RenameBuilder, RenameParams}; +use crate::{RenameBuilder, RenameInformation, RenameParams}; + +struct PrefixInformation<'a> { + def_prefixes: &'a Vec<(String, String)>, + ref_prefixes: &'a Vec<(String, String)>, +} + +fn label_has_prefix(pref_info: &PrefixInformation, label: &tex::Label) -> Option { + match label.kind { + tex::LabelKind::Definition => pref_info.def_prefixes.iter(), + _ => pref_info.ref_prefixes.iter(), + } + .find_map(|(k, v)| { + if k == &label.cmd.clone().unwrap_or(String::new()) { + Some(v) + } else { + None + } + }) + .cloned() +} + +fn find_prefix_in_any( + builder: &mut RenameBuilder, + pref_info: &PrefixInformation, + name: &str, +) -> Option { + let project = &builder.params.feature.project; + queries::objects_with_name::(project, name) + .find_map(|(_, label)| label_has_prefix(&pref_info, label)) +} pub(super) fn prepare_rename(params: &RenameParams) -> Option { let data = params.feature.document.data.as_tex()?; let labels = &data.semantics.labels; let label = queries::object_at_cursor(labels, params.offset, queries::SearchMode::Name)?; + Some(Span::new(label.object.name.text.clone(), label.range)) } pub(super) fn rename(builder: &mut RenameBuilder) -> Option<()> { let name = prepare_rename(&builder.params)?; + let syn = &builder.params.feature.workspace.config().syntax; + let pref_info = PrefixInformation { + def_prefixes: &syn.label_definition_prefixes, + ref_prefixes: &syn.label_reference_prefixes, + }; + let prefix = find_prefix_in_any(builder, &pref_info, &name.text); + let project = &builder.params.feature.project; for (document, label) in queries::objects_with_name::(project, &name.text) { + let prefix = label_has_prefix(&pref_info, label).map_or(prefix.clone(), |_| None); + let entry = builder.result.changes.entry(document); - entry.or_default().push(label.name_range()); + entry.or_default().push(RenameInformation { + range: label.name_range(), + prefix: prefix.clone(), + }); } Some(()) diff --git a/crates/rename/src/lib.rs b/crates/rename/src/lib.rs index bf9023dd..e6c05ad2 100644 --- a/crates/rename/src/lib.rs +++ b/crates/rename/src/lib.rs @@ -12,9 +12,15 @@ pub struct RenameParams<'a> { pub offset: TextSize, } +#[derive(Debug, Default)] +pub struct RenameInformation { + pub range: TextRange, + pub prefix: Option, +} + #[derive(Debug, Default)] pub struct RenameResult<'a> { - pub changes: FxHashMap<&'a Document, Vec>, + pub changes: FxHashMap<&'a Document, Vec>, } struct RenameBuilder<'a> { @@ -22,6 +28,20 @@ struct RenameBuilder<'a> { result: RenameResult<'a>, } +impl From for RenameInformation { + fn from(range: TextRange) -> Self { + RenameInformation { + range, + prefix: None, + } + } +} +impl PartialEq for RenameInformation { + fn eq(&self, other: &Self) -> bool { + self.range == other.range + } +} + pub fn prepare_rename(params: &RenameParams) -> Option { command::prepare_rename(params) .or_else(|| entry::prepare_rename(params)) diff --git a/crates/rename/src/tests.rs b/crates/rename/src/tests.rs index 0f426303..4da866fb 100644 --- a/crates/rename/src/tests.rs +++ b/crates/rename/src/tests.rs @@ -1,23 +1,37 @@ use rustc_hash::FxHashMap; -use crate::RenameParams; +use base_db::Config; +use parser::SyntaxConfig; -fn check(input: &str) { - let fixture = test_utils::fixture::Fixture::parse(input); +use crate::{RenameInformation, RenameParams}; + +fn check_with_syntax_config(config: SyntaxConfig, input: &str) { + let mut fixture = test_utils::fixture::Fixture::parse(input); + fixture.workspace.set_config(Config { + syntax: config, + ..Config::default() + }); + let fixture = fixture; - let mut expected = FxHashMap::default(); + let mut expected: FxHashMap<_, Vec> = FxHashMap::default(); for spec in &fixture.documents { if !spec.ranges.is_empty() { let document = fixture.workspace.lookup(&spec.uri).unwrap(); - expected.insert(document, spec.ranges.clone()); + expected.insert( + document, + spec.ranges.iter().map(|r| r.clone().into()).collect(), + ); } } - let (feature, offset) = fixture.make_params().unwrap(); let actual = crate::rename(RenameParams { feature, offset }); assert_eq!(actual.changes, expected); } +fn check(input: &str) { + check_with_syntax_config(SyntaxConfig::default(), input) +} + #[test] fn test_command() { check( @@ -78,12 +92,97 @@ fn test_label() { | ^^^ +%! baz.tex +\ref{foo} + %! bar.tex \ref{foo} ^^^ +"#, + ) +} + +#[test] +fn test_custom_label_ref() { + let mut config = SyntaxConfig::default(); + config + .label_definition_commands + .extend(vec!["asm", "goal"].into_iter().map(String::from)); + config.label_definition_prefixes.extend( + vec![("asm", "asm:"), ("goal", "goal:")] + .into_iter() + .map(|(x, y)| (String::from(x), String::from(y))), + ); + config + .label_reference_commands + .extend(vec!["asmref", "goalref"].into_iter().map(String::from)); + config.label_reference_prefixes.extend( + vec![("asmref", "asm:"), ("goalref", "goal:")] + .into_iter() + .map(|(x, y)| (String::from(x), String::from(y))), + ); + check_with_syntax_config( + config, + r#" +%! foo.tex +\goal{foo} + +\asm{foo}\include{bar}\include{baz} + | + ^^^ + +%! bar.tex +\asmref{foo} + ^^^ + +%! baz.tex +\ref{foo} + +\ref{asm:foo} + ^^^^^^^ + +"#, + ) +} + +#[test] +fn test_custom_label_def() { + let mut config = SyntaxConfig::default(); + config + .label_definition_commands + .extend(vec!["asm", "goal"].into_iter().map(String::from)); + config.label_definition_prefixes.extend( + vec![("asm", "asm:"), ("goal", "goal:")] + .into_iter() + .map(|(x, y)| (String::from(x), String::from(y))), + ); + config + .label_reference_commands + .extend(vec!["asmref", "goalref"].into_iter().map(String::from)); + config.label_reference_prefixes.extend( + vec![("asmref", "asm:"), ("goalref", "goal:")] + .into_iter() + .map(|(x, y)| (String::from(x), String::from(y))), + ); + check_with_syntax_config( + config, + r#" +%! foo.tex +\goal{foo} + +\label{asm:foo}\include{bar}\include{baz} + | + ^^^^^^^ + +%! bar.tex +\asmref{foo} + ^^^ %! baz.tex \ref{foo} + +\ref{asm:foo} + ^^^^^^^ "#, ) } diff --git a/crates/texlab/src/server/options.rs b/crates/texlab/src/server/options.rs index 3f50faef..1640c9cf 100644 --- a/crates/texlab/src/server/options.rs +++ b/crates/texlab/src/server/options.rs @@ -1,6 +1,7 @@ -use regex::Regex; use serde::{Deserialize, Serialize}; +use regex::Regex; + #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[serde(default)] @@ -125,7 +126,9 @@ pub struct ExperimentalOptions { pub verbatim_environments: Vec, pub citation_commands: Vec, pub label_definition_commands: Vec, + pub label_definition_prefixes: Vec<(String, String)>, pub label_reference_commands: Vec, + pub label_reference_prefixes: Vec<(String, String)>, } #[derive(Debug, PartialEq, Eq, Clone, Default, Serialize, Deserialize)] diff --git a/crates/texlab/src/util/from_proto.rs b/crates/texlab/src/util/from_proto.rs index 789edbf9..01fd6518 100644 --- a/crates/texlab/src/util/from_proto.rs +++ b/crates/texlab/src/util/from_proto.rs @@ -356,10 +356,20 @@ pub fn config(value: Options) -> Config { .label_definition_commands .extend(value.experimental.label_definition_commands); + config + .syntax + .label_definition_prefixes + .extend(value.experimental.label_definition_prefixes); + config .syntax .label_reference_commands .extend(value.experimental.label_reference_commands); + config + .syntax + .label_reference_prefixes + .extend(value.experimental.label_reference_prefixes); + config } diff --git a/crates/texlab/src/util/to_proto.rs b/crates/texlab/src/util/to_proto.rs index f5cf2edf..d2c261c8 100644 --- a/crates/texlab/src/util/to_proto.rs +++ b/crates/texlab/src/util/to_proto.rs @@ -396,8 +396,15 @@ pub fn workspace_edit(result: RenameResult, new_name: &str) -> lsp_types::Worksp let mut edits = Vec::new(); ranges .into_iter() - .filter_map(|range| document.line_index.line_col_lsp_range(range)) - .for_each(|range| edits.push(lsp_types::TextEdit::new(range, new_name.into()))); + .filter_map(|info| { + document.line_index.line_col_lsp_range(info.range).map(|i| { + ( + i, + info.prefix.map_or(new_name.into(), |p| p + new_name.into()), + ) + }) + }) + .for_each(|(range, new_name)| edits.push(lsp_types::TextEdit::new(range, new_name))); changes.insert(document.uri.clone(), edits); }