From 5b7f117d0e75975fc4c3087bccaa38565bbced92 Mon Sep 17 00:00:00 2001 From: Adam Welc Date: Tue, 10 Sep 2024 12:53:34 -0700 Subject: [PATCH] [move-ide] Refactored auto-completions implementation to multiple files (#19236) ## Description The auto-completions implementation was becoming unwieldy and this PR splits it up to multiple files in a separate directory. This a pure refactoring - other than some name changes, added comments, and moving code around, no modifications have been made ## Test plan All existing tests must pass --- .../move/crates/move-analyzer/src/analyzer.rs | 2 +- .../move-analyzer/src/completions/dot.rs | 115 ++ .../move-analyzer/src/completions/mod.rs | 428 ++++++ .../name_chain.rs} | 1326 ++++------------- .../move-analyzer/src/completions/snippets.rs | 215 +++ .../move-analyzer/src/completions/utils.rs | 130 ++ .../move/crates/move-analyzer/src/lib.rs | 2 +- .../move-analyzer/tests/ide_testsuite.rs | 4 +- 8 files changed, 1157 insertions(+), 1065 deletions(-) create mode 100644 external-crates/move/crates/move-analyzer/src/completions/dot.rs create mode 100644 external-crates/move/crates/move-analyzer/src/completions/mod.rs rename external-crates/move/crates/move-analyzer/src/{completion.rs => completions/name_chain.rs} (50%) create mode 100644 external-crates/move/crates/move-analyzer/src/completions/snippets.rs create mode 100644 external-crates/move/crates/move-analyzer/src/completions/utils.rs diff --git a/external-crates/move/crates/move-analyzer/src/analyzer.rs b/external-crates/move/crates/move-analyzer/src/analyzer.rs index 37f31ec5c6cfd..bb8c067e97b6c 100644 --- a/external-crates/move/crates/move-analyzer/src/analyzer.rs +++ b/external-crates/move/crates/move-analyzer/src/analyzer.rs @@ -19,7 +19,7 @@ use std::{ }; use crate::{ - completion::on_completion_request, context::Context, inlay_hints, symbols, + completions::on_completion_request, context::Context, inlay_hints, symbols, vfs::on_text_document_sync_notification, }; use url::Url; diff --git a/external-crates/move/crates/move-analyzer/src/completions/dot.rs b/external-crates/move/crates/move-analyzer/src/completions/dot.rs new file mode 100644 index 0000000000000..387f6441c2fab --- /dev/null +++ b/external-crates/move/crates/move-analyzer/src/completions/dot.rs @@ -0,0 +1,115 @@ +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +// Auto-completion for the dot operator, e.g., `struct.` or `value.foo()`.` + +use crate::{ + completions::utils::{call_completion_item, mod_defs}, + symbols::{type_to_ide_string, DefInfo, FunType, Symbols}, + utils::lsp_position_to_loc, +}; +use lsp_types::{ + CompletionItem, CompletionItemKind, CompletionItemLabelDetails, InsertTextFormat, Position, +}; +use move_compiler::{ + expansion::ast::ModuleIdent_, + shared::{ide::AutocompleteMethod, Identifier}, +}; +use move_symbol_pool::Symbol; + +use std::path::Path; + +/// Handle "dot" auto-completion at a given position. +pub fn dot_completions( + symbols: &Symbols, + use_fpath: &Path, + position: &Position, +) -> (Vec, bool) { + let mut completions = vec![]; + let mut completion_finalized = false; + let Some(fhash) = symbols.file_hash(use_fpath) else { + eprintln!("no dot completions due to missing file"); + return (completions, completion_finalized); + }; + let Some(loc) = lsp_position_to_loc(&symbols.files, fhash, position) else { + eprintln!("no dot completions due to missing loc"); + return (completions, completion_finalized); + }; + let Some(info) = symbols.compiler_info.get_autocomplete_info(fhash, &loc) else { + return (completions, completion_finalized); + }; + // we found auto-completion info, so don't look for any more completions + // even if if it does not contain any + completion_finalized = true; + for AutocompleteMethod { + method_name, + target_function: (mod_ident, function_name), + } in &info.methods + { + let call_completion = if let Some(DefInfo::Function( + .., + fun_type, + _, + type_args, + arg_names, + arg_types, + ret_type, + _, + )) = fun_def_info(symbols, mod_ident.value, function_name.value()) + { + call_completion_item( + &mod_ident.value, + matches!(fun_type, FunType::Macro), + Some(method_name), + &function_name.value(), + type_args, + arg_names, + arg_types, + ret_type, + /* inside_use */ false, + ) + } else { + // this shouldn't really happen as we should be able to get + // `DefInfo` for a function but if for some reason we cannot, + // let's generate simpler autotompletion value + eprintln!("incomplete dot item"); + CompletionItem { + label: format!("{method_name}()"), + kind: Some(CompletionItemKind::METHOD), + insert_text: Some(method_name.to_string()), + insert_text_format: Some(InsertTextFormat::PLAIN_TEXT), + ..Default::default() + } + }; + completions.push(call_completion); + } + for (n, t) in &info.fields { + let label_details = Some(CompletionItemLabelDetails { + detail: None, + description: Some(type_to_ide_string(t, /* verbose */ false)), + }); + let init_completion = CompletionItem { + label: n.to_string(), + label_details, + kind: Some(CompletionItemKind::FIELD), + insert_text: Some(n.to_string()), + insert_text_format: Some(InsertTextFormat::PLAIN_TEXT), + ..Default::default() + }; + completions.push(init_completion); + } + + (completions, completion_finalized) +} + +/// Get the `DefInfo` for a function definition. +fn fun_def_info(symbols: &Symbols, mod_ident: ModuleIdent_, name: Symbol) -> Option<&DefInfo> { + let Some(mod_defs) = mod_defs(symbols, &mod_ident) else { + return None; + }; + + let Some(fdef) = mod_defs.functions.get(&name) else { + return None; + }; + symbols.def_info(&fdef.name_loc) +} diff --git a/external-crates/move/crates/move-analyzer/src/completions/mod.rs b/external-crates/move/crates/move-analyzer/src/completions/mod.rs new file mode 100644 index 0000000000000..2bdfe139cd0a3 --- /dev/null +++ b/external-crates/move/crates/move-analyzer/src/completions/mod.rs @@ -0,0 +1,428 @@ +// Copyright (c) The Diem Core Contributors +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + completions::{ + dot::dot_completions, + name_chain::{name_chain_completions, use_decl_completions}, + snippets::{init_completion, object_completion}, + utils::{completion_item, PRIMITIVE_TYPE_COMPLETIONS}, + }, + context::Context, + symbols::{self, CursorContext, PrecompiledPkgDeps, SymbolicatorRunner, Symbols}, +}; +use lsp_server::Request; +use lsp_types::{CompletionItem, CompletionItemKind, CompletionParams, Position}; +use move_command_line_common::files::FileHash; +use move_compiler::{ + editions::Edition, + linters::LintLevel, + parser::{ + keywords::{BUILTINS, CONTEXTUAL_KEYWORDS, KEYWORDS, PRIMITIVE_TYPES}, + lexer::{Lexer, Tok}, + }, +}; +use move_symbol_pool::Symbol; + +use once_cell::sync::Lazy; + +use std::{ + collections::{BTreeMap, HashSet}, + path::{Path, PathBuf}, + sync::{Arc, Mutex}, +}; +use vfs::VfsPath; + +mod dot; +mod name_chain; +mod snippets; +mod utils; + +/// List of completion items corresponding to each one of Move's keywords. +/// +/// Currently, this does not filter keywords out based on whether they are valid at the completion +/// request's cursor position, but in the future it ought to. For example, this function returns +/// all specification language keywords, but in the future it should be modified to only do so +/// within a spec block. +static KEYWORD_COMPLETIONS: Lazy> = Lazy::new(|| { + let mut keywords = KEYWORDS + .iter() + .chain(CONTEXTUAL_KEYWORDS.iter()) + .chain(PRIMITIVE_TYPES.iter()) + .map(|label| { + let kind = if label == &"copy" || label == &"move" { + CompletionItemKind::OPERATOR + } else { + CompletionItemKind::KEYWORD + }; + completion_item(label, kind) + }) + .collect::>(); + keywords.extend(PRIMITIVE_TYPE_COMPLETIONS.clone()); + keywords +}); + +/// List of completion items corresponding to each one of Move's builtin functions. +static BUILTIN_COMPLETIONS: Lazy> = Lazy::new(|| { + BUILTINS + .iter() + .map(|label| completion_item(label, CompletionItemKind::FUNCTION)) + .collect() +}); + +/// Sends the given connection a response to a completion request. +/// +/// The completions returned depend upon where the user's cursor is positioned. +pub fn on_completion_request( + context: &Context, + request: &Request, + ide_files_root: VfsPath, + pkg_dependencies: Arc>>, +) { + eprintln!("handling completion request"); + let parameters = serde_json::from_value::(request.params.clone()) + .expect("could not deserialize completion request"); + + let path = parameters + .text_document_position + .text_document + .uri + .to_file_path() + .unwrap(); + + let mut pos = parameters.text_document_position.position; + if pos.character != 0 { + // adjust column to be at the character that has just been inserted rather than right after + // it (unless we are at the very first column) + pos = Position::new(pos.line, pos.character - 1); + } + let completions = + completions(context, ide_files_root, pkg_dependencies, &path, pos).unwrap_or_default(); + let completions_len = completions.len(); + + let result = + serde_json::to_value(completions).expect("could not serialize completion response"); + eprintln!("about to send completion response with {completions_len} items"); + let response = lsp_server::Response::new_ok(request.id.clone(), result); + if let Err(err) = context + .connection + .sender + .send(lsp_server::Message::Response(response)) + { + eprintln!("could not send completion response: {:?}", err); + } +} + +/// Computes a list of auto-completions for a given position in a file, +/// given the current context. +fn completions( + context: &Context, + ide_files_root: VfsPath, + pkg_dependencies: Arc>>, + path: &Path, + pos: Position, +) -> Option> { + let Some(pkg_path) = SymbolicatorRunner::root_dir(path) else { + eprintln!("failed completion for {:?} (package root not found)", path); + return None; + }; + let symbol_map = context.symbols.lock().unwrap(); + let current_symbols = symbol_map.get(&pkg_path)?; + Some(compute_completions( + current_symbols, + ide_files_root, + pkg_dependencies, + path, + pos, + )) +} + +/// Computes a list of auto-completions for a given position in a file, +/// based on the current symbols. +pub fn compute_completions( + current_symbols: &Symbols, + ide_files_root: VfsPath, + pkg_dependencies: Arc>>, + path: &Path, + pos: Position, +) -> Vec { + compute_completions_new_symbols(ide_files_root, pkg_dependencies, path, pos) + .unwrap_or_else(|| compute_completions_with_symbols(current_symbols, path, pos)) +} + +/// Computes a list of auto-completions for a given position in a file, +/// after attempting to re-compute the symbols to get the most up-to-date +/// view of the code (returns `None` if the symbols could not be re-computed). +fn compute_completions_new_symbols( + ide_files_root: VfsPath, + pkg_dependencies: Arc>>, + path: &Path, + cursor_position: Position, +) -> Option> { + let Some(pkg_path) = SymbolicatorRunner::root_dir(path) else { + eprintln!("failed completion for {:?} (package root not found)", path); + return None; + }; + let cursor_path = path.to_path_buf(); + let cursor_info = Some((&cursor_path, cursor_position)); + let (symbols, _diags) = symbols::get_symbols( + pkg_dependencies, + ide_files_root, + &pkg_path, + LintLevel::None, + cursor_info, + ) + .ok()?; + let symbols = symbols?; + Some(compute_completions_with_symbols( + &symbols, + path, + cursor_position, + )) +} + +/// Computes a list of auto-completions for a given position in a file +/// using the symbols provided as argument. +fn compute_completions_with_symbols( + symbols: &Symbols, + path: &Path, + pos: Position, +) -> Vec { + let mut completions = vec![]; + + let Some(fhash) = symbols.file_hash(path) else { + return completions; + }; + let Some(file_id) = symbols.files.file_mapping().get(&fhash) else { + return completions; + }; + let Ok(file) = symbols.files.files().get(*file_id) else { + return completions; + }; + + let file_source = file.source().clone(); + if !file_source.is_empty() { + let completion_finalized; + match &symbols.cursor_context { + Some(cursor_context) => { + eprintln!("cursor completion"); + let (cursor_completions, cursor_finalized) = + cursor_completion_items(symbols, path, &file_source, pos, cursor_context); + completion_finalized = cursor_finalized; + completions.extend(cursor_completions); + } + None => { + eprintln!("non-cursor completion"); + let (no_cursor_completions, no_cursor_finalized) = + no_cursor_completion_items(symbols, path, &file_source, pos); + completion_finalized = no_cursor_finalized; + completions.extend(no_cursor_completions); + } + } + if !completion_finalized { + eprintln!("including identifiers"); + let identifiers = identifiers(&file_source, symbols, path); + completions.extend(identifiers); + } + } else { + // no file content + completions.extend(KEYWORD_COMPLETIONS.clone()); + completions.extend(BUILTIN_COMPLETIONS.clone()); + } + completions +} + +/// Return completion items in case cursor is available plus a flag indicating +/// if we should continue searching for more completions. +fn cursor_completion_items( + symbols: &Symbols, + path: &Path, + file_source: &str, + pos: Position, + cursor: &CursorContext, +) -> (Vec, bool) { + let cursor_leader = get_cursor_token(file_source, &pos); + match cursor_leader { + // TODO: consider using `cursor.position` for this instead + Some(Tok::Period) => dot_completions(symbols, path, &pos), + Some(Tok::ColonColon) => { + let mut completions = vec![]; + let mut completion_finalized = false; + let (name_chain_completions, name_chain_finalized) = + name_chain_completions(symbols, cursor, /* colon_colon_triggered */ true); + completions.extend(name_chain_completions); + completion_finalized |= name_chain_finalized; + if !completion_finalized { + let (use_decl_completions, use_decl_finalized) = + use_decl_completions(symbols, cursor); + completions.extend(use_decl_completions); + completion_finalized |= use_decl_finalized; + } + (completions, completion_finalized) + } + // Carve out to suggest UID for struct with key ability + Some(Tok::LBrace) => { + let mut completions = vec![]; + let mut completion_finalized = false; + let (custom_completions, custom_finalized) = lbrace_cursor_completions(symbols, cursor); + completions.extend(custom_completions); + completion_finalized |= custom_finalized; + if !completion_finalized { + let (use_decl_completions, use_decl_finalized) = + use_decl_completions(symbols, cursor); + completions.extend(use_decl_completions); + completion_finalized |= use_decl_finalized; + } + (completions, completion_finalized) + } + // TODO: should we handle auto-completion on `:`? If we model our support after + // rust-analyzer then it does not do this - it starts auto-completing types after the first + // character beyond `:` is typed + _ => { + eprintln!("no relevant cursor leader"); + let mut completions = vec![]; + let mut completion_finalized = false; + let (name_chain_completions, name_chain_finalized) = + name_chain_completions(symbols, cursor, /* colon_colon_triggered */ false); + completions.extend(name_chain_completions); + completion_finalized |= name_chain_finalized; + if !completion_finalized { + if matches!(cursor_leader, Some(Tok::Colon)) { + // much like rust-analyzer we do not auto-complete in the middle of `::` + completion_finalized = true; + } else { + let (use_decl_completions, use_decl_finalized) = + use_decl_completions(symbols, cursor); + completions.extend(use_decl_completions); + completion_finalized |= use_decl_finalized; + } + } + if !completion_finalized { + eprintln!("checking default items"); + let (default_completions, default_finalized) = + no_cursor_completion_items(symbols, path, file_source, pos); + completions.extend(default_completions); + completion_finalized |= default_finalized; + } + (completions, completion_finalized) + } + } +} + +/// Returns the token corresponding to the "trigger character" if it is one of `.`, `:`, '{', or +/// `::`. Otherwise, returns `None` (position points at the potential trigger character itself). +fn get_cursor_token(buffer: &str, position: &Position) -> Option { + let line = match buffer.lines().nth(position.line as usize) { + Some(line) => line, + None => return None, // Our buffer does not contain the line, and so must be out of date. + }; + match line.chars().nth(position.character as usize) { + Some('.') => Some(Tok::Period), + Some(':') => { + if position.character > 0 + && line.chars().nth(position.character as usize - 1) == Some(':') + { + Some(Tok::ColonColon) + } else { + Some(Tok::Colon) + } + } + Some('{') => Some(Tok::LBrace), + _ => None, + } +} + +/// Handle auto-completion requests with lbrace (`{`) trigger character +/// when cursor is available. +fn lbrace_cursor_completions( + symbols: &Symbols, + cursor: &CursorContext, +) -> (Vec, bool) { + let completions = vec![]; + let (completion_item_opt, completion_finalized) = object_completion(symbols, cursor); + if let Some(completion_item) = completion_item_opt { + return (vec![completion_item], completion_finalized); + } + (completions, completion_finalized) +} + +/// Return completion items no cursor is available plus a flag indicating +/// if we should continue searching for more completions. +fn no_cursor_completion_items( + symbols: &Symbols, + path: &Path, + file_source: &str, + pos: Position, +) -> (Vec, bool) { + // If the user's cursor is positioned anywhere other than following a `.`, `:`, or `::`, + // offer them context-specific autocompletion items and, if needed, + // Move's keywords, and builtins. + let (mut completions, mut completion_finalized) = dot_completions(symbols, path, &pos); + if !completion_finalized { + let (init_completions, init_finalized) = init_completion(symbols, path, file_source, &pos); + completions.extend(init_completions); + completion_finalized |= init_finalized; + } + + if !completion_finalized { + completions.extend(KEYWORD_COMPLETIONS.clone()); + completions.extend(BUILTIN_COMPLETIONS.clone()); + } + (completions, true) +} + +/// Lexes the Move source file at the given path and returns a list of completion items +/// corresponding to the non-keyword identifiers therein. +/// +/// Currently, this does not perform semantic analysis to determine whether the identifiers +/// returned are valid at the request's cursor position. However, this list of identifiers is akin +/// to what editors like Visual Studio Code would provide as completion items if this language +/// server did not initialize with a response indicating it's capable of providing completions. In +/// the future, the server should be modified to return semantically valid completion items, not +/// simple textual suggestions. +fn identifiers(buffer: &str, symbols: &Symbols, path: &Path) -> Vec { + // TODO thread through package configs + let mut lexer = Lexer::new(buffer, FileHash::new(buffer), Edition::LEGACY); + if lexer.advance().is_err() { + return vec![]; + } + let mut ids = HashSet::new(); + while lexer.peek() != Tok::EOF { + // Some tokens, such as "phantom", are contextual keywords that are only reserved in + // certain contexts. Since for now this language server doesn't analyze semantic context, + // tokens such as "phantom" are always present in keyword suggestions. To avoid displaying + // these keywords to the user twice in the case that the token "phantom" is present in the + // source program (once as a keyword, and once as an identifier), we filter out any + // identifier token that has the same text as a keyword. + if lexer.peek() == Tok::Identifier && !KEYWORDS.contains(&lexer.content()) { + // The completion item kind "text" indicates the item is not based on any semantic + // context of the request cursor's position. + ids.insert(lexer.content()); + } + if lexer.advance().is_err() { + break; + } + } + + let mods_opt = symbols.file_mods.get(path); + + // The completion item kind "text" indicates that the item is based on simple textual matching, + // not any deeper semantic analysis. + ids.iter() + .map(|label| { + if let Some(mods) = mods_opt { + if mods + .iter() + .any(|m| m.functions().contains_key(&Symbol::from(*label))) + { + completion_item(label, CompletionItemKind::FUNCTION) + } else { + completion_item(label, CompletionItemKind::TEXT) + } + } else { + completion_item(label, CompletionItemKind::TEXT) + } + }) + .collect() +} diff --git a/external-crates/move/crates/move-analyzer/src/completion.rs b/external-crates/move/crates/move-analyzer/src/completions/name_chain.rs similarity index 50% rename from external-crates/move/crates/move-analyzer/src/completion.rs rename to external-crates/move/crates/move-analyzer/src/completions/name_chain.rs index b53a58f60779c..5fd8896b36121 100644 --- a/external-crates/move/crates/move-analyzer/src/completion.rs +++ b/external-crates/move/crates/move-analyzer/src/completions/name_chain.rs @@ -1,55 +1,32 @@ -// Copyright (c) The Diem Core Contributors // Copyright (c) The Move Contributors // SPDX-License-Identifier: Apache-2.0 +// Auto-completions for name chains, such as `mod::struct::field` or `mod::function`, +// both in the code (e.g., types) and in `use` statements. + use crate::{ - context::Context, + completions::utils::{ + call_completion_item, completion_item, mod_defs, PRIMITIVE_TYPE_COMPLETIONS, + }, symbols::{ - self, expansion_mod_ident_to_map_key, mod_ident_to_ide_string, ret_type_to_ide_str, - type_args_to_ide_string, type_list_to_ide_string, type_to_ide_string, ChainCompletionKind, - ChainInfo, CursorContext, CursorDefinition, DefInfo, FunType, MemberDef, MemberDefInfo, - ModuleDefs, PrecompiledPkgDeps, SymbolicatorRunner, Symbols, VariantInfo, + expansion_mod_ident_to_map_key, ChainCompletionKind, ChainInfo, CursorContext, DefInfo, + FunType, MemberDef, MemberDefInfo, Symbols, VariantInfo, }, - utils, }; use itertools::Itertools; -use lsp_server::Request; -use lsp_types::{ - CompletionItem, CompletionItemKind, CompletionItemLabelDetails, CompletionParams, - Documentation, InsertTextFormat, Position, -}; -use move_command_line_common::files::FileHash; +use lsp_types::{CompletionItem, CompletionItemKind, InsertTextFormat}; use move_compiler::{ - editions::Edition, expansion::ast::{Address, ModuleIdent, ModuleIdent_, Visibility}, - linters::LintLevel, - naming::ast::{Type, Type_}, - parser::{ - ast::{self as P, Ability_, LeadingNameAccess, LeadingNameAccess_}, - keywords::{BUILTINS, CONTEXTUAL_KEYWORDS, KEYWORDS, PRIMITIVE_TYPES}, - lexer::{Lexer, Tok}, - }, - shared::{ - ide::{AliasAutocompleteInfo, AutocompleteMethod}, - Identifier, Name, NumericalAddress, - }, + parser::ast as P, + shared::{ide::AliasAutocompleteInfo, Identifier, Name, NumericalAddress}, }; use move_ir_types::location::{sp, Loc}; use move_symbol_pool::Symbol; - -use once_cell::sync::Lazy; - -use std::{ - collections::{BTreeMap, BTreeSet, HashSet}, - path::{Path, PathBuf}, - sync::{Arc, Mutex}, - vec, -}; -use vfs::VfsPath; +use std::collections::BTreeSet; /// Describes kind of the name access chain component. enum ChainComponentKind { - Package(LeadingNameAccess), + Package(P::LeadingNameAccess), Module(ModuleIdent), Member(ModuleIdent, Symbol), } @@ -66,372 +43,242 @@ impl ChainComponentInfo { } } -/// Constructs an `lsp_types::CompletionItem` with the given `label` and `kind`. -fn completion_item(label: &str, kind: CompletionItemKind) -> CompletionItem { - CompletionItem { - label: label.to_owned(), - kind: Some(kind), - ..Default::default() - } -} +/// Handle name chain auto-completion at a given position. The gist of this approach is to first +/// identify what the first component of the access chain represents (as it may be a package, module +/// or a member) and if the chain has other components, recursively process them in turn to either +/// - finish auto-completion if cursor is on a given component's identifier +/// - identify what the subsequent component represents and keep going +pub fn name_chain_completions( + symbols: &Symbols, + cursor: &CursorContext, + colon_colon_triggered: bool, +) -> (Vec, bool) { + eprintln!("looking for name access chains"); + let mut completions = vec![]; + let mut completion_finalized = false; + let Some(ChainInfo { + chain, + kind: chain_kind, + inside_use, + }) = cursor.find_access_chain() + else { + eprintln!("no access chain"); + return (completions, completion_finalized); + }; -/// List of completion items corresponding to each one of Move's keywords. -/// -/// Currently, this does not filter keywords out based on whether they are valid at the completion -/// request's cursor position, but in the future it ought to. For example, this function returns -/// all specification language keywords, but in the future it should be modified to only do so -/// within a spec block. -static KEYWORD_COMPLETIONS: Lazy> = Lazy::new(|| { - let mut keywords = KEYWORDS - .iter() - .chain(CONTEXTUAL_KEYWORDS.iter()) - .chain(PRIMITIVE_TYPES.iter()) - .map(|label| { - let kind = if label == &"copy" || label == &"move" { - CompletionItemKind::OPERATOR - } else { - CompletionItemKind::KEYWORD - }; - completion_item(label, kind) - }) - .collect::>(); - keywords.extend(PRIMITIVE_TYPE_COMPLETIONS.clone()); - keywords -}); - -/// List of completion items of Move's primitive types. -static PRIMITIVE_TYPE_COMPLETIONS: Lazy> = Lazy::new(|| { - let mut primitive_types = PRIMITIVE_TYPES - .iter() - .map(|label| completion_item(label, CompletionItemKind::KEYWORD)) - .collect::>(); - primitive_types.push(completion_item("address", CompletionItemKind::KEYWORD)); - primitive_types -}); - -/// List of completion items corresponding to each one of Move's builtin functions. -static BUILTIN_COMPLETIONS: Lazy> = Lazy::new(|| { - BUILTINS - .iter() - .map(|label| completion_item(label, CompletionItemKind::FUNCTION)) - .collect() -}); - -/// Lexes the Move source file at the given path and returns a list of completion items -/// corresponding to the non-keyword identifiers therein. -/// -/// Currently, this does not perform semantic analysis to determine whether the identifiers -/// returned are valid at the request's cursor position. However, this list of identifiers is akin -/// to what editors like Visual Studio Code would provide as completion items if this language -/// server did not initialize with a response indicating it's capable of providing completions. In -/// the future, the server should be modified to return semantically valid completion items, not -/// simple textual suggestions. -fn identifiers(buffer: &str, symbols: &Symbols, path: &Path) -> Vec { - // TODO thread through package configs - let mut lexer = Lexer::new(buffer, FileHash::new(buffer), Edition::LEGACY); - if lexer.advance().is_err() { - return vec![]; - } - let mut ids = HashSet::new(); - while lexer.peek() != Tok::EOF { - // Some tokens, such as "phantom", are contextual keywords that are only reserved in - // certain contexts. Since for now this language server doesn't analyze semantic context, - // tokens such as "phantom" are always present in keyword suggestions. To avoid displaying - // these keywords to the user twice in the case that the token "phantom" is present in the - // source program (once as a keyword, and once as an identifier), we filter out any - // identifier token that has the same text as a keyword. - if lexer.peek() == Tok::Identifier && !KEYWORDS.contains(&lexer.content()) { - // The completion item kind "text" indicates the item is not based on any semantic - // context of the request cursor's position. - ids.insert(lexer.content()); - } - if lexer.advance().is_err() { - break; - } - } + let (leading_name, path_entries) = match &chain.value { + P::NameAccessChain_::Single(entry) => ( + sp(entry.name.loc, P::LeadingNameAccess_::Name(entry.name)), + vec![], + ), + P::NameAccessChain_::Path(name_path) => ( + name_path.root.name, + name_path.entries.iter().map(|e| e.name).collect::>(), + ), + }; + + // there may be access chains for which there is not auto-completion info generated by the + // compiler but which still have to be handled (e.g., chains starting with numeric address) + let info = symbols + .compiler_info + .path_autocomplete_info + .get(&leading_name.loc) + .cloned() + .unwrap_or_else(AliasAutocompleteInfo::new); - let mods_opt = symbols.file_mods.get(path); + eprintln!("found access chain for auto-completion (adddreses: {}, modules: {}, members: {}, tparams: {}", + info.addresses.len(), info.modules.len(), info.members.len(), info.type_params.len()); - // The completion item kind "text" indicates that the item is based on simple textual matching, - // not any deeper semantic analysis. - ids.iter() - .map(|label| { - if let Some(mods) = mods_opt { - if mods - .iter() - .any(|m| m.functions().contains_key(&Symbol::from(*label))) - { - completion_item(label, CompletionItemKind::FUNCTION) - } else { - completion_item(label, CompletionItemKind::TEXT) - } - } else { - completion_item(label, CompletionItemKind::TEXT) - } - }) - .collect() -} + // if we are auto-completing for an access chain, there is no need to include default completions + completion_finalized = true; -/// Returns the token corresponding to the "trigger character" if it is one of `.`, `:`, '{', or -/// `::`. Otherwise, returns `None` (position points at the potential trigger character itself). -fn get_cursor_token(buffer: &str, position: &Position) -> Option { - let line = match buffer.lines().nth(position.line as usize) { - Some(line) => line, - None => return None, // Our buffer does not contain the line, and so must be out of date. - }; - match line.chars().nth(position.character as usize) { - Some('.') => Some(Tok::Period), - Some(':') => { - if position.character > 0 - && line.chars().nth(position.character as usize - 1) == Some(':') - { - Some(Tok::ColonColon) - } else { - Some(Tok::Colon) + if leading_name.loc.contains(&cursor.loc) { + // at first position of the chain suggest all packages that are available regardless of what + // the leading name represents, as a package always fits at that position, for example: + // OxCAFE::... + // some_name::... + // ::some_name + // + completions.extend( + all_packages(symbols, &info) + .iter() + .map(|n| completion_item(n.as_str(), CompletionItemKind::UNIT)), + ); + + // only if leading name is actually a name, modules or module members are a correct + // auto-completion in the first position + if let P::LeadingNameAccess_::Name(_) = &leading_name.value { + completions.extend( + info.modules + .keys() + .map(|n| completion_item(n.as_str(), CompletionItemKind::MODULE)), + ); + completions.extend(all_single_name_member_completions( + symbols, + cursor, + &info.members, + chain_kind, + )); + if matches!(chain_kind, ChainCompletionKind::Type) { + completions.extend(PRIMITIVE_TYPE_COMPLETIONS.clone()); + completions.extend( + info.type_params + .iter() + .map(|t| completion_item(t.as_str(), CompletionItemKind::TYPE_PARAMETER)), + ); } } - Some('{') => Some(Tok::LBrace), - _ => None, + } else if let Some(next_kind) = first_name_chain_component_kind(symbols, &info, leading_name) { + completions_for_name_chain_entry( + symbols, + cursor, + &info, + ChainComponentInfo::new(leading_name.loc, next_kind), + chain_kind, + &path_entries, + /* path_index */ 0, + colon_colon_triggered, + inside_use, + &mut completions, + ); } -} -/// Checks if the cursor is at the opening brace of a struct definition and returns -/// auto-completion of this struct into an object if the struct has the `key` ability. -fn object_completion(symbols: &Symbols, cursor: &CursorContext) -> (Option, bool) { - let mut only_custom_items = false; - // look for a struct definition on the line that contains `{`, check its abilities, - // and do auto-completion if `key` ability is present - let Some(CursorDefinition::Struct(sname)) = &cursor.defn_name else { - return (None, only_custom_items); - }; - only_custom_items = true; - let Some(mod_ident) = cursor.module else { - return (None, only_custom_items); - }; - let Some(mod_defs) = mod_defs(symbols, &mod_ident.value) else { - return (None, only_custom_items); - }; - let Some(struct_def) = mod_defs.structs.get(&sname.value()) else { - return (None, only_custom_items); - }; - - let Some(DefInfo::Struct(_, _, _, _, abilities, ..)) = - symbols.def_info.get(&struct_def.name_loc) - else { - return (None, only_custom_items); - }; + eprintln!("found {} access chain completions", completions.len()); - if !abilities.has_ability_(Ability_::Key) { - return (None, only_custom_items); - } - let obj_snippet = "\n\tid: UID,\n\t$1\n".to_string(); - let init_completion = CompletionItem { - label: "id: UID".to_string(), - kind: Some(CompletionItemKind::SNIPPET), - documentation: Some(Documentation::String("Object snippet".to_string())), - insert_text: Some(obj_snippet), - insert_text_format: Some(InsertTextFormat::SNIPPET), - ..Default::default() - }; - (Some(init_completion), only_custom_items) + (completions, completion_finalized) } -/// Handle context-specific auto-completion requests with lbrace (`{`) trigger character. -fn context_specific_lbrace( +/// Handles auto-completions for "regular" `use` declarations (name access chains in `use fun` +/// declarations are handled as part of name chain completions). +pub fn use_decl_completions( symbols: &Symbols, cursor: &CursorContext, ) -> (Vec, bool) { - let completions = vec![]; - let (completion_item_opt, only_custom_items) = object_completion(symbols, cursor); - if let Some(completion_item) = completion_item_opt { - return (vec![completion_item], only_custom_items); - } - (completions, only_custom_items) -} - -/// Get definitions for a given module. -fn mod_defs<'a>(symbols: &'a Symbols, mod_ident: &ModuleIdent_) -> Option<&'a ModuleDefs> { - symbols - .file_mods - .values() - .flatten() - .find(|mdef| mdef.ident == *mod_ident) -} - -fn fun_def_info(symbols: &Symbols, mod_ident: ModuleIdent_, name: Symbol) -> Option<&DefInfo> { - let Some(mod_defs) = mod_defs(symbols, &mod_ident) else { - return None; + eprintln!("looking for use declarations"); + let mut completions = vec![]; + let mut completion_finalized = false; + let Some(use_) = cursor.find_use_decl() else { + eprintln!("no use declaration"); + return (completions, completion_finalized); }; + eprintln!("use declaration {:?}", use_); - let Some(fdef) = mod_defs.functions.get(&name) else { - return None; - }; - symbols.def_info(&fdef.name_loc) -} + // if we are auto-completing for a use decl, there is no need to include default completions + completion_finalized = true; -fn lambda_snippet(sp!(_, ty): &Type, snippet_idx: &mut i32) -> Option { - if let Type_::Fun(vec, _) = ty { - let arg_snippets = vec - .iter() - .map(|_| { - *snippet_idx += 1; - format!("${{{snippet_idx}}}") - }) - .collect::>() - .join(", "); - *snippet_idx += 1; - return Some(format!("|{arg_snippets}| ${{{snippet_idx}}}")); - } - None -} + // there is no auto-completion info generated by the compiler for this but helper methods used + // here are shared with name chain completion where it may exist, so we create an "empty" one + // here + let info = AliasAutocompleteInfo::new(); -fn call_completion_item( - mod_ident: &ModuleIdent_, - is_macro: bool, - method_name_opt: Option<&Symbol>, - function_name: &Symbol, - type_args: &[Type], - arg_names: &[Name], - arg_types: &[Type], - ret_type: &Type, - inside_use: bool, -) -> CompletionItem { - let sig_string = format!( - "fun {}({}){}", - type_args_to_ide_string(type_args, /* verbose */ false), - type_list_to_ide_string(arg_types, /* verbose */ false), - ret_type_to_ide_str(ret_type, /* verbose */ false) - ); - // if it's a method call we omit the first argument which is guaranteed to be there as this is a - // method and needs a receiver - let omitted_arg_count = if method_name_opt.is_some() { 1 } else { 0 }; - let mut snippet_idx = 0; - let arg_snippet = arg_names - .iter() - .zip(arg_types) - .skip(omitted_arg_count) - .map(|(name, ty)| { - lambda_snippet(ty, &mut snippet_idx).unwrap_or_else(|| { - let mut arg_name = name.to_string(); - if arg_name.starts_with('$') { - arg_name = arg_name[1..].to_string(); + match use_ { + P::Use::ModuleUse(sp!(_, mod_ident), mod_use) => { + if mod_ident.address.loc.contains(&cursor.loc) { + // cursor on package (e.g., on `some_pkg` in `some_pkg::some_mod`) + completions.extend( + all_packages(symbols, &info) + .iter() + .map(|n| completion_item(n.as_str(), CompletionItemKind::UNIT)), + ); + } else if cursor.loc.start() > mod_ident.address.loc.end() + && cursor.loc.end() <= mod_ident.module.loc().end() + { + // cursor is either at the `::` succeeding package/address or at the identifier + // following that particular `::` + for ident in pkg_mod_identifiers(symbols, &info, &mod_ident.address) { + completions.push(completion_item( + ident.value.module.value().as_str(), + CompletionItemKind::MODULE, + )); + } + } else { + completions.extend(module_use_completions( + symbols, + cursor, + &info, + &mod_use, + &mod_ident.address, + &mod_ident.module, + )); + } + } + P::Use::NestedModuleUses(leading_name, uses) => { + if leading_name.loc.contains(&cursor.loc) { + // cursor on package + completions.extend( + all_packages(symbols, &info) + .iter() + .map(|n| completion_item(n.as_str(), CompletionItemKind::UNIT)), + ); + } else { + if let Some((first_name, _)) = uses.first() { + if cursor.loc.start() > leading_name.loc.end() + && cursor.loc.end() <= first_name.loc().start() + { + // cursor is after `::` succeeding address/package but before the first + // module + for ident in pkg_mod_identifiers(symbols, &info, &leading_name) { + completions.push(completion_item( + ident.value.module.value().as_str(), + CompletionItemKind::MODULE, + )); + } + // no point in falling through to the uses loop below + return (completions, completion_finalized); + } } - snippet_idx += 1; - format!("${{{}:{}}}", snippet_idx, arg_name) - }) - }) - .collect::>() - .join(", "); - let macro_suffix = if is_macro { "!" } else { "" }; - let label_details = Some(CompletionItemLabelDetails { - detail: Some(format!( - " ({}::{})", - mod_ident_to_ide_string(mod_ident), - function_name - )), - description: Some(sig_string), - }); - - let method_name = method_name_opt.unwrap_or(function_name); - let (insert_text, insert_text_format) = if inside_use { - ( - Some(format!("{method_name}")), - Some(InsertTextFormat::PLAIN_TEXT), - ) - } else { - ( - Some(format!("{method_name}{macro_suffix}({arg_snippet})")), - Some(InsertTextFormat::SNIPPET), - ) - }; - - CompletionItem { - label: format!("{method_name}{macro_suffix}()"), - label_details, - kind: Some(CompletionItemKind::METHOD), - insert_text, - insert_text_format, - ..Default::default() - } -} -/// Handle dot auto-completion at a given position. -fn dot_completions( - symbols: &Symbols, - use_fpath: &Path, - position: &Position, -) -> Vec { - let mut completions = vec![]; - let Some(fhash) = symbols.file_hash(use_fpath) else { - eprintln!("no dot completions due to missing file"); - return completions; - }; - let Some(loc) = utils::lsp_position_to_loc(&symbols.files, fhash, position) else { - eprintln!("no dot completions due to missing loc"); - return completions; - }; - let Some(info) = symbols.compiler_info.get_autocomplete_info(fhash, &loc) else { - eprintln!("no dot completions due to no compiler autocomplete info"); - return completions; - }; - for AutocompleteMethod { - method_name, - target_function: (mod_ident, function_name), - } in &info.methods - { - let call_completion = if let Some(DefInfo::Function( - .., - fun_type, - _, - type_args, - arg_names, - arg_types, - ret_type, - _, - )) = fun_def_info(symbols, mod_ident.value, function_name.value()) - { - call_completion_item( - &mod_ident.value, - matches!(fun_type, FunType::Macro), - Some(method_name), - &function_name.value(), - type_args, - arg_names, - arg_types, - ret_type, - /* inside_use */ false, - ) - } else { - // this shouldn't really happen as we should be able to get - // `DefInfo` for a function but if for some reason we cannot, - // let's generate simpler autotompletion value - eprintln!("incomplete dot item"); - CompletionItem { - label: format!("{method_name}()"), - kind: Some(CompletionItemKind::METHOD), - insert_text: Some(method_name.to_string()), - insert_text_format: Some(InsertTextFormat::PLAIN_TEXT), - ..Default::default() + for (mod_name, mod_use) in &uses { + if mod_name.loc().contains(&cursor.loc) { + for ident in pkg_mod_identifiers(symbols, &info, &leading_name) { + completions.push(completion_item( + ident.value.module.value().as_str(), + CompletionItemKind::MODULE, + )); + } + // no point checking other locations + break; + } + completions.extend(module_use_completions( + symbols, + cursor, + &info, + mod_use, + &leading_name, + mod_name, + )); + } } - }; - completions.push(call_completion); - } - for (n, t) in &info.fields { - let label_details = Some(CompletionItemLabelDetails { - detail: None, - description: Some(type_to_ide_string(t, /* verbose */ false)), - }); - let init_completion = CompletionItem { - label: n.to_string(), - label_details, - kind: Some(CompletionItemKind::FIELD), - insert_text: Some(n.to_string()), - insert_text_format: Some(InsertTextFormat::PLAIN_TEXT), - ..Default::default() - }; - completions.push(init_completion); + } + P::Use::Fun { .. } => (), // already handled as part of name chain completion + P::Use::Partial { + package, + colon_colon, + opening_brace: _, + } => { + if package.loc.contains(&cursor.loc) { + // cursor on package name/address + completions.extend( + all_packages(symbols, &info) + .iter() + .map(|n| completion_item(n.as_str(), CompletionItemKind::UNIT)), + ); + } + if let Some(colon_colon_loc) = colon_colon { + if cursor.loc.start() >= colon_colon_loc.start() { + // cursor is on or past `::` + for ident in pkg_mod_identifiers(symbols, &info, &package) { + completions.push(completion_item( + ident.value.module.value().as_str(), + CompletionItemKind::MODULE, + )); + } + } + } + } } - completions + + (completions, completion_finalized) } /// Handles auto-completion for structs and enums variants, including fields contained @@ -735,17 +582,18 @@ fn all_single_name_member_completions( /// Checks if a given module identifier represents a module in a package identifier by /// `leading_name`. -fn is_pkg_mod_ident(mod_ident: &ModuleIdent_, leading_name: &LeadingNameAccess) -> bool { +fn is_pkg_mod_ident(mod_ident: &ModuleIdent_, leading_name: &P::LeadingNameAccess) -> bool { match mod_ident.address { Address::NamedUnassigned(name) => matches!(leading_name.value, - LeadingNameAccess_::Name(n) | LeadingNameAccess_::GlobalAddress(n) if name == n), + P::LeadingNameAccess_::Name(n) | P::LeadingNameAccess_::GlobalAddress(n) if name == n), Address::Numerical { name, value, name_conflict: _, } => match leading_name.value { - LeadingNameAccess_::AnonymousAddress(addr) if addr == value.value => true, - LeadingNameAccess_::Name(addr_name) | LeadingNameAccess_::GlobalAddress(addr_name) + P::LeadingNameAccess_::AnonymousAddress(addr) if addr == value.value => true, + P::LeadingNameAccess_::Name(addr_name) + | P::LeadingNameAccess_::GlobalAddress(addr_name) if Some(addr_name) == name => { true @@ -759,7 +607,7 @@ fn is_pkg_mod_ident(mod_ident: &ModuleIdent_, leading_name: &LeadingNameAccess) fn pkg_mod_identifiers( symbols: &Symbols, info: &AliasAutocompleteInfo, - leading_name: &LeadingNameAccess, + leading_name: &P::LeadingNameAccess, ) -> BTreeSet { info.modules .values() @@ -1028,152 +876,47 @@ fn all_packages(symbols: &Symbols, info: &AliasAutocompleteInfo) -> BTreeSet Option { match leading_name.value { - LeadingNameAccess_::Name(n) => { + P::LeadingNameAccess_::Name(n) => { if is_package_name(symbols, info, n) { Some(ChainComponentKind::Package(leading_name)) } else if let Some(mod_ident) = info.modules.get(&n.value) { Some(ChainComponentKind::Module(*mod_ident)) } else if let Some((mod_ident, member_name)) = - info.members - .iter() - .find_map(|(alias_name, mod_ident, member_name)| { - if alias_name == &n.value { - Some((*mod_ident, member_name)) - } else { - None - } - }) - { - Some(ChainComponentKind::Member(mod_ident, member_name.value)) - } else { - None - } - } - LeadingNameAccess_::AnonymousAddress(addr) => { - if is_package_address(symbols, info, addr) { - Some(ChainComponentKind::Package(leading_name)) - } else { - None - } - } - LeadingNameAccess_::GlobalAddress(n) => { - // if leading name is global address then the first component can only be a - // package - if is_package_name(symbols, info, n) { - Some(ChainComponentKind::Package(leading_name)) - } else { - None - } - } - } -} - -/// Handle name chain auto-completion at a given position. The gist of this approach is to first -/// identify what the first component of the access chain represents (as it may be a package, module -/// or a member) and if the chain has other components, recursively process them in turn to either -/// - finish auto-completion if cursor is on a given component's identifier -/// - identify what the subsequent component represents and keep going -fn name_chain_completions( - symbols: &Symbols, - cursor: &CursorContext, - colon_colon_triggered: bool, -) -> (Vec, bool) { - eprintln!("looking for name access chains"); - let mut completions = vec![]; - let mut only_custom_items = false; - let Some(ChainInfo { - chain, - kind: chain_kind, - inside_use, - }) = cursor.find_access_chain() - else { - eprintln!("no access chain"); - return (completions, only_custom_items); - }; - - let (leading_name, path_entries) = match &chain.value { - P::NameAccessChain_::Single(entry) => ( - sp(entry.name.loc, LeadingNameAccess_::Name(entry.name)), - vec![], - ), - P::NameAccessChain_::Path(name_path) => ( - name_path.root.name, - name_path.entries.iter().map(|e| e.name).collect::>(), - ), - }; - - // there may be access chains for which there is not auto-completion info generated by the - // compiler but which still have to be handled (e.g., chains starting with numeric address) - let info = symbols - .compiler_info - .path_autocomplete_info - .get(&leading_name.loc) - .cloned() - .unwrap_or_else(AliasAutocompleteInfo::new); - - eprintln!("found access chain for auto-completion (adddreses: {}, modules: {}, members: {}, tparams: {}", - info.addresses.len(), info.modules.len(), info.members.len(), info.type_params.len()); - - // if we are auto-completing for an access chain, there is no need to include default completions - only_custom_items = true; - - if leading_name.loc.contains(&cursor.loc) { - // at first position of the chain suggest all packages that are available regardless of what - // the leading name represents, as a package always fits at that position, for example: - // OxCAFE::... - // some_name::... - // ::some_name - // - completions.extend( - all_packages(symbols, &info) - .iter() - .map(|n| completion_item(n.as_str(), CompletionItemKind::UNIT)), - ); - - // only if leading name is actually a name, modules or module members are a correct - // auto-completion in the first position - if let LeadingNameAccess_::Name(_) = &leading_name.value { - completions.extend( - info.modules - .keys() - .map(|n| completion_item(n.as_str(), CompletionItemKind::MODULE)), - ); - completions.extend(all_single_name_member_completions( - symbols, - cursor, - &info.members, - chain_kind, - )); - if matches!(chain_kind, ChainCompletionKind::Type) { - completions.extend(PRIMITIVE_TYPE_COMPLETIONS.clone()); - completions.extend( - info.type_params - .iter() - .map(|t| completion_item(t.as_str(), CompletionItemKind::TYPE_PARAMETER)), - ); + info.members + .iter() + .find_map(|(alias_name, mod_ident, member_name)| { + if alias_name == &n.value { + Some((*mod_ident, member_name)) + } else { + None + } + }) + { + Some(ChainComponentKind::Member(mod_ident, member_name.value)) + } else { + None + } + } + P::LeadingNameAccess_::AnonymousAddress(addr) => { + if is_package_address(symbols, info, addr) { + Some(ChainComponentKind::Package(leading_name)) + } else { + None + } + } + P::LeadingNameAccess_::GlobalAddress(n) => { + // if leading name is global address then the first component can only be a + // package + if is_package_name(symbols, info, n) { + Some(ChainComponentKind::Package(leading_name)) + } else { + None } } - } else if let Some(next_kind) = first_name_chain_component_kind(symbols, &info, leading_name) { - completions_for_name_chain_entry( - symbols, - cursor, - &info, - ChainComponentInfo::new(leading_name.loc, next_kind), - chain_kind, - &path_entries, - /* path_index */ 0, - colon_colon_triggered, - inside_use, - &mut completions, - ); } - - eprintln!("found {} access chain completions", completions.len()); - - (completions, only_custom_items) } /// Computes auto-completions for module uses. @@ -1182,7 +925,7 @@ fn module_use_completions( cursor: &CursorContext, info: &AliasAutocompleteInfo, mod_use: &P::ModuleUse, - package: &LeadingNameAccess, + package: &P::LeadingNameAccess, mod_name: &P::ModuleName, ) -> Vec { use P::ModuleUse as MU; @@ -1251,542 +994,3 @@ fn module_use_completions( completions } - -/// Handles auto-completions for "regular" `use` declarations (name access chains in `use fun` -/// declarations are handled as part of name chain completions). -fn use_decl_completions(symbols: &Symbols, cursor: &CursorContext) -> (Vec, bool) { - eprintln!("looking for use declarations"); - let mut completions = vec![]; - let mut only_custom_items = false; - let Some(use_) = cursor.find_use_decl() else { - eprintln!("no use declaration"); - return (completions, only_custom_items); - }; - eprintln!("use declaration {:?}", use_); - - // if we are auto-completing for a use decl, there is no need to include default completions - only_custom_items = true; - - // there is no auto-completion info generated by the compiler for this but helper methods used - // here are shared with name chain completion where it may exist, so we create an "empty" one - // here - let info = AliasAutocompleteInfo::new(); - - match use_ { - P::Use::ModuleUse(sp!(_, mod_ident), mod_use) => { - if mod_ident.address.loc.contains(&cursor.loc) { - // cursor on package (e.g., on `some_pkg` in `some_pkg::some_mod`) - completions.extend( - all_packages(symbols, &info) - .iter() - .map(|n| completion_item(n.as_str(), CompletionItemKind::UNIT)), - ); - } else if cursor.loc.start() > mod_ident.address.loc.end() - && cursor.loc.end() <= mod_ident.module.loc().end() - { - // cursor is either at the `::` succeeding package/address or at the identifier - // following that particular `::` - for ident in pkg_mod_identifiers(symbols, &info, &mod_ident.address) { - completions.push(completion_item( - ident.value.module.value().as_str(), - CompletionItemKind::MODULE, - )); - } - } else { - completions.extend(module_use_completions( - symbols, - cursor, - &info, - &mod_use, - &mod_ident.address, - &mod_ident.module, - )); - } - } - P::Use::NestedModuleUses(leading_name, uses) => { - if leading_name.loc.contains(&cursor.loc) { - // cursor on package - completions.extend( - all_packages(symbols, &info) - .iter() - .map(|n| completion_item(n.as_str(), CompletionItemKind::UNIT)), - ); - } else { - if let Some((first_name, _)) = uses.first() { - if cursor.loc.start() > leading_name.loc.end() - && cursor.loc.end() <= first_name.loc().start() - { - // cursor is after `::` succeeding address/package but before the first - // module - for ident in pkg_mod_identifiers(symbols, &info, &leading_name) { - completions.push(completion_item( - ident.value.module.value().as_str(), - CompletionItemKind::MODULE, - )); - } - // no point in falling through to the uses loop below - return (completions, only_custom_items); - } - } - - for (mod_name, mod_use) in &uses { - if mod_name.loc().contains(&cursor.loc) { - for ident in pkg_mod_identifiers(symbols, &info, &leading_name) { - completions.push(completion_item( - ident.value.module.value().as_str(), - CompletionItemKind::MODULE, - )); - } - // no point checking other locations - break; - } - completions.extend(module_use_completions( - symbols, - cursor, - &info, - mod_use, - &leading_name, - mod_name, - )); - } - } - } - P::Use::Fun { .. } => (), // already handled as part of name chain completion - P::Use::Partial { - package, - colon_colon, - opening_brace: _, - } => { - if package.loc.contains(&cursor.loc) { - // cursor on package name/address - completions.extend( - all_packages(symbols, &info) - .iter() - .map(|n| completion_item(n.as_str(), CompletionItemKind::UNIT)), - ); - } - if let Some(colon_colon_loc) = colon_colon { - if cursor.loc.start() >= colon_colon_loc.start() { - // cursor is on or past `::` - for ident in pkg_mod_identifiers(symbols, &info, &package) { - completions.push(completion_item( - ident.value.module.value().as_str(), - CompletionItemKind::MODULE, - )); - } - } - } - } - } - - (completions, only_custom_items) -} - -/// Handle context-specific auto-completion requests with no trigger character. -fn context_specific_no_trigger( - symbols: &Symbols, - use_fpath: &Path, - buffer: &str, - position: &Position, -) -> (Vec, bool) { - eprintln!("looking for dot"); - let mut completions = dot_completions(symbols, use_fpath, position); - eprintln!("dot found: {}", !completions.is_empty()); - if !completions.is_empty() { - // found dot completions - do not look for any other - return (completions, true); - } - - let mut only_custom_items = false; - - let strings = preceding_strings(buffer, position); - - if strings.is_empty() { - return (completions, only_custom_items); - } - - // at this point only try to auto-complete init function declararation - get the last string - // and see if it represents the beginning of init function declaration - const INIT_FN_NAME: &str = "init"; - let (n, use_col) = strings.last().unwrap(); - for u in symbols.line_uses(use_fpath, position.line) { - if *use_col >= u.col_start() && *use_col <= u.col_end() { - let def_loc = u.def_loc(); - let Some(use_file_mod_definition) = symbols.file_mods.get(use_fpath) else { - break; - }; - let Some(use_file_mod_def) = use_file_mod_definition.first() else { - break; - }; - if is_definition( - symbols, - position.line, - u.col_start(), - use_file_mod_def.fhash(), - def_loc, - ) { - // since it's a definition, there is no point in trying to suggest a name - // if one is about to create a fresh identifier - only_custom_items = true; - } - let Some(def_info) = symbols.def_info(&def_loc) else { - break; - }; - let DefInfo::Function(mod_ident, v, ..) = def_info else { - // not a function - break; - }; - if !INIT_FN_NAME.starts_with(n) { - // starting to type "init" - break; - } - if !matches!(v, Visibility::Internal) { - // private (otherwise perhaps it's "init_something") - break; - } - - // get module info containing the init function - let Some(mdef) = symbols.mod_defs(&u.def_loc().file_hash(), *mod_ident) else { - break; - }; - - if mdef.functions().contains_key(&(INIT_FN_NAME.into())) { - // already has init function - break; - } - - let sui_ctx_arg = "ctx: &mut TxContext"; - - // decide on the list of parameters depending on whether a module containing - // the init function has a struct thats an one-time-witness candidate struct - let otw_candidate = Symbol::from(mod_ident.module.value().to_uppercase()); - let init_snippet = if mdef.structs().contains_key(&otw_candidate) { - format!("{INIT_FN_NAME}(${{1:witness}}: {otw_candidate}, {sui_ctx_arg}) {{\n\t${{2:}}\n}}\n") - } else { - format!("{INIT_FN_NAME}({sui_ctx_arg}) {{\n\t${{1:}}\n}}\n") - }; - - let init_completion = CompletionItem { - label: INIT_FN_NAME.to_string(), - kind: Some(CompletionItemKind::SNIPPET), - documentation: Some(Documentation::String( - "Module initializer snippet".to_string(), - )), - insert_text: Some(init_snippet), - insert_text_format: Some(InsertTextFormat::SNIPPET), - ..Default::default() - }; - completions.push(init_completion); - break; - } - } - (completions, only_custom_items) -} - -/// Checks if a use at a given position is also a definition. -fn is_definition( - symbols: &Symbols, - use_line: u32, - use_col: u32, - use_fhash: FileHash, - def_loc: Loc, -) -> bool { - if let Some(use_loc) = symbols - .files - .line_char_offset_to_loc_opt(use_fhash, use_line, use_col) - { - // TODO: is overlapping better? - def_loc.contains(&use_loc) - } else { - false - } -} - -/// Finds white-space separated strings on the line containing auto-completion request and their -/// locations. -fn preceding_strings(buffer: &str, position: &Position) -> Vec<(String, u32)> { - let mut strings = vec![]; - let line = match buffer.lines().nth(position.line as usize) { - Some(line) => line, - None => return strings, // Our buffer does not contain the line, and so must be out of date. - }; - - let mut chars = line.chars(); - let mut cur_col = 0; - let mut cur_str_start = 0; - let mut cur_str = "".to_string(); - while cur_col <= position.character { - let Some(c) = chars.next() else { - return strings; - }; - if c == ' ' || c == '\t' { - if !cur_str.is_empty() { - // finish an already started string - strings.push((cur_str, cur_str_start)); - cur_str = "".to_string(); - } - } else { - if cur_str.is_empty() { - // start a new string - cur_str_start = cur_col; - } - cur_str.push(c); - } - - cur_col += c.len_utf8() as u32; - } - if !cur_str.is_empty() { - // finish the last string - strings.push((cur_str, cur_str_start)); - } - strings -} - -/// Sends the given connection a response to a completion request. -/// -/// The completions returned depend upon where the user's cursor is positioned. -pub fn on_completion_request( - context: &Context, - request: &Request, - ide_files_root: VfsPath, - pkg_dependencies: Arc>>, -) { - eprintln!("handling completion request"); - let parameters = serde_json::from_value::(request.params.clone()) - .expect("could not deserialize completion request"); - - let path = parameters - .text_document_position - .text_document - .uri - .to_file_path() - .unwrap(); - - let mut pos = parameters.text_document_position.position; - if pos.character != 0 { - // adjust column to be at the character that has just been inserted rather than right after - // it (unless we are at the very first column) - pos = Position::new(pos.line, pos.character - 1); - } - let items = completions_with_context(context, ide_files_root, pkg_dependencies, &path, pos) - .unwrap_or_default(); - let items_len = items.len(); - - let result = serde_json::to_value(items).expect("could not serialize completion response"); - eprintln!("about to send completion response with {items_len} items"); - let response = lsp_server::Response::new_ok(request.id.clone(), result); - if let Err(err) = context - .connection - .sender - .send(lsp_server::Message::Response(response)) - { - eprintln!("could not send completion response: {:?}", err); - } -} - -pub fn completions_with_context( - context: &Context, - ide_files_root: VfsPath, - pkg_dependencies: Arc>>, - path: &Path, - pos: Position, -) -> Option> { - let Some(pkg_path) = SymbolicatorRunner::root_dir(path) else { - eprintln!("failed completion for {:?} (package root not found)", path); - return None; - }; - let symbol_map = context.symbols.lock().unwrap(); - let current_symbols = symbol_map.get(&pkg_path)?; - Some(completion_items( - current_symbols, - ide_files_root, - pkg_dependencies, - path, - pos, - )) -} - -pub fn completion_items( - current_symbols: &Symbols, - ide_files_root: VfsPath, - pkg_dependencies: Arc>>, - path: &Path, - pos: Position, -) -> Vec { - compute_cursor_completion_items(ide_files_root, pkg_dependencies, path, pos) - .unwrap_or_else(|| compute_completion_items(current_symbols, path, pos)) -} - -fn compute_cursor_completion_items( - ide_files_root: VfsPath, - pkg_dependencies: Arc>>, - path: &Path, - cursor_position: Position, -) -> Option> { - let Some(pkg_path) = SymbolicatorRunner::root_dir(path) else { - eprintln!("failed completion for {:?} (package root not found)", path); - return None; - }; - let cursor_path = path.to_path_buf(); - let cursor_info = Some((&cursor_path, cursor_position)); - let (symbols, _diags) = symbols::get_symbols( - pkg_dependencies, - ide_files_root, - &pkg_path, - LintLevel::None, - cursor_info, - ) - .ok()?; - let symbols = symbols?; - Some(compute_completion_items(&symbols, path, cursor_position)) -} - -/// Computes completion items for a given completion request. -fn compute_completion_items(symbols: &Symbols, path: &Path, pos: Position) -> Vec { - let mut items = vec![]; - - let Some(fhash) = symbols.file_hash(path) else { - return items; - }; - let Some(file_id) = symbols.files.file_mapping().get(&fhash) else { - return items; - }; - let Ok(file) = symbols.files.files().get(*file_id) else { - return items; - }; - - let file_source = file.source().clone(); - if !file_source.is_empty() { - let only_custom_items; - match &symbols.cursor_context { - Some(cursor_context) => { - eprintln!("cursor completion"); - let (new_items, only_has_custom_items) = - cursor_completion_items(symbols, path, &file_source, pos, cursor_context); - only_custom_items = only_has_custom_items; - items.extend(new_items); - } - None => { - eprintln!("non-cursor completion"); - let (new_items, only_has_custom_items) = - default_items(symbols, path, &file_source, pos); - only_custom_items = only_has_custom_items; - items.extend(new_items); - } - } - if !only_custom_items { - eprintln!("including identifiers"); - let identifiers = identifiers(&file_source, symbols, path); - items.extend(identifiers); - } - } else { - // no file content - items.extend(KEYWORD_COMPLETIONS.clone()); - items.extend(BUILTIN_COMPLETIONS.clone()); - } - items -} - -/// Return completion items, plus a flag indicating if we should only use the custom items returned -/// (i.e., when the flag is false, default items and identifiers should also be added). -fn cursor_completion_items( - symbols: &Symbols, - path: &Path, - file_source: &str, - pos: Position, - cursor: &CursorContext, -) -> (Vec, bool) { - let cursor_leader = get_cursor_token(file_source, &pos); - match cursor_leader { - // TODO: consider using `cursor.position` for this instead - Some(Tok::Period) => { - eprintln!("found period"); - let items = dot_completions(symbols, path, &pos); - let items_is_empty = items.is_empty(); - eprintln!("found items: {}", !items_is_empty); - // whether completions have been found for the dot or not - // it makes no sense to try offering "dumb" autocompletion - // options here as they will not fit (an example would - // be dot completion of u64 variable without any methods - // with u64 receiver being visible) - (items, true) - } - Some(Tok::ColonColon) => { - let mut items = vec![]; - let mut only_custom_items = false; - let (path_items, path_custom) = - name_chain_completions(symbols, cursor, /* colon_colon_triggered */ true); - items.extend(path_items); - only_custom_items |= path_custom; - if !only_custom_items { - let (path_items, path_custom) = use_decl_completions(symbols, cursor); - items.extend(path_items); - only_custom_items |= path_custom; - } - (items, only_custom_items) - } - // Carve out to suggest UID for struct with key ability - Some(Tok::LBrace) => { - let mut items = vec![]; - let mut only_custom_items = false; - let (path_items, path_custom) = context_specific_lbrace(symbols, cursor); - items.extend(path_items); - only_custom_items |= path_custom; - if !only_custom_items { - let (path_items, path_custom) = use_decl_completions(symbols, cursor); - items.extend(path_items); - only_custom_items |= path_custom; - } - (items, only_custom_items) - } - // TODO: should we handle auto-completion on `:`? If we model our support after - // rust-analyzer then it does not do this - it starts auto-completing types after the first - // character beyond `:` is typed - _ => { - eprintln!("no relevant cursor leader"); - let mut items = vec![]; - let mut only_custom_items = false; - let (path_items, path_custom) = - name_chain_completions(symbols, cursor, /* colon_colon_triggered */ false); - items.extend(path_items); - only_custom_items |= path_custom; - if !only_custom_items { - if matches!(cursor_leader, Some(Tok::Colon)) { - // much like rust-analyzer we do not auto-complete in the middle of `::` - only_custom_items = true; - } else { - let (path_items, path_custom) = use_decl_completions(symbols, cursor); - items.extend(path_items); - only_custom_items |= path_custom; - } - } - if !only_custom_items { - eprintln!("checking default items"); - let (default_items, default_custom) = - default_items(symbols, path, file_source, pos); - items.extend(default_items); - only_custom_items |= default_custom; - } - (items, only_custom_items) - } - } -} - -fn default_items( - symbols: &Symbols, - path: &Path, - file_source: &str, - pos: Position, -) -> (Vec, bool) { - // If the user's cursor is positioned anywhere other than following a `.`, `:`, or `::`, - // offer them context-specific autocompletion items as well as - // Move's keywords, operators, and builtins. - let (custom_items, only_custom_items) = - context_specific_no_trigger(symbols, path, file_source, &pos); - let mut items = custom_items; - if !only_custom_items { - items.extend(KEYWORD_COMPLETIONS.clone()); - items.extend(BUILTIN_COMPLETIONS.clone()); - } - (items, only_custom_items) -} diff --git a/external-crates/move/crates/move-analyzer/src/completions/snippets.rs b/external-crates/move/crates/move-analyzer/src/completions/snippets.rs new file mode 100644 index 0000000000000..d1c2c969cc8bc --- /dev/null +++ b/external-crates/move/crates/move-analyzer/src/completions/snippets.rs @@ -0,0 +1,215 @@ +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +// Snippets auto-completion for various language elements, such as `init` function +// or structs representing objects. + +use crate::{ + completions::utils::mod_defs, + symbols::{CursorContext, CursorDefinition, DefInfo, Symbols}, +}; +use lsp_types::{CompletionItem, CompletionItemKind, Documentation, InsertTextFormat, Position}; +use move_command_line_common::files::FileHash; +use move_compiler::{expansion::ast::Visibility, parser::ast::Ability_, shared::Identifier}; +use move_ir_types::location::Loc; +use move_symbol_pool::Symbol; + +use std::path::Path; + +/// Checks if the cursor is at the opening brace of a struct definition and returns +/// auto-completion of this struct into an object if the struct has the `key` ability. +pub fn object_completion( + symbols: &Symbols, + cursor: &CursorContext, +) -> (Option, bool) { + let mut completion_finalized = false; + // look for a struct definition on the line that contains `{`, check its abilities, + // and do auto-completion if `key` ability is present + let Some(CursorDefinition::Struct(sname)) = &cursor.defn_name else { + return (None, completion_finalized); + }; + completion_finalized = true; + let Some(mod_ident) = cursor.module else { + return (None, completion_finalized); + }; + let Some(mod_defs) = mod_defs(symbols, &mod_ident.value) else { + return (None, completion_finalized); + }; + let Some(struct_def) = mod_defs.structs.get(&sname.value()) else { + return (None, completion_finalized); + }; + + let Some(DefInfo::Struct(_, _, _, _, abilities, ..)) = + symbols.def_info.get(&struct_def.name_loc) + else { + return (None, completion_finalized); + }; + + if !abilities.has_ability_(Ability_::Key) { + return (None, completion_finalized); + } + let obj_snippet = "\n\tid: UID,\n\t$1\n".to_string(); + let init_completion = CompletionItem { + label: "id: UID".to_string(), + kind: Some(CompletionItemKind::SNIPPET), + documentation: Some(Documentation::String("Object snippet".to_string())), + insert_text: Some(obj_snippet), + insert_text_format: Some(InsertTextFormat::SNIPPET), + ..Default::default() + }; + (Some(init_completion), completion_finalized) +} + +/// Auto-completion for `init` function snippet. +pub fn init_completion( + symbols: &Symbols, + use_fpath: &Path, + buffer: &str, + position: &Position, +) -> (Vec, bool) { + let mut completions = vec![]; + let mut completion_finalized = false; + + let strings = preceding_strings(buffer, position); + + if strings.is_empty() { + return (completions, completion_finalized); + } + + // try to auto-complete init function declararation - get the last string + // and see if it represents the beginning of init function declaration + const INIT_FN_NAME: &str = "init"; + let (n, use_col) = strings.last().unwrap(); + for u in symbols.line_uses(use_fpath, position.line) { + if *use_col >= u.col_start() && *use_col <= u.col_end() { + let def_loc = u.def_loc(); + let Some(use_file_mod_definition) = symbols.file_mods.get(use_fpath) else { + break; + }; + let Some(use_file_mod_def) = use_file_mod_definition.first() else { + break; + }; + if is_definition( + symbols, + position.line, + u.col_start(), + use_file_mod_def.fhash(), + def_loc, + ) { + // since it's a definition, there is no point in trying to suggest a name + // if one is about to create a fresh identifier + completion_finalized = true; + } + let Some(def_info) = symbols.def_info(&def_loc) else { + break; + }; + let DefInfo::Function(mod_ident, v, ..) = def_info else { + // not a function + break; + }; + if !INIT_FN_NAME.starts_with(n) { + // starting to type "init" + break; + } + if !matches!(v, Visibility::Internal) { + // private (otherwise perhaps it's "init_something") + break; + } + + // get module info containing the init function + let Some(mdef) = symbols.mod_defs(&u.def_loc().file_hash(), *mod_ident) else { + break; + }; + + if mdef.functions().contains_key(&(INIT_FN_NAME.into())) { + // already has init function + break; + } + + let sui_ctx_arg = "ctx: &mut TxContext"; + + // decide on the list of parameters depending on whether a module containing + // the init function has a struct thats an one-time-witness candidate struct + let otw_candidate = Symbol::from(mod_ident.module.value().to_uppercase()); + let init_snippet = if mdef.structs().contains_key(&otw_candidate) { + format!("{INIT_FN_NAME}(${{1:witness}}: {otw_candidate}, {sui_ctx_arg}) {{\n\t${{2:}}\n}}\n") + } else { + format!("{INIT_FN_NAME}({sui_ctx_arg}) {{\n\t${{1:}}\n}}\n") + }; + + let init_completion = CompletionItem { + label: INIT_FN_NAME.to_string(), + kind: Some(CompletionItemKind::SNIPPET), + documentation: Some(Documentation::String( + "Module initializer snippet".to_string(), + )), + insert_text: Some(init_snippet), + insert_text_format: Some(InsertTextFormat::SNIPPET), + ..Default::default() + }; + completions.push(init_completion); + break; + } + } + + (completions, completion_finalized) +} + +/// Finds white-space separated strings on the line containing auto-completion request and their +/// locations. +fn preceding_strings(buffer: &str, position: &Position) -> Vec<(String, u32)> { + let mut strings = vec![]; + let line = match buffer.lines().nth(position.line as usize) { + Some(line) => line, + None => return strings, // Our buffer does not contain the line, and so must be out of date. + }; + + let mut chars = line.chars(); + let mut cur_col = 0; + let mut cur_str_start = 0; + let mut cur_str = "".to_string(); + while cur_col <= position.character { + let Some(c) = chars.next() else { + return strings; + }; + if c == ' ' || c == '\t' { + if !cur_str.is_empty() { + // finish an already started string + strings.push((cur_str, cur_str_start)); + cur_str = "".to_string(); + } + } else { + if cur_str.is_empty() { + // start a new string + cur_str_start = cur_col; + } + cur_str.push(c); + } + + cur_col += c.len_utf8() as u32; + } + if !cur_str.is_empty() { + // finish the last string + strings.push((cur_str, cur_str_start)); + } + strings +} + +/// Checks if a use at a given position is also a definition. +fn is_definition( + symbols: &Symbols, + use_line: u32, + use_col: u32, + use_fhash: FileHash, + def_loc: Loc, +) -> bool { + if let Some(use_loc) = symbols + .files + .line_char_offset_to_loc_opt(use_fhash, use_line, use_col) + { + // TODO: is overlapping better? + def_loc.contains(&use_loc) + } else { + false + } +} diff --git a/external-crates/move/crates/move-analyzer/src/completions/utils.rs b/external-crates/move/crates/move-analyzer/src/completions/utils.rs new file mode 100644 index 0000000000000..93ac2d7f08686 --- /dev/null +++ b/external-crates/move/crates/move-analyzer/src/completions/utils.rs @@ -0,0 +1,130 @@ +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +use crate::symbols::{ + mod_ident_to_ide_string, ret_type_to_ide_str, type_args_to_ide_string, type_list_to_ide_string, + ModuleDefs, Symbols, +}; +use lsp_types::{CompletionItem, CompletionItemKind, CompletionItemLabelDetails, InsertTextFormat}; +use move_compiler::{ + expansion::ast::ModuleIdent_, + naming::ast::{Type, Type_}, + parser::keywords::PRIMITIVE_TYPES, + shared::Name, +}; +use move_symbol_pool::Symbol; +use once_cell::sync::Lazy; + +/// List of completion items of Move's primitive types. +pub static PRIMITIVE_TYPE_COMPLETIONS: Lazy> = Lazy::new(|| { + let mut primitive_types = PRIMITIVE_TYPES + .iter() + .map(|label| completion_item(label, CompletionItemKind::KEYWORD)) + .collect::>(); + primitive_types.push(completion_item("address", CompletionItemKind::KEYWORD)); + primitive_types +}); + +/// Get definitions for a given module. +pub fn mod_defs<'a>(symbols: &'a Symbols, mod_ident: &ModuleIdent_) -> Option<&'a ModuleDefs> { + symbols + .file_mods + .values() + .flatten() + .find(|mdef| mdef.ident == *mod_ident) +} + +/// Constructs an `lsp_types::CompletionItem` with the given `label` and `kind`. +pub fn completion_item(label: &str, kind: CompletionItemKind) -> CompletionItem { + CompletionItem { + label: label.to_owned(), + kind: Some(kind), + ..Default::default() + } +} + +pub fn call_completion_item( + mod_ident: &ModuleIdent_, + is_macro: bool, + method_name_opt: Option<&Symbol>, + function_name: &Symbol, + type_args: &[Type], + arg_names: &[Name], + arg_types: &[Type], + ret_type: &Type, + inside_use: bool, +) -> CompletionItem { + let sig_string = format!( + "fun {}({}){}", + type_args_to_ide_string(type_args, /* verbose */ false), + type_list_to_ide_string(arg_types, /* verbose */ false), + ret_type_to_ide_str(ret_type, /* verbose */ false) + ); + // if it's a method call we omit the first argument which is guaranteed to be there as this is a + // method and needs a receiver + let omitted_arg_count = if method_name_opt.is_some() { 1 } else { 0 }; + let mut snippet_idx = 0; + let arg_snippet = arg_names + .iter() + .zip(arg_types) + .skip(omitted_arg_count) + .map(|(name, ty)| { + lambda_snippet(ty, &mut snippet_idx).unwrap_or_else(|| { + let mut arg_name = name.to_string(); + if arg_name.starts_with('$') { + arg_name = arg_name[1..].to_string(); + } + snippet_idx += 1; + format!("${{{}:{}}}", snippet_idx, arg_name) + }) + }) + .collect::>() + .join(", "); + let macro_suffix = if is_macro { "!" } else { "" }; + let label_details = Some(CompletionItemLabelDetails { + detail: Some(format!( + " ({}::{})", + mod_ident_to_ide_string(mod_ident), + function_name + )), + description: Some(sig_string), + }); + + let method_name = method_name_opt.unwrap_or(function_name); + let (insert_text, insert_text_format) = if inside_use { + ( + Some(format!("{method_name}")), + Some(InsertTextFormat::PLAIN_TEXT), + ) + } else { + ( + Some(format!("{method_name}{macro_suffix}({arg_snippet})")), + Some(InsertTextFormat::SNIPPET), + ) + }; + + CompletionItem { + label: format!("{method_name}{macro_suffix}()"), + label_details, + kind: Some(CompletionItemKind::METHOD), + insert_text, + insert_text_format, + ..Default::default() + } +} + +fn lambda_snippet(sp!(_, ty): &Type, snippet_idx: &mut i32) -> Option { + if let Type_::Fun(vec, _) = ty { + let arg_snippets = vec + .iter() + .map(|_| { + *snippet_idx += 1; + format!("${{{snippet_idx}}}") + }) + .collect::>() + .join(", "); + *snippet_idx += 1; + return Some(format!("|{arg_snippets}| ${{{snippet_idx}}}")); + } + None +} diff --git a/external-crates/move/crates/move-analyzer/src/lib.rs b/external-crates/move/crates/move-analyzer/src/lib.rs index dc51721087232..0cb8aeb3e2820 100644 --- a/external-crates/move/crates/move-analyzer/src/lib.rs +++ b/external-crates/move/crates/move-analyzer/src/lib.rs @@ -8,7 +8,7 @@ extern crate move_ir_types; pub mod analysis; pub mod analyzer; pub mod compiler_info; -pub mod completion; +pub mod completions; pub mod context; pub mod diagnostics; pub mod inlay_hints; diff --git a/external-crates/move/crates/move-analyzer/tests/ide_testsuite.rs b/external-crates/move/crates/move-analyzer/tests/ide_testsuite.rs index 97d0d6d39b7a6..ba82d49dbdf13 100644 --- a/external-crates/move/crates/move-analyzer/tests/ide_testsuite.rs +++ b/external-crates/move/crates/move-analyzer/tests/ide_testsuite.rs @@ -12,7 +12,7 @@ use std::{ use json_comments::StripComments; use lsp_types::{InlayHintKind, InlayHintLabel, InlayHintTooltip, Position}; use move_analyzer::{ - completion::completion_items, + completions::compute_completions, inlay_hints::inlay_hints_internal, symbols::{ def_info_doc_string, get_symbols, maybe_convert_for_guard, PrecompiledPkgDeps, Symbols, @@ -189,7 +189,7 @@ impl CompletionTest { line: lsp_use_line, character: lsp_use_col, }; - let items = completion_items( + let items = compute_completions( symbols, ide_files_root, pkg_dependencies,