Skip to main content

LSP SDK

The LSP SDK provides target-aware Language Server Protocol integration. Unlike traditional LSP setups that configure paths globally, RBS resolves dependencies per-target — your editor’s autocomplete, go-to-definition, and diagnostics reflect only the dependencies your specific target can actually import.

Why target-aware LSP?

Traditional LSP setups give you completions for every package installed on your system. RBS does it differently:
Traditional LSPRBS LSP
ScopeAll installed packagesOnly your target’s declared deps
AccuracyMay suggest unrelated packagesOnly suggests what you can actually import
ConsistencyVaries by developer machineSame results for everyone (hermetic)
ConfigurationManual path setupAutomatic from build graph

How it works

  1. You open a file — e.g., src/main.py in your editor.
  2. RBS resolves the target — determines that src/main.py belongs to //src:app.
  3. Dependencies are analyzed//src:app declares deps = ["@pip//requests", "@pip//flask", ":utils"].
  4. Paths are resolved — dependencies map to filesystem paths in .rbs/external-deps/.
  5. LSP is configured — the language server receives only the relevant paths.
  6. You get accurate completions — autocomplete suggests requests and flask, but not unrelated packages.

Quick start

1. Load a built-in provider

In your WORKSPACE.rbs:
# Python LSP support
load("@rbs//rules/python:lsp.rbs", "python_lsp_provider")

# TypeScript LSP support
load("@rbs//rules/typescript:lsp.rbs", "typescript_lsp_provider")

# Go LSP support
load("@rbs//rules/go:lsp.rbs", "go_lsp_provider")

2. Sync dependencies

# Sync all dependencies for LSP resolution
rbs sync //...

# Sync specific targets
rbs sync //myapp:server //lib:utils

3. Connect your editor

Your editor connects to the LSP through the RBS server automatically when you open files. If you’re building a custom integration, connect via:
  • WebSocket: ws://your-server/ws/lsp/{language}
  • REST API: http://your-server/api/lsp/*

Built-in providers

Python

  • Server: python-lsp-server (pylsp)
  • Extensions: .py, .pyi
  • Features: Jedi-based completion, rope refactoring, pyflakes linting

TypeScript / JavaScript

  • Server: typescript-language-server
  • Extensions: .ts, .tsx, .js, .jsx, .mjs, .cjs
  • Features: Full TypeScript/JavaScript support with type checking

Go

  • Server: gopls
  • Extensions: .go
  • Features: Official Go language server with full LSP support

Creating custom LSP providers

The SDK-first design means you can define LSP support for any language through configuration:
native.lsp_provider(
    name = "my_language",
    language = "my_language",
    file_extensions = [".ml", ".mli"],
    server = "@custom//my-lsp-server",
    server_args = ["--stdio"],
    resolve_paths = """
def resolve(ctx):
    paths = []
    for dep in ctx.target.deps:
        paths.append(ctx.get_dep_path(dep))
    return paths
""",
    initialization_options = {
        "my.option": True,
    },
    root_markers = ["my_project.json", "BUILD.rbs"],
)

Parameters

ParameterTypeRequiredDescription
namestringYesProvider name.
languagestringYesLanguage identifier.
file_extensionslistYesFile extensions to handle.
serverstringYesServer binary reference.
server_argslistNoArguments passed to the server.
resolve_pathsstringNoRBS DSL function to resolve dependency paths.
initialization_optionsdictNoLSP initialization options sent to the server.
root_markerslistNoFiles that indicate the project root.

Server references

The server parameter supports multiple formats:
# From an external dependency
server = "@pip//python-lsp-server"
server = "@npm//typescript-language-server"

# From a toolchain binary
server = ":gopls"

# From system PATH (for development/testing)
server = "gopls"

Path resolution function

