plum

#treesitter#compiler#wasm

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
}