Expand Gotlin language and tooling

This commit is contained in:
pavel 2026-08-27 01:46:57 +02:00
commit de1262b4cf
41 changed files with 6059 additions and 379 deletions

View file

@ -11,6 +11,7 @@ import (
"go/parser"
"go/token"
"io"
"net/url"
"os"
"os/exec"
"path/filepath"
@ -35,6 +36,7 @@ const (
symbolKindVariable = 13
symbolKindFunction = 12
symbolKindInterface = 11
symbolKindEnum = 10
symbolKindModule = 2
)
@ -62,10 +64,11 @@ type server struct {
}
type documentState struct {
text string
program *lang.Program
diagnostics []diagnostic
symbols []symbol
text string
program *lang.Program
diagnostics []diagnostic
symbols []symbol
packageSymbols []symbol
}
type symbol struct {
@ -74,6 +77,7 @@ type symbol struct {
Detail string
Range rng
Targets []string
URI string
}
type variableDeclInfo struct {
@ -238,7 +242,7 @@ func (s *server) handle(req request) error {
if err := json.Unmarshal(req.Params, &params); err != nil {
return err
}
s.docs[params.TextDocument.URI] = buildDocumentState(params.TextDocument.Text)
s.docs[params.TextDocument.URI] = s.buildDocumentState(params.TextDocument.URI, params.TextDocument.Text)
return s.publishDiagnostics(params.TextDocument.URI)
case "textDocument/didChange":
var params didChangeParams
@ -249,7 +253,7 @@ func (s *server) handle(req request) error {
if len(params.ContentChanges) > 0 {
text = params.ContentChanges[len(params.ContentChanges)-1].Text
}
s.docs[params.TextDocument.URI] = buildDocumentState(text)
s.docs[params.TextDocument.URI] = s.buildDocumentState(params.TextDocument.URI, text)
return s.publishDiagnostics(params.TextDocument.URI)
case "textDocument/didClose":
var params didCloseParams
@ -310,6 +314,89 @@ func buildDocumentState(text string) documentState {
return state
}
func (s *server) buildDocumentState(uri, text string) documentState {
state := documentState{text: text, diagnostics: []diagnostic{}}
program, err := lang.Parse(text)
if err != nil {
state.diagnostics = []diagnostic{diagnosticFromError(text, err)}
return state
}
state.program = program
state.symbols = indexSymbols(text, program)
programs, symbols := s.packageContext(uri, program.PackagePath, text)
state.packageSymbols = symbols
state.diagnostics = semanticDiagnosticsWithPackage(text, program, state.symbols, programs)
return state
}
func (s *server) packageContext(currentURI, packagePath, currentText string) ([]*lang.Program, []symbol) {
path, ok := filePathFromURI(currentURI)
if !ok {
return nil, nil
}
root := nearestModuleRoot(filepath.Dir(path))
if root == "" {
root = filepath.Dir(path)
}
var programs []*lang.Program
var symbols []symbol
_ = filepath.WalkDir(root, func(candidate string, entry os.DirEntry, err error) error {
if err != nil || entry.IsDir() || filepath.Ext(candidate) != ".gt" {
return nil
}
candidateURI := fileURI(candidate)
text := ""
if candidateURI == currentURI {
text = currentText
} else if open, found := s.docs[candidateURI]; found {
text = open.text
} else if data, readErr := os.ReadFile(candidate); readErr == nil {
text = string(data)
}
if text == "" {
return nil
}
program, parseErr := lang.Parse(text)
if parseErr != nil || program.PackagePath != packagePath {
return nil
}
programs = append(programs, program)
for _, sym := range indexSymbols(text, program) {
sym.URI = candidateURI
symbols = append(symbols, sym)
}
return nil
})
return programs, symbols
}
func nearestModuleRoot(directory string) string {
for {
if _, err := os.Stat(filepath.Join(directory, "go.mod")); err == nil {
return directory
}
parent := filepath.Dir(directory)
if parent == directory {
return ""
}
directory = parent
}
}
func filePathFromURI(uri string) (string, bool) {
parsed, err := url.Parse(uri)
if err != nil || parsed.Scheme != "file" {
return "", false
}
path, err := url.PathUnescape(parsed.Path)
if err != nil {
return "", false
}
return filepath.Clean(filepath.FromSlash(path)), true
}
func fileURI(path string) string { return "file://" + filepath.ToSlash(filepath.Clean(path)) }
func (s *server) publishDiagnostics(uri string) error {
state, ok := s.docs[uri]
if !ok {
@ -355,6 +442,11 @@ func (s *server) hover(uri string, pos position) any {
},
}
}
for _, sym := range state.packageSymbols {
if sym.Name == word {
return map[string]any{"contents": map[string]any{"kind": "markdown", "value": "```gotlin\n" + sym.Detail + "\n```"}}
}
}
if value, ok := builtinHoverDetail(word); ok {
return map[string]any{
"contents": map[string]any{
@ -386,6 +478,11 @@ func (s *server) definition(uri string, pos position) any {
return []location{{URI: uri, Range: sym.Range}}
}
}
for _, sym := range state.packageSymbols {
if sym.Name == word && sym.URI != "" {
return []location{{URI: sym.URI, Range: sym.Range}}
}
}
if result := s.goplsDefinition(state, pos); result != nil {
return result
}
@ -541,7 +638,19 @@ func indexSymbols(text string, program *lang.Program) []symbol {
}
}
classRanges := findAllLineMatches(lines, regexp.MustCompile(`^\s*class\s+([A-Za-z_][A-Za-z0-9_]*)`))
enumRanges := findAllLineMatches(lines, regexp.MustCompile(`^\s*enum\s+([A-Za-z_][A-Za-z0-9_]*)`))
for i, decl := range program.Enums {
r := rng{}
if i < len(enumRanges) {
r = enumRanges[i]
}
symbols = append(symbols, symbol{Name: decl.Name, Kind: symbolKindEnum, Detail: "enum " + decl.Name, Range: r})
for _, variant := range decl.Variants {
symbols = append(symbols, symbol{Name: variant.Name, Kind: symbolKindVariable, Detail: decl.Name + "::" + variant.Name, Range: r, Targets: []string{decl.Name}})
}
}
classRanges := findAllLineMatches(lines, regexp.MustCompile(`^\s*(?:data\s+)?class\s+([A-Za-z_][A-Za-z0-9_]*)`))
for i, decl := range program.Classes {
r := rng{}
if i < len(classRanges) {
@ -650,6 +759,10 @@ func indexSymbols(text string, program *lang.Program) []symbol {
}
func semanticDiagnostics(text string, program *lang.Program, symbols []symbol) []diagnostic {
return semanticDiagnosticsWithPackage(text, program, symbols, nil)
}
func semanticDiagnosticsWithPackage(text string, program *lang.Program, symbols []symbol, packagePrograms []*lang.Program) []diagnostic {
var diagnostics []diagnostic
funcSymbols := map[string]symbol{}
@ -671,7 +784,7 @@ func semanticDiagnostics(text string, program *lang.Program, symbols []symbol) [
} else {
importSymbols[sym.Name] = sym
}
case symbolKindClass, symbolKindInterface:
case symbolKindClass, symbolKindInterface, symbolKindEnum:
if prev, ok := typeSymbols[sym.Name]; ok {
diagnostics = append(diagnostics, duplicateDiagnostic(sym.Range, "duplicate type "+sym.Name))
diagnostics = append(diagnostics, duplicateDiagnostic(prev.Range, "duplicate type "+sym.Name))
@ -699,6 +812,26 @@ func semanticDiagnostics(text string, program *lang.Program, symbols []symbol) [
for _, decl := range program.Workers {
types[decl.Name] = true
}
for _, decl := range program.Enums {
types[decl.Name] = true
}
for _, sibling := range packagePrograms {
for _, fn := range sibling.Functions {
functions[fn.Name] = fn
}
for _, decl := range sibling.Interfaces {
types[decl.Name] = true
}
for _, decl := range sibling.Classes {
types[decl.Name] = true
}
for _, decl := range sibling.Workers {
types[decl.Name] = true
}
for _, decl := range sibling.Enums {
types[decl.Name] = true
}
}
for _, fn := range program.Functions {
diagnostics = append(diagnostics, functionSemanticDiagnostics(text, fn, functions, imports, types, nil)...)
}
@ -782,6 +915,8 @@ func functionSemanticDiagnostics(text string, fn lang.FunctionDecl, functions ma
walkExpr(s.Value, scope)
case lang.GoStmt:
walkExpr(s.Value, scope)
case lang.DeferStmt:
walkExpr(s.Value, scope)
case lang.ExprStmt:
walkExpr(s.Value, scope)
case lang.IfStmt:
@ -794,6 +929,11 @@ func functionSemanticDiagnostics(text string, fn lang.FunctionDecl, functions ma
walkExpr(s.Cond, scope)
bodyScope := copyScope(scope)
walkStmts(s.Body, bodyScope)
case lang.ForEachStmt:
walkExpr(s.Source, scope)
bodyScope := copyScope(scope)
bodyScope[s.Name] = true
walkStmts(s.Body, bodyScope)
case lang.SelectStmt:
for _, c := range s.Cases {
walkExpr(c.Source, scope)
@ -801,6 +941,15 @@ func functionSemanticDiagnostics(text string, fn lang.FunctionDecl, functions ma
caseScope["it"] = true
walkStmts(c.Body, caseScope)
}
case lang.MatchStmt:
walkExpr(s.Value, scope)
for _, matchCase := range s.Cases {
caseScope := copyScope(scope)
for _, binding := range matchCase.Bindings {
caseScope[binding] = true
}
walkStmts(matchCase.Body, caseScope)
}
case lang.TryCatchStmt:
tryScope := copyScope(scope)
walkStmts(s.TryBody, tryScope)
@ -823,32 +972,39 @@ func functionSemanticDiagnostics(text string, fn lang.FunctionDecl, functions ma
case lang.BinaryExpr:
walkExpr(e.Left, scope)
walkExpr(e.Right, scope)
case lang.CallExpr:
if ident, ok := e.Callee.(lang.IdentExpr); ok {
switch ident.Name {
case "after", "every":
if len(e.Args) != 1 {
diagnostics = append(diagnostics, undefinedNameDiagnostic(text, ident.Name, ident.Name+"(ms) expects exactly one Int argument"))
case lang.CallExpr:
if ident, ok := e.Callee.(lang.IdentExpr); ok {
switch ident.Name {
case "after", "every":
if len(e.Args) != 1 {
diagnostics = append(diagnostics, undefinedNameDiagnostic(text, ident.Name, ident.Name+"(ms) expects exactly one Int argument"))
}
if len(e.Args) == 1 {
switch e.Args[0].(type) {
case lang.StringExpr, lang.BoolExpr, lang.NullExpr:
diagnostics = append(diagnostics, undefinedNameDiagnostic(text, ident.Name, ident.Name+"(ms) expects an Int argument"))
}
if len(e.Args) == 1 {
switch e.Args[0].(type) {
case lang.StringExpr, lang.BoolExpr, lang.NullExpr:
diagnostics = append(diagnostics, undefinedNameDiagnostic(text, ident.Name, ident.Name+"(ms) expects an Int argument"))
}
}
if ident.Name == "every" && len(e.Args) == 1 {
if value, ok := staticIntExprValue(e.Args[0]); ok && value <= 0 {
diagnostics = append(diagnostics, undefinedNameDiagnostic(text, ident.Name, "every(ms) requires ms > 0"))
}
}
if ident.Name == "every" && len(e.Args) == 1 {
if value, ok := staticIntExprValue(e.Args[0]); ok && value <= 0 {
diagnostics = append(diagnostics, undefinedNameDiagnostic(text, ident.Name, "every(ms) requires ms > 0"))
}
}
}
walkExpr(e.Callee, scope)
for _, arg := range e.Args {
walkExpr(arg, scope)
}
}
walkExpr(e.Callee, scope)
for _, arg := range e.Args {
walkExpr(arg, scope)
}
case lang.SelectorExpr:
walkExpr(e.Receiver, scope)
case lang.IndexExpr:
walkExpr(e.Receiver, scope)
walkExpr(e.Index, scope)
case lang.EnumVariantExpr:
for _, value := range e.Values {
walkExpr(value, scope)
}
case lang.LambdaExpr:
lambdaScope := copyScope(scope)
if e.ImplicitIt {
@ -2026,23 +2182,43 @@ func copyScope(scope map[string]bool) map[string]bool {
}
func isBuiltin(name string) bool {
switch name {
case "println", "runCatching", "Channel", "after", "every", "listOf", "mutableListOf", "mapOf", "mutableMapOf":
return true
default:
return false
}
_, ok := builtinDetails[name]
return ok
}
func builtinHoverDetail(name string) (string, bool) {
switch name {
case "after":
return "fun after(ms: Int): Channel<time.Time>", true
case "every":
return "fun every(ms: Int): Channel<time.Time>", true
default:
return "", false
}
value, ok := builtinDetails[name]
return value, ok
}
var builtinDetails = map[string]string{
"println": "fun println(value: Any): Unit",
"runCatching": "fun runCatching(block: () -> Unit): Result",
"Channel": "fun Channel<T>(capacity: Int = 0): Channel<T>",
"after": "fun after(ms: Int): Channel<time.Time>",
"every": "fun every(ms: Int): Channel<time.Time>",
"listOf": "fun listOf<T>(values: T...): List<T>",
"mutableListOf": "fun mutableListOf<T>(values: T...): MutableList<T>",
"mapOf": "fun mapOf<K, V>(pairs: Any...): Map<K, V>",
"mutableMapOf": "fun mutableMapOf<K, V>(pairs: Any...): MutableMap<K, V>",
"ByteSlice": "fun ByteSlice(value: String): ByteSlice",
"append": "fun append<T>(values: List<T>, value: T): List<T>",
"len": "fun len(value: Any): Int",
"cap": "fun cap(value: Any): Int",
"make": "fun make<T>(size: Int): T",
"new": "fun new<T>(): *T",
"copy": "fun copy(target: Any, source: Any): Int",
"delete": "fun delete(map: Any, key: Any): Unit",
"close": "fun close(channel: Any): Unit",
"panic": "fun panic(value: Any): Unit",
"recover": "fun recover(): Any",
"string": "fun string(value: Any): String",
"int": "fun int(value: Any): Int",
"float64": "fun float64(value: Any): Double",
"bool": "fun bool(value: Any): Boolean",
"sql": "typed PostgreSQL query DSL",
"set": "fun set(target: Any, value: Any): Unit",
"now": "fun now(): time.Time",
}
func contains(values []string, needle string) bool {