lsp

package
v0.5.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 11, 2026 License: MIT Imports: 11 Imported by: 0

README

lsp

lsp is a small Language Server Protocol client for the ds4 agent loop. It drives a single persistent language-server subprocess with in-memory documents, so a DeepSeek generation/self-correction loop can compile-check its own output: open a document, apply edits, and read back diagnostics, hover, symbols, and completions.

It is built on charmbracelet/x/powernap and pairs with the lsptool subpackage, which adapts a *lsp.Client into ds4go.ToolHandler values.

How it Works

  1. One persistent server: lsp.New launches the configured language server (which must be on PATH), runs the LSP initialize handshake, and declares a workspace folder from RootDir. The server stays up for the life of the Client.
  2. In-memory documents: Open / Update / Close sync document text to the server via didOpen / didChange / didClose notifications. Documents need not exist on disk — virtual paths under RootDir work for pure in-memory analysis. The client tracks a per-document version internally.
  3. Push-based diagnostics: the server publishes diagnostics asynchronously. The client buffers the latest set per document. WaitForDiagnostics correlates on the document version when the server reports one (honoring the full timeout); for servers that don't report a version, it falls back to a time-settle heuristic (FirstWait then SettleWait). Stale diagnostics from a previous edit or reopen are dropped so they are never returned as current.
  4. URI normalization: documents are keyed by their normalized (percent-decoded) filesystem path, so a server that echoes back a differently-encoded URI than the client sent still resolves to the same document.

Usage

Start a server, open an in-memory document, edit it, and read diagnostics:

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/NimbleMarkets/ds4go/lsp"
)

func main() {
	ctx := context.Background()

	c, err := lsp.New(ctx, lsp.ServerConfig{Command: "gopls", RootDir: "/abs/work"})
	if err != nil {
		panic(err)
	}
	defer c.Shutdown(ctx)

	uri := c.URI("main.go")
	_ = c.Open(ctx, uri, "go", "package main\nfunc main() {}\n")

	// Apply generated code, then check it.
	_, _ = c.Update(ctx, uri, "package main\nfunc main() { undefinedFunc() }\n")
	diags, _ := c.WaitForDiagnostics(ctx, uri, 5*time.Second) // empty == clean
	for _, d := range diags {
		fmt.Printf("%d:%d [%s] %s\n", d.Line, d.Col, d.Severity, d.Message)
	}
}

Hover, Symbols, and Completion provide additional context for a model:

md, _ := c.Hover(ctx, uri, 2, 14)                 // hover markdown at 1-based line:col
syms, _ := c.Symbols(ctx, uri)                    // flattened document outline
labels, more, _ := c.Completion(ctx, uri, 2, 14, 25) // up to 25 labels + count truncated
Common servers

gopls (Go):

lsp.ServerConfig{Command: "gopls", RootDir: moduleDir}

lua-language-server (LuaLS):

lsp.ServerConfig{
	Command: "lua-language-server",
	RootDir: projectDir,
	Settings: map[string]any{"Lua": map[string]any{
		"diagnostics": map[string]any{"globals": []string{"vim"}},
	}},
}

Tool loop

The lsptool subpackage wraps a *lsp.Client as ds4go.ToolHandler values so a model running in a ds4go.ToolLoop can query the language server and self-correct. Register the tools you want on a ds4go.ToolRegistry:

import (
	ds4go "github.com/NimbleMarkets/ds4go"
	"github.com/NimbleMarkets/ds4go/lsp/lsptool"
)

reg := ds4go.NewToolRegistry()
reg.MustRegister(lsptool.NewDiagnosticsTool(client))
reg.MustRegister(lsptool.NewHoverTool(client))
reg.MustRegister(lsptool.NewSymbolsTool(client))
reg.MustRegister(lsptool.NewCompletionTool(client))

Each tool decodes its JSON arguments and enforces that uri is present. All positions are 1-based line / character.

Available Tools
lsp_diagnostics
  • Description: Report language-server diagnostics (errors/warnings) for an open document. Optionally pass updated code to re-check first.
  • Arguments:
    {
      "uri": "document URI",
      "code": "optional new full document text to apply before checking"
    }
    
    With code, the document is updated and re-checked before reporting; without it, the latest buffered diagnostics are returned (which may lag the most recent edit).
lsp_hover
  • Description: Hover info (type/doc) at a 1-based line and character in a document.
  • Arguments:
    {
      "uri": "document URI",
      "line": 1,
      "character": 1
    }
    
lsp_symbols
  • Description: List the document outline (functions/types) for a document.
  • Arguments:
    {
      "uri": "document URI"
    }
    
lsp_completion
  • Description: Completion suggestions at a 1-based line and character in a document.
  • Arguments:
    {
      "uri": "document URI",
      "line": 1,
      "character": 1
    }
    

Configuration Options

The lsp.ServerConfig struct accepts the following fields:

