plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
plum-lsp/src/completion.rs
| 6fc9e6d | 1 | // Completion support. Three tiers, matching what's cheaply available: |
| 6fc9e6d | 2 | // |
| 6fc9e6d | 3 | // 1. Keywords + top-level names (fn/enum/variant/trait/const, own file and |
| 6fc9e6d | 4 | // already-imported ones) — pure AST, no type inference. |
| 6fc9e6d | 5 | // 2. Member completion after `expr.` — needs to know `expr`'s type. There's |
| 6fc9e6d | 6 | // no real "infer the type at this cursor" facility in `plum_checker` |
| 6fc9e6d | 7 | // (it type-checks whole functions, not arbitrary positions), so this |
| 6fc9e6d | 8 | // reconstructs a best-effort local variable environment by walking the |
| 6fc9e6d | 9 | // enclosing function's OWN body top-to-bottom (see `localVarTypes`) — |
| 6fc9e6d | 10 | // correct for the common case (a variable assigned once, in a straight |
| 6fc9e6d | 11 | // line above the cursor), not control-flow- or shadowing-aware. |
| 6fc9e6d | 12 | // 3. Std lib symbols not yet imported, offered with an `additionalTextEdit` |
| 6fc9e6d | 13 | // that inserts the `import` line — built once per `lib_path` and cached |
| 6fc9e6d | 14 | // by the caller (parsing every std file on every keystroke would be |
| 6fc9e6d | 15 | // wasteful). |
| 6fc9e6d | 16 | |
| 6fc9e6d | 17 | #![allow(non_snake_case)] |
| 6fc9e6d | 18 | |
| 6fc9e6d | 19 | use std::collections::HashMap; |
| 6fc9e6d | 20 | use std::path::Path; |
| 6fc9e6d | 21 | |
| 6fc9e6d | 22 | use plum_checker::types::{PlumType, TypeEnv, TypeScheme}; |
| 6fc9e6d | 23 | use plum_checker::{inferExpr, methodReceiverName, plumTypeFromAst, CheckCtx}; |
| 6fc9e6d | 24 | use plum_core::ast; |
| 6fc9e6d | 25 | use tower_lsp::lsp_types::*; |
| 6fc9e6d | 26 | |
| 6fc9e6d | 27 | const KEYWORDS: &[&str] = &[ |
| 6fc9e6d | 28 | "fun", "enum", "trait", "import", "module", "extern", "if", "else", "while", "for", "match", |
| 6fc9e6d | 29 | "case", "when", "break", "continue", "return", "todo", "assert", "test", "self", "True", |
| 6fc9e6d | 30 | "False", |
| 6fc9e6d | 31 | ]; |
| 6fc9e6d | 32 | |
| 6fc9e6d | 33 | pub fn keywordItems() -> Vec<CompletionItem> { |
| 6fc9e6d | 34 | KEYWORDS |
| 6fc9e6d | 35 | .iter() |
| 6fc9e6d | 36 | .map(|k| CompletionItem { |
| 6fc9e6d | 37 | label: k.to_string(), |
| 6fc9e6d | 38 | kind: Some(CompletionItemKind::KEYWORD), |
| 6fc9e6d | 39 | ..Default::default() |
| 6fc9e6d | 40 | }) |
| 6fc9e6d | 41 | .collect() |
| 6fc9e6d | 42 | } |
| 6fc9e6d | 43 | |
| 6fc9e6d | 44 | /// Every top-level name `source` declares: free functions, enums (and their |
| 6fc9e6d | 45 | /// bare variant names), traits, consts. Excludes methods (`type_param.is_some()`) |
| 6fc9e6d | 46 | /// — those only make sense after a `.`, handled by `memberItems` instead. |
| 6fc9e6d | 47 | pub fn topLevelItems(source: &ast::Source) -> Vec<CompletionItem> { |
| 6fc9e6d | 48 | let mut items = Vec::new(); |
| 6fc9e6d | 49 | for item in &source.items { |
| 6fc9e6d | 50 | match item { |
| 6fc9e6d | 51 | ast::Item::Fn(f) if f.type_param.is_none() => { |
| 6fc9e6d | 52 | items.push(CompletionItem { |
| 6fc9e6d | 53 | label: f.name.clone(), |
| 6fc9e6d | 54 | kind: Some(CompletionItemKind::FUNCTION), |
| 6fc9e6d | 55 | detail: Some("fn".to_string()), |
| 6fc9e6d | 56 | ..Default::default() |
| 6fc9e6d | 57 | }); |
| 6fc9e6d | 58 | } |
| 6fc9e6d | 59 | ast::Item::Fn(_) => {} |
| 6fc9e6d | 60 | ast::Item::Enum(e) => { |
| 6fc9e6d | 61 | items.push(CompletionItem { |
| 6fc9e6d | 62 | label: e.name.clone(), |
| 6fc9e6d | 63 | kind: Some(CompletionItemKind::ENUM), |
| 6fc9e6d | 64 | ..Default::default() |
| 6fc9e6d | 65 | }); |
| 6fc9e6d | 66 | for v in &e.variants { |
| 6fc9e6d | 67 | items.push(CompletionItem { |
| 6fc9e6d | 68 | label: v.name.clone(), |
| 6fc9e6d | 69 | kind: Some(CompletionItemKind::ENUM_MEMBER), |
| 6fc9e6d | 70 | detail: Some(format!("{}.{}", e.name, v.name)), |
| 6fc9e6d | 71 | ..Default::default() |
| 6fc9e6d | 72 | }); |
| 6fc9e6d | 73 | } |
| 6fc9e6d | 74 | } |
| 6fc9e6d | 75 | ast::Item::Trait(t) => { |
| 6fc9e6d | 76 | items.push(CompletionItem { |
| 6fc9e6d | 77 | label: t.name.clone(), |
| 6fc9e6d | 78 | kind: Some(CompletionItemKind::INTERFACE), |
| 6fc9e6d | 79 | ..Default::default() |
| 6fc9e6d | 80 | }); |
| 6fc9e6d | 81 | } |
| 6fc9e6d | 82 | ast::Item::Const(c) => { |
| 6fc9e6d | 83 | items.push(CompletionItem { |
| 6fc9e6d | 84 | label: c.name.clone(), |
| 6fc9e6d | 85 | kind: Some(CompletionItemKind::CONSTANT), |
| 6fc9e6d | 86 | ..Default::default() |
| 6fc9e6d | 87 | }); |
| 6fc9e6d | 88 | } |
| 6fc9e6d | 89 | ast::Item::Test(_) => {} |
| 6fc9e6d | 90 | } |
| 6fc9e6d | 91 | } |
| 6fc9e6d | 92 | items |
| 6fc9e6d | 93 | } |
| 6fc9e6d | 94 | |
| 6fc9e6d | 95 | /// Replaces the identifier run ending exactly at `pos` (possibly empty, e.g. |
| 6fc9e6d | 96 | /// right after a bare `.`) with a placeholder identifier, so the document |
| 6fc9e6d | 97 | /// parses cleanly for `mergedSource`'s sake — mid-edit, that run is |
| 6fc9e6d | 98 | /// necessarily incomplete (`self.` or `self.ag` while typing `self.age`), |
| 6fc9e6d | 99 | /// which `tree-sitter` turns into an ERROR node, which `loadAndMergeWithOverrides` |
| 6fc9e6d | 100 | /// treats as a hard parse failure for the WHOLE file (see `parseFile`'s |
| 6fc9e6d | 101 | /// `firstErrorNode` check) — without this, completion would fail on almost |
| 6fc9e6d | 102 | /// every real keystroke, since that's exactly when it's requested. |
| 6fc9e6d | 103 | pub fn sanitizeForParsing(text: &str, pos: Position) -> String { |
| 6fc9e6d | 104 | let mut out = String::with_capacity(text.len() + 8); |
| 6fc9e6d | 105 | for (i, line) in text.lines().enumerate() { |
| 6fc9e6d | 106 | if i > 0 { |
| 6fc9e6d | 107 | out.push('\n'); |
| 6fc9e6d | 108 | } |
| 6fc9e6d | 109 | if i as u32 == pos.line { |
| 6fc9e6d | 110 | let chars: Vec<char> = line.chars().collect(); |
| 6fc9e6d | 111 | let end = (pos.character as usize).min(chars.len()); |
| 6fc9e6d | 112 | let mut start = end; |
| 6fc9e6d | 113 | while start > 0 && (chars[start - 1].is_alphanumeric() || chars[start - 1] == '_') { |
| 6fc9e6d | 114 | start -= 1; |
| 6fc9e6d | 115 | } |
| 6fc9e6d | 116 | out.extend(&chars[..start]); |
| 6fc9e6d | 117 | out.push_str("zzzzzz"); |
| 6fc9e6d | 118 | out.extend(&chars[end..]); |
| 6fc9e6d | 119 | } else { |
| 6fc9e6d | 120 | out.push_str(line); |
| 6fc9e6d | 121 | } |
| 6fc9e6d | 122 | } |
| 6fc9e6d | 123 | out |
| 6fc9e6d | 124 | } |
| 6fc9e6d | 125 | |
| 6fc9e6d | 126 | /// If the cursor sits right after `<ident>.<partial>` (or `<ident>.`), |
| 6fc9e6d | 127 | /// returns `<ident>`. `line` is just the cursor's own line text, `char_idx` |
| 6fc9e6d | 128 | /// its column treated as a char index (not real UTF-16 offset math — fine |
| 6fc9e6d | 129 | /// for the ASCII identifiers this looks for). |
| 6fc9e6d | 130 | pub fn receiverBeforeDot(line: &str, char_idx: usize) -> Option<String> { |
| 6fc9e6d | 131 | let chars: Vec<char> = line.chars().collect(); |
| 6fc9e6d | 132 | let mut i = char_idx.min(chars.len()); |
| 6fc9e6d | 133 | while i > 0 && (chars[i - 1].is_alphanumeric() || chars[i - 1] == '_') { |
| 6fc9e6d | 134 | i -= 1; |
| 6fc9e6d | 135 | } |
| 6fc9e6d | 136 | if i == 0 || chars[i - 1] != '.' { |
| 6fc9e6d | 137 | return None; |
| 6fc9e6d | 138 | } |
| 6fc9e6d | 139 | let dot = i - 1; |
| 6fc9e6d | 140 | let mut j = dot; |
| 6fc9e6d | 141 | while j > 0 && (chars[j - 1].is_alphanumeric() || chars[j - 1] == '_') { |
| 6fc9e6d | 142 | j -= 1; |
| 6fc9e6d | 143 | } |
| 6fc9e6d | 144 | if j == dot { |
| 6fc9e6d | 145 | return None; |
| 6fc9e6d | 146 | } |
| 6fc9e6d | 147 | Some(chars[j..dot].iter().collect()) |
| 6fc9e6d | 148 | } |
| 6fc9e6d | 149 | |
| 6fc9e6d | 150 | /// The `Fn` item (own free functions AND methods) whose declaration is the |
| 6fc9e6d | 151 | /// closest one at or before `cursor_line` (1-based, matching `ast::Fn::line`) |
| 6fc9e6d | 152 | /// — a stand-in for "which function contains the cursor" since blocks don't |
| 6fc9e6d | 153 | /// carry end positions to check containment properly. |
| 6fc9e6d | 154 | pub fn enclosingFn(source: &ast::Source, cursor_line: usize) -> Option<&ast::Fn> { |
| 6fc9e6d | 155 | source |
| 6fc9e6d | 156 | .items |
| 6fc9e6d | 157 | .iter() |
| 6fc9e6d | 158 | .filter_map(|i| match i { |
| 6fc9e6d | 159 | ast::Item::Fn(f) if f.line > 0 && f.line <= cursor_line => Some(f), |
| 6fc9e6d | 160 | _ => None, |
| 6fc9e6d | 161 | }) |
| 6fc9e6d | 162 | .max_by_key(|f| f.line) |
| 6fc9e6d | 163 | } |
| 6fc9e6d | 164 | |
| 6fc9e6d | 165 | /// Best-effort local variable types for `f`'s body, built by walking every |
| 6fc9e6d | 166 | /// `Stmt::Assign` (at any nesting depth) top-to-bottom and re-inferring with |
| 6fc9e6d | 167 | /// `inferExpr` each time — see this module's doc comment for the tradeoffs. |
| 6fc9e6d | 168 | fn localVarTypes(f: &ast::Fn, global: &TypeEnv, ctx: &CheckCtx) -> HashMap<String, PlumType> { |
| 6fc9e6d | 169 | let mut env = HashMap::new(); |
| 6fc9e6d | 170 | if let Some(recv) = &f.type_param { |
| 6fc9e6d | 171 | env.insert( |
| 6fc9e6d | 172 | "self".to_string(), |
| 6fc9e6d | 173 | plumTypeFromAst(&ast::Type { |
| 6fc9e6d | 174 | name: recv.clone(), |
| 6fc9e6d | 175 | generics: vec![], |
| 6fc9e6d | 176 | }), |
| 6fc9e6d | 177 | ); |
| 6fc9e6d | 178 | } |
| 6fc9e6d | 179 | for p in &f.params { |
| 6fc9e6d | 180 | let ty = match &p.ty { |
| 6fc9e6d | 181 | ast::ParamType::Type(t) => plumTypeFromAst(t), |
| 6fc9e6d | 182 | ast::ParamType::Variadic(t) => PlumType::TVariadic(Box::new(plumTypeFromAst(t))), |
| 6fc9e6d | 183 | ast::ParamType::Fn(params, ret) => PlumType::TFun( |
| 6fc9e6d | 184 | params.iter().map(plumTypeFromAst).collect(), |
| 6fc9e6d | 185 | Box::new( |
| 6fc9e6d | 186 | ret.as_ref() |
| 6fc9e6d | 187 | .map(|r| plumTypeFromAst(r)) |
| 6fc9e6d | 188 | .unwrap_or(PlumType::TUnit), |
| 6fc9e6d | 189 | ), |
| 6fc9e6d | 190 | ), |
| 6fc9e6d | 191 | }; |
| 6fc9e6d | 192 | env.insert(p.name.clone(), ty); |
| 6fc9e6d | 193 | } |
| 6fc9e6d | 194 | if let ast::FnBody::Block(block) = &f.body { |
| 6fc9e6d | 195 | walkBlock(block, &mut env, global, ctx); |
| 6fc9e6d | 196 | } |
| 6fc9e6d | 197 | env |
| 6fc9e6d | 198 | } |
| 6fc9e6d | 199 | |
| 6fc9e6d | 200 | fn walkBlock( |
| 6fc9e6d | 201 | block: &ast::Block, |
| 6fc9e6d | 202 | env: &mut HashMap<String, PlumType>, |
| 6fc9e6d | 203 | global: &TypeEnv, |
| 6fc9e6d | 204 | ctx: &CheckCtx, |
| 6fc9e6d | 205 | ) { |
| 6fc9e6d | 206 | for stmt in &block.stmts { |
| 6fc9e6d | 207 | walkStmt(stmt, env, global, ctx); |
| 6fc9e6d | 208 | } |
| 6fc9e6d | 209 | } |
| 6fc9e6d | 210 | |
| 6fc9e6d | 211 | fn walkStmt( |
| 6fc9e6d | 212 | stmt: &ast::Stmt, |
| 6fc9e6d | 213 | env: &mut HashMap<String, PlumType>, |
| 6fc9e6d | 214 | global: &TypeEnv, |
| 6fc9e6d | 215 | ctx: &CheckCtx, |
| 6fc9e6d | 216 | ) { |
| 6fc9e6d | 217 | match stmt { |
| 6fc9e6d | 218 | ast::Stmt::Assign(a) => { |
| 6fc9e6d | 219 | for (target, value) in a.targets.iter().zip(a.values.iter()) { |
| 6fc9e6d | 220 | if let ast::AssignTarget::Var(name) = target { |
| 6fc9e6d | 221 | let mut combined = global.clone(); |
| 6fc9e6d | 222 | for (k, v) in env.iter() { |
| 6fc9e6d | 223 | combined.insert(k.clone(), TypeScheme::mono(v.clone())); |
| 6fc9e6d | 224 | } |
| 6fc9e6d | 225 | if let Ok(t) = inferExpr(value, &combined, ctx) { |
| 6fc9e6d | 226 | env.insert(name.clone(), t); |
| 6fc9e6d | 227 | } |
| 6fc9e6d | 228 | } |
| 6fc9e6d | 229 | } |
| 6fc9e6d | 230 | } |
| 6fc9e6d | 231 | ast::Stmt::If(if_) => { |
| 6fc9e6d | 232 | walkBlock(&if_.body, env, global, ctx); |
| 6fc9e6d | 233 | for ei in &if_.else_ifs { |
| 6fc9e6d | 234 | walkBlock(&ei.body, env, global, ctx); |
| 6fc9e6d | 235 | } |
| 6fc9e6d | 236 | if let Some(b) = &if_.else_ { |
| 6fc9e6d | 237 | walkBlock(b, env, global, ctx); |
| 6fc9e6d | 238 | } |
| 6fc9e6d | 239 | } |
| 6fc9e6d | 240 | ast::Stmt::While(w) => walkBlock(&w.body, env, global, ctx), |
| 6fc9e6d | 241 | ast::Stmt::For(f) => { |
| 6fc9e6d | 242 | for v in &f.vars { |
| 6fc9e6d | 243 | env.entry(v.clone()).or_insert(PlumType::TInt); |
| 6fc9e6d | 244 | } |
| 6fc9e6d | 245 | walkBlock(&f.body, env, global, ctx); |
| 6fc9e6d | 246 | } |
| 6fc9e6d | 247 | ast::Stmt::Match(m) => { |
| 6fc9e6d | 248 | for case in &m.cases { |
| 6fc9e6d | 249 | walkBlock(&case.body, env, global, ctx); |
| 6fc9e6d | 250 | } |
| 6fc9e6d | 251 | } |
| 6fc9e6d | 252 | _ => {} |
| 6fc9e6d | 253 | } |
| 6fc9e6d | 254 | } |
| 6fc9e6d | 255 | |
| 6fc9e6d | 256 | /// Completion items for `receiver`'s methods/fields, given the function the |
| 6fc9e6d | 257 | /// cursor is inside (`None` if the cursor isn't inside any function — e.g. a |
| 6fc9e6d | 258 | /// top-level `test` body isn't covered, see this module's doc comment). |
| 6fc9e6d | 259 | pub fn memberItems( |
| 6fc9e6d | 260 | cursor_fn: Option<&ast::Fn>, |
| 6fc9e6d | 261 | receiver: &str, |
| 6fc9e6d | 262 | global_env: &TypeEnv, |
| 6fc9e6d | 263 | ctx: &CheckCtx, |
| 6fc9e6d | 264 | ) -> Vec<CompletionItem> { |
| 6fc9e6d | 265 | let Some(f) = cursor_fn else { |
| 6fc9e6d | 266 | return Vec::new(); |
| 6fc9e6d | 267 | }; |
| 6fc9e6d | 268 | |
| 6fc9e6d | 269 | let receiver_ty = if receiver == "self" { |
| 6fc9e6d | 270 | f.type_param.as_ref().map(|r| { |
| 6fc9e6d | 271 | plumTypeFromAst(&ast::Type { |
| 6fc9e6d | 272 | name: r.clone(), |
| 6fc9e6d | 273 | generics: vec![], |
| 6fc9e6d | 274 | }) |
| 6fc9e6d | 275 | }) |
| 6fc9e6d | 276 | } else { |
| 6fc9e6d | 277 | localVarTypes(f, global_env, ctx).get(receiver).cloned() |
| 6fc9e6d | 278 | }; |
| 6fc9e6d | 279 | |
| 6fc9e6d | 280 | let Some(ty) = receiver_ty else { |
| 6fc9e6d | 281 | return Vec::new(); |
| 6fc9e6d | 282 | }; |
| 6fc9e6d | 283 | let Some(recv_name) = methodReceiverName(&ty) else { |
| 6fc9e6d | 284 | return Vec::new(); |
| 6fc9e6d | 285 | }; |
| 6fc9e6d | 286 | |
| 6fc9e6d | 287 | let mut items = Vec::new(); |
| 6fc9e6d | 288 | for ((r, name), fn_ty) in ctx.methods.iter() { |
| 6fc9e6d | 289 | if r != &recv_name { |
| 6fc9e6d | 290 | continue; |
| 6fc9e6d | 291 | } |
| 6fc9e6d | 292 | let detail = match fn_ty { |
| 6fc9e6d | 293 | PlumType::TFun(params, ret) => Some(format!( |
| 6fc9e6d | 294 | "({}) -> {:?}", |
| 6fc9e6d | 295 | params |
| 6fc9e6d | 296 | .iter() |
| 6fc9e6d | 297 | .map(|p| format!("{p:?}")) |
| 6fc9e6d | 298 | .collect::<Vec<_>>() |
| 6fc9e6d | 299 | .join(", "), |
| 6fc9e6d | 300 | ret |
| 6fc9e6d | 301 | )), |
| 6fc9e6d | 302 | other => Some(format!("{other:?}")), |
| 6fc9e6d | 303 | }; |
| 6fc9e6d | 304 | items.push(CompletionItem { |
| 6fc9e6d | 305 | label: name.clone(), |
| 6fc9e6d | 306 | kind: Some(CompletionItemKind::METHOD), |
| 6fc9e6d | 307 | detail, |
| 6fc9e6d | 308 | ..Default::default() |
| 6fc9e6d | 309 | }); |
| 6fc9e6d | 310 | } |
| 6fc9e6d | 311 | for info in ctx.enum_variants.values() { |
| 6fc9e6d | 312 | if info.enum_name != recv_name { |
| 6fc9e6d | 313 | continue; |
| 6fc9e6d | 314 | } |
| 6fc9e6d | 315 | for field_name in &info.field_names { |
| 6fc9e6d | 316 | items.push(CompletionItem { |
| 6fc9e6d | 317 | label: field_name.clone(), |
| 6fc9e6d | 318 | kind: Some(CompletionItemKind::FIELD), |
| 6fc9e6d | 319 | ..Default::default() |
| 6fc9e6d | 320 | }); |
| 6fc9e6d | 321 | } |
| 6fc9e6d | 322 | } |
| 6fc9e6d | 323 | items |
| 6fc9e6d | 324 | } |
| 6fc9e6d | 325 | |
| 6fc9e6d | 326 | /// Maps every top-level name declared anywhere under `<lib_path>/plum-std/*.plum` |
| 6fc9e6d | 327 | /// to that file's `import` path (`Str.plum` -> `"std/Str"`) — built by parsing |
| 6fc9e6d | 328 | /// each file standalone (no import resolution needed just to list names). |
| 6fc9e6d | 329 | /// Meant to be computed once and cached by the caller. |
| 6fc9e6d | 330 | pub fn stdLibIndex(lib_path: &Path) -> HashMap<String, String> { |
| 6fc9e6d | 331 | let mut map = HashMap::new(); |
| 6fc9e6d | 332 | let std_dir = lib_path.join("plum-std"); |
| 6fc9e6d | 333 | let Ok(entries) = std::fs::read_dir(&std_dir) else { |
| 6fc9e6d | 334 | return map; |
| 6fc9e6d | 335 | }; |
| 6fc9e6d | 336 | for entry in entries.flatten() { |
| 6fc9e6d | 337 | let path = entry.path(); |
| 6fc9e6d | 338 | if path.extension().and_then(|e| e.to_str()) != Some("plum") { |
| 6fc9e6d | 339 | continue; |
| 6fc9e6d | 340 | } |
| 6fc9e6d | 341 | let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else { |
| 6fc9e6d | 342 | continue; |
| 6fc9e6d | 343 | }; |
| 6fc9e6d | 344 | let Ok(text) = std::fs::read_to_string(&path) else { |
| 6fc9e6d | 345 | continue; |
| 6fc9e6d | 346 | }; |
| 6fc9e6d | 347 | let mut parser = tree_sitter::Parser::new(); |
| 6fc9e6d | 348 | if parser |
| 6fc9e6d | 349 | .set_language(&tree_sitter_plum::LANGUAGE.into()) |
| 6fc9e6d | 350 | .is_err() |
| 6fc9e6d | 351 | { |
| 6fc9e6d | 352 | continue; |
| 6fc9e6d | 353 | } |
| 6fc9e6d | 354 | let Some(tree) = parser.parse(&text, None) else { |
| 6fc9e6d | 355 | continue; |
| 6fc9e6d | 356 | }; |
| 6fc9e6d | 357 | let source = plum_core::AstParser::new(&text).parseSource(tree.root_node()); |
| 6fc9e6d | 358 | let import_path = format!("std/{stem}"); |
| 6fc9e6d | 359 | for item in &source.items { |
| 6fc9e6d | 360 | let name = match item { |
| 6fc9e6d | 361 | ast::Item::Fn(f) if f.type_param.is_none() => Some(f.name.clone()), |
| 6fc9e6d | 362 | ast::Item::Enum(e) => { |
| 6fc9e6d | 363 | map.entry(e.name.clone()) |
| 6fc9e6d | 364 | .or_insert_with(|| import_path.clone()); |
| 6fc9e6d | 365 | for v in &e.variants { |
| 6fc9e6d | 366 | map.entry(v.name.clone()) |
| 6fc9e6d | 367 | .or_insert_with(|| import_path.clone()); |
| 6fc9e6d | 368 | } |
| 6fc9e6d | 369 | None |
| 6fc9e6d | 370 | } |
| 6fc9e6d | 371 | ast::Item::Trait(t) => Some(t.name.clone()), |
| 6fc9e6d | 372 | ast::Item::Const(c) => Some(c.name.clone()), |
| 6fc9e6d | 373 | _ => None, |
| 6fc9e6d | 374 | }; |
| 6fc9e6d | 375 | if let Some(name) = name { |
| 6fc9e6d | 376 | map.entry(name).or_insert_with(|| import_path.clone()); |
| 6fc9e6d | 377 | } |
| 6fc9e6d | 378 | } |
| 6fc9e6d | 379 | } |
| 6fc9e6d | 380 | map |
| 6fc9e6d | 381 | } |
| 6fc9e6d | 382 | |
| 6fc9e6d | 383 | /// A `TextEdit` inserting `import <import_path>` right after the last |
| 6fc9e6d | 384 | /// existing `import` line (or after the `module` line, or at the very top of |
| 6fc9e6d | 385 | /// the file if neither exists) — plain line scanning, since `ast::Import` |
| 6fc9e6d | 386 | /// doesn't carry a position to look this up structurally. |
| 6fc9e6d | 387 | pub fn importInsertionEdit(text: &str, import_path: &str) -> TextEdit { |
| 6fc9e6d | 388 | let mut last_import_line: Option<usize> = None; |
| 6fc9e6d | 389 | let mut module_line: Option<usize> = None; |
| 6fc9e6d | 390 | for (i, line) in text.lines().enumerate() { |
| 6fc9e6d | 391 | let trimmed = line.trim_start(); |
| 6fc9e6d | 392 | if trimmed.starts_with("import ") { |
| 6fc9e6d | 393 | last_import_line = Some(i); |
| 6fc9e6d | 394 | } else if trimmed.starts_with("module ") { |
| 6fc9e6d | 395 | module_line = Some(i); |
| 6fc9e6d | 396 | } |
| 6fc9e6d | 397 | } |
| 6fc9e6d | 398 | let at_line = last_import_line.or(module_line).map(|l| l + 1).unwrap_or(0) as u32; |
| 6fc9e6d | 399 | TextEdit { |
| 6fc9e6d | 400 | range: Range { |
| 6fc9e6d | 401 | start: Position::new(at_line, 0), |
| 6fc9e6d | 402 | end: Position::new(at_line, 0), |
| 6fc9e6d | 403 | }, |
| 6fc9e6d | 404 | new_text: format!("import {import_path}\n"), |
| 6fc9e6d | 405 | } |
| 6fc9e6d | 406 | } |