diff --git a/internal/index/index.go b/internal/index/index.go index 7563763..6c8cb89 100644 --- a/internal/index/index.go +++ b/internal/index/index.go @@ -7,7 +7,15 @@ import ( ) type ProjectTree struct { - Root *ProjectNode + Root *ProjectNode + References []Reference +} + +type Reference struct { + Name string + Position parser.Position + File string + Target *ProjectNode // Resolved target } type ProjectNode struct { @@ -15,13 +23,14 @@ type ProjectNode struct { RealName string // The actual name used in definition (e.g. +Node) Fragments []*Fragment Children map[string]*ProjectNode + Parent *ProjectNode } type Fragment struct { File string Definitions []parser.Definition - IsObject bool // True if this fragment comes from an ObjectNode, False if from File/Package body - ObjectPos parser.Position // Position of the object node if IsObject is true + IsObject bool + ObjectPos parser.Position } func NewProjectTree() *ProjectTree { @@ -39,8 +48,37 @@ func NormalizeName(name string) string { return name } +func (pt *ProjectTree) RemoveFile(file string) { + // Remove references from this file + newRefs := []Reference{} + for _, ref := range pt.References { + if ref.File != file { + newRefs = append(newRefs, ref) + } + } + pt.References = newRefs + + // Remove fragments from tree + pt.removeFileFromNode(pt.Root, file) +} + +func (pt *ProjectTree) removeFileFromNode(node *ProjectNode, file string) { + newFragments := []*Fragment{} + for _, frag := range node.Fragments { + if frag.File != file { + newFragments = append(newFragments, frag) + } + } + node.Fragments = newFragments + + for _, child := range node.Children { + pt.removeFileFromNode(child, file) + } +} + func (pt *ProjectTree) AddFile(file string, config *parser.Configuration) { - // Determine root node for this file based on package + pt.RemoveFile(file) // Ensure clean state for this file + node := pt.Root if config.Package != nil { parts := strings.Split(config.Package.URI, ".") @@ -49,62 +87,42 @@ func (pt *ProjectTree) AddFile(file string, config *parser.Configuration) { if part == "" { continue } - // Navigate or Create if _, ok := node.Children[part]; !ok { node.Children[part] = &ProjectNode{ Name: part, - RealName: part, // Default, might be updated if we find a +Part later? - // Actually, package segments are just names. - // If they refer to an object defined elsewhere as +Part, we hope to match it. + RealName: part, Children: make(map[string]*ProjectNode), + Parent: node, } } node = node.Children[part] } } - // Now 'node' is the container for the file's definitions. - // We add a Fragment to this node containing the top-level definitions. - // But wait, definitions can be ObjectNodes (which start NEW nodes) or Fields (which belong to 'node'). - - // We need to split definitions: - // Fields -> go into a Fragment for 'node'. - // ObjectNodes -> create/find Child node and add Fragment there. - - // Actually, the Build Process says: "#package ... implies all definitions ... are children". - // So if I have "Field = 1", it is a child of the package node. - // If I have "+Sub = {}", it is a child of the package node. - - // So we can just iterate definitions. - - // But for merging, we need to treat "+Sub" as a Node, not just a field. - fileFragment := &Fragment{ - File: file, + File: file, IsObject: false, } for _, def := range config.Definitions { switch d := def.(type) { case *parser.Field: - // Fields belong to the current package node fileFragment.Definitions = append(fileFragment.Definitions, d) + pt.indexValue(file, d.Value) case *parser.ObjectNode: - // Object starts a new child node norm := NormalizeName(d.Name) if _, ok := node.Children[norm]; !ok { node.Children[norm] = &ProjectNode{ Name: norm, RealName: d.Name, Children: make(map[string]*ProjectNode), + Parent: node, } } child := node.Children[norm] if child.RealName == norm && d.Name != norm { - child.RealName = d.Name // Update to specific name if we had generic + child.RealName = d.Name } - - // Recursively add definitions of the object pt.addObjectFragment(child, file, d) } } @@ -125,6 +143,7 @@ func (pt *ProjectTree) addObjectFragment(node *ProjectNode, file string, obj *pa switch d := def.(type) { case *parser.Field: frag.Definitions = append(frag.Definitions, d) + pt.indexValue(file, d.Value) case *parser.ObjectNode: norm := NormalizeName(d.Name) if _, ok := node.Children[norm]; !ok { @@ -132,6 +151,7 @@ func (pt *ProjectTree) addObjectFragment(node *ProjectNode, file string, obj *pa Name: norm, RealName: d.Name, Children: make(map[string]*ProjectNode), + Parent: node, } } child := node.Children[norm] @@ -144,3 +164,93 @@ func (pt *ProjectTree) addObjectFragment(node *ProjectNode, file string, obj *pa node.Fragments = append(node.Fragments, frag) } + +func (pt *ProjectTree) indexValue(file string, val parser.Value) { + switch v := val.(type) { + case *parser.ReferenceValue: + pt.References = append(pt.References, Reference{ + Name: v.Value, + Position: v.Position, + File: file, + }) + case *parser.ArrayValue: + for _, elem := range v.Elements { + pt.indexValue(file, elem) + } + } +} + +func (pt *ProjectTree) ResolveReferences() { + for i := range pt.References { + ref := &pt.References[i] + ref.Target = pt.findNode(pt.Root, ref.Name) + } +} + +func (pt *ProjectTree) findNode(root *ProjectNode, name string) *ProjectNode { + if root.RealName == name || root.Name == name { + return root + } + for _, child := range root.Children { + if res := pt.findNode(child, name); res != nil { + return res + } + } + return nil +} + +// QueryResult holds the result of a query at a position +type QueryResult struct { + Node *ProjectNode + Field *parser.Field + Reference *Reference +} + +func (pt *ProjectTree) Query(file string, line, col int) *QueryResult { + // 1. Check References + for i := range pt.References { + ref := &pt.References[i] + if ref.File == file { + // Check if pos is within reference range + // Approx length + if line == ref.Position.Line && col >= ref.Position.Column && col < ref.Position.Column+len(ref.Name) { + return &QueryResult{Reference: ref} + } + } + } + + // 2. Check Definitions (traverse tree) + return pt.queryNode(pt.Root, file, line, col) +} + +func (pt *ProjectTree) queryNode(node *ProjectNode, file string, line, col int) *QueryResult { + for _, frag := range node.Fragments { + if frag.File == file { + // Check Object definition itself + if frag.IsObject { + // Object definition usually starts at 'Name'. + // Position is start of Name. + if line == frag.ObjectPos.Line && col >= frag.ObjectPos.Column && col < frag.ObjectPos.Column+len(node.RealName) { + return &QueryResult{Node: node} + } + } + + // Check definitions in fragment + for _, def := range frag.Definitions { + if f, ok := def.(*parser.Field); ok { + // Check field name range + if line == f.Position.Line && col >= f.Position.Column && col < f.Position.Column+len(f.Name) { + return &QueryResult{Field: f} + } + } + } + } + } + + for _, child := range node.Children { + if res := pt.queryNode(child, file, line, col); res != nil { + return res + } + } + return nil +} diff --git a/internal/lsp/server.go b/internal/lsp/server.go index ec11d48..0362dfa 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -6,6 +6,10 @@ import ( "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 { @@ -22,6 +26,56 @@ type JsonRpcError struct { 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 { @@ -39,7 +93,6 @@ func RunServer() { } func readMessage(reader *bufio.Reader) (*JsonRpcMessage, error) { - // LSP uses Content-Length header var contentLength int for { line, err := reader.ReadString('\n') @@ -74,9 +127,6 @@ func handleMessage(msg *JsonRpcMessage) { "hoverProvider": true, "definitionProvider": true, "referencesProvider": true, - "completionProvider": map[string]interface{}{ - "triggerCharacters": []string{"=", ".", "{", "+", "$"}, - }, }, }) case "initialized": @@ -86,11 +136,102 @@ func handleMessage(msg *JsonRpcMessage) { case "exit": os.Exit(0) case "textDocument/didOpen": - // Handle file open + var params DidOpenTextDocumentParams + if err := json.Unmarshal(msg.Params, ¶ms); err == nil { + handleDidOpen(params) + } case "textDocument/didChange": - // Handle file change + var params DidChangeTextDocumentParams + if err := json.Unmarshal(msg.Params, ¶ms); err == nil { + handleDidChange(params) + } case "textDocument/hover": - // Handle 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 + } + // Full sync: text is in ContentChanges[0].Text + 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) + // LSP 0-based to Parser 1-based + 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 { + // Try to find Class field + class := "Unknown" + for _, frag := range res.Node.Fragments { + for _, def := range frag.Definitions { + if f, ok := def.(*parser.Field); ok && f.Name == "Class" { + if s, ok := f.Value.(*parser.StringValue); ok { + class = s.Value + } else if r, ok := f.Value.(*parser.ReferenceValue); ok { + class = r.Value + } + } + } + } + content = fmt.Sprintf("**Object**: `%s`\n\n**Class**: `%s`", res.Node.RealName, class) + } else if res.Field != nil { + content = fmt.Sprintf("**Field**: `%s`", res.Field.Name) + } else if res.Reference != nil { + targetName := "Unresolved" + if res.Reference.Target != nil { + targetName = res.Reference.Target.RealName + } + content = fmt.Sprintf("**Reference**: `%s` -> `%s`", res.Reference.Name, targetName) + } + + if content == "" { + return nil + } + + return &Hover{ + Contents: MarkupContent{ + Kind: "markdown", + Value: content, + }, } } @@ -106,4 +247,4 @@ func respond(id interface{}, result interface{}) { func send(msg interface{}) { body, _ := json.Marshal(msg) fmt.Printf("Content-Length: %d\r\n\r\n%s", len(body), body) -} +} \ No newline at end of file diff --git a/mdt b/mdt index 73ced4f..bbff76a 100755 Binary files a/mdt and b/mdt differ