package lsp import ( "bufio" "encoding/json" "fmt" "io" "os" "strings" "github.com/marte-dev/marte-dev-tools/internal/index" "github.com/marte-dev/marte-dev-tools/internal/parser" ) type JsonRpcMessage struct { Jsonrpc string `json:"jsonrpc"` Method string `json:"method,omitempty"` Params json.RawMessage `json:"params,omitempty"` ID interface{} `json:"id,omitempty"` Result interface{} `json:"result,omitempty"` Error *JsonRpcError `json:"error,omitempty"` } type JsonRpcError struct { Code int `json:"code"` Message string `json:"message"` } type DidOpenTextDocumentParams struct { TextDocument TextDocumentItem `json:"textDocument"` } type DidChangeTextDocumentParams struct { TextDocument VersionedTextDocumentIdentifier `json:"textDocument"` ContentChanges []TextDocumentContentChangeEvent `json:"contentChanges"` } type TextDocumentItem struct { URI string `json:"uri"` LanguageID string `json:"languageId"` Version int `json:"version"` Text string `json:"text"` } type VersionedTextDocumentIdentifier struct { URI string `json:"uri"` Version int `json:"version"` } type TextDocumentContentChangeEvent struct { Text string `json:"text"` } type HoverParams struct { TextDocument TextDocumentIdentifier `json:"textDocument"` Position Position `json:"position"` } type TextDocumentIdentifier struct { URI string `json:"uri"` } type Position struct { Line int `json:"line"` Character int `json:"character"` } type Hover struct { Contents interface{} `json:"contents"` } type MarkupContent struct { Kind string `json:"kind"` Value string `json:"value"` } var tree = index.NewProjectTree() func RunServer() { reader := bufio.NewReader(os.Stdin) for { msg, err := readMessage(reader) if err != nil { if err == io.EOF { break } fmt.Fprintf(os.Stderr, "Error reading message: %v\n", err) continue } handleMessage(msg) } } func readMessage(reader *bufio.Reader) (*JsonRpcMessage, error) { var contentLength int for { line, err := reader.ReadString('\n') if err != nil { return nil, err } if line == "\r\n" { break } if _, err := fmt.Sscanf(line, "Content-Length: %d", &contentLength); err == nil { continue } } body := make([]byte, contentLength) _, err := io.ReadFull(reader, body) if err != nil { return nil, err } var msg JsonRpcMessage err = json.Unmarshal(body, &msg) return &msg, err } func handleMessage(msg *JsonRpcMessage) { switch msg.Method { case "initialize": respond(msg.ID, map[string]interface{}{ "capabilities": map[string]interface{}{ "textDocumentSync": 1, // Full sync "hoverProvider": true, "definitionProvider": true, "referencesProvider": true, }, }) case "initialized": // Do nothing case "shutdown": respond(msg.ID, nil) case "exit": os.Exit(0) case "textDocument/didOpen": var params DidOpenTextDocumentParams if err := json.Unmarshal(msg.Params, ¶ms); err == nil { handleDidOpen(params) } case "textDocument/didChange": var params DidChangeTextDocumentParams if err := json.Unmarshal(msg.Params, ¶ms); err == nil { handleDidChange(params) } case "textDocument/hover": var params HoverParams if err := json.Unmarshal(msg.Params, ¶ms); err == nil { res := handleHover(params) respond(msg.ID, res) } else { respond(msg.ID, nil) } } } func uriToPath(uri string) string { return strings.TrimPrefix(uri, "file://") } func handleDidOpen(params DidOpenTextDocumentParams) { path := uriToPath(params.TextDocument.URI) p := parser.NewParser(params.TextDocument.Text) config, err := p.Parse() if err == nil { tree.AddFile(path, config) tree.ResolveReferences() } } func handleDidChange(params DidChangeTextDocumentParams) { if len(params.ContentChanges) == 0 { return } text := params.ContentChanges[0].Text path := uriToPath(params.TextDocument.URI) p := parser.NewParser(text) config, err := p.Parse() if err == nil { tree.AddFile(path, config) tree.ResolveReferences() } } func handleHover(params HoverParams) *Hover { path := uriToPath(params.TextDocument.URI) line := params.Position.Line + 1 col := params.Position.Character + 1 res := tree.Query(path, line, col) if res == nil { return nil } var content string if res.Node != nil { content = formatNodeInfo(res.Node) } else if res.Field != nil { content = fmt.Sprintf("**Field**: `%s`", res.Field.Name) } else if res.Reference != nil { targetName := "Unresolved" fullInfo := "" targetDoc := "" if res.Reference.Target != nil { targetName = res.Reference.Target.RealName targetDoc = res.Reference.Target.Doc fullInfo = formatNodeInfo(res.Reference.Target) } content = fmt.Sprintf("**Reference**: `%s` -> `%s`", res.Reference.Name, targetName) if fullInfo != "" { content += fmt.Sprintf("\n\n---\n%s", fullInfo) } else if targetDoc != "" { // Fallback if formatNodeInfo returned empty (unlikely) content += fmt.Sprintf("\n\n%s", targetDoc) } } if content == "" { return nil } return &Hover{ Contents: MarkupContent{ Kind: "markdown", Value: content, }, } } func formatNodeInfo(node *index.ProjectNode) string { class := node.Metadata["Class"] if class == "" { class = "Unknown" } info := fmt.Sprintf("**Object**: `%s`\n\n**Class**: `%s`", node.RealName, class) // Check if it's a Signal (has Type or DataSource) typ := node.Metadata["Type"] ds := node.Metadata["DataSource"] if typ != "" || ds != "" { sigInfo := "\n" if typ != "" { sigInfo += fmt.Sprintf("**Type**: `%s` ", typ) } if ds != "" { sigInfo += fmt.Sprintf("**DataSource**: `%s` ", ds) } // Size dims := node.Metadata["NumberOfDimensions"] elems := node.Metadata["NumberOfElements"] if dims != "" || elems != "" { sigInfo += fmt.Sprintf("**Size**: `[%s]`, `%s` dims ", elems, dims) } info += sigInfo } if node.Doc != "" { info += fmt.Sprintf("\n\n%s", node.Doc) } return info } func respond(id interface{}, result interface{}) { msg := JsonRpcMessage{ Jsonrpc: "2.0", ID: id, Result: result, } send(msg) } func send(msg interface{}) { body, _ := json.Marshal(msg) fmt.Printf("Content-Length: %d\r\n\r\n%s", len(body), body) }