fix(lsp): recover from handler panics and decode client uris

This commit is contained in:
2026-09-14 18:22:00 +02:00
parent e307bf830f
commit 909f874797
4 changed files with 190 additions and 23 deletions
+31 -6
View File
@@ -8,6 +8,7 @@ import (
"encoding/json"
"fmt"
"io"
"net/url"
"strconv"
"strings"
"sync"
@@ -18,10 +19,11 @@ import (
// Server is a GAsm language server bound to a byte stream.
type Server struct {
in *bufio.Reader
out io.Writer
mu sync.Mutex // guards writes to out
docs map[string]string
in *bufio.Reader
out io.Writer
mu sync.Mutex // guards writes to out
docs map[string]string
version string // reported in the initialize result ("" omits it)
}
// New returns a server reading from in and writing to out.
@@ -33,6 +35,9 @@ func New(in io.Reader, out io.Writer) *Server {
}
}
// SetVersion records the server version reported in the initialize result.
func (s *Server) SetVersion(v string) { s.version = v }
// Run serves requests until the input is exhausted or an exit is requested.
func (s *Server) Run() error {
for {
@@ -109,9 +114,24 @@ func (s *Server) notify(method string, params any) {
}
// dispatch routes one message. It returns true when the server should stop.
// A panic in any handler is recovered and answered as an internal error:
// handlers parse live editor buffers, so malformed input must never take
// the whole server down.
func (s *Server) dispatch(msg *rpcMessage) (exit bool) {
defer func() {
if r := recover(); r != nil {
if msg != nil && msg.ID != nil {
s.respondError(msg.ID, errInternalError, fmt.Sprintf("internal error: %v", r))
}
}
}()
switch msg.Method {
case "initialize":
info := map[string]string{"name": "gasm"}
if s.version != "" {
info["version"] = s.version
}
s.respond(msg.ID, initializeResult{
Capabilities: ServerCapabilities{
TextDocumentSync: 1, // full sync
@@ -138,7 +158,7 @@ func (s *Server) dispatch(msg *rpcMessage) (exit bool) {
DocumentLinkProvider: map[string]any{},
FoldingRangeProvider: true,
},
ServerInfo: map[string]string{"name": "gasm", "version": "0.31.1"},
ServerInfo: info,
})
case "initialized", "textDocument/didSave":
@@ -292,9 +312,14 @@ func lintSeverity(s lint.Severity) int {
}
}
// uriPath strips a file:// scheme and returns the path component.
// uriPath strips a file:// scheme and percent-decodes the path component.
// LSP clients percent-encode URIs, so a raw slice would break every on-disk
// lookup for paths containing spaces or non-ASCII characters.
func uriPath(uri string) string {
if rest, ok := strings.CutPrefix(uri, "file://"); ok {
if decoded, err := url.PathUnescape(rest); err == nil {
return decoded
}
return rest
}
return uri