The resolve_paths function receives a context object with access to the target’s build graph:
resolve_paths = """
def resolve(ctx):
    # ctx.target       — the target owning the file
    # ctx.target.deps  — list of declared dependencies
    # ctx.target.srcs  — list of source files
    # ctx.workspace    — workspace root path
    # ctx.platform     — current platform (e.g., "darwin-arm64")
    # ctx.get_dep_path(dep) — filesystem path for a dependency

    paths = []
    for dep in ctx.target.deps:
        paths.append(ctx.get_dep_path(dep))
    return paths
"""

Initialization options

Use ${RESOLVED_PATHS} as a placeholder that will be replaced with the resolved dependency paths:
initialization_options = {
    "python.analysis.extraPaths": "${RESOLVED_PATHS}",
}

REST API

List providers

GET /api/lsp/providers
{
  "providers": [
    {
      "name": "python",
      "language": "python",
      "file_extensions": [".py", ".pyi"],
      "server": "@pip//python-lsp-server",
      "root_markers": ["pyproject.toml", "BUILD.rbs"]
    }
  ]
}

Get a provider

GET /api/lsp/providers/{language}

List running servers

GET /api/lsp/servers
{
  "servers": [
    {
      "language": "python",
      "provider": "python",
      "server": "@pip//python-lsp-server",
      "pid": 12345,
      "configured": 3
    }
  ]
}

Start / stop a server

POST /api/lsp/{language}/start
POST /api/lsp/{language}/stop

Resolve a file to its target

Find which target a file belongs to and what dependency paths are resolved:
POST /api/lsp/resolve
Content-Type: application/json

{
  "path": "src/main.py"
}
{
  "file": "src/main.py",
  "language": "python",
  "target": {
    "name": "app",
    "package": "//src",
    "kind": "py_binary"
  },
  "deps": ["@pip//requests", "@pip//flask", ":utils"],
  "paths": [
    ".rbs/external-deps/darwin-arm64/python/requests",
    ".rbs/external-deps/darwin-arm64/python/flask"
  ]
}

WebSocket protocol

Connect

WebSocket: /ws/lsp/{language}
On connection:
{
  "type": "connected",
  "language": "python",
  "server": "python"
}

Open a file (triggers path resolution)

{
  "type": "file_opened",
  "path": "src/main.py"
}
Response:
{
  "type": "file_resolved",
  "file": "src/main.py",
  "language": "python",
  "target": { "name": "app", "package": "//src", "kind": "py_binary" },
  "deps": ["@pip//requests", "@pip//flask", ":utils"],
  "paths_configured": 3
}

LSP requests/responses

Standard JSON-RPC 2.0 LSP messages are proxied through the WebSocket:
// Request: autocomplete at line 10, character 5
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "textDocument/completion",
  "params": {
    "textDocument": { "uri": "file:///src/main.py" },
    "position": { "line": 10, "character": 5 }
  }
}

// Response
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "items": [
      { "label": "requests", "kind": 9 },
      { "label": "flask", "kind": 9 }
    ]
  }
}

Editor SDK integration

import { RBSLSPClient } from "@rbs/editor-sdk";

const client = new RBSLSPClient("python", {
  serverUrl: "https://your-project.rbs.dev",
});

await client.connect();

// Open a file (triggers target-aware path resolution)
client.fileOpened("src/main.py");

// Get completions
const completions = await client.completion(
  "file:///src/main.py", 10, 5
);

// Go to definition
const definition = await client.definition(
  "file:///src/main.py", 15, 12
);

Troubleshooting

No completions

  1. Run rbs sync //your:target to ensure dependencies are downloaded.
  2. Verify the file belongs to a target with declared deps.
  3. Check that paths are resolved: POST /api/lsp/resolve with your file path.

LSP server not starting

# Check if the provider is registered
curl http://your-server/api/lsp/providers

# Check server status
curl http://your-server/api/lsp/servers

Missing dependencies

# Sync all dependencies
rbs sync //...

# Verify external-deps directory
ls -la .rbs/external-deps/