Fork of daniellemaywood.uk/gleam — Wasm codegen work
11 kB
289 lines
1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2024 The Gleam contributors
3
4use std::{
5 collections::{HashMap, HashSet},
6 sync::Arc,
7};
8
9use ecow::EcoString;
10use lsp_types::{
11 ActiveParameter, Documentation, MarkupContent, MarkupKind, ParameterInformation,
12 ParameterInformationLabel, SignatureHelp, SignatureInformation,
13};
14
15use gleam_core::{
16 ast::{CallArg, ImplicitCallArgOrigin, TypedExpr},
17 build::Module,
18 type_::{FieldMap, ModuleValueConstructor, Type, printer::Printer},
19};
20
21pub fn for_expression(expr: &TypedExpr, module: &Module) -> Option<SignatureHelp> {
22 // If we're inside a function call we can provide signature help,
23 // otherwise we don't want anything to pop up.
24 let TypedExpr::Call { fun, arguments, .. } = expr else {
25 return None;
26 };
27
28 match fun.as_ref() {
29 // If the thing being called is a local variable then we want to
30 // use it's name as the function name to be used in the signature
31 // help.
32 TypedExpr::Var {
33 constructor, name, ..
34 } => signature_help(
35 name.clone(),
36 fun,
37 arguments,
38 constructor.field_map(),
39 module,
40 ),
41
42 // If we're making a qualified call to another module's function
43 // then we want to show its type, documentation and the exact name
44 // being used (that is "<module_name>.<function_name>").
45 //
46 // eg. list.map(|)
47 // ^ When the cursor is here we are going to show
48 // "list.map(List(a), with: fn(a) -> b) -> List(b)"
49 // as the help signature.
50 //
51 TypedExpr::ModuleSelect {
52 module_alias,
53 label,
54 constructor,
55 ..
56 } => {
57 let field_map = match constructor {
58 ModuleValueConstructor::Constant { .. } => None,
59 ModuleValueConstructor::Record { field_map, .. }
60 | ModuleValueConstructor::Fn { field_map, .. } => field_map.into(),
61 };
62 let name = format!("{module_alias}.{label}").into();
63 signature_help(name, fun, arguments, field_map, module)
64 }
65
66 // If the function being called is an invalid node we don't want to
67 // provide any hint, otherwise one might be under the impression that
68 // that function actually exists somewhere.
69 //
70 TypedExpr::Invalid { .. } => None,
71
72 // In all other cases we can't figure out a good name to show in the
73 // signature help so we use an anonymous `fn` as the name to be
74 // shown.
75 //
76 // eg. fn(a){a}(|)
77 // ^ When the cursor is here we are going to show
78 // "fn(a: a) -> a" as the help signature.
79 //
80 TypedExpr::Int { .. }
81 | TypedExpr::Float { .. }
82 | TypedExpr::String { .. }
83 | TypedExpr::Block { .. }
84 | TypedExpr::Pipeline { .. }
85 | TypedExpr::Fn { .. }
86 | TypedExpr::List { .. }
87 | TypedExpr::Call { .. }
88 | TypedExpr::BinOp { .. }
89 | TypedExpr::Case { .. }
90 | TypedExpr::RecordAccess { .. }
91 | TypedExpr::PositionalAccess { .. }
92 | TypedExpr::Tuple { .. }
93 | TypedExpr::TupleIndex { .. }
94 | TypedExpr::Todo { .. }
95 | TypedExpr::Panic { .. }
96 | TypedExpr::Echo { .. }
97 | TypedExpr::BitArray { .. }
98 | TypedExpr::RecordUpdate { .. }
99 | TypedExpr::NegateBool { .. }
100 | TypedExpr::NegateInt { .. } => signature_help("fn".into(), fun, arguments, None, module),
101 }
102}
103
104/// Show the signature help of a function with the given name.
105/// Besides the function's typed expression `fun`, this function needs a bit of
106/// additional data to properly display a useful help signature:
107///
108/// - `fun_name` is used as the display name of the function in the help
109/// signature.
110/// - `supplied_arguments` are arguments being passed to the function call, those
111/// might not be of the correct arity or have wrong types but are used to
112/// deduce which argument should be highlighted next in the help signature.
113/// - `field_map` is the function's field map (if any) that will be used to
114/// display labels and understand which labelled argument should be
115/// highlighted next in the help signature.
116///
117fn signature_help(
118 fun_name: EcoString,
119 fun: &TypedExpr,
120 supplied_arguments: &[CallArg<TypedExpr>],
121 field_map: Option<&FieldMap>,
122 module: &Module,
123) -> Option<SignatureHelp> {
124 let (arguments, return_) = fun.type_().fn_types()?;
125
126 // If the function has no arguments, we don't want to show any help.
127 let arity = arguments.len() as u32;
128 if arity == 0 {
129 return None;
130 }
131
132 let index_to_label = match field_map {
133 Some(field_map) => field_map
134 .fields
135 .iter()
136 .map(|(name, index)| (*index, name))
137 .collect(),
138 None => HashMap::new(),
139 };
140
141 let printer = Printer::new(&module.ast.names);
142 let (label, parameters) =
143 print_signature_help(printer, fun_name, arguments, return_, &index_to_label);
144
145 let active_parameter = active_parameter_index(arity, supplied_arguments, index_to_label)
146 // If we don't want to highlight any arg in the suggestion we have to
147 // explicitly provide an out of bound index.
148 .or(Some(arity))
149 .map(ActiveParameter::Int);
150
151 Some(SignatureHelp {
152 signatures: vec![SignatureInformation {
153 label,
154 documentation: fun.get_documentation().map(|d| {
155 Documentation::MarkupContent(MarkupContent {
156 kind: MarkupKind::Markdown,
157 value: d.into(),
158 })
159 }),
160 parameters: Some(parameters),
161 active_parameter: None,
162 }],
163 active_signature: Some(0),
164 active_parameter,
165 })
166}
167
168fn active_parameter_index(
169 arity: u32,
170 supplied_arguments: &[CallArg<TypedExpr>],
171 mut index_to_label: HashMap<u32, &EcoString>,
172) -> Option<u32> {
173 let mut is_use_call = false;
174 let mut found_labelled_argument = false;
175 let mut used_labels = HashSet::new();
176
177 let mut supplied_unlabelled_arguments = 0;
178 let unlabelled_arguments = arity - index_to_label.len() as u32;
179
180 for (i, arg) in supplied_arguments.iter().enumerate() {
181 // If there's an unlabelled argument after a labelled one, we can't
182 // figure out what to suggest since arguments were passed in a wrong
183 // order.
184 if found_labelled_argument && arg.label.is_none() && !arg.is_implicit() {
185 return None;
186 }
187
188 // Once we reach to an implicit use argument (be it the callback or the
189 // missing implicitly inserted ones) we can break since those must be
190 // the last arguments of the function and are not explicitly supplied by
191 // the programmer.
192 if let Some(ImplicitCallArgOrigin::Use | ImplicitCallArgOrigin::IncorrectArityUse) =
193 arg.implicit
194 {
195 is_use_call = true;
196 break;
197 }
198
199 match &arg.label {
200 Some(label) => {
201 found_labelled_argument = true;
202 let _ = used_labels.insert(label);
203 }
204
205 // If the argument is unlabelled we just remove the label
206 // corresponding to it from the field map since it has already been
207 // passed as an unlabelled argument.
208 None => {
209 supplied_unlabelled_arguments += 1;
210 let _ = index_to_label.remove(&(i as u32));
211 }
212 }
213 }
214
215 let active_index = if supplied_unlabelled_arguments < unlabelled_arguments {
216 if found_labelled_argument {
217 // If I have supplied some labelled args but I haven't supplied all
218 // unlabelled args before a labelled one then we can't safely
219 // suggest anything as the next argument.
220 None
221 } else {
222 // If I haven't supplied enough unlabelled arguments then I have to
223 // set the next one as active (be it labelled or not).
224 Some(supplied_unlabelled_arguments)
225 }
226 } else {
227 // If I have supplied all the unlabelled arguments (and we could have
228 // also supplied some labelled ones as unlabelled!) then we pick the
229 // leftmost labelled argument that hasn't been supplied yet.
230 index_to_label
231 .into_iter()
232 .filter(|(_index, label)| !used_labels.contains(label))
233 .map(|(index, _label)| index)
234 .min()
235 .or(Some(supplied_arguments.len() as u32))
236 };
237
238 // If we're showing hints for a use call and we end up deciding that the
239 // only index we can suggest is the one of the use callback then we do not
240 // highlight it or it would lead people into believing they can manually
241 // pass that argument in.
242 if is_use_call && active_index == Some(arity - 1) {
243 None
244 } else {
245 active_index
246 }
247}
248
249/// To produce a signature that can be used by the LS, we need to also keep
250/// track of the arguments' positions in the printed signature. So this function
251/// prints the signature help producing at the same time a list of correct
252/// `ParameterInformation` for all its arguments.
253///
254fn print_signature_help(
255 mut printer: Printer<'_>,
256 function_name: EcoString,
257 arguments: Vec<Arc<Type>>,
258 return_: Arc<Type>,
259 index_to_label: &HashMap<u32, &EcoString>,
260) -> (String, Vec<ParameterInformation>) {
261 let arguments_count = arguments.len();
262 let mut signature = format!("{function_name}(");
263 let mut parameter_informations = Vec::with_capacity(arguments_count);
264
265 for (i, argument) in arguments.iter().enumerate() {
266 let arg_start = signature.len();
267 if let Some(label) = index_to_label.get(&(i as u32)) {
268 signature.push_str(label);
269 signature.push_str(": ");
270 }
271 signature.push_str(&printer.print_type(argument));
272 let arg_end = signature.len();
273 let label = ParameterInformationLabel::Tuple((arg_start as u32, arg_end as u32));
274
275 parameter_informations.push(ParameterInformation {
276 label,
277 documentation: None,
278 });
279
280 let is_last = i == arguments_count - 1;
281 if !is_last {
282 signature.push_str(", ");
283 }
284 }
285
286 signature.push_str(") -> ");
287 signature.push_str(&printer.print_type(&return_));
288 (signature, parameter_informations)
289}