gitstack

grindlemire/go-tui code browser

6.4 KB Go 239 lines 2026-03-07 · c926df1 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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
package tuigen

import "strings"

// TailwindValidationResult contains validation results for a class
type TailwindValidationResult struct {
	Valid      bool
	Class      string
	Suggestion string // "did you mean...?" hint
}

// TailwindClassWithPosition tracks a class and its position within the attribute value
type TailwindClassWithPosition struct {
	Class      string
	StartCol   int // column offset relative to attribute value start
	EndCol     int // column offset relative to attribute value start
	Valid      bool
	Suggestion string
}

// similarClasses maps common typos/alternatives to correct class names
var similarClasses = map[string]string{
	"flex-column":    "flex-col",
	"flex-columns":   "flex-col",
	"flex-rows":      "flex-row",
	"gap":            "gap-1",
	"padding":        "p-1",
	"margin":         "m-1",
	"bold":           "font-bold",
	"italic":         "italic",
	"dim":            "font-dim",
	"width":          "w-1",
	"height":         "h-1",
	"center":         "text-center",
	"left":           "text-left",
	"right":          "text-right",
	"align-center":   "text-center",
	"align-left":     "text-left",
	"align-right":    "text-right",
	"no-grow":        "grow-0",
	"no-shrink":      "shrink-0",
	"padding-top":    "pt-1",
	"padding-bottom": "pb-1",
	"padding-left":   "pl-1",
	"padding-right":  "pr-1",
	"margin-top":     "mt-1",
	"margin-bottom":  "mb-1",
	"margin-left":    "ml-1",
	"margin-right":   "mr-1",
	"col":            "flex-col",
	"row":            "flex-row",
	"column":         "flex-col",
	"columns":        "flex-col",
	"rows":           "flex-row",
	"focus":          "focusable",
	"hide":           "hidden",
	"invisible":      "hidden",
	"visible":        "hidden",
	"clip":           "overflow-hidden",
	"ellipsis":       "truncate",
	"text-truncate":  "truncate",
	"text-ellipsis":  "truncate",
	"display-block":  "block",
	"display-flex":   "flex",
	"wrap-reverse":   "flex-wrap-reverse",
	"no-wrap":        "flex-nowrap",
	"align-content":  "content-start",
}

// levenshteinDistance calculates the edit distance between two strings
func levenshteinDistance(a, b string) int {
	if len(a) == 0 {
		return len(b)
	}
	if len(b) == 0 {
		return len(a)
	}

	// Create a 2D slice for dynamic programming
	matrix := make([][]int, len(a)+1)
	for i := range matrix {
		matrix[i] = make([]int, len(b)+1)
	}

	// Initialize first column
	for i := 0; i <= len(a); i++ {
		matrix[i][0] = i
	}

	// Initialize first row
	for j := 0; j <= len(b); j++ {
		matrix[0][j] = j
	}

	// Fill in the rest of the matrix
	for i := 1; i <= len(a); i++ {
		for j := 1; j <= len(b); j++ {
			cost := 1
			if a[i-1] == b[j-1] {
				cost = 0
			}
			matrix[i][j] = min(
				matrix[i-1][j]+1,      // deletion
				matrix[i][j-1]+1,      // insertion
				matrix[i-1][j-1]+cost, // substitution
			)
		}
	}

	return matrix[len(a)][len(b)]
}

