Fork of daniellemaywood.uk/gleam — Wasm codegen work
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}