Field Type Description
Command string Required. Language-server executable, looked up on PATH.
Args []string Process arguments.
RootDir string Workspace root (absolute path); "" is allowed but some servers only emit diagnostics when a workspace is declared and points at real files.
InitOptions map[string]any LSP initializationOptions.
Settings map[string]any workspace/configuration settings.
Environment map[string]string Extra environment variables for the server process.
Timeout time.Duration Per-request RPC timeout. Zero means no timeout.
ShutdownTimeout time.Duration Bounds the graceful shutdown handshake before the server is force-killed. Zero falls back to DefaultShutdownTimeout (5s).
FirstWait time.Duration Bounds how long WaitForDiagnostics waits for the first publish before assuming a silent server is clean. Zero falls back to DefaultFirstWait (5s). Raise it for servers with heavy cold-start latency.
SettleWait time.Duration Time-settle window for servers that don't report document versions. Zero falls back to DefaultSettleWait (300ms).

Caveats

  • Diagnostics are best-effort, not a guarantee. WaitForDiagnostics correlates on the document version when the server reports one; otherwise it uses the time-settle heuristic. A pathologically slow server may publish after the wait returns. Tune FirstWait above your server's cold first-publish latency to avoid a premature "clean" result.
  • Workspace required for semantic diagnostics. Some servers (e.g. LuaLS resolving require()) only produce full semantic diagnostics when RootDir points at the real project on disk. Pure in-memory documents reliably yield syntactic diagnostics.
  • No auto-restart. v1 does not restart a crashed server. Query methods return ErrServerDown; recreate the Client to recover.

Documentation

Overview

Package lsp is a small client for Language Server Protocol servers, built for driving generation/self-correction loops: start one persistent server, sync in-memory documents, and query diagnostics, hover, symbols, and completion.

See the sections below for usage examples, common server configurations (gopls, LuaLS), integrating the client as ds4go.ToolHandler values via the lsptool subpackage, and important caveats about the push-based diagnostics model.

Usage

Start a server, open an in-memory document, edit it, and read diagnostics:

ctx := context.Background()
c, err := lsp.New(ctx, lsp.ServerConfig{Command: "gopls", RootDir: "/abs/work"})
if err != nil { return err }
defer c.Shutdown(ctx)

uri := c.URI("main.go")
_ = c.Open(ctx, uri, "go", initialSrc)
_, _ = c.Update(ctx, uri, generatedSrc)
diags, _ := c.WaitForDiagnostics(ctx, uri, 5*time.Second) // empty == clean

Common servers

gopls (Go):

lsp.ServerConfig{Command: "gopls", RootDir: moduleDir}

lua-language-server (LuaLS):

lsp.ServerConfig{
    Command: "lua-language-server",
    RootDir: projectDir,
    Settings: map[string]any{"Lua": map[string]any{
        "diagnostics": map[string]any{"globals": []string{"vim"}},
    }},
}

Tool loop

Wrap the client as ds4go tools via the lsptool subpackage and register them on a ds4go.ToolRegistry so a model can self-correct.

Caveats

  • Diagnostics are push-based. WaitForDiagnostics correlates on the document version when the server reports one; otherwise it falls back to a time-settle heuristic (FirstWait then SettleWait). A pathologically slow server may publish after the wait returns. This is best-effort, not a guarantee.
  • Some servers only emit diagnostics when a workspace is declared and RootDir points at real files on disk. The client always declares a workspace folder from RootDir; for full semantic diagnostics (e.g. LuaLS resolving require()) point RootDir at the real project. Pure in-memory documents reliably yield syntactic diagnostics.
  • v1 does not auto-restart a crashed server. Query methods return ErrServerDown; recreate the Client to recover.

Index

Constants

View Source
const (
	// DefaultFirstWait bounds how long WaitForDiagnostics waits for the first
	// publish before assuming a silent server is clean. This is a tradeoff: too
	// low and a cold server that has not yet published its first diagnostics
	// gets a freshly-opened broken file reported as clean; too high and a server
	// that simply stays silent on clean files makes every first wait block this
	// long. 5s clears the cold first-publish latency of common servers (gopls,
	// LuaLS) for a single small file. Servers with heavier cold starts should
	// raise ServerConfig.FirstWait.
	DefaultFirstWait  = 5 * time.Second
	DefaultSettleWait = 300 * time.Millisecond
)

Defaults for the diagnostics time-settle fallback.

View Source
const DefaultShutdownTimeout = 5 * time.Second

DefaultShutdownTimeout bounds the graceful shutdown handshake when ServerConfig.ShutdownTimeout is unset.

Variables

View Source
var (
	// ErrServerDown is returned by query methods when the language server
	// process is not running. v1 does not auto-restart; recreate the Client.
	ErrServerDown = errors.New("lsp: language server not running")
	// ErrDiagnosticsTimeout is returned by WaitForDiagnostics when the wait
	// elapses before diagnostics settle. The returned snapshot is best-effort.
	ErrDiagnosticsTimeout = errors.New("lsp: timed out waiting for diagnostics")
)

Sentinel errors returned by Client.

Functions

This section is empty.

Types

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client drives a single persistent language-server subprocess with in-memory documents. It is safe for concurrent use.

