Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ target_sources(Luau.LanguageServer PRIVATE
src/CliConfigurationParser.cpp
src/platform/AutoImports.cpp
src/platform/LSPPlatform.cpp
src/platform/RotrieverResolver.cpp
src/platform/StringRequireAutoImporter.cpp
src/platform/StringRequireSuggester.cpp
src/platform/roblox/RobloxCodeAction.cpp
Expand Down Expand Up @@ -128,6 +129,7 @@ target_sources(Luau.LanguageServer.Test PRIVATE
tests/Glob.test.cpp
tests/Workspace.test.cpp
tests/RequireGraph.test.cpp
tests/RotrieverResolver.test.cpp
tests/AnalyzeCli.test.cpp
src/AnalyzeCli.cpp # For testing
)
Expand Down
Binary file added editors/code/bin/server
Binary file not shown.
6 changes: 3 additions & 3 deletions editors/code/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"bugs": {
"url": "https://github.com/JohnnyMorganz/luau-lsp/issues"
},
"version": "1.58.0",
"version": "1.59.0",
"engines": {
"vscode": "^1.67.0"
},
Expand Down Expand Up @@ -674,7 +674,7 @@
},
"luau-lsp.index.maxFiles": {
"type": "number",
"default": 10000,
"default": 50000,
"scope": "window",
"markdownDescription": "The maximum amount of files that can be indexed. If more files are indexed, more memory is needed"
},
Expand Down Expand Up @@ -798,4 +798,4 @@
"undici": "^6.21.2",
"vscode-languageclient": "^8.1.0-next.6"
}
}
}
72 changes: 72 additions & 0 deletions src/Workspace.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,75 @@ void WorkspaceFolder::indexFiles(const ClientConfiguration& config)
client->sendTrace("workspace: indexing all files COMPLETED");
}

void WorkspaceFolder::discoverRotrieverPackages()
{
LUAU_TIMETRACE_SCOPE("WorkspaceFolder::discoverRotrieverPackages", "LSP");

if (isNullWorkspace())
return;

client->sendTrace("workspace: discovering rotriever packages");

rotrieverPackages.clear();
Luau::LanguageServer::RotrieverResolver resolver;

Luau::FileUtils::traverseDirectoryRecursive(rootUri.fsPath(),
[&](auto& path)
{
auto uri = Uri::file(path);
if (uri.filename() == "rotriever.toml")
{
client->sendTrace("workspace: found rotriever.toml at " + path);
auto result = resolver.parseManifest(uri);
if (result)
{
// Try to parse exports from the package's init.lua
auto initLuaPath = result->packageRoot.resolvePath(result->contentRoot + "/init.lua");
auto exports = Luau::LanguageServer::RotrieverResolver::parseExports(initLuaPath);
result->exports = std::move(exports.values);
result->typeExports = std::move(exports.types);

// Discover internal modules for intra-package imports
auto contentRootUri = result->packageRoot.resolvePath(result->contentRoot);
result->internalModules = Luau::LanguageServer::RotrieverResolver::discoverInternalModules(contentRootUri);

client->sendLogMessage(lsp::MessageType::Info, "Discovered Rotriever package: " + result->name + " (" +
std::to_string(result->exports.size()) + " exports, " +
std::to_string(result->typeExports.size()) + " types, " +
std::to_string(result->internalModules.size()) + " internal modules)");
rotrieverPackages.emplace(result->packageRoot, std::move(*result));
}
else
{
client->sendLogMessage(lsp::MessageType::Warning, "Failed to parse rotriever.toml at " + path);
}
}
});

client->sendLogMessage(lsp::MessageType::Info, "Discovered " + std::to_string(rotrieverPackages.size()) + " Rotriever packages");
client->sendTrace("workspace: discovering rotriever packages COMPLETED");
}

