Skip to content

Commit e233374

Browse files
Add support for auto imports (#86)
* Support auto service importing * Update changelog * Alphabeticalise location of service * Update changelog * Change to off by default
1 parent bf56554 commit e233374

File tree

5 files changed

+171
-3
lines changed

5 files changed

+171
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
1717
- Added configuration option `luau-lsp.hover.showTableKinds` (default: off) to indicate whether kinds (`{+ ... +}`, `{| ... |}`) are shown in hover information
1818
- Added configuration option `luau-lsp.hover.multilineFunctionDefinitions` (default: off) to spread function definitions in hover panel across multiple lines
1919
- 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
20+
- 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`
2021

2122
### Changed
2223

editors/code/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,11 @@
8686
"type": "boolean",
8787
"default": false
8888
},
89+
"luau-lsp.completion.suggestImports": {
90+
"markdownDescription": "Suggest automatic imports in completion items",
91+
"type": "boolean",
92+
"default": false
93+
},
8994
"luau-lsp.ignoreGlobs": {
9095
"markdownDescription": "Diagnostics will not be reported for any file matching these globs unless the file is currently open",
9196
"type": "array",

src/include/LSP/ClientConfiguration.hpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,11 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(
6969
struct ClientCompletionConfiguration
7070
{
7171
bool enabled = true;
72+
/// Whether we should suggest automatic imports in completions
73+
bool suggestImports = false;
7274
};
7375

74-
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ClientCompletionConfiguration, enabled);
76+
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ClientCompletionConfiguration, enabled, suggestImports);
7577

7678
struct ClientSignatureHelpConfiguration
7779
{
@@ -97,4 +99,4 @@ struct ClientConfiguration
9799
ClientSignatureHelpConfiguration signatureHelp;
98100
};
99101
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(
100-
ClientConfiguration, autocompleteEnd, ignoreGlobs, sourcemap, diagnostics, types, inlayHints, hover, completion, signatureHelp);
102+
ClientConfiguration, autocompleteEnd, ignoreGlobs, sourcemap, diagnostics, types, inlayHints, hover, completion, signatureHelp);

src/include/LSP/Workspace.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ class WorkspaceFolder
5959

6060
private:
6161
void endAutocompletion(const lsp::CompletionParams& params);
62+
void suggestImports(const Luau::ModuleName& moduleName, const Luau::Position& position, const ClientConfiguration& config,
63+
std::vector<lsp::CompletionItem>& result);
6264

6365
public:
6466
std::vector<lsp::CompletionItem> completion(const lsp::CompletionParams& params);

src/operations/Completion.cpp

Lines changed: 159 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,156 @@ void WorkspaceFolder::endAutocompletion(const lsp::CompletionParams& params)
113113
}
114114
}
115115

116+
bool isGetService(const Luau::AstExpr* expr)
117+
{
118+
if (auto call = expr->as<Luau::AstExprCall>())
119+
if (auto index = call->func->as<Luau::AstExprIndexName>())
120+
if (index->index == "GetService")
121+
if (auto name = index->expr->as<Luau::AstExprGlobal>())
122+
if (name->name == "game")
123+
return true;
124+
125+
return false;
126+
}
127+
128+
struct ImportLocationVisitor : public Luau::AstVisitor
129+
{
130+
std::unordered_map<std::string, size_t> serviceLineMap;
131+
132+
bool visit(Luau::AstStatLocal* local) override
133+
{
134+
if (local->vars.size != 1)
135+
return false;
136+
137+
auto localName = local->vars.data[0];
138+
auto expr = local->values.data[0];
139+
140+
if (!localName || !expr)
141+
return false;
142+
143+
if (isGetService(expr))
144+
serviceLineMap.emplace(std::string(localName->name.value), localName->location.begin.line);
145+
146+
return false;
147+
}
148+
149+
bool visit(Luau::AstStatBlock* block) override
150+
{
151+
for (Luau::AstStat* stat : block->body)
152+
{
153+
stat->visit(this);
154+
}
155+
156+
return false;
157+
}
158+
};
159+
160+
/// Attempts to retrieve a list of service names by inspecting the global type definitions
161+
static std::vector<std::string> getServiceNames(const Luau::ScopePtr scope)
162+
{
163+
std::vector<std::string> services;
164+
165+
if (auto dataModelType = scope->lookupType("ServiceProvider"))
166+
{
167+
if (auto ctv = Luau::get<Luau::ClassTypeVar>(dataModelType->type))
168+
{
169+
if (auto getService = Luau::lookupClassProp(ctv, "GetService"))
170+
{
171+
if (auto itv = Luau::get<Luau::IntersectionTypeVar>(getService->type))
172+
{
173+
for (auto part : itv->parts)
174+
{
175+
if (auto ftv = Luau::get<Luau::FunctionTypeVar>(part))
176+
{
177+
auto it = Luau::begin(ftv->argTypes);
178+
auto end = Luau::end(ftv->argTypes);
179+
180+
if (it != end && ++it != end)
181+
{
182+
if (auto stv = Luau::get<Luau::SingletonTypeVar>(*it))
183+
{
184+
if (auto ss = Luau::get<Luau::StringSingleton>(stv))
185+
{
186+
services.emplace_back(ss->value);
187+
}
188+
}
189+
}
190+
}
191+
}
192+
}
193+
}
194+
}
195+
}
196+
197+
return services;
198+
}
199+
200+
void WorkspaceFolder::suggestImports(
201+
const Luau::ModuleName& moduleName, const Luau::Position& position, const ClientConfiguration& config, std::vector<lsp::CompletionItem>& result)
202+
{
203+
auto sourceModule = frontend.getSourceModule(moduleName);
204+
auto module = frontend.moduleResolverForAutocomplete.getModule(moduleName);
205+
if (!sourceModule || !module)
206+
return;
207+
208+
// If in roblox mode - suggest services
209+
if (config.types.roblox)
210+
{
211+
auto scope = Luau::findScopeAtPosition(*module, position);
212+
if (!scope)
213+
return;
214+
215+
// Place after any hot comments and TODO: already imported services
216+
size_t minimumLineNumber = 0;
217+
for (auto hotComment : sourceModule->hotcomments)
218+
{
219+
if (!hotComment.header)
220+
continue;
221+
if (hotComment.location.begin.line >= minimumLineNumber)
222+
minimumLineNumber = hotComment.location.begin.line + 1;
223+
}
224+
225+
ImportLocationVisitor visitor;
226+
visitor.visit(sourceModule->root);
227+
228+
auto services = getServiceNames(frontend.typeCheckerForAutocomplete.globalScope);
229+
for (auto& service : services)
230+
{
231+
// ASSUMPTION: if the service was defined, it was defined with the exact same name
232+
bool isAlreadyDefined = false;
233+
size_t lineNumber = minimumLineNumber;
234+
for (auto& [definedService, location] : visitor.serviceLineMap)
235+
{
236+
if (definedService == service)
237+
{
238+
isAlreadyDefined = true;
239+
break;
240+
}
241+
242+
if (definedService < service && location >= lineNumber)
243+
lineNumber = location + 1;
244+
}
245+
246+
if (isAlreadyDefined)
247+
continue;
248+
249+
auto importText = "local " + service + " = game:GetService(\"" + service + "\")\n";
250+
251+
lsp::CompletionItem item;
252+
item.label = service;
253+
item.kind = lsp::CompletionItemKind::Class;
254+
item.detail = "Auto-import";
255+
item.documentation = {lsp::MarkupKind::Markdown, codeBlock("lua", importText)};
256+
item.insertText = service;
257+
258+
lsp::Position placement{lineNumber, 0};
259+
item.additionalTextEdits.emplace_back(lsp::TextEdit{{placement, placement}, importText});
260+
261+
result.emplace_back(item);
262+
}
263+
}
264+
}
265+
116266
std::vector<lsp::CompletionItem> WorkspaceFolder::completion(const lsp::CompletionParams& params)
117267
{
118268
auto config = client->getConfiguration(rootUri);
@@ -127,7 +277,9 @@ std::vector<lsp::CompletionItem> WorkspaceFolder::completion(const lsp::Completi
127277
return {};
128278
}
129279

130-
auto result = Luau::autocomplete(frontend, fileResolver.getModuleName(params.textDocument.uri), convertPosition(params.position), nullCallback);
280+
auto moduleName = fileResolver.getModuleName(params.textDocument.uri);
281+
auto position = convertPosition(params.position);
282+
auto result = Luau::autocomplete(frontend, moduleName, position, nullCallback);
131283
std::vector<lsp::CompletionItem> items;
132284

133285
for (auto& [name, entry] : result.entryMap)
@@ -255,6 +407,12 @@ std::vector<lsp::CompletionItem> WorkspaceFolder::completion(const lsp::Completi
255407
items.emplace_back(item);
256408
}
257409

410+
if (config.completion.suggestImports &&
411+
(result.context == Luau::AutocompleteContext::Expression || result.context == Luau::AutocompleteContext::Statement))
412+
{
413+
suggestImports(moduleName, position, config, items);
414+
}
415+
258416
return items;
259417
}
260418

0 commit comments

Comments
 (0)