use crate::{line_numbers::LineNumbers, utils::span_to_lsp_range}; use aiken_lang::ast::{Definition, ModuleKind, Span, UntypedDefinition, Use}; use aiken_project::module::CheckedModule; use itertools::Itertools; use std::fs; /// A freshly parsed module alongside its line numbers. pub struct ParsedDocument { definitions: Vec, line_numbers: LineNumbers, } pub type AnnotatedEdit = (String, lsp_types::TextEdit); /// Parse the target document as an 'UntypedModule' alongside its line numbers. This is useful in /// case we need to manipulate the AST for a quickfix. pub fn parse_document(document: &lsp_types::TextDocumentIdentifier) -> Option { let file_path = document .uri .to_file_path() .expect("invalid text document uri?"); let source_code = fs::read_to_string(file_path).ok()?; let line_numbers = LineNumbers::new(&source_code); // NOTE: The 'ModuleKind' second argument doesn't matter. This is just added to the final // object but has no influence on the parsing. let (untyped_module, _) = aiken_lang::parser::module(&source_code, ModuleKind::Lib).ok()?; Some(ParsedDocument { definitions: untyped_module.definitions, line_numbers, }) } /// Insert some text at the given location. fn insert_text(at: usize, line_numbers: &LineNumbers, new_text: String) -> lsp_types::TextEdit { let range = span_to_lsp_range(Span { start: at, end: at }, line_numbers); lsp_types::TextEdit { range, new_text } } /// Find a suitable location (Span) in the import list. The boolean in the answer indicates /// whether the import is a newline or not. It is set to 'false' when adding a qualified import /// to an existing list. impl ParsedDocument { pub fn import( &self, import: &CheckedModule, unqualified: Option<&str>, ) -> Option { let import_path = import.name.split('/').collect_vec(); let mut last_import = None; for def in self.definitions.iter() { match def { Definition::Use(Use { location, module: existing_module, unqualified: unqualified_list, .. }) => { last_import = Some(*location); if import_path != existing_module.as_slice() { continue; } match unqualified { // There's already a matching qualified import, so we have nothing to do. None => return None, Some(unqualified) => { let mut last_unqualified = None; // Insert lexicographically, assuming unqualified imports are already // ordered. If they are not, it doesn't really matter where we insert // anyway. for existing_unqualified in unqualified_list { last_unqualified = Some(existing_unqualified.location); let existing_name = existing_unqualified .as_name .as_ref() .unwrap_or(&existing_unqualified.name); // The unqualified import already exist, nothing to do. if unqualified == existing_name { return None; // Current import is lexicographically greater, we can insert before } else if unqualified < existing_name.as_str() { return Some(self.insert_qualified_before( import, unqualified, existing_unqualified.location, )); } else { continue; } } return match last_unqualified { // Only happens if 'unqualified_list' is empty, in which case, we // simply create a new unqualified list of import. None => { Some(self.add_new_qualified(import, unqualified, *location)) } // Happens if the new qualified import is lexicographically after // all existing ones. Some(location) => { Some(self.insert_qualified_after(import, unqualified, location)) } }; } } } _ => continue, } } // If the search above didn't lead to anything, we simply insert the import either: // // (a) After the last import statement if any; // (b) As the first statement in the module. Some(self.add_new_import_line(import, unqualified, last_import)) } fn insert_qualified_before( &self, import: &CheckedModule, unqualified: &str, location: Span, ) -> AnnotatedEdit { let title = format!( "Insert new unqualified import '{}' to {}", unqualified, import.name ); ( title, insert_text( location.start, &self.line_numbers, format!("{}, ", unqualified), ), ) } fn insert_qualified_after( &self, import: &CheckedModule, unqualified: &str, location: Span, ) -> AnnotatedEdit { let title = format!( "Insert new unqualified import '{}' to {}", unqualified, import.name ); ( title, insert_text( location.end, &self.line_numbers, format!(", {}", unqualified), ), ) } fn add_new_qualified( &self, import: &CheckedModule, unqualified: &str, location: Span, ) -> AnnotatedEdit { let title = format!( "Add new unqualified import '{}' to {}", unqualified, import.name ); ( title, insert_text( location.end, &self.line_numbers, format!(".{{{}}}", unqualified), ), ) } fn add_new_import_line( &self, import: &CheckedModule, unqualified: Option<&str>, location: Option, ) -> AnnotatedEdit { let import_line = format!( "use {}{}", import.name, match unqualified { Some(unqualified) => format!(".{{{}}}", unqualified), None => String::new(), } ); let title = format!("Add new import line: {import_line}"); ( title, match location { None => insert_text(0, &self.line_numbers, format!("{import_line}\n")), Some(Span { end, .. }) => { insert_text(end, &self.line_numbers, format!("\n{import_line}")) } }, ) } }