const Luau::LanguageServer::RotrieverPackage* WorkspaceFolder::findRotrieverPackageForFile(const Uri& fileUri) const
{
const std::string filePath = fileUri.fsPath();

for (const auto& [packageRoot, package] : rotrieverPackages)
{
// Compute the content root path: packageRoot + contentRoot
Uri contentRootUri = packageRoot.resolvePath(package.contentRoot);
const std::string contentRootPath = contentRootUri.fsPath();

// Check if the file is under this package's content root
if (Luau::startsWith(filePath, contentRootPath))
{
return &package;
}
}

return nullptr;
}

static void clearDisabledGlobals(const Client* client, const Luau::GlobalTypes& globalTypes, const std::vector<std::string>& disabledGlobals)
{
const auto targetScope = globalTypes.globalScope;
Expand Down Expand Up @@ -600,6 +669,9 @@ void WorkspaceFolder::setupWithConfiguration(const ClientConfiguration& configur

platform->setupWithConfiguration(configuration);

// Discover Rotriever packages in the workspace
discoverRotrieverPackages();

if (configuration.index.enabled)
indexFiles(configuration);

Expand Down
6 changes: 4 additions & 2 deletions src/include/LSP/ClientConfiguration.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -157,9 +157,11 @@ struct ClientCompletionImportsConfiguration
bool separateGroupsWithLine = false;
/// Files that match these globs will not be shown during auto-import
std::vector<std::string> ignoreGlobs{};
/// Whether to suggest imports based on Rotriever package dependencies
bool suggestRotrieverImports = true;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ClientCompletionImportsConfiguration, enabled, suggestServices, includedServices, excludedServices,
suggestRequires, requireStyle, stringRequires, separateGroupsWithLine, ignoreGlobs);
suggestRequires, requireStyle, stringRequires, separateGroupsWithLine, ignoreGlobs, suggestRotrieverImports);

struct ClientCompletionConfiguration
{
Expand Down Expand Up @@ -213,7 +215,7 @@ struct ClientIndexConfiguration
// available for features such as "Find All References" and "Rename"
bool enabled = true;
/// The maximum amount of files that can be indexed
size_t maxFiles = 10000;
size_t maxFiles = 50000;
};

NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ClientIndexConfiguration, enabled, maxFiles);
Expand Down
1 change: 1 addition & 0 deletions src/include/LSP/Completion.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ static constexpr SortTextT CorrectFunctionResult = "3";
static constexpr SortTextT Default = "4";
static constexpr SortTextT WrongIndexType = "5";
static constexpr SortTextT MetatableIndex = "6";
static constexpr SortTextT AutoImportsRotriever = "65"; // Prioritized over regular auto-imports
static constexpr SortTextT AutoImports = "7";
static constexpr SortTextT AutoImportsAbsolute = "71";
static constexpr SortTextT Keywords = "8";
Expand Down
16 changes: 16 additions & 0 deletions src/include/LSP/Workspace.hpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once
#include <memory>
#include "Platform/LSPPlatform.hpp"
#include "Platform/RotrieverResolver.hpp"
#include "Luau/TypeCheckLimits.h"
#include "Luau/Frontend.h"
#include "Luau/Autocomplete.h"
Expand Down Expand Up @@ -55,6 +56,10 @@ class WorkspaceFolder
/// Used for documentation comment lookup within definition files.
std::unordered_map<std::string, std::pair<TextDocument, Luau::SourceModule>> definitionsSourceModules{};

/// Discovered Rotriever packages in this workspace.
/// Key is the directory containing the rotriever.toml file.
std::unordered_map<Uri, Luau::LanguageServer::RotrieverPackage, UriHash> rotrieverPackages{};

public:
WorkspaceFolder(Client* client, std::string name, const lsp::DocumentUri& uri, std::optional<Luau::Config> defaultConfig)
: client(client)
Expand Down Expand Up @@ -110,6 +115,7 @@ class WorkspaceFolder

private:
void registerTypes(const std::vector<std::string>& disabledGlobals);
void discoverRotrieverPackages();
void endAutocompletion(const lsp::CompletionParams& params);
void suggestImports(const Luau::ModuleName& moduleName, const Luau::Position& position, const ClientConfiguration& config,
const TextDocument& textDocument, std::vector<lsp::CompletionItem>& result, bool completingTypeReferencePrefix = true);
Expand Down Expand Up @@ -168,6 +174,16 @@ class WorkspaceFolder
{
return name == "$NULL_WORKSPACE";
};

