展示 HN:使用 Go 创建一个完全的语言服务器,支持 3.17 规范
Show HN: Create a full language server in Go with 3.17 spec support

原始链接: https://github.com/owenrumney/go-lsp

## Go-LSP:Go 语言的语言服务器协议实现 Go-LSP 是一个 Go 库,旨在简化构建语言服务器协议 (LSP) 服务器,支持 LSP 3.17。它处理 JSON-RPC 框架和消息分发的复杂性,允许开发者专注于语言特定的逻辑。 该库提供对 LSP 功能的广泛支持,包括代码补全、悬停提示、诊断、格式化、重命名、语义标记、工作区符号等。它会根据实现的接口自动通告服务器能力,但也可以进行显式配置。 构建服务器涉及定义一个实现必需的 `LifecycleHandler` 接口和可选功能处理程序(例如 `HoverHandler`)的处理器结构体。服务器可以通过标准 I/O、TCP 或 WebSockets 运行。 Go-LSP 提供了服务器到客户端通信的工具(发布诊断信息、显示消息),并支持自定义 JSON-RPC 方法。它还包含通过 `log/slog` 进行的结构化日志记录,以及用于单元测试的综合测试工具 (`servertest`)。内置调试 UI 提供实时 LSP 流量检查和性能指标。 该库分为四个包:`server`、`lsp`、`servertest` 和 `internal/jsonrpc`。可通过 `go get github.com/owenrumney/go-lsp` 获取。

黑客新闻 新的 | 过去的 | 评论 | 提问 | 展示 | 工作 | 提交 登录 展示HN:用Go创建具有3.17规范支持的完整语言服务器 (github.com/owenrumney) 10 分,由 rumno0 2小时前 | 隐藏 | 过去的 | 收藏 | 2 评论 帮助 bbkane 8分钟前 | 下一个 [–] 以防万一我将来构建一个语言服务器,谢谢!接口看起来非常易于理解,调试服务器看起来也很不错。现在想想,为我的CLI框架[0]添加LSP可能真的非常酷(我已经有了shell的自动补全,如果这么容易,为什么不做一个编辑器插件呢..) 0: https://github.com/bbkane/wargreply SwiftyBug 5分钟前 | 上一个 [–] 非常好。现在我想构建一个语言服务器。如果我没有东西可以构建它就好了。 指南 | 常见问题 | 列表 | API | 安全 | 法律 | 申请YC | 联系 搜索:
相关文章

原文

A Go library for building Language Server Protocol servers. It handles JSON-RPC framing, message dispatch, and LSP type definitions so you can focus on your language logic.

This library targets LSP 3.17. The table below shows which parts of the specification are currently supported.

Category Feature Supported
Lifecycle initialize / shutdown / exit Yes
$/cancelRequest Yes
$/setTrace Yes
Text Document Sync didOpen / didChange / didClose Yes
didSave Yes
willSave / willSaveWaitUntil Yes
Language Features completion Yes
completionItem/resolve Yes
hover Yes
signatureHelp Yes
declaration Yes
definition Yes
typeDefinition Yes
implementation Yes
references Yes
documentHighlight Yes
documentSymbol Yes
codeAction Yes
codeAction/resolve Yes
codeLens Yes
codeLens/resolve Yes
documentLink Yes
documentLink/resolve Yes
documentColor / colorPresentation Yes
formatting Yes
rangeFormatting Yes
onTypeFormatting Yes
rename Yes
prepareRename Yes
foldingRange Yes
selectionRange Yes
callHierarchy Yes
semanticTokens (full / delta / range) Yes
linkedEditingRange Yes
moniker Yes
Workspace workspaceSymbol Yes
executeCommand Yes
didChangeWorkspaceFolders Yes
didChangeConfiguration Yes
didChangeWatchedFiles Yes
workspace/willCreateFiles Yes
workspace/willRenameFiles Yes
workspace/willDeleteFiles Yes
Window showMessage (server-to-client) Yes
showMessageRequest Yes
logMessage Yes
progress Yes
showDocument Yes
Diagnostics publishDiagnostics (server-to-client) Yes
LSP 3.17
Language Features typeHierarchy (prepare / supertypes / subtypes) Yes
inlayHint Yes
inlayHint/resolve Yes
inlineValue Yes
textDocument/diagnostic (pull) Yes
Workspace workspace/diagnostic Yes
workspace/codeLens/refresh Yes
workspace/semanticTokens/refresh Yes
workspace/inlayHint/refresh Yes
workspace/inlineValue/refresh Yes
workspace/diagnostic/refresh Yes
go get github.com/owenrumney/go-lsp

Define a handler struct that implements server.LifecycleHandler and any optional interfaces you need, then pass it to NewServer:

package main

import (
	"context"
	"os"

	"github.com/owenrumney/go-lsp/lsp"
	"github.com/owenrumney/go-lsp/server"
)

type Handler struct{}

func (h *Handler) Initialize(ctx context.Context, params *lsp.InitializeParams) (*lsp.InitializeResult, error) {
	return &lsp.InitializeResult{
		Capabilities: lsp.ServerCapabilities{
			HoverProvider: &lsp.HoverOptions{},
		},
	}, nil
}