func New

func New(ctx context.Context, cfg ServerConfig) (*Client, error)

New launches and initializes a language server per cfg, returning a ready Client. The server command must be on PATH.

func (*Client) Close

func (c *Client) Close(ctx context.Context, uri string) error

Close stops tracking a document and notifies the server.

func (*Client) Completion

func (c *Client) Completion(ctx context.Context, uri string, line, col, limit int) (labels []string, truncated int, err error)

Completion returns up to limit completion labels at a 1-based (line, col). A non-positive limit means no cap.

func (*Client) Diagnostics

func (c *Client) Diagnostics(uri string) []Diagnostic

Diagnostics returns the latest buffered diagnostics for uri.

func (*Client) Hover

func (c *Client) Hover(ctx context.Context, uri string, line, col int) (string, error)

Hover returns hover markdown at a 1-based (line, col), or "" if none.

The client advertises contentFormat support during initialization, so compliant servers return MarkupContent. Servers that return plain strings or MarkedStrings will surface an error from the underlying RPC layer.

func (*Client) OnDiagnostics

func (c *Client) OnDiagnostics(cb func(uri string, version int32, diags []Diagnostic))

OnDiagnostics registers a callback fired on every publishDiagnostics. Pass nil to clear.

func (*Client) Open

func (c *Client) Open(ctx context.Context, uri, languageID, text string) error

Open registers an in-memory document and notifies the server.

func (*Client) Shutdown

func (c *Client) Shutdown(ctx context.Context) error

Shutdown gracefully stops the server, falling back to Kill on timeout.

func (*Client) Symbols

func (c *Client) Symbols(ctx context.Context, uri string) ([]Symbol, error)

Symbols returns the document outline for uri. The URI is converted to a filesystem path before sending to the server because powernap's RequestDocumentSymbols accepts a filepath; callers should still use c.URI(name) to construct a valid uri argument.

func (*Client) URI

func (c *Client) URI(name string) string

URI returns the file URI this Client uses for a document name (relative names are resolved under the configured RootDir).

func (*Client) Update

func (c *Client) Update(ctx context.Context, uri, text string) (int, error)

Update replaces a document's text (whole-document change) and notifies the server. Returns the new document version.

func (*Client) WaitForDiagnostics

func (c *Client) WaitForDiagnostics(ctx context.Context, uri string, timeout time.Duration) ([]Diagnostic, error)

WaitForDiagnostics waits for diagnostics for the current version of uri, returning a best-effort snapshot. On timeout it returns the snapshot plus ErrDiagnosticsTimeout. The document must be open.

type Diagnostic

type Diagnostic struct {
	Line     int
	Col      int
	Severity Severity
	Message  string
	Source   string
}

Diagnostic is a single problem reported for a document. Line and Col are 1-based (LSP wire positions are 0-based; we convert at the boundary).

type ServerConfig

type ServerConfig struct {
	Command     string            // executable, looked up on PATH
	Args        []string          // process arguments
	RootDir     string            // workspace root (absolute path); "" allowed
	InitOptions map[string]any    // LSP initializationOptions
	Settings    map[string]any    // workspace/configuration settings
	Environment map[string]string // extra environment variables

	// Timeout is the per-request timeout passed to the underlying RPC client.
	// Zero means no timeout.
	Timeout time.Duration

	// ShutdownTimeout bounds the graceful shutdown handshake before the server
	// is force-killed. Zero falls back to DefaultShutdownTimeout. This is
	// deliberately separate from Timeout (the per-request RPC timeout) so the
	// shutdown grace period can be tuned without affecting request latency.
	ShutdownTimeout time.Duration

	// FirstWait and SettleWait tune the time-settle fallback used by
	// WaitForDiagnostics when the server does not report a document version.
	// FirstWait also bounds how long WaitForDiagnostics waits for the first
	// publish before assuming a silent server is clean; once the server has
	// published a versioned diagnostic, the version-correlated wait honors the
	// full timeout instead. Set FirstWait above your server's cold first-
	// publish latency to avoid a premature "clean" result. Zero values fall
	// back to defaults (DefaultFirstWait/DefaultSettleWait).
	FirstWait  time.Duration
	SettleWait time.Duration
}

ServerConfig describes how to launch and initialize a language server.

type Severity

type Severity int

Severity classifies a Diagnostic.

const (
	SeverityError Severity = iota + 1
	SeverityWarning
	SeverityInformation
	SeverityHint
)

func (Severity) String

func (s Severity) String() string

type Symbol

type Symbol struct {
	Name string
	Line int
	Kind int // LSP SymbolKind (e.g. 12 = Function, 13 = Variable)
}

Symbol is one entry from a document outline. Line is 1-based.

Directories

Path Synopsis
Package lsptool adapts an *lsp.Client into ds4go.ToolHandler values so a model running in a ds4go.ToolLoop can query a language server.
Package lsptool adapts an *lsp.Client into ds4go.ToolHandler values so a model running in a ds4go.ToolLoop can query a language server.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL