Skip to content
710 words 4 min read

LCUI Language Service ​

NOTE

The following content is taken from the code structure of LOICollectionA 1.17.1 and may differ in later versions.

Purpose ​

src/LOICollectionA/frontend/lsp/ is the language service core for .lcui scripts. It reuses the compiler front end (Lexer → Parser → ComponentExpander → SemanticAnalyzer) to produce diagnostics, symbols, completions, hovers and definitions, and wraps them into JSON-RPC messages per the LSP specification.

It does protocol and analysis only, never I/O:

  • no listening sockets, no stdio access, no threads
  • input is a chunk of bytes, output is the bytes to send back
  • the transport (stdio, pipes, WebSocket, an in-game command) is the caller's choice

Two reasons for this split: the plugin itself must not host a resident server just to support editors, and the language core can be driven directly by gtest (tests/common/frontend/LspTest.cpp) without any inter-process communication.

Module Layout ​

FileResponsibility
Protocol.h / .cppLSP data types (Position / Range / Diagnostic / CompletionItem / Hover / Location), JSON conversion, JSON-RPC message construction, Content-Length framing and deframing
Analysis.h / .cppRuns the full front end over a script text and returns a DocumentAnalysis (diagnostics + symbol table)
LanguageServer.h / .cppDocument sync (full sync), request dispatch, capability declaration, plus the pure functions completionsAt / hoverAt / definitionAt

Supported Capabilities ​

LSP methodBehavior
initializeReturns textDocumentSync: 1 (full sync), completionProvider (trigger character .), hoverProvider, definitionProvider, codeActionProvider
textDocument/didOpenRegisters the document and immediately publishes diagnostics
textDocument/didChangeReplaces the whole text (last contentChanges entry wins) and republishes diagnostics
textDocument/didCloseRemoves the document and clears its diagnostics
textDocument/completionSee "Completion Rules"
textDocument/hoverReturns the declaration signature of the identifier under the cursor
textDocument/definitionReturns the Location of the declaration
textDocument/codeActionReturns a quickfix for diagnostics inside the requested range
shutdown / exitTerminates cleanly

Unimplemented methods return -32601 Method not found; $/... notifications are ignored silently.

Wiring It Up ​

LanguageServer::handle accepts an arbitrary chunk of bytes and returns the bytes to send back to the client (possibly several messages, possibly empty). The caller only has to feed in what it reads and write out what it gets:

cpp
lsp::LanguageServer server;

std::string outbound = server.handle(bytesReadFromClient);
if (!outbound.empty())
    writeToClient(outbound);

Incomplete messages are buffered internally: a frame split across several reads is neither lost nor mis-framed. A single frame larger than 64 MiB, or one that fails JSON parsing, is dropped so the buffer cannot grow without bound.

WARNING

Encoding uses dump(-1, ' ', false, error_handler_t::replace). Script sources may contain invalid UTF-8 (the fuzz harness covers that path), and the default strict handler throws — this project compiles with set_exceptions("none"), where a throw terminates the process. Use the same arguments for any new serialization point.

Symbol Collection Rules ​

analyze walks the AST after semantic analysis, so methods merged into their target class by impl blocks and nodes generated by component expansion are all included:

SourceSymbol kindContainer
classClassglobal
class field / static fieldField / StaticFieldclass name
class method / static methodMethod / StaticMethodclass name
traitInterfaceglobal
method inside a traitMethodtrait name
methods and associated constants in implMethod / StaticMethod / Constantimpl target type name
top-level funcFunctionglobal
top-level let / constVariable / Constantglobal

detail holds a readable signature, e.g. size() -> int or add(x: int, y: int) -> int.

Completion Rules ​

After . (member completion): the identifier before the dot is the receiver.

  • this → the class of the nearest class <Name> declaration before the cursor
  • a known class name → that class's static members
  • a variable with a type annotation (let deck: Deck = ...) → members of the annotated type
  • unknown receiver → an empty list (no guessed suggestions)

Anywhere else (global completion): global symbols + keywords + primitive types + builtin functions (tr / score / entity / print / println / error) + form classes (CustomForm / MessageBox / PaginatedForm / ScriptForm).

Known Limitations ​

  • Local variables (function parameters, block-scoped let) are not in the symbol table, so they do not participate in completion or navigation
  • import is not resolved; cross-file symbols require the caller to expand the import graph and feed each document separately
  • Every request re-runs the whole front end — there is no incremental analysis; the cost is negligible at single-file script scale
  • Diagnostic ranges are one character long (the diagnostic engine records a start position, not the token length)

LOICollectionA · A ready-to-use multifunctional plugin set for LeviLamina