func (h *Handler) Shutdown(ctx context.Context) error {
	return nil
}

func (h *Handler) Hover(ctx context.Context, params *lsp.HoverParams) (*lsp.Hover, error) {
	return &lsp.Hover{
		Contents: lsp.MarkupContent{
			Kind:  lsp.MarkupKindMarkdown,
			Value: "Hello from the server",
		},
	}, nil
}


func main() {
	srv := server.NewServer(&Handler{})

	if err := srv.Run(context.Background(), server.RunStdio()); err != nil {
		os.Exit(1)
	}
}

The server accepts any io.ReadWriteCloser, so you can use it with stdio, TCP, WebSockets, or anything else:

// Stdio (most common for editors)
srv.Run(ctx, stdRWC{})

// TCP
ln, _ := net.Listen("tcp", ":9999")
conn, _ := ln.Accept()
srv.Run(ctx, conn) // net.Conn implements io.ReadWriteCloser

// WebSocket (using nhooyr.io/websocket)
ws, _ := websocket.Accept(w, r, nil)
srv.Run(ctx, websocket.NetConn(ctx, ws, websocket.MessageText))

The server automatically advertises capabilities based on which interfaces your handler implements. If your handler satisfies HoverHandler, the server tells the client it supports hover -- you don't need to wire that up yourself. You can still set capabilities explicitly in Initialize if you need finer control; explicit values take precedence.

LifecycleHandler is the only required interface. Everything else is opt-in: implement the interface on your handler struct and the server registers the corresponding LSP method automatically.

Interface Methods
LifecycleHandler Initialize, Shutdown
SetTraceHandler SetTrace
Interface Methods
TextDocumentSyncHandler DidOpen, DidChange, DidClose
TextDocumentSaveHandler DidSave
TextDocumentWillSaveHandler WillSave
TextDocumentWillSaveWaitUntilHandler WillSaveWaitUntil
Interface Methods
CompletionHandler Completion
CompletionResolveHandler ResolveCompletionItem
HoverHandler Hover
SignatureHelpHandler SignatureHelp
DeclarationHandler Declaration
DefinitionHandler Definition
TypeDefinitionHandler TypeDefinition
ImplementationHandler Implementation
ReferencesHandler References
DocumentHighlightHandler DocumentHighlight
DocumentSymbolHandler DocumentSymbol
CodeActionHandler CodeAction
CodeActionResolveHandler ResolveCodeAction
CodeLensHandler CodeLens
CodeLensResolveHandler ResolveCodeLens
DocumentLinkHandler DocumentLink
DocumentLinkResolveHandler ResolveDocumentLink
DocumentColorHandler DocumentColor
ColorPresentationHandler ColorPresentation
DocumentFormattingHandler Formatting
DocumentRangeFormattingHandler RangeFormatting
DocumentOnTypeFormattingHandler OnTypeFormatting
RenameHandler Rename
PrepareRenameHandler PrepareRename
FoldingRangeHandler FoldingRange
SelectionRangeHandler SelectionRange
CallHierarchyHandler PrepareCallHierarchy, IncomingCalls, OutgoingCalls
SemanticTokensFullHandler SemanticTokensFull
SemanticTokensDeltaHandler SemanticTokensDelta
SemanticTokensRangeHandler SemanticTokensRange
LinkedEditingRangeHandler LinkedEditingRange
MonikerHandler Moniker
TypeHierarchyHandler PrepareTypeHierarchy, Supertypes, Subtypes
InlayHintHandler InlayHint
InlayHintResolveHandler ResolveInlayHint
InlineValueHandler InlineValue
DocumentDiagnosticHandler DocumentDiagnostic
Interface Methods
WorkspaceSymbolHandler WorkspaceSymbol
ExecuteCommandHandler ExecuteCommand
WorkspaceFoldersHandler DidChangeWorkspaceFolders
DidChangeConfigurationHandler DidChangeConfiguration
DidChangeWatchedFilesHandler DidChangeWatchedFiles
WillCreateFilesHandler WillCreateFiles
WillRenameFilesHandler WillRenameFiles
WillDeleteFilesHandler WillDeleteFiles
WorkspaceDiagnosticHandler WorkspaceDiagnostic

Server-to-client communication

After the server starts, srv.Client is available for sending notifications and requests back to the editor:

// Publish diagnostics for a file
srv.Client.PublishDiagnostics(ctx, &lsp.PublishDiagnosticsParams{
	URI:         "file:///path/to/file.go",
	Diagnostics: diagnostics,
})

// Show a message popup in the editor
srv.Client.ShowMessage(ctx, &lsp.ShowMessageParams{
	Type:    lsp.MessageTypeInfo,
	Message: "Indexing complete",
})

// Write to the editor's log output
srv.Client.LogMessage(ctx, &lsp.LogMessageParams{
	Type:    lsp.MessageTypeLog,
	Message: "processed 42 files",
})

