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 / signature_help.rs
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}