Plugin - NightrainsRbx/RobloxLsp GitHub Wiki

Plugins are a powerful feature that can be used to modify how the language server works.

Setup

  1. Create a .lua file for the plugin, you can create it anywhere.
  2. Set robloxLsp.runtime.plugin to the path of that file, you can use an absolute or relative path.
  3. For better debugging, set robloxLsp.develop.enable to true, only use it while developing the plugin.
  4. Define a global function in the plugin file with the name of an event, it will be called when the event triggers.

VSCode Extension Location

  • Windows: %homepath%\.vscode\extensions\nightrains.robloxlsp-X.X.X\
  • WSL: \\wsl$\Ubuntu-20.04\home\USERNAME\.vscode-server\extensions\nightrains.robloxlsp-X.X.X\
  • Linux/MacOS: ~/.vscode/extensions/nightrains.robloxlsp-X.X.X/server/

Recommendations

Do not use print, os.execute or the io library, check Useful API below for alternative methods.

You can append ---@module path.to.file above a variable to get IntelliSense from a module.

Events

OnSetText

Changes the text that the language server reads from a file, called before parsing the text of a file into an Ast, must return a list of replacements or a string as a full replacement of the text.

Roblox LSP calculates the differences for offsetting the Ast to accommodate it to the original source text.

-- uri - The URI of the source file
-- text - The source text
function OnSetText(uri: string, text: string) -> string | {
    {
        -- The number of bytes at the beginning of the replacement
        start: number,
        -- The number of bytes at the end of the replacement
        finish: number,
        -- Text replacement
        text: string
    }
}

If robloxLsp.develop.enable is enabled, the result or errors will the outputted to extension\server\log\diffed.lua

Example

function OnSetText(uri, text)
    local diffs = {}
    for start, path, finish in text:gmatch("()import%(\"(.-)\"%)()") do
        table.insert(diffs, {
            start  = start,
            finish = finish - 1,
            text   = ("require(game.ReplicatedStorage.Libraries[\"%s\"])"):format(path),
        })
    end
    return diffs
end

x

OnCompletion

Called when triggering a completion suggestion, must return a list of completion items. Completion suggestions are cached and thus are not triggered every key stroke.

-- uri - The URI of the source file
-- text - The source text
-- offset - The position in bytes of the cursor
function OnCompletion(uri: string, text: string, offset: number) -> {CompletionItem}

CompletionItem is a table following this format https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#completionItem

NOTE: The text and offset passed may be transformed by OnSetText before OnCompletion is called.

If robloxLsp.develop.enable is enabled, errors will the outputted to extension\server\log\diffed.lua

Example

function OnCompletion(uri, text, offset)
    local items = {}
    if text:sub(0, offset):match("createElement%(\"$") then
        table.insert(items, {
            kind = 13,
            label = "Frame",
            detail = "Insert Frame"
        })
        table.insert(items, {
            kind = 13,
            label = "TextButton",
            detail = "Insert TextButton"
        })
    end
    return items
end

x

API

Some useful functions when making a plugin, the full language server can be accessed via plugins.

Use local module = require("name")

log (global)

Appends text to the log file for your workspace located at extension\server\log\.

function log.debug(...: string) -> void

proto

Sends a notification to the client, for specifications check: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/

You can use "window/logMessage" to log directly to the output, check the specification here.

function proto.notify(method: string, params: table) -> void

proto.define

Enums for CompletionItemKind

define.CompletionItemKind: {[string]: number}

Enums for MessageType

define.MessageType: {[string]: number}

files

Returns the text of a file, or nil if the file has not been loaded.

function files.getText(uri: string) -> string?

Same as files.getText, but without the changes made by OnSetText.

function files.getOriginText(uri: string) -> string?

Returns the AST of the file, or nil if the file has not been loaded.

function files.getAst(uri: string) -> Ast?

parser

Returns a list of all lines in a string. Call it with a colon.

function parser:lines(text: string) -> {string}

Returns the row and column of a byte position in a string.

function parser.calcline.rowcol(text: string, position: number) -> (number, number)

library.rbxlibs

