From e23337498a36d04b5432ff8593ebe18de4afc6f9 Mon Sep 17 00:00:00 2001 From: JohnnyMorganz Date: Tue, 16 Aug 2022 20:13:01 +0100 Subject: [PATCH] Add support for auto imports (#86) * Support auto service importing * Update changelog * Alphabeticalise location of service * Update changelog * Change to off by default --- CHANGELOG.md | 1 + editors/code/package.json | 5 + src/include/LSP/ClientConfiguration.hpp | 6 +- src/include/LSP/Workspace.hpp | 2 + src/operations/Completion.cpp | 160 +++++++++++++++++++++++- 5 files changed, 171 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index efe6ad8d..3ef888ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Added configuration option `luau-lsp.hover.showTableKinds` (default: off) to indicate whether kinds (`{+ ... +}`, `{| ... |}`) are shown in hover information - Added configuration option `luau-lsp.hover.multilineFunctionDefinitions` (default: off) to spread function definitions in hover panel across multiple lines - Added configuration option `luau-lsp.hover.strictDatamodelTypes` (default: on) to use strict DataModel type information in hover panel (equivalent to autocomplete). When disabled, the same type information that the diagnostic type checker uses is displayed +- Added support for automatic service importing. When using a service which has not yet been defined, it will be added (alphabetically) to the top of the file. Config setting: `luau-lsp.completion.suggestImports` ### Changed diff --git a/editors/code/package.json b/editors/code/package.json index 58388a44..072724c2 100644 --- a/editors/code/package.json +++ b/editors/code/package.json @@ -86,6 +86,11 @@ "type": "boolean", "default": false }, + "luau-lsp.completion.suggestImports": { + "markdownDescription": "Suggest automatic imports in completion items", + "type": "boolean", + "default": false + }, "luau-lsp.ignoreGlobs": { "markdownDescription": "Diagnostics will not be reported for any file matching these globs unless the file is currently open", "type": "array", diff --git a/src/include/LSP/ClientConfiguration.hpp b/src/include/LSP/ClientConfiguration.hpp index 788eecfc..24e8ae92 100644 --- a/src/include/LSP/ClientConfiguration.hpp +++ b/src/include/LSP/ClientConfiguration.hpp @@ -69,9 +69,11 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( struct ClientCompletionConfiguration { bool enabled = true; + /// Whether we should suggest automatic imports in completions + bool suggestImports = false; }; -NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ClientCompletionConfiguration, enabled); +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ClientCompletionConfiguration, enabled, suggestImports); struct ClientSignatureHelpConfiguration { @@ -97,4 +99,4 @@ struct ClientConfiguration ClientSignatureHelpConfiguration signatureHelp; }; NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( - ClientConfiguration, autocompleteEnd, ignoreGlobs, sourcemap, diagnostics, types, inlayHints, hover, completion, signatureHelp); \ No newline at end of file + ClientConfiguration, autocompleteEnd, ignoreGlobs, sourcemap, diagnostics, types, inlayHints, hover, completion, signatureHelp); diff --git a/src/include/LSP/Workspace.hpp b/src/include/LSP/Workspace.hpp index 395f4697..a1a36433 100644 --- a/src/include/LSP/Workspace.hpp +++ b/src/include/LSP/Workspace.hpp @@ -59,6 +59,8 @@ class WorkspaceFolder private: void endAutocompletion(const lsp::CompletionParams& params); + void suggestImports(const Luau::ModuleName& moduleName, const Luau::Position& position, const ClientConfiguration& config, + std::vector& result); public: std::vector completion(const lsp::CompletionParams& params); diff --git a/src/operations/Completion.cpp b/src/operations/Completion.cpp index 85afcd87..75eed181 100644 --- a/src/operations/Completion.cpp +++ b/src/operations/Completion.cpp @@ -113,6 +113,156 @@ void WorkspaceFolder::endAutocompletion(const lsp::CompletionParams& params) } } +bool isGetService(const Luau::AstExpr* expr) +{ + if (auto call = expr->as()) + if (auto index = call->func->as()) + if (index->index == "GetService") + if (auto name = index->expr->as()) + if (name->name == "game") + return true; + + return false; +} + +struct ImportLocationVisitor : public Luau::AstVisitor +{ + std::unordered_map serviceLineMap; + + bool visit(Luau::AstStatLocal* local) override + { + if (local->vars.size != 1) + return false; + + auto localName = local->vars.data[0]; + auto expr = local->values.data[0]; + + if (!localName || !expr) + return false; + + if (isGetService(expr)) + serviceLineMap.emplace(std::string(localName->name.value), localName->location.begin.line); + + return false; + } + + bool visit(Luau::AstStatBlock* block) override + { + for (Luau::AstStat* stat : block->body) + { + stat->visit(this); + } + + return false; + } +}; + +/// Attempts to retrieve a list of service names by inspecting the global type definitions +static std::vector getServiceNames(const Luau::ScopePtr scope) +{ + std::vector services; + + if (auto dataModelType = scope->lookupType("ServiceProvider")) + { + if (auto ctv = Luau::get(dataModelType->type)) + { + if (auto getService = Luau::lookupClassProp(ctv, "GetService")) + { + if (auto itv = Luau::get(getService->type)) + { + for (auto part : itv->parts) + { + if (auto ftv = Luau::get(part)) + { + auto it = Luau::begin(ftv->argTypes); + auto end = Luau::end(ftv->argTypes); + + if (it != end && ++it != end) + { + if (auto stv = Luau::get(*it)) + { + if (auto ss = Luau::get(stv)) + { + services.emplace_back(ss->value); + } + } + } + } + } + } + } + } + } + + return services; +} + +void WorkspaceFolder::suggestImports( + const Luau::ModuleName& moduleName, const Luau::Position& position, const ClientConfiguration& config, std::vector& result) +{ + auto sourceModule = frontend.getSourceModule(moduleName); + auto module = frontend.moduleResolverForAutocomplete.getModule(moduleName); + if (!sourceModule || !module) + return; + + // If in roblox mode - suggest services + if (config.types.roblox) + { + auto scope = Luau::findScopeAtPosition(*module, position); + if (!scope) + return; + + // Place after any hot comments and TODO: already imported services + size_t minimumLineNumber = 0; + for (auto hotComment : sourceModule->hotcomments) + { + if (!hotComment.header) + continue; + if (hotComment.location.begin.line >= minimumLineNumber) + minimumLineNumber = hotComment.location.begin.line + 1; + } + + ImportLocationVisitor visitor; + visitor.visit(sourceModule->root); + + auto services = getServiceNames(frontend.typeCheckerForAutocomplete.globalScope); + for (auto& service : services) + { + // ASSUMPTION: if the service was defined, it was defined with the exact same name + bool isAlreadyDefined = false; + size_t lineNumber = minimumLineNumber; + for (auto& [definedService, location] : visitor.serviceLineMap) + { + if (definedService == service) + { + isAlreadyDefined = true; + break; + } + + if (definedService < service && location >= lineNumber) + lineNumber = location + 1; + } + + if (isAlreadyDefined) + continue; + + auto importText = "local " + service + " = game:GetService(\"" + service + "\")\n"; + + lsp::CompletionItem item; + item.label = service; + item.kind = lsp::CompletionItemKind::Class; + item.detail = "Auto-import"; + item.documentation = {lsp::MarkupKind::Markdown, codeBlock("lua", importText)}; + item.insertText = service; + + lsp::Position placement{lineNumber, 0}; + item.additionalTextEdits.emplace_back(lsp::TextEdit{{placement, placement}, importText}); + + result.emplace_back(item); + } + } +} + std::vector WorkspaceFolder::completion(const lsp::CompletionParams& params) { auto config = client->getConfiguration(rootUri); @@ -127,7 +277,9 @@ std::vector WorkspaceFolder::completion(const lsp::Completi return {}; } - auto result = Luau::autocomplete(frontend, fileResolver.getModuleName(params.textDocument.uri), convertPosition(params.position), nullCallback); + auto moduleName = fileResolver.getModuleName(params.textDocument.uri); + auto position = convertPosition(params.position); + auto result = Luau::autocomplete(frontend, moduleName, position, nullCallback); std::vector items; for (auto& [name, entry] : result.entryMap) @@ -255,6 +407,12 @@ std::vector WorkspaceFolder::completion(const lsp::Completi items.emplace_back(item); } + if (config.completion.suggestImports && + (result.context == Luau::AutocompleteContext::Expression || result.context == Luau::AutocompleteContext::Statement)) + { + suggestImports(moduleName, position, config, items); + } + return items; }