LSP Architecture

PynescriptLanguageServer lifecycle, Workspace document model, capability declaration, and STDIO transport.

This page

LSP Architecture

Abstract

The language server is a single-process pygls application. It owns a Workspace of open TextDocumentState records, re-parses on every mutation, and dispatches LSP methods to pure-ish feature handlers that take (params, source) rather than holding server globals. Capabilities are declared once at initialize; transport defaults to STDIO for editor integration.

Conceptual model

Diagram

Rendering…

Interface surface

Process entry

# Editable install
pip install -e ".[lsp]"
python -m pynescript.langserver   # same as pyne-lsp / pynescript-lsp
make run-lsp

Entry: src/pynescript/langserver/__main__.py constructs PynescriptLanguageServer() and calls server.start_io() (STDIO JSON-RPC).

pyproject.toml registers:

pyne-lsp = "pynescript.langserver.__main__:main"
pynescript-lsp = "pynescript.langserver.__main__:main"   # alias

Server class

PynescriptLanguageServer (server.py):

  • Subclasses pygls.lsp.server.LanguageServer with name="Pynescript" and version=__version__ from src/pynescript/__about__.py (0.3.14).
  • Instantiates self.pine_workspace = Workspace().
  • Registers handlers in setup_method_handlers() via @self.feature(...).

Document sync

EventBehavior
didOpenput_document → parse/lint → push diagnostics
didChangeIncremental or full-text apply → re-parse/lint → push diagnostics
didCloseRemove document; publish empty diagnostic list
didSaveRe-publish diagnostics for current buffer

Sync options (config.get_server_capabilities):

  • TextDocumentSyncKind.Incremental
  • open_close=True
  • save with include_text=True
  • will_save / will_save_wait_until off

Capability set (declared)

From config.py:

  • Diagnostic provider (identifier="pynescript-diagnostics", workspace_diagnostics=False in options; workspace pull handler still exists)
  • Completion: trigger ".", resolve_provider=True
  • Hover, definition, references
  • Document + workspace symbols
  • Full + range formatting
  • Code action (Convert to Pine v6) + executeCommand pynescript.convertToV6
  • Inlay hints (resolve_provider=False)
  • Semantic tokens full (legend of standard token types/modifiers; range=false)
  • Signature help is not advertised.

File filters (get_filter_options): *.pine, *.pinev5, *.pinev6 under language id pinescript. .pyne is not in this helper — VS Code still maps it via the extension contribution (extensions: .pyne first).

Internals

Workspace

Workspace (workspace.py) maps uri → TextDocumentState:

TextDocumentState
  uri, source, version
  ast | None
  diagnostics: list[LintWarning]
  parse_error, parse_error_line

_parse_and_lint:

  1. parse(source, filename=uri) — on success, lint_script(source, filename=uri).
  2. On exception: clear AST, store parse_error string, extract line via regex line[:\s]+(\d+).

Incremental edits: _apply_text_edit splits on \n, pads the line list when the range sits at/past EOF (append without a trailing newline), clamps columns, then replaces the range. Out-of-range positions no longer leave a stale buffer. Whole-document change events replace source wholesale. Identical text after apply skips re-parse/lint (version still updates).

Diagnostics conversion (_lint_warnings_to_diagnostics):

  • Map severity strings → DiagnosticSeverity.
  • Source tag "PineScript", code from lint rule.
  • Append synthetic E001 error for parse failures at the extracted line.

Feature dispatch pattern

Handlers are functions, not server methods, e.g.:

@self.feature(lsp.TEXT_DOCUMENT_COMPLETION)
def text_completion(params):
    source = self.pine_workspace.get_source(params.text_document.uri)
    return completion_feature.handle_completion(params, source)

Definition / references / symbols / hover / completion / inlay / semantic tokens receive the workspace-cached AST (tree=doc.ast) so they skip a redundant parse. Pass tree=None when the last parse failed. Omit tree only in isolated tests (handler then parses from source).

Pull diagnostics

  • textDocument/diagnosticRelatedFullDocumentDiagnosticReport with result_id=f"{uri}-{version}".
  • workspace/diagnostic → one full report per open document from get_all_diagnostics().

Workspace symbols

workspace/symbol walks each open document’s AST via _collect_workspace_symbols: FunctionDef → Function, TypeDef → Class, EnumDef → Enum, Assign to Name → Variable. Filter is case-insensitive substring on params.query.

Invariants and edge cases

  1. Parse failure is non-fatal for the process. AST-dependent features return None or []; diagnostics still surface E001.
  2. Source of truth is the open buffer, not disk. Save only re-publishes; it does not re-read files.
  3. Incremental sync pads/clamps rather than dropping the edit. Ranges at EOF grow the line list; columns are clamped to the current line length.
  4. Version is stored and used in pull-diagnostic result_id; the server does not reject out-of-order versions itself.
  5. Nuitka onefile embeds providers data and may rely on encrypted metadata — see builtin metadata. Architecture of handlers is identical to the pure-Python path.

Worked example — minimal initialize

Client → server (conceptual):

{
  "method": "initialize",
  "params": {
    "capabilities": {},
    "clientInfo": { "name": "example", "version": "1" }
  }
}

Server returns InitializeResult with serverInfo.name = "Pynescript Language Server" and the full ServerCapabilities table from get_server_capabilities().

Failure modes

SymptomLikely cause
No diagnostics on openClient did not send didOpen or language id is not pinescript
Completions emptyMetadata file missing/undecryptable; see builtin metadata
Format no-opsParse error (handler returns []) or unparsed text equals source
Extension can't spawn serverpynescript-lsp not on PATH; check VS Code extension

See also