// Report progress
srv.Client.Progress(ctx, &lsp.ProgressParams{
	Token: "indexing",
	Value: progressValue,
})

// Show a message with action buttons (request/response)
item, err := srv.Client.ShowMessageRequest(ctx, &lsp.ShowMessageRequestParams{
	Type:    lsp.MessageTypeInfo,
	Message: "Restart server?",
	Actions: []lsp.MessageActionItem{{Title: "Yes"}, {Title: "No"}},
})

// Ask the editor to show a document
result, err := srv.Client.ShowDocument(ctx, &lsp.ShowDocumentParams{
	URI: "file:///path/to/file.go",
})

You can register custom JSON-RPC methods and notifications for server-specific extensions:

srv := server.NewServer(&handler{})

// Custom request method
srv.HandleMethod("custom/myMethod", func(ctx context.Context, params json.RawMessage) (any, error) {
	var p MyParams
	if err := json.Unmarshal(params, &p); err != nil {
		return nil, err
	}
	return MyResult{Value: "hello"}, nil
})

// Custom notification
srv.HandleNotification("custom/myNotification", func(ctx context.Context, params json.RawMessage) error {
	// handle notification
	return nil
})

srv.Run(ctx, rwc)

Custom handlers must be registered before calling Run.

The server supports structured logging via log/slog. Pass a logger with the WithLogger option:

srv := server.NewServer(&handler{}, server.WithLogger(slog.Default()))

When enabled, the server logs:

Event Level Attributes
Server starting Info
Initialized Info serverName
Shutdown Info
Method handled Debug method, duration
Notification handled Debug method, duration
Handler error Error method, duration, error

If no logger is set, no logging is performed. Use any slog.Handler — JSON for production, text for development:

// JSON to stderr
logger := slog.New(slog.NewJSONHandler(os.Stderr, nil))
srv := server.NewServer(&handler{}, server.WithLogger(logger))

// Debug-level text output
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug}))
srv := server.NewServer(&handler{}, server.WithLogger(logger))

The servertest package provides a test harness that simulates an LSP client over in-memory pipes. It handles JSON-RPC framing, initialization, and cleanup automatically:

func TestHover(t *testing.T) {
    h := servertest.New(t, &myHandler{})

    h.DidOpen("file:///test.txt", "plaintext", "hello world")

    hover, err := h.Hover("file:///test.txt", 0, 5)
    if err != nil {
        t.Fatal(err)
    }
    // assert on hover.Contents
}

The harness collects server-to-client notifications (diagnostics, messages) so you can assert on them:

h.DidSave("file:///test.txt")

ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()

diags, err := h.WaitForDiagnostics(ctx, "file:///test.txt")

See the testing guide for the full API.

The library includes an optional debug UI that captures all LSP traffic and displays it in a web interface. This is useful for inspecting the messages flowing between client and server during development.

Enable it with the WithDebugUI option:

srv := server.NewServer(&Handler{}, server.WithDebugUI(":7100"))
srv.Run(ctx, server.RunStdio())

Then open http://localhost:7100 in a browser. The tap is transparent -- it intercepts LSP frames for display without modifying them.

Real-time view of all JSON-RPC traffic between client and server:

  • Request/response pairing -- responses are matched to their originating request with latency displayed inline
  • Direction and type filters -- narrow the list to client-to-server, server-to-client, requests, or notifications
  • Full-text search across methods and JSON bodies (including paired responses)
  • Pretty-printed detail pane -- click any message to see the full JSON with syntax formatting

Aggregated log output with level filtering (error, warning, info, log). window/logMessage notifications are automatically cross-posted here. Supports search and CSV export.

Waterfall view of request latency, grouped by method:

  • Each method gets a single row with all its request bars laid out on a time track
  • Bars are color-coded: green (<100ms), orange (100ms--1s), red (>1s), animated blue stripes (pending)
  • Minimap header with time axis ticks -- click or drag to scroll to a point in time
  • Zoom in/out to adjust the time scale
  • Click any bar to jump to that message in the Messages tab

Runtime metrics updated every 2 seconds: uptime, heap usage, goroutine count, GC runs, request/response/notification counts, and average latency.

Method sparklines appear below the stats bar once latency data is available, showing an inline SVG chart of the last 50 latency samples per method (top 10 by volume).

  • Capabilities panel -- slide-out panel showing which LSP capabilities the server advertised during initialization
  • Dark / light theme -- toggle between Catppuccin Mocha (dark) and Catppuccin Latte (light) themes, persisted in localStorage

Debug UI

Debug UI Timeline

The library is split into four packages:

  • server -- The Server that ties it together: accepts a handler, registers LSP methods based on the interfaces it implements, and manages the connection lifecycle.
  • lsp -- Go types for LSP structures (capabilities, params, results, enums). No logic, just data definitions.
  • servertest -- Test harness that simulates an LSP client over in-memory pipes for writing unit tests.
  • internal/jsonrpc -- JSON-RPC 2.0 framing over an io.ReadWriteCloser, with method and notification dispatch.
联系我们 contact @ memedata.com