diff --git a/crates/base-db/src/semantics/tex.rs b/crates/base-db/src/semantics/tex.rs index 7d150fc4b..89746db16 100644 --- a/crates/base-db/src/semantics/tex.rs +++ b/crates/base-db/src/semantics/tex.rs @@ -109,6 +109,7 @@ impl Semantics { self.links.push(Link { kind, path: Span::from(&path), + full_range: latex::small_range(&include), base_dir: None, }); } @@ -132,11 +133,13 @@ impl Semantics { }; let text = format!("{base_dir}{}", path.to_string()); let range = latex::small_range(&path); + let full_range = latex::small_range(&import); self.links.push(Link { kind: LinkKind::Tex, path: Span { text, range }, base_dir: Some(base_dir), + full_range, }); } @@ -373,6 +376,7 @@ pub struct Link { pub kind: LinkKind, pub path: Span, pub base_dir: Option, + pub full_range: TextRange, } impl Link { diff --git a/crates/base-db/src/util/queries.rs b/crates/base-db/src/util/queries.rs index 6ddf20104..d593d24c1 100644 --- a/crates/base-db/src/util/queries.rs +++ b/crates/base-db/src/util/queries.rs @@ -44,6 +44,33 @@ pub trait Object { } } +impl Object for tex::Link { + fn name_text(&self) -> &str { + &self.path.text + } + + fn name_range(&self) -> TextRange { + self.path.range + } + + fn full_range(&self) -> TextRange { + self.full_range + } + + fn kind(&self) -> ObjectKind { + ObjectKind::Definition + } + + fn find<'db>(document: &'db Document) -> Box + 'db> { + let data = document.data.as_tex(); + let iter = data + .into_iter() + .flat_map(|data| data.semantics.links.iter()); + + Box::new(iter) + } +} + impl Object for tex::Label { fn name_text(&self) -> &str { &self.name.text diff --git a/crates/diagnostics/src/imports.rs b/crates/diagnostics/src/imports.rs new file mode 100644 index 000000000..0aae9b407 --- /dev/null +++ b/crates/diagnostics/src/imports.rs @@ -0,0 +1,25 @@ +use base_db::{semantics::tex::Link, util::queries, Workspace}; +use rustc_hash::FxHashMap; +use url::Url; + +use crate::{types::Diagnostic, ImportError}; + +pub fn detect_duplicate_imports( + workspace: &Workspace, + results: &mut FxHashMap>, +) { + for conflict in queries::Conflict::find_all::(workspace) { + let others = conflict + .rest + .iter() + .map(|location| (location.document.uri.clone(), location.range)) + .collect(); + + let diagnostic = + Diagnostic::Import(conflict.main.range, ImportError::DuplicateImport(others)); + results + .entry(conflict.main.document.uri.clone()) + .or_default() + .push(diagnostic); + } +} diff --git a/crates/diagnostics/src/lib.rs b/crates/diagnostics/src/lib.rs index e5baf9ebd..42f5bd338 100644 --- a/crates/diagnostics/src/lib.rs +++ b/crates/diagnostics/src/lib.rs @@ -5,6 +5,7 @@ mod grammar; mod labels; mod manager; mod types; +mod imports; pub use manager::Manager; pub use types::*; diff --git a/crates/diagnostics/src/manager.rs b/crates/diagnostics/src/manager.rs index 2829286a1..d074bb9f4 100644 --- a/crates/diagnostics/src/manager.rs +++ b/crates/diagnostics/src/manager.rs @@ -89,6 +89,7 @@ impl Manager { super::citations::detect_duplicate_entries(workspace, &mut results); super::labels::detect_duplicate_labels(workspace, &mut results); super::labels::detect_undefined_and_unused_labels(workspace, &mut results); + super::imports::detect_duplicate_imports(workspace, &mut results); results.retain(|uri, _| { workspace diff --git a/crates/diagnostics/src/types.rs b/crates/diagnostics/src/types.rs index 9cc195863..9634cf079 100644 --- a/crates/diagnostics/src/types.rs +++ b/crates/diagnostics/src/types.rs @@ -35,6 +35,12 @@ impl std::fmt::Debug for TexError { } } +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum ImportError { + OrderingConflict, + DuplicateImport(Vec<(Url, TextRange)>), +} + #[derive(PartialEq, Eq, Clone)] pub enum BibError { ExpectingLCurly, @@ -89,6 +95,7 @@ pub enum Diagnostic { Bib(TextRange, BibError), Build(TextRange, BuildError), Chktex(ChktexError), + Import(TextRange, ImportError) } impl Diagnostic { @@ -114,6 +121,10 @@ impl Diagnostic { }, Diagnostic::Build(_, error) => &error.message, Diagnostic::Chktex(error) => &error.message, + Diagnostic::Import(_, error) => match error { + ImportError::OrderingConflict => todo!(), + ImportError::DuplicateImport(_) => "Duplicate Package imported.", + } } } @@ -127,6 +138,7 @@ impl Diagnostic { let end = line_index.offset(error.end)?; TextRange::new(start, end) } + Diagnostic::Import(range, _) => *range, }) } @@ -152,6 +164,7 @@ impl Diagnostic { }, Diagnostic::Chktex(_) => None, Diagnostic::Build(_, _) => None, + Diagnostic::Import(_, _) => None, } } } diff --git a/crates/texlab/src/action.rs b/crates/texlab/src/action.rs new file mode 100644 index 000000000..23d12b232 --- /dev/null +++ b/crates/texlab/src/action.rs @@ -0,0 +1,180 @@ +use std::collections::HashMap; + +use base_db::Workspace; +use diagnostics::{Diagnostic, ImportError}; +use lsp_types::{CodeAction, CodeActionKind, CodeActionParams, TextEdit, WorkspaceEdit}; +use rowan::{TextRange, TextSize}; +use rustc_hash::FxBuildHasher; + +use crate::util::{line_index_ext::LineIndexExt, to_proto}; + +pub fn remove_duplicate_imports( + diagnostics_map: HashMap, FxBuildHasher>, + params: CodeActionParams, + workspace: parking_lot::lock_api::RwLockReadGuard<'_, parking_lot::RawRwLock, Workspace>, +) -> Vec { + let mut actions = Vec::new(); + let url = params.text_document.uri.clone(); + let document = workspace.lookup(&url).unwrap(); + + let diagnostics = diagnostics_map + .get(&url) + .unwrap() + .iter() + .filter(|diag| matches!(diag, Diagnostic::Import(_, ImportError::DuplicateImport(_)))) + .collect::>(); + + let cursor_position = params.range.start; + + let cursor_diag = diagnostics.iter().find(|diag| { + let line_index = &workspace + .lookup(¶ms.text_document.uri) + .unwrap() + .line_index; + let range = diag.range(&line_index).unwrap(); + range.contains(line_index.offset_lsp(cursor_position).unwrap()) + }); + + if let Some(diag) = cursor_diag { + let package_name = get_package_name(&workspace, ¶ms, diag); + + let filtered_diagnostics: Vec<_> = diagnostics + .clone() + .into_iter() + .filter(|diag| get_package_name(&workspace, ¶ms, diag) == package_name) + .collect(); + + let import_diags: Vec = filtered_diagnostics + .clone() + .iter() + .map(|diag| to_proto::diagnostic(&workspace, document, diag).unwrap()) + .collect(); + + for diag in filtered_diagnostics { + let line_index = &workspace + .lookup(¶ms.text_document.uri) + .unwrap() + .line_index; + let range = diag.range(&line_index).unwrap(); + let start_line = line_index + .line_col_lsp_range(TextRange::at(range.start(), TextSize::from(0))) + .unwrap() + .start + .line; + + actions.push(CodeAction { + title: format!( + "Remove duplicate package: {}, line {}.", + get_package_name(&workspace, ¶ms, diag), + (1 + start_line) + ), + kind: Some(CodeActionKind::QUICKFIX), + diagnostics: Some(import_diags.clone()), + edit: Some(WorkspaceEdit { + changes: Some( + vec![( + url.clone(), + vec![TextEdit { + range: get_line_range(&workspace, ¶ms, diag), + new_text: "".to_string(), + }], + )] + .into_iter() + .collect(), + ), + document_changes: None, + change_annotations: None, + }), + command: None, + is_preferred: None, + disabled: None, + data: None, + }); + } + } + + let mut seen_packages = HashMap::new(); + let all_diags = diagnostics + .iter() + .filter(|diag| { + let package_name = get_package_name(&workspace, ¶ms, diag); + if seen_packages.contains_key(&package_name) { + true + } else { + seen_packages.insert(package_name, true); + false + } + }) + .collect::>(); + + let mut all_diags_edit = Vec::new(); + + for diag in all_diags { + all_diags_edit.push(TextEdit { + range: get_line_range(&workspace, ¶ms, diag), + new_text: "".to_string(), + }); + } + + if all_diags_edit.is_empty() { + return actions; + } + + let import_diags: Vec = diagnostics + .clone() + .iter() + .map(|diag| to_proto::diagnostic(&workspace, document, diag).unwrap()) + .collect(); + + actions.push(CodeAction { + title: "Remove all duplicate package.".to_string(), + kind: Some(CodeActionKind::QUICKFIX), + diagnostics: Some(import_diags.clone()), + edit: Some(WorkspaceEdit { + changes: Some(vec![(url.clone(), all_diags_edit)].into_iter().collect()), + document_changes: None, + change_annotations: None, + }), + command: None, + is_preferred: None, + disabled: None, + data: None, + }); + + actions +} + +fn get_line_range( + workspace: &Workspace, + params: &CodeActionParams, + diag: &Diagnostic, +) -> lsp_types::Range { + let line_index = &workspace + .lookup(¶ms.text_document.uri) + .unwrap() + .line_index; + let range = diag.range(&line_index).unwrap(); + let start_line = line_index + .line_col_lsp_range(TextRange::at(range.start(), TextSize::from(0))) + .unwrap() + .start + .line; + let end_line = line_index + .line_col_lsp_range(TextRange::new(range.end(), range.end())) + .unwrap() + .end + .line; + let start = lsp_types::Position::new(start_line, 0); + let end = lsp_types::Position::new(end_line + 1, 0); + lsp_types::Range::new(start, end) +} + +fn get_package_name(workspace: &Workspace, params: &CodeActionParams, diag: &Diagnostic) -> String { + let document = workspace.lookup(¶ms.text_document.uri).unwrap(); + let line_index = document.line_index.clone(); + let range = diag.range(&line_index).unwrap(); + let start = usize::from(range.start()); + let end = usize::from(range.end()); + let text = &document.text[start..end]; + text.to_string() +} diff --git a/crates/texlab/src/lib.rs b/crates/texlab/src/lib.rs index 50bddc318..02918592a 100644 --- a/crates/texlab/src/lib.rs +++ b/crates/texlab/src/lib.rs @@ -2,5 +2,6 @@ mod client; pub(crate) mod features; mod server; pub(crate) mod util; +mod action; pub use self::{client::LspClient, server::Server}; diff --git a/crates/texlab/src/server.rs b/crates/texlab/src/server.rs index 4a0d795a6..b3cea28c6 100644 --- a/crates/texlab/src/server.rs +++ b/crates/texlab/src/server.rs @@ -10,6 +10,7 @@ use std::{ time::Duration, }; +use crate::action; use anyhow::Result; use base_db::{deps, Owner, Workspace}; use commands::{BuildCommand, CleanCommand, CleanTarget, ForwardSearch}; @@ -174,6 +175,7 @@ impl Server { ..Default::default() }), inlay_hint_provider: Some(OneOf::Left(true)), + code_action_provider: Some(CodeActionProviderCapability::Simple(true)), ..ServerCapabilities::default() } } @@ -781,9 +783,14 @@ impl Server { Ok(()) } - fn code_actions(&self, id: RequestId, _params: CodeActionParams) -> Result<()> { + fn code_actions(&self, id: RequestId, params: CodeActionParams) -> Result<()> { + let workspace = self.workspace.read(); + let diagnostics = self.diagnostic_manager.get(&workspace); + + let actions = action::remove_duplicate_imports(diagnostics, params, workspace); + self.client - .send_response(lsp_server::Response::new_ok(id, Vec::::new()))?; + .send_response(lsp_server::Response::new_ok(id, actions))?; Ok(()) } diff --git a/crates/texlab/src/util/to_proto.rs b/crates/texlab/src/util/to_proto.rs index c8e48abfa..726dea243 100644 --- a/crates/texlab/src/util/to_proto.rs +++ b/crates/texlab/src/util/to_proto.rs @@ -4,7 +4,7 @@ use base_db::{ data::BibtexEntryTypeCategory, util::RenderedObject, Document, DocumentLocation, Workspace, }; use definition::DefinitionResult; -use diagnostics::{BibError, ChktexSeverity, Diagnostic, TexError}; +use diagnostics::{BibError, ChktexSeverity, Diagnostic, ImportError, TexError}; use folding::{FoldingRange, FoldingRangeKind}; use highlights::{Highlight, HighlightKind}; use hover::{Hover, HoverData}; @@ -23,9 +23,10 @@ pub fn diagnostic( diagnostic: &Diagnostic, ) -> Option { let range = match diagnostic { - Diagnostic::Tex(range, _) | Diagnostic::Bib(range, _) | Diagnostic::Build(range, _) => { - document.line_index.line_col_lsp_range(*range)? - } + Diagnostic::Tex(range, _) + | Diagnostic::Bib(range, _) + | Diagnostic::Build(range, _) + | Diagnostic::Import(range, _) => document.line_index.line_col_lsp_range(*range)?, Diagnostic::Chktex(range) => { let start = lsp_types::Position::new(range.start.line, range.start.col); let end = lsp_types::Position::new(range.end.line, range.end.col); @@ -61,6 +62,10 @@ pub fn diagnostic( ChktexSeverity::Warning => lsp_types::DiagnosticSeverity::WARNING, ChktexSeverity::Error => lsp_types::DiagnosticSeverity::ERROR, }, + Diagnostic::Import(_, error) => match error { + ImportError::OrderingConflict => lsp_types::DiagnosticSeverity::WARNING, + ImportError::DuplicateImport(_) => lsp_types::DiagnosticSeverity::ERROR, + }, }; let code: Option = match &diagnostic { @@ -84,10 +89,14 @@ pub fn diagnostic( }, Diagnostic::Build(_, _) => None, Diagnostic::Chktex(error) => Some(NumberOrString::String(error.code.clone())), + Diagnostic::Import(_, error) => match error { + ImportError::OrderingConflict => todo!(), + ImportError::DuplicateImport(_) => Some(NumberOrString::Number(15)), + }, }; let source = match &diagnostic { - Diagnostic::Tex(_, _) | Diagnostic::Bib(_, _) => "texlab", + Diagnostic::Tex(_, _) | Diagnostic::Bib(_, _) | Diagnostic::Import(_, _) => "texlab", Diagnostic::Build(_, _) => "latex", Diagnostic::Chktex(_) => "ChkTeX", }; @@ -113,6 +122,10 @@ pub fn diagnostic( }, Diagnostic::Build(_, error) => &error.message, Diagnostic::Chktex(error) => &error.message, + Diagnostic::Import(_, error) => match error { + ImportError::OrderingConflict => "Possible Conflict in Package Ordering.", + ImportError::DuplicateImport(_) => "Duplicate Package." + } }); let tags = match &diagnostic { @@ -136,6 +149,10 @@ pub fn diagnostic( }, Diagnostic::Build(_, _) => None, Diagnostic::Chktex(_) => None, + Diagnostic::Import(_, error) => match error { + ImportError::DuplicateImport(_) => Some(vec![lsp_types::DiagnosticTag::UNNECESSARY]), + _ => None, + }, }; fn make_conflict_info( @@ -179,6 +196,10 @@ pub fn diagnostic( }, Diagnostic::Build(_, _) => None, Diagnostic::Chktex(_) => None, + Diagnostic::Import(_, error) => match error { + ImportError::OrderingConflict => None, + ImportError::DuplicateImport(others) => make_conflict_info(workspace, others, "package"), + } }; Some(lsp_types::Diagnostic {