gitstack

grindlemire/go-tui code browser

4.2 KB Go 152 lines 2026-06-03 · d15bb9f raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
package lsp

import (
	"github.com/grindlemire/go-tui/internal/tuigen"
)

// NodeKind classifies what language construct the cursor is on.
type NodeKind int

const (
	NodeKindUnknown NodeKind = iota
	NodeKindComponent
	NodeKindElement
	NodeKindAttribute
	NodeKindRefAttr // Cursor on ref={} attribute value
	NodeKindGoExpr
	NodeKindForLoop
	NodeKindIfStmt
	NodeKindLetBinding
	NodeKindStateDecl
	NodeKindStateAccess
	NodeKindParameter
	NodeKindFunction
	NodeKindGoDecl // Top-level var, type, or const declaration
	NodeKindComponentCall
	NodeKindEventHandler
	NodeKindText
	NodeKindKeyword
	NodeKindTailwindClass
	NodeKindImportPath
)

// String returns a human-readable name for the NodeKind.
func (k NodeKind) String() string {
	switch k {
	case NodeKindComponent:
		return "Component"
	case NodeKindElement:
		return "Element"
	case NodeKindAttribute:
		return "Attribute"
	case NodeKindRefAttr:
		return "RefAttr"
	case NodeKindGoExpr:
		return "GoExpr"
	case NodeKindForLoop:
		return "ForLoop"
	case NodeKindIfStmt:
		return "IfStmt"
	case NodeKindLetBinding:
		return "LetBinding"
	case NodeKindStateDecl:
		return "StateDecl"
	case NodeKindStateAccess:
		return "StateAccess"
	case NodeKindParameter:
		return "Parameter"
	case NodeKindFunction:
		return "Function"
	case NodeKindGoDecl:
		return "GoDecl"
	case NodeKindComponentCall:
		return "ComponentCall"
	case NodeKindEventHandler:
		return "EventHandler"
	case NodeKindText:
		return "Text"
	case NodeKindKeyword:
		return "Keyword"
	case NodeKindTailwindClass:
		return "TailwindClass"
	case NodeKindImportPath:
		return "ImportPath"
	default:
		return "Unknown"
	}
}

// Scope holds the enclosing scope information for a cursor position.
type Scope struct {
	Component *tuigen.Component // Enclosing component (nil if at file level)
	Function  *tuigen.GoFunc    // Enclosing function (nil if not in a function)
	ForLoop   *tuigen.ForLoop   // Enclosing for loop (nil if not in a loop)
	IfStmt    *tuigen.IfStmt    // Enclosing if statement (nil if not in conditional)
	Refs      []tuigen.RefInfo  // Element refs in scope (from ref={} attributes)
	StateVars []tuigen.StateVar // State variables in scope
	LetBinds  []*tuigen.LetBinding
	Params    []*tuigen.Param
}

// CursorContext contains resolved information about the cursor position.
// Providers receive this instead of raw positions, centralizing all
// "what is under the cursor" logic.
type CursorContext struct {
	Document *Document
	Position Position
	Offset   int

	// Resolved AST information
	Node        tuigen.Node   // The AST node at the cursor (may be nil)
	NodeKind    NodeKind      // Classification of the cursor position
	Scope       *Scope        // Enclosing scope information
	ParentChain []tuigen.Node // Path from root to current node

	// Convenience fields
	Word        string // Word under cursor
	Line        string // Full line text
	InGoExpr    bool   // Inside a Go expression {..}
	InClassAttr bool   // Inside class="..."
	InElement   bool   // Inside an element tag (between < and >)

	// For attribute context
	AttrTag  string // Element tag when cursor is on an attribute
	AttrName string // Attribute name when cursor is on an attribute

	// For import context
	ImportPath string // Import path when cursor is on an import line
}

// ResolveCursorContext resolves the cursor context for a document position.
// This walks the AST and surrounding text to determine what the cursor is
// pointing at and what scope it's in.
func ResolveCursorContext(doc *Document, pos Position) *CursorContext {
	ctx := &CursorContext{
		Document: doc,
		Position: pos,
		Offset:   PositionToOffset(doc.Content, pos),
		Scope:    &Scope{},
	}

	// Extract line text
	ctx.Line = getLineText(doc.Content, pos.Line)

	// Extract word under cursor
	ctx.Word = getWordAtOffset(doc.Content, ctx.Offset)

	// Check text-level context flags
	ctx.InGoExpr = isOffsetInGoExpr(doc.Content, ctx.Offset)
	ctx.InClassAttr = isOffsetInClassAttr(doc.Content, ctx.Offset)
	ctx.InElement = isOffsetInElementTag(doc.Content, ctx.Offset)

	// If no AST, classify from text heuristics only
	if doc.AST == nil {
		ctx.NodeKind = classifyFromText(ctx)
		return ctx
	}

	// Walk AST to resolve node, scope, and kind
	resolveFromAST(ctx, doc.AST)

	return ctx
}