Similar to Instance:IsA()

function rbxlibs.isA(className: string, otherClass: string) -> boolean

A set of all Roblox Instance class names.

rbxlibs.ClassNames: {[string]: true}

A set of all Roblox Services names

rbxlibs.Services: {[string]: true}

A set of all creatable instances class names.

rbxlibs.CreatableInstances: {[string]: true}

The full Roblox API Dump with DataTypes included.

rbxlibs.Api: table

utility

Converts a table into a readable string.

function utility.dump(table: table) -> string

Returns the content of a file, or nil if the file doesn't exists.

function utility.loadFile(path: string | path) -> string?

file-uri

Converts a path string into an URI

function uri.encode(path: string) -> string

Converts an URI into a path string.

function uri.decode(uri: string) -> string

json

Converts a JSON string into a table.

function json.decode(content: string) -> table

Converts a table into a JSON string.

function json.encode(table: table) -> string

library.rojo

A map of all scripts found in the rojo project, the format is the full name of the instance as the key, i.e. "ServerScriptService.Script", and the absolute path string of the source file as the value.

rojo.SourceMap: {[string]: string}

Returns the path to the rojo executable.

function rojo:getRojoPath() -> path | string

bee.filesystem

Returns the root path of the current workspace.

function fs.current_path() -> path

Creates a path object from a string.

function fs.path(path: string) -> path

Checks if a file/directory exists.

function fs.exists(path: path) -> boolean

Checks if a path points to a directory.

function fs.is_directory(path: path) -> boolean

Returns an iterator for the files inside a directory, similar to pairs.

function fs.pairs(path: path) -> function

Gets the absolute path.

function fs.absolute(path: path) -> path

Gets the relative path to another path.

function fs.relative(path: path, base: path) -> path

path (object)

Returns the name of the file/directory with the extension.

function path:filename() -> path

Returns the stem of the file (The name without the extension).

function path:stem() -> path

Returns the extension of the file.

function path:extension() -> path

Returns the path of the file/directory's parent.

function path:parent_path() -> path

Converts the path object into a string.

function path:__string() -> string

Returns the path with another path appended. Usage: local new = path / name

function path:__div(other: path | string) -> path

bee.subprocess

Opens a program in a separate process and returns a handle for reading/writing to the process's stdout/stdin/stderr. If fails returns nil and an error message.

It may not recognize the PATH env variable, in that case, you would have to manually find the executable using os.getenv("PATH") and the bee.filesystem library.

-- args - A table with the path of the executable followed by the arguments and optional fields.
function sp.spawn(
    args: {
        ...string,
        cwd: path?,
        stdin: boolean?,
        stdout: boolean?,
        stderr: boolean?
    }
) -> (process?, string)

process (object)

Wait for the process, may yield indefinitely.

function process:wait() -> void

Kills the process.

function process:kill() -> void

A file handle to the stdout.

process.stdout: file

A file handle to the stdin.

process.stdin: file

A file handle to the stderr.

process.stderr: file

The file object is the same as the one used by the standard io library.

Traverse the AST and getting type info.

Most people won't need to use the AST or type inference, but if you want to make more complex or precise plugins, you have to look into core.guide to traverse the AST and vm for looking into the type inference. You may also use find-source.

The arguments for vm are nodes of the AST. The most important functions are:

Returns a list of inferred types of an AstNode with the Luau type as a string and its source, including Luau types.

-- deep parameter must be 0
function vm.getInfers(source: AstNode, deep: number) -> {
    {
        type: string,
        level: number,
        source: AstNode | TypeDef
    }
}

Returns a list of definitions in the AST or type definitions of an AstNode.

-- deep parameter must be 0
function vm.getDefs(source: AstNode, deep: number) -> {AstNode | TypeDef}

Returns a list of inferred fields AST definitions or type definitions of an AstNode.

-- deep parameter must be 0
function vm.getFields(source: AstNode, deep: number) -> {AstNode | TypeDef}

For checking the type of function returns, search the returns in the AstNode or the TypeDef of the function and use the functions above.

TypeDef are Luau types in an AST-like structure.