/// Get discovered Rotriever packages
const std::unordered_map<Uri, Luau::LanguageServer::RotrieverPackage, UriHash>& getRotrieverPackages() const
{
return rotrieverPackages;
}

/// Find the Rotriever package that contains a given file
/// Returns nullptr if no package contains the file
const Luau::LanguageServer::RotrieverPackage* findRotrieverPackageForFile(const Uri& fileUri) const;
};

void throwIfCancelled(const LSPCancellationToken& cancellationToken);
14 changes: 13 additions & 1 deletion src/include/Platform/AutoImports.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ struct FindImportsVisitor : public Luau::AstVisitor
{
private:
std::optional<size_t> previousRequireLine = std::nullopt;
bool visitedRootBlock = false;

public:
std::optional<size_t> firstRequireLine = std::nullopt;
Expand All @@ -36,6 +37,15 @@ struct FindImportsVisitor : public Luau::AstVisitor
}

bool containsRequire(const std::string& module) const;
/// Get the line number where a specific require/local is defined
/// Returns std::nullopt if the module is not found
std::optional<size_t> getRequireDefinitionLine(const std::string& module) const;
/// Get the last require line (end of all requires block)
/// Returns std::nullopt if no requires exist
[[nodiscard]] std::optional<size_t> getLastRequireLine() const
{
return previousRequireLine;
}
bool visit(Luau::AstStatLocal* local) override;
bool visit(Luau::AstStatBlock* block) override;
};
Expand All @@ -47,4 +57,6 @@ lsp::CompletionItem createSuggestRequire(const std::string& name, const std::vec
size_t computeMinimumLineNumberForRequire(const FindImportsVisitor& importsVisitor, size_t hotCommentsLineNumber);
size_t computeBestLineForRequire(
const FindImportsVisitor& importsVisitor, const TextDocument& textDocument, const std::string& require, size_t minimumLineNumber);
}
/// Compute the line number for type imports (after all requires)
size_t computeLineForTypeImport(const FindImportsVisitor& importsVisitor, size_t minimumLineNumber);
} // namespace Luau::LanguageServer::AutoImports
115 changes: 103 additions & 12 deletions src/include/Platform/RobloxPlatform.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,22 @@ NLOHMANN_DEFINE_OPTIONAL(RobloxDefinitionsFileMetadata, CREATABLE_INSTANCES, SER
struct RobloxFindImportsVisitor : public Luau::LanguageServer::AutoImports::FindImportsVisitor
{
public:
BaseClient* client = nullptr;
std::optional<size_t> firstServiceDefinitionLine = std::nullopt;
std::optional<size_t> lastServiceDefinitionLine = std::nullopt;
std::map<std::string, Luau::AstStatLocal*> serviceLineMap{};

/// The name of the "Packages" local variable (e.g., "Packages")
/// Empty if not found
std::string packagesLocalName;
/// Line where the Packages local is defined
std::optional<size_t> packagesDefinitionLine = std::nullopt;
/// The name of the package ancestor local (e.g., "GameCollections")
/// Used to generate `local PackageName = script:FindFirstAncestor("PackageName")`
std::string packageAncestorLocalName;
/// Line where the package ancestor local is defined
std::optional<size_t> packageAncestorDefinitionLine = std::nullopt;

size_t findBestLineForService(const std::string& serviceName, size_t minimumLineNumber)
{
if (firstServiceDefinitionLine)
Expand All @@ -37,32 +49,111 @@ struct RobloxFindImportsVisitor : public Luau::LanguageServer::AutoImports::Find
return lineNumber;
}

/// Check if this is a script:FindFirstAncestor("PackageName") call
static std::optional<std::string> getFindFirstAncestorName(Luau::AstExpr* expr)
{
auto* call = expr->as<Luau::AstExprCall>();
if (!call || call->args.size != 1)
return std::nullopt;

// Check for script:FindFirstAncestor(...)
auto* indexExpr = call->func->as<Luau::AstExprIndexName>();
if (!indexExpr || std::string(indexExpr->index.value) != "FindFirstAncestor")
return std::nullopt;

auto* scriptGlobal = indexExpr->expr->as<Luau::AstExprGlobal>();
if (!scriptGlobal || std::string(scriptGlobal->name.value) != "script")
return std::nullopt;

// Get the string argument
auto* arg = call->args.data[0]->as<Luau::AstExprConstantString>();
if (!arg)
return std::nullopt;

return std::string(arg->value.data, arg->value.size);
}

/// Check if this is X.Parent where X is a known local
std::optional<std::string> getParentOfLocal(Luau::AstExpr* expr) const
{
auto* indexExpr = expr->as<Luau::AstExprIndexName>();
if (!indexExpr || std::string(indexExpr->index.value) != "Parent")
return std::nullopt;

auto* localRef = indexExpr->expr->as<Luau::AstExprLocal>();
if (!localRef)
return std::nullopt;

return std::string(localRef->local->name.value);
}

bool handleLocal(Luau::AstStatLocal* local, Luau::AstLocal* localName, Luau::AstExpr* expr, unsigned int startLine, unsigned int endLine) override
{
if (!isGetService(expr))
return false;
// Check for GetService pattern
if (isGetService(expr))
{
firstServiceDefinitionLine = !firstServiceDefinitionLine.has_value() || firstServiceDefinitionLine.value() >= startLine
? startLine
: firstServiceDefinitionLine.value();
lastServiceDefinitionLine =
!lastServiceDefinitionLine.has_value() || lastServiceDefinitionLine.value() <= endLine ? endLine : lastServiceDefinitionLine.value();
serviceLineMap.emplace(std::string(localName->name.value), local);
return true;
}

// Check for script:FindFirstAncestor("PackageName") pattern
if (auto ancestorName = getFindFirstAncestorName(expr))
{
packageAncestorLocalName = std::string(localName->name.value);
packageAncestorDefinitionLine = startLine;
return true;
}

// Check for X.Parent pattern (usually Packages = PackageName.Parent)
if (auto parentOf = getParentOfLocal(expr))
{
// This is likely the "Packages" local
packagesLocalName = std::string(localName->name.value);
packagesDefinitionLine = endLine;
return true;
}

firstServiceDefinitionLine = !firstServiceDefinitionLine.has_value() || firstServiceDefinitionLine.value() >= startLine
? startLine
: firstServiceDefinitionLine.value();
lastServiceDefinitionLine =
!lastServiceDefinitionLine.has_value() || lastServiceDefinitionLine.value() <= endLine ? endLine : lastServiceDefinitionLine.value();
serviceLineMap.emplace(std::string(localName->name.value), local);
return false;
}

return true;
[[nodiscard]] bool hasPackagesLocal() const
{
return !packagesLocalName.empty();
}

[[nodiscard]] bool hasPackageAncestorLocal() const
{
return !packageAncestorLocalName.empty();
}

[[nodiscard]] size_t getMinimumRequireLine() const override
{
size_t minLine = 0;

// After last service definition (e.g., game:GetService)
if (lastServiceDefinitionLine)
return *lastServiceDefinitionLine + 1;
minLine = *lastServiceDefinitionLine + 1;

// After Packages definition (e.g., local Packages = X.Parent)
if (packagesDefinitionLine && *packagesDefinitionLine >= minLine)
minLine = *packagesDefinitionLine + 1;

return 0;
return minLine;
}

[[nodiscard]] bool shouldPrependNewline(size_t lineNumber) const override
{
return lastServiceDefinitionLine && lineNumber - *lastServiceDefinitionLine == 1;
// Add newline separator if inserting right after services or Packages definition
if (lastServiceDefinitionLine && lineNumber - *lastServiceDefinitionLine == 1)
return true;
if (packagesDefinitionLine && lineNumber - *packagesDefinitionLine == 1)
return true;
return false;
}
};

Expand Down
Loading