⭐️ A friendly language for building type-safe, scalable systems!
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor remove unused imports into its own code action builder

authored by giacomocavalieri.me and committed by

Louis Pilfold d27e6dfe 9d49b250

+155 -81
+141 -7
compiler-core/src/language_server/code_action.rs
··· 4 4 Error, STDLIB_PACKAGE_NAME, analyse, 5 5 ast::{ 6 6 self, AssignName, AssignmentKind, BitArraySegmentTruncation, CallArg, CustomType, 7 - FunctionLiteralKind, ImplicitCallArgOrigin, PIPE_PRECEDENCE, Pattern, 7 + FunctionLiteralKind, ImplicitCallArgOrigin, Import, PIPE_PRECEDENCE, Pattern, 8 8 PatternUnusedArguments, PipelineAssignmentKind, RecordConstructor, SrcSpan, TodoKind, 9 9 TypedArg, TypedAssignment, TypedExpr, TypedModuleConstant, TypedPattern, 10 10 TypedPipelineAssignment, TypedRecordConstructor, TypedStatement, TypedUse, ··· 1300 1300 } 1301 1301 1302 1302 pub struct QualifiedConstructor<'a> { 1303 - import: &'a ast::Import<EcoString>, 1303 + import: &'a Import<EcoString>, 1304 1304 module_aliased: bool, 1305 1305 used_name: EcoString, 1306 1306 constructor: EcoString, ··· 1343 1343 module_name: &EcoString, 1344 1344 constructor: &EcoString, 1345 1345 layer: ast::Layer, 1346 - ) -> Option<&'a ast::Import<EcoString>> { 1346 + ) -> Option<&'a Import<EcoString>> { 1347 1347 let mut matching_import = None; 1348 1348 1349 1349 for def in &self.module.ast.definitions { ··· 1608 1608 1609 1609 fn find_last_char_before_closing_brace(&self) -> Option<(usize, char)> { 1610 1610 let QualifiedConstructor { 1611 - import: ast::Import { location, .. }, 1611 + import: Import { location, .. }, 1612 1612 .. 1613 1613 } = self.qualified_constructor; 1614 1614 let import_code = self.get_import_code(); ··· 1632 1632 1633 1633 fn get_import_code(&self) -> &str { 1634 1634 let QualifiedConstructor { 1635 - import: ast::Import { location, .. }, 1635 + import: Import { location, .. }, 1636 1636 .. 1637 1637 } = self.qualified_constructor; 1638 1638 self.module ··· 1658 1658 // Handle inserting into an unbraced import 1659 1659 fn insert_into_unbraced_import(&self, name: String, module_aliased: bool) -> (u32, String) { 1660 1660 let QualifiedConstructor { 1661 - import: ast::Import { location, .. }, 1661 + import: Import { location, .. }, 1662 1662 .. 1663 1663 } = self.qualified_constructor; 1664 1664 if !module_aliased { ··· 1682 1682 // Handle inserting into a braced import 1683 1683 fn insert_into_braced_import(&self, name: String) -> (u32, String) { 1684 1684 let QualifiedConstructor { 1685 - import: ast::Import { location, .. }, 1685 + import: Import { location, .. }, 1686 1686 .. 1687 1687 } = self.qualified_constructor; 1688 1688 if let Some((pos, c)) = self.find_last_char_before_closing_brace() { ··· 7008 7008 ast::visit::visit_typed_expr_bit_array_segment(self, segment); 7009 7009 } 7010 7010 } 7011 + 7012 + /// Code action builder to remove unused imports and values. 7013 + /// 7014 + pub struct RemoveUnusedImports<'a> { 7015 + module: &'a Module, 7016 + params: &'a CodeActionParams, 7017 + imports: Vec<&'a Import<EcoString>>, 7018 + edits: TextEdits<'a>, 7019 + } 7020 + 7021 + #[derive(Debug)] 7022 + enum UnusedImport { 7023 + Value(SrcSpan), 7024 + Module(SrcSpan), 7025 + ModuleAlias(SrcSpan), 7026 + } 7027 + 7028 + impl UnusedImport { 7029 + fn location(&self) -> SrcSpan { 7030 + match self { 7031 + UnusedImport::Value(location) 7032 + | UnusedImport::Module(location) 7033 + | UnusedImport::ModuleAlias(location) => *location, 7034 + } 7035 + } 7036 + } 7037 + 7038 + impl<'a> RemoveUnusedImports<'a> { 7039 + pub fn new( 7040 + module: &'a Module, 7041 + line_numbers: &'a LineNumbers, 7042 + params: &'a CodeActionParams, 7043 + ) -> Self { 7044 + Self { 7045 + module, 7046 + params, 7047 + edits: TextEdits::new(line_numbers), 7048 + imports: vec![], 7049 + } 7050 + } 7051 + 7052 + pub fn code_actions(mut self) -> Vec<CodeAction> { 7053 + // If there's no import in the module then there can't be any unused 7054 + // import to remove. 7055 + self.visit_typed_module(&self.module.ast); 7056 + if self.imports.is_empty() { 7057 + return vec![]; 7058 + } 7059 + 7060 + let unused_imports = (self.module.ast.type_info.warnings.iter()) 7061 + .filter_map(|warning| match warning { 7062 + type_::Warning::UnusedImportedValue { location, .. } => { 7063 + Some(UnusedImport::Value(*location)) 7064 + } 7065 + type_::Warning::UnusedImportedModule { location, .. } => { 7066 + Some(UnusedImport::Module(*location)) 7067 + } 7068 + type_::Warning::UnusedImportedModuleAlias { location, .. } => { 7069 + Some(UnusedImport::ModuleAlias(*location)) 7070 + } 7071 + _ => None, 7072 + }) 7073 + .collect_vec(); 7074 + 7075 + println!("{:#?}", unused_imports); 7076 + println!("{:#?}", self.params.range); 7077 + 7078 + // If the cursor is not over any of the unused imports then we don't offer 7079 + // the code action. 7080 + let hovering_unused_import = unused_imports.iter().any(|import| { 7081 + let unused_range = self.edits.src_span_to_lsp_range(import.location()); 7082 + overlaps(self.params.range, unused_range) 7083 + }); 7084 + if !hovering_unused_import { 7085 + return vec![]; 7086 + } 7087 + 7088 + // Otherwise we start removing all unused imports: 7089 + for import in unused_imports { 7090 + match import { 7091 + // When an entire module is unused we can delete its entire location 7092 + // in the source code. 7093 + UnusedImport::Module(location) | UnusedImport::ModuleAlias(location) => { 7094 + if self.edits.line_numbers.spans_entire_line(&location) { 7095 + // If the unused module spans over the entire line then 7096 + // we also take care of removing the following newline 7097 + // characther! 7098 + self.edits.delete(SrcSpan { 7099 + start: location.start, 7100 + end: location.end + 1, 7101 + }) 7102 + } else { 7103 + self.edits.delete(location) 7104 + } 7105 + } 7106 + 7107 + // When removing unused imported values we have to be a bit more 7108 + // careful: an unused value might be followed by a comma that we 7109 + // also need to remove! So we have to look for this imported 7110 + // value and also delete all characters between it and the 7111 + // following imported value. For example: 7112 + // 7113 + // ```gleam 7114 + // import wibble.{wobble, woo} 7115 + // // ^^^^^^^^^^^ If wobble is unused we must remove 7116 + // // all of this! 7117 + // ``` 7118 + // 7119 + UnusedImport::Value(_) => continue, 7120 + } 7121 + } 7122 + 7123 + let mut action = Vec::with_capacity(1); 7124 + CodeActionBuilder::new("Remove unused imports") 7125 + .kind(CodeActionKind::REFACTOR_REWRITE) 7126 + .changes(self.params.text_document.uri.clone(), self.edits.edits) 7127 + .preferred(true) 7128 + .push_to(&mut action); 7129 + action 7130 + } 7131 + } 7132 + 7133 + impl<'ast> ast::visit::Visit<'ast> for RemoveUnusedImports<'ast> { 7134 + fn visit_typed_module(&mut self, module: &'ast ast::TypedModule) { 7135 + self.imports = module 7136 + .definitions 7137 + .iter() 7138 + .filter_map(|definition| match definition { 7139 + ast::Definition::Import(import) => Some(import), 7140 + _ => None, 7141 + }) 7142 + .collect_vec(); 7143 + } 7144 + }
+4 -72
compiler-core/src/language_server/engine.rs
··· 41 41 FillInMissingLabelledArgs, FillUnusedFields, FixBinaryOperation, 42 42 FixTruncatedBitArraySegment, GenerateDynamicDecoder, GenerateFunction, GenerateJsonEncoder, 43 43 GenerateVariant, InlineVariable, InterpolateString, LetAssertToCase, PatternMatchOnValue, 44 - RedundantTupleInCaseSubject, RemoveEchos, UseLabelShorthandSyntax, WrapInBlock, 45 - code_action_add_missing_patterns, code_action_convert_qualified_constructor_to_unqualified, 44 + RedundantTupleInCaseSubject, RemoveEchos, RemoveUnusedImports, UseLabelShorthandSyntax, 45 + WrapInBlock, code_action_add_missing_patterns, 46 + code_action_convert_qualified_constructor_to_unqualified, 46 47 code_action_convert_unqualified_constructor_to_qualified, code_action_import_module, 47 48 code_action_inexhaustive_let_to_case, 48 49 }, ··· 382 383 let lines = LineNumbers::new(&module.code); 383 384 384 385 code_action_unused_values(module, &lines, &params, &mut actions); 385 - code_action_unused_imports(module, &lines, &params, &mut actions); 386 + actions.extend(RemoveUnusedImports::new(module, &lines, &params).code_actions()); 386 387 code_action_convert_qualified_constructor_to_unqualified( 387 388 module, 388 389 &lines, ··· 1459 1460 } 1460 1461 } 1461 1462 1462 - /// Code action to remove unused imports. 1463 - /// 1464 - fn code_action_unused_imports( 1465 - module: &Module, 1466 - line_numbers: &LineNumbers, 1467 - params: &lsp::CodeActionParams, 1468 - actions: &mut Vec<CodeAction>, 1469 - ) { 1470 - let uri = &params.text_document.uri; 1471 - let unused: Vec<&SrcSpan> = module 1472 - .ast 1473 - .type_info 1474 - .warnings 1475 - .iter() 1476 - .filter_map(|warning| match warning { 1477 - type_::Warning::UnusedImportedModuleAlias { location, .. } 1478 - | type_::Warning::UnusedImportedModule { location, .. } => Some(location), 1479 - _ => None, 1480 - }) 1481 - .collect(); 1482 - 1483 - if unused.is_empty() { 1484 - return; 1485 - } 1486 - 1487 - let mut hovered = false; 1488 - let mut edits = Vec::with_capacity(unused.len()); 1489 - 1490 - for unused in unused { 1491 - let SrcSpan { start, end } = *unused; 1492 - 1493 - // If removing an unused alias or at the beginning of the file, don't backspace 1494 - // Otherwise, adjust the end position by 1 to ensure the entire line is deleted with the import. 1495 - let adjusted_end = if delete_line(unused, line_numbers) { 1496 - end + 1 1497 - } else { 1498 - end 1499 - }; 1500 - 1501 - let range = src_span_to_lsp_range(SrcSpan::new(start, adjusted_end), line_numbers); 1502 - // Keep track of whether any unused import has is where the cursor is 1503 - hovered = hovered || overlaps(params.range, range); 1504 - 1505 - edits.push(TextEdit { 1506 - range, 1507 - new_text: "".into(), 1508 - }); 1509 - } 1510 - 1511 - // If none of the imports are where the cursor is we do nothing 1512 - if !hovered { 1513 - return; 1514 - } 1515 - edits.sort_by_key(|edit| edit.range.start); 1516 - 1517 - CodeActionBuilder::new("Remove unused imports") 1518 - .kind(lsp_types::CodeActionKind::QUICKFIX) 1519 - .changes(uri.clone(), edits) 1520 - .preferred(true) 1521 - .push_to(actions); 1522 - } 1523 - 1524 1463 struct NameCorrection { 1525 1464 pub location: SrcSpan, 1526 1465 pub correction: EcoString, ··· 1576 1515 .push_to(actions); 1577 1516 } 1578 1517 } 1579 - } 1580 - 1581 - // Check if the edit empties a whole line; if so, delete the line. 1582 - fn delete_line(span: &SrcSpan, line_numbers: &LineNumbers) -> bool { 1583 - line_numbers.line_starts.iter().any(|&line_start| { 1584 - line_start == span.start && line_numbers.line_starts.contains(&(span.end + 1)) 1585 - }) 1586 1518 } 1587 1519 1588 1520 fn get_expr_qualified_name(expression: &TypedExpr) -> Option<(&EcoString, &EcoString)> {
+10 -2
compiler-core/src/line_numbers.rs
··· 1 - use std::collections::HashMap; 2 - 1 + use crate::ast::SrcSpan; 3 2 use lsp_types::Position; 3 + use std::collections::HashMap; 4 4 5 5 /// A struct which contains information about line numbers of a source file, 6 6 /// and can convert between byte offsets that are used in the compiler and ··· 135 135 } 136 136 137 137 u8_offset 138 + } 139 + 140 + /// Checks if the given span spans an entire line (excluding the newline 141 + /// character itself). 142 + pub fn spans_entire_line(&self, span: &SrcSpan) -> bool { 143 + self.line_starts.iter().any(|&line_start| { 144 + line_start == span.start && self.line_starts.contains(&(span.end + 1)) 145 + }) 138 146 } 139 147 } 140 148