// getAllKnownClassNames returns all known class names for fuzzy matching
func getAllKnownClassNames() []string {
	classes := make([]string, 0, len(tailwindClasses)+50)

	// Add all static class names
	for name := range tailwindClasses {
		classes = append(classes, name)
	}

	// Add common pattern-based class examples
	patternExamples := []string{
		"gap-1", "gap-2", "gap-3", "gap-4",
		"p-1", "p-2", "p-3", "p-4",
		"px-1", "px-2", "px-3", "px-4",
		"py-1", "py-2", "py-3", "py-4",
		"pt-1", "pt-2", "pt-3", "pt-4",
		"pr-1", "pr-2", "pr-3", "pr-4",
		"pb-1", "pb-2", "pb-3", "pb-4",
		"pl-1", "pl-2", "pl-3", "pl-4",
		"m-1", "m-2", "m-3", "m-4",
		"mt-1", "mt-2", "mt-3", "mt-4",
		"mr-1", "mr-2", "mr-3", "mr-4",
		"mb-1", "mb-2", "mb-3", "mb-4",
		"ml-1", "ml-2", "ml-3", "ml-4",
		"mx-1", "mx-2", "mx-3", "mx-4",
		"my-1", "my-2", "my-3", "my-4",
		"w-1", "w-10", "w-20", "w-50", "w-100",
		"w-full", "w-auto", "w-1/2", "w-1/3", "w-2/3", "w-1/4", "w-3/4",
		"h-1", "h-10", "h-20", "h-50", "h-100",
		"h-full", "h-auto", "h-1/2", "h-1/3", "h-2/3", "h-1/4", "h-3/4",
		"min-w-1", "min-w-10", "max-w-50", "max-w-100",
		"min-h-1", "min-h-10", "max-h-50", "max-h-100",
		"flex-grow-0", "flex-grow-1", "flex-grow-2",
		"flex-shrink-0", "flex-shrink-1", "flex-shrink-2",
		// Tailwind standard flex utilities
		"grow", "grow-0", "shrink", "shrink-0",
		"flex-1", "flex-auto", "flex-initial", "flex-none",
	}
	classes = append(classes, patternExamples...)

	return classes
}

// findSimilarClass finds a similar valid class for a given invalid class
func findSimilarClass(class string) string {
	// First check exact match in similarClasses map
	if suggestion, ok := similarClasses[class]; ok {
		return suggestion
	}

	// Use Levenshtein distance for fuzzy matching
	allClasses := getAllKnownClassNames()
	bestMatch := ""
	bestDistance := 4 // Only suggest if distance <= 3

	for _, knownClass := range allClasses {
		dist := levenshteinDistance(class, knownClass)
		if dist < bestDistance {
			bestDistance = dist
			bestMatch = knownClass
		}
	}

	return bestMatch
}

// ValidateTailwindClass validates a single class and returns suggestions
func ValidateTailwindClass(class string) TailwindValidationResult {
	class = strings.TrimSpace(class)
	if class == "" {
		return TailwindValidationResult{Valid: false, Class: class}
	}

	// Check if it's a valid class using ParseTailwindClass
	_, ok := ParseTailwindClass(class)
	if ok {
		return TailwindValidationResult{Valid: true, Class: class}
	}

	// Invalid class - find a suggestion
	suggestion := findSimilarClass(class)
	return TailwindValidationResult{
		Valid:      false,
		Class:      class,
		Suggestion: suggestion,
	}
}

// ParseTailwindClassesWithPositions parses classes and tracks their positions
func ParseTailwindClassesWithPositions(classes string, attrStartCol int) []TailwindClassWithPosition {
	var result []TailwindClassWithPosition

	// Track position as we iterate through the string
	pos := 0
	for pos < len(classes) {
		// Skip leading whitespace
		for pos < len(classes) && (classes[pos] == ' ' || classes[pos] == '\t') {
			pos++
		}
		if pos >= len(classes) {
			break
		}

		// Find the end of this class (next whitespace or end of string)
		startPos := pos
		for pos < len(classes) && classes[pos] != ' ' && classes[pos] != '\t' {
			pos++
		}
		endPos := pos

		class := classes[startPos:endPos]
		if class == "" {
			continue
		}

		validation := ValidateTailwindClass(class)
		result = append(result, TailwindClassWithPosition{
			Class:      class,
			StartCol:   attrStartCol + startPos,
			EndCol:     attrStartCol + endPos,
			Valid:      validation.Valid,
			Suggestion: validation.Suggestion,
		})
	}

	return result
}