Fork of daniellemaywood.uk/gleam — Wasm codegen work
2

Configure Feed

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

gleam / language-server / src / edits.rs
5.3 kB 175 lines
1// SPDX-License-Identifier: Apache-2.0 2// SPDX-FileCopyrightText: 2024 The Gleam contributors 3 4use ecow::EcoString; 5use lsp_types::{Position, Range, TextEdit}; 6 7use gleam_core::{ 8 ast::{Import, SrcSpan, TypedDefinitions}, 9 build::Module, 10 line_numbers::LineNumbers, 11}; 12 13use super::src_span_to_lsp_range; 14 15// Gets the position of the import statement if it's the first definition in the module. 16pub fn position_of_first_definition_if_import( 17 module: &Module, 18 line_numbers: &LineNumbers, 19) -> Option<Position> { 20 let TypedDefinitions { 21 imports, 22 constants, 23 custom_types, 24 type_aliases, 25 functions, 26 } = &module.ast.definitions; 27 28 // We first find the firts import by position 29 let first_import = imports.iter().min_by_key(|import| import.location)?; 30 31 // Then we need to make sure it actually comes before any other definition. 32 let import_is_first_definition = constants 33 .iter() 34 .map(|constant| constant.location) 35 .chain(custom_types.iter().map(|custom_type| custom_type.location)) 36 .chain(type_aliases.iter().map(|type_alias| type_alias.location)) 37 .chain(functions.iter().map(|function| function.location)) 38 .all(|location| location >= first_import.location); 39 40 if import_is_first_definition { 41 Some(src_span_to_lsp_range(first_import.location, line_numbers).start) 42 } else { 43 None 44 } 45} 46 47pub enum Newlines { 48 Single, 49 Double, 50} 51 52// Returns how many newlines should be added after an import statement. By default `Newlines::Single`, 53// but if there's not any import statement, it returns `Newlines::Double`. 54// 55// * ``import_location`` - The position of the first import statement in the source code. 56pub fn add_newlines_after_import( 57 import_location: Position, 58 has_imports: bool, 59 line_numbers: &LineNumbers, 60 src: &str, 61) -> Newlines { 62 let import_start_cursor = line_numbers.byte_index(import_location); 63 let is_new_line = src 64 .chars() 65 .nth(import_start_cursor as usize) 66 .unwrap_or_default() 67 == '\n'; 68 match !has_imports && !is_new_line { 69 true => Newlines::Double, 70 false => Newlines::Single, 71 } 72} 73 74pub fn get_import_edit( 75 import_location: Position, 76 module_full_name: &str, 77 insert_newlines: &Newlines, 78) -> TextEdit { 79 let new_lines = match insert_newlines { 80 Newlines::Single => "\n", 81 Newlines::Double => "\n\n", 82 }; 83 TextEdit { 84 range: Range { 85 start: import_location, 86 end: import_location, 87 }, 88 new_text: ["import ", module_full_name, new_lines].concat(), 89 } 90} 91 92pub fn insert_unqualified_import( 93 import: &Import<EcoString>, 94 code: &str, 95 name: String, 96) -> (u32, String) { 97 let SrcSpan { start, end } = import.location; 98 99 let import_code = code 100 .get(start as usize..end as usize) 101 .expect("Import location is invalid"); 102 let has_brace = import_code.contains('}'); 103 104 if has_brace { 105 insert_into_braced_import(name, import.location, import_code) 106 } else { 107 insert_into_unbraced_import(name, import, import_code) 108 } 109} 110 111// Handle inserting into an unbraced import 112fn insert_into_unbraced_import( 113 name: String, 114 import: &Import<EcoString>, 115 import_code: &str, 116) -> (u32, String) { 117 let location = import.location; 118 if import.as_name.is_none() { 119 // Case: import module 120 (location.end, format!(".{{{name}}}")) 121 } else { 122 // Case: import module as alias 123 let as_pos = import_code 124 .find(" as ") 125 .expect("Expected ' as ' in import statement"); 126 let before_as_pos = import_code 127 .get(..as_pos) 128 .and_then(|s| s.rfind(|c: char| !c.is_whitespace())) 129 .map(|pos| location.start as usize + pos + 1) 130 .expect("Expected non-whitespace character before ' as '"); 131 (before_as_pos as u32, format!(".{{{name}}}")) 132 } 133} 134 135// Handle inserting into a braced import 136fn insert_into_braced_import(name: String, location: SrcSpan, import_code: &str) -> (u32, String) { 137 if let Some((pos, c)) = find_last_char_before_closing_brace(location, import_code) { 138 // Case: import module.{Existing, } (as alias) 139 if c == ',' { 140 (pos as u32 + 1, format!(" {name}")) 141 } else { 142 // Case: import module.{Existing} (as alias) 143 (pos as u32 + 1, format!(", {name}")) 144 } 145 } else { 146 // Case: import module.{} (as alias) 147 let left_brace_pos = import_code 148 .find('{') 149 .map(|pos| location.start as usize + pos) 150 .expect("Expected '{' in import statement"); 151 (left_brace_pos as u32 + 1, name) 152 } 153} 154 155fn find_last_char_before_closing_brace( 156 location: SrcSpan, 157 import_code: &str, 158) -> Option<(usize, char)> { 159 let closing_brace_pos = import_code.rfind('}')?; 160 161 let bytes = import_code.as_bytes(); 162 let mut pos = closing_brace_pos; 163 while pos > 0 { 164 pos -= 1; 165 let c = (*bytes.get(pos)?) as char; 166 if c.is_whitespace() { 167 continue; 168 } 169 if c == '{' { 170 break; 171 } 172 return Some((location.start as usize + pos, c)); 173 } 174 None 175}