diff options
Diffstat (limited to 'support/texlab/src/features/rename/label.rs')
-rw-r--r-- | support/texlab/src/features/rename/label.rs | 142 |
1 files changed, 142 insertions, 0 deletions
diff --git a/support/texlab/src/features/rename/label.rs b/support/texlab/src/features/rename/label.rs new file mode 100644 index 0000000000..11b414ccfa --- /dev/null +++ b/support/texlab/src/features/rename/label.rs @@ -0,0 +1,142 @@ +use std::collections::HashMap; + +use cancellation::CancellationToken; +use lsp_types::{Range, RenameParams, TextEdit, WorkspaceEdit}; + +use crate::{ + features::cursor::{CursorContext, HasPosition}, + syntax::{latex, CstNode}, + LineIndexExt, +}; + +pub fn prepare_label_rename<P: HasPosition>( + context: &CursorContext<P>, + _cancellation_token: &CancellationToken, +) -> Option<Range> { + let name = context.cursor.as_latex()?; + name.parent().parent().filter(|node| { + matches!( + node.kind(), + latex::LABEL_DEFINITION | latex::LABEL_REFERENCE | latex::LABEL_REFERENCE_RANGE + ) + })?; + + Some( + context + .request + .main_document() + .line_index + .line_col_lsp_range(name.text_range()), + ) +} + +pub fn rename_label( + context: &CursorContext<RenameParams>, + cancellation_token: &CancellationToken, +) -> Option<WorkspaceEdit> { + prepare_label_rename(context, cancellation_token)?; + let name_text = context.cursor.as_latex()?.text(); + let mut changes = HashMap::new(); + for document in &context.request.subset.documents { + cancellation_token.result().ok()?; + if let Some(data) = document.data.as_latex() { + let mut edits = Vec::new(); + for node in data.root.descendants() { + if let Some(range) = latex::LabelDefinition::cast(node) + .and_then(|label| label.name()) + .and_then(|name| name.word()) + .filter(|name| name.text() == name_text) + .map(|name| document.line_index.line_col_lsp_range(name.text_range())) + { + edits.push(TextEdit::new( + range, + context.request.params.new_name.clone(), + )); + } + + latex::LabelReference::cast(node) + .and_then(|label| label.name_list()) + .into_iter() + .flat_map(|label| label.words()) + .filter(|name| name.text() == name_text) + .map(|name| document.line_index.line_col_lsp_range(name.text_range())) + .for_each(|range| { + edits.push(TextEdit::new( + range, + context.request.params.new_name.clone(), + )); + }); + + if let Some(label) = latex::LabelReferenceRange::cast(node) { + if let Some(name1) = label + .from() + .and_then(|name| name.word()) + .filter(|name| name.text() == name_text) + { + edits.push(TextEdit::new( + document.line_index.line_col_lsp_range(name1.text_range()), + context.request.params.new_name.clone(), + )); + } + + if let Some(name2) = label + .from() + .and_then(|name| name.word()) + .filter(|name| name.text() == name_text) + { + edits.push(TextEdit::new( + document.line_index.line_col_lsp_range(name2.text_range()), + context.request.params.new_name.clone(), + )); + } + } + } + + changes.insert(document.uri.as_ref().clone().into(), edits); + } + } + + Some(WorkspaceEdit::new(changes)) +} + +#[cfg(test)] +mod tests { + use crate::{features::testing::FeatureTester, RangeExt}; + + use super::*; + + #[test] + fn test_label() { + let tester = FeatureTester::builder() + .files(vec![ + ("foo.tex", r#"\label{foo}\include{bar}"#), + ("bar.tex", r#"\ref{foo}"#), + ("baz.tex", r#"\ref{foo}"#), + ]) + .main("foo.tex") + .line(0) + .character(7) + .new_name("bar") + .build(); + + let uri1 = tester.uri("foo.tex"); + let uri2 = tester.uri("bar.tex"); + let request = tester.rename(); + + let context = CursorContext::new(request); + let actual_edit = rename_label(&context, CancellationToken::none()).unwrap(); + + let mut expected_changes = HashMap::new(); + expected_changes.insert( + uri1.as_ref().clone().into(), + vec![TextEdit::new(Range::new_simple(0, 7, 0, 10), "bar".into())], + ); + expected_changes.insert( + uri2.as_ref().clone().into(), + vec![TextEdit::new(Range::new_simple(0, 5, 0, 8), "bar".into())], + ); + let expected_edit = WorkspaceEdit::new(expected_changes); + + assert_eq!(actual_edit, expected_edit); + } +} |