feat(lsp): add textDocument/rangeFormatting support
CI / Test (push) Successful in 50s
CI / Build snapshot (push) Failing after 1m12s

Advertise DocumentRangeFormattingProvider in server capabilities and
implement rangeFormat: formats the complete document, then returns an
edit covering only the minimal changed-line region that overlaps the
client's selection. Also includes gofmt alignment fixes across lint
and format packages.
This commit is contained in:
2026-06-28 16:28:27 +02:00
parent a98dee1877
commit d1150a7eea
6 changed files with 168 additions and 23 deletions
+83 -8
View File
@@ -38,8 +38,8 @@ func Serve(ctx context.Context, r io.Reader, w io.Writer, startDir string) error
}
type server struct {
docs map[string]string // URI → current text
fixes map[string][]diagnostics.Diagnostic // URI → diagnostics that have fixes
docs map[string]string // URI → current text
fixes map[string][]diagnostics.Diagnostic // URI → diagnostics that have fixes
cfg config.Style
w io.Writer
}
@@ -76,9 +76,10 @@ func (s *server) handle(raw []byte) bool {
case "initialize":
s.reply(req.ID, initResult{
Capabilities: serverCaps{
TextDocumentSync: 1, // full sync
DocumentFormattingProvider: true,
CodeActionProvider: true,
TextDocumentSync: 1, // full sync
DocumentFormattingProvider: true,
DocumentRangeFormattingProvider: true,
CodeActionProvider: true,
},
})
case "initialized": // no-op notification
@@ -124,6 +125,15 @@ func (s *server) handle(raw []byte) bool {
return false
}
s.reply(req.ID, []textEdit{fullReplace(text, formatted)})
case "textDocument/rangeFormatting":
var p rangeFormattingParams
json.Unmarshal(req.Params, &p)
text, ok := s.docs[p.TextDocument.URI]
if !ok {
s.reply(req.ID, []textEdit{})
return false
}
s.reply(req.ID, s.rangeFormat(text, p.Range))
case "textDocument/codeAction":
var p codeActionParams
json.Unmarshal(req.Params, &p)
@@ -205,6 +215,61 @@ func (s *server) handleCodeAction(id json.RawMessage, p codeActionParams) {
s.reply(id, actions)
}
// rangeFormat formats the region of text that overlaps the given LSP range.
// It formats the complete document, then returns an edit covering only the
// minimal set of changed lines that intersects the selection. Returns nil when
// the document is already formatted or no change falls within the selection.
func (s *server) rangeFormat(text string, r lspRange) []textEdit {
formatted := format.File(parser.Parse(text), s.cfg)
if formatted == text {
return nil
}
// Split both versions into lines, keeping the trailing newline attached to
// each element so that joining them reconstructs the original string.
origLines := strings.SplitAfter(text, "\n")
fmtLines := strings.SplitAfter(formatted, "\n")
// Longest common prefix of unchanged lines.
pfx := 0
for pfx < len(origLines) && pfx < len(fmtLines) && origLines[pfx] == fmtLines[pfx] {
pfx++
}
// Longest common suffix (must not overlap the prefix).
sfx := 0
for sfx < len(origLines)-pfx && sfx < len(fmtLines)-pfx &&
origLines[len(origLines)-1-sfx] == fmtLines[len(fmtLines)-1-sfx] {
sfx++
}
// The changed region in the original spans lines [pfx, changedEnd).
changedStart := pfx
changedEnd := len(origLines) - sfx
// Only return an edit when the changed region overlaps the selection.
selStart := int(r.Start.Line)
selEnd := int(r.End.Line)
if changedEnd <= selStart || changedStart > selEnd {
return nil
}
// Compute byte offsets for the changed region boundary.
startByte := 0
for i := 0; i < changedStart; i++ {
startByte += len(origLines[i])
}
endByte := startByte
for i := changedStart; i < changedEnd; i++ {
endByte += len(origLines[i])
}
newText := strings.Join(fmtLines[pfx:len(fmtLines)-sfx], "")
return []textEdit{{
Range: lspRange{Start: offsetToPosition(text, startByte), End: offsetToPosition(text, endByte)},
NewText: newText,
}}
}
// offsetToPosition converts a byte offset in text to an LSP position.
func offsetToPosition(text string, byteOff int) position {
if byteOff > len(text) {
@@ -360,9 +425,10 @@ type initResult struct {
}
type serverCaps struct {
TextDocumentSync int `json:"textDocumentSync"`
DocumentFormattingProvider bool `json:"documentFormattingProvider"`
CodeActionProvider bool `json:"codeActionProvider"`
TextDocumentSync int `json:"textDocumentSync"`
DocumentFormattingProvider bool `json:"documentFormattingProvider"`
DocumentRangeFormattingProvider bool `json:"documentRangeFormattingProvider"`
CodeActionProvider bool `json:"codeActionProvider"`
}
type textDocItem struct {
@@ -397,6 +463,15 @@ type formattingParams struct {
} `json:"options"`
}
type rangeFormattingParams struct {
TextDocument textDocID `json:"textDocument"`
Range lspRange `json:"range"`
Options struct {
TabSize int `json:"tabSize"`
InsertSpaces bool `json:"insertSpaces"`
} `json:"options"`
}
type lspDiagnostic struct {
Range lspRange `json:"range"`
Severity int `json:"severity"`