Skip to content

Commit 73f69e9

Browse files
committed
better
1 parent 71f1d15 commit 73f69e9

File tree

4 files changed

+132
-76
lines changed

4 files changed

+132
-76
lines changed

formatter.go

+50-41
Original file line numberDiff line numberDiff line change
@@ -19,24 +19,33 @@ func NewFormatter(config *Config) *Formatter {
1919
}
2020

2121
// Format converts a StackTrace into a formatted string
22-
func (f *Formatter) Format(trace *StackTrace) string {
22+
func (f *Formatter) Format(traces []*StackTrace) string {
2323
if !f.config.Colorize {
2424
f.disableColors()
2525
}
2626

2727
var result []string
2828

29-
// Add goroutine header
30-
if trace.GoroutineID != "" {
31-
header := fmt.Sprintf("Goroutine %s: %s",
32-
trace.GoroutineID, trace.GoroutineState)
33-
result = append(result, f.config.Theme.Goroutine.Render(header))
29+
for _, trace := range traces {
30+
result = append(result, f.formatTrace(trace))
3431
}
3532

36-
// Format entries
33+
return strings.Join(result, "\n\n")
34+
}
35+
36+
func (f *Formatter) formatTrace(trace *StackTrace) string {
37+
var result []string
38+
39+
// Format goroutine header
40+
header := fmt.Sprintf("Goroutine %s: %s",
41+
trace.GoroutineID, trace.GoroutineState)
42+
result = append(result, f.config.Theme.Goroutine.Render(header))
43+
44+
// Calculate widths and counts
3745
functionCounts := f.countFunctions(trace.Entries)
3846
maxWidths := f.calculateMaxWidths(trace.Entries)
3947

48+
// Format entries
4049
for _, entry := range trace.Entries {
4150
formattedEntry := f.formatEntry(entry, functionCounts, maxWidths)
4251
result = append(result, formattedEntry)
@@ -73,39 +82,47 @@ func (f *Formatter) countFunctions(entries []StackEntry) map[string]int {
7382
func (f *Formatter) formatEntry(entry StackEntry, functionCounts map[string]int, maxWidths MaxWidths) string {
7483
var currentTree *tree.Tree
7584

76-
if functionCounts[entry.FunctionName] <= 1 {
77-
if entry.IsCreatedBy {
78-
currentTree = tree.New().Root(
79-
f.config.Theme.CreatedBy.Render("Created by: ") +
80-
entry.FunctionName,
81-
)
82-
} else {
83-
currentTree = tree.New().Root(
84-
f.config.Theme.Function.Render("Function: ") +
85-
lipgloss.NewStyle().Width(maxWidths.Function).Render(entry.FunctionName) +
86-
f.config.Theme.Args.Render(fmt.Sprintf("(%s)", entry.Args)),
85+
// Function name with potential truncation indicator
86+
displayName := entry.FunctionName
87+
if entry.FullName != "" && entry.FullName != entry.FunctionName {
88+
displayName = displayName + " ..."
89+
}
90+
91+
if entry.IsCreatedBy {
92+
createdByInfo := fmt.Sprintf("Created by: %s (goroutine %s)",
93+
displayName, entry.CreatedByGoroutine)
94+
currentTree = tree.New().Root(
95+
f.config.Theme.CreatedBy.Render(createdByInfo),
96+
)
97+
} else {
98+
// Create base function node
99+
funcInfo := lipgloss.JoinHorizontal(
100+
lipgloss.Left,
101+
f.config.Theme.Function.Render(displayName),
102+
f.config.Theme.Args.Render(fmt.Sprintf("(%s)", entry.Args)),
103+
)
104+
105+
// Add repeat count if needed
106+
if count := functionCounts[entry.FunctionName]; count > 1 {
107+
funcInfo += f.config.Theme.Repeat.Render(
108+
fmt.Sprintf(" (repeated %d times)", count),
87109
)
88110
}
89111

90-
// Add file location as a child
112+
currentTree = tree.New().Root(funcInfo)
113+
}
114+
115+
// Add file location as child node if available
116+
if entry.File != "" {
91117
fileInfo := lipgloss.JoinHorizontal(
92118
lipgloss.Left,
93-
f.config.Theme.File.Render("At: "),
94-
lipgloss.NewStyle().Width(maxWidths.File).Render(entry.File),
95-
f.config.Theme.Line.Render(fmt.Sprintf(" Line: %s", entry.Line)),
119+
f.config.Theme.File.Render(entry.File),
120+
f.config.Theme.Line.Render(fmt.Sprintf(":%s", entry.Line)),
96121
)
122+
if entry.Offset != "" {
123+
fileInfo += f.config.Theme.Line.Render(fmt.Sprintf(" +%s", entry.Offset))
124+
}
97125
currentTree.Child(fileInfo)
98-
} else {
99-
repCount := f.config.Theme.Repeat.Render(
100-
fmt.Sprintf(" (repeated %d times)",
101-
functionCounts[entry.FunctionName]),
102-
)
103-
currentTree = tree.New().Root(
104-
f.config.Theme.Function.Render("Function: ") +
105-
lipgloss.NewStyle().Width(maxWidths.Function).Render(entry.FunctionName) +
106-
f.config.Theme.Args.Render(fmt.Sprintf("(%s)", entry.Args)) +
107-
repCount,
108-
)
109126
}
110127

111128
return currentTree.String()
@@ -120,11 +137,3 @@ func (f *Formatter) disableColors() {
120137
f.config.Theme.CreatedBy.UnsetForeground()
121138
f.config.Theme.Repeat.UnsetForeground()
122139
}
123-
124-
// Helper functions
125-
func max(a, b int) int {
126-
if a > b {
127-
return a
128-
}
129-
return b
130-
}

models.go

+8-5
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@ package stackparse
22

33
// StackEntry represents a single entry in the stack trace
44
type StackEntry struct {
5-
FunctionName string
6-
Args string
7-
File string
8-
Line string
9-
IsCreatedBy bool
5+
FunctionName string
6+
FullName string
7+
Args string
8+
File string
9+
Line string
10+
Offset string
11+
IsCreatedBy bool
12+
CreatedByGoroutine string
1013
}
1114

1215
// StackTrace represents a complete stack trace

parser.go

+64-21
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ type Patterns struct {
1818
Function *regexp.Regexp
1919
Location *regexp.Regexp
2020
CreatedBy *regexp.Regexp
21+
LongFunc *regexp.Regexp
2122
}
2223

2324
// NewParser creates a new Parser instance with the given options
@@ -30,22 +31,25 @@ func NewParser(options ...Option) *Parser {
3031
return &Parser{
3132
config: config,
3233
patterns: &Patterns{
33-
Goroutine: regexp.MustCompile(`goroutine (\d+) \[([\w\.]+)\]:`),
34-
Function: regexp.MustCompile(`^(\S+)\((.*)\)$`),
35-
Location: regexp.MustCompile(`^\s*(.+\.go):(\d+)(.*)$`),
34+
// universal all function regex ^(\S+)\((.*)\)$
35+
Goroutine: regexp.MustCompile(`goroutine (\d+) \[(.*?)\]:`),
36+
Function: regexp.MustCompile(`^([^\s/]+)\((.*)\)$`),
37+
Location: regexp.MustCompile(`^\s*(.+\.go):(\d+)(?:\s+\+([0-9a-fA-Fx]+))?$`),
3638
CreatedBy: regexp.MustCompile(`created by (.+) in goroutine (\d+)`),
39+
LongFunc: regexp.MustCompile(`^(\S*/\S+)\((.*)\)$`),
3740
},
3841
}
3942
}
4043

4144
// Parse converts a byte slice containing a stack trace into a StackTrace
42-
func (p *Parser) Parse(stack []byte) *StackTrace {
45+
func (p *Parser) Parse(stack []byte) []*StackTrace {
4346
lines := strings.Split(string(stack), "\n")
4447
return p.parseLines(lines)
4548
}
4649

47-
func (p *Parser) parseLines(lines []string) *StackTrace {
48-
trace := &StackTrace{}
50+
func (p *Parser) parseLines(lines []string) []*StackTrace {
51+
var traces []*StackTrace
52+
var currentTrace *StackTrace
4953
var currentEntry *StackEntry
5054

5155
for i := 0; i < len(lines); i++ {
@@ -54,25 +58,50 @@ func (p *Parser) parseLines(lines []string) *StackTrace {
5458
continue
5559
}
5660

57-
// Handle goroutine header
61+
// Handle goroutine header - starts a new trace
5862
if match := p.patterns.Goroutine.FindStringSubmatch(line); match != nil {
59-
if currentEntry != nil {
60-
trace.Entries = append(trace.Entries, *currentEntry)
63+
// Save current entry if exists
64+
if currentEntry != nil && currentTrace != nil {
65+
currentTrace.Entries = append(currentTrace.Entries, *currentEntry)
6166
currentEntry = nil
6267
}
63-
trace.GoroutineID = match[1]
64-
trace.GoroutineState = match[2]
68+
69+
// Create new trace
70+
currentTrace = &StackTrace{
71+
GoroutineID: match[1],
72+
GoroutineState: match[2],
73+
}
74+
traces = append(traces, currentTrace)
75+
continue
76+
}
77+
78+
// Skip if no current trace
79+
if currentTrace == nil {
6580
continue
6681
}
6782

6883
// Handle function calls
6984
if match := p.patterns.Function.FindStringSubmatch(line); match != nil {
7085
if currentEntry != nil {
71-
trace.Entries = append(trace.Entries, *currentEntry)
86+
currentTrace.Entries = append(currentTrace.Entries, *currentEntry)
87+
}
88+
89+
funcName := match[1]
90+
args := match[2]
91+
92+
// Handle long function names
93+
if len(funcName) > 60 { // Configurable threshold
94+
parts := strings.Split(funcName, "/")
95+
if len(parts) > 3 {
96+
// Keep the last three parts
97+
funcName = ".../" + strings.Join(parts[len(parts)-3:], "/")
98+
}
7299
}
100+
73101
currentEntry = &StackEntry{
74-
FunctionName: match[1],
75-
Args: match[2],
102+
FunctionName: funcName,
103+
Args: args,
104+
FullName: match[1], // Store full name for reference
76105
}
77106
continue
78107
}
@@ -85,18 +114,25 @@ func (p *Parser) parseLines(lines []string) *StackTrace {
85114
}
86115
currentEntry.File = filePath
87116
currentEntry.Line = match[2]
117+
if len(match) > 3 && match[3] != "" {
118+
currentEntry.Offset = match[3]
119+
}
88120
continue
89121
}
90122

91123
// Handle "created by" lines
92124
if match := p.patterns.CreatedBy.FindStringSubmatch(line); match != nil {
93125
if currentEntry != nil {
94-
trace.Entries = append(trace.Entries, *currentEntry)
126+
currentTrace.Entries = append(currentTrace.Entries, *currentEntry)
95127
}
128+
96129
currentEntry = &StackEntry{
97-
FunctionName: match[1],
98-
IsCreatedBy: true,
130+
FunctionName: match[1],
131+
IsCreatedBy: true,
132+
CreatedByGoroutine: match[2],
99133
}
134+
135+
// Look ahead for location
100136
if i+1 < len(lines) && p.patterns.Location.MatchString(lines[i+1]) {
101137
locMatch := p.patterns.Location.FindStringSubmatch(lines[i+1])
102138
filePath := locMatch[1]
@@ -105,19 +141,26 @@ func (p *Parser) parseLines(lines []string) *StackTrace {
105141
}
106142
currentEntry.File = filePath
107143
currentEntry.Line = locMatch[2]
144+
if len(locMatch) > 3 && locMatch[3] != "" {
145+
currentEntry.Offset = locMatch[3]
146+
}
108147
i++
109148
}
110149
continue
111150
}
112151
}
113152

114-
if currentEntry != nil {
115-
trace.Entries = append(trace.Entries, *currentEntry)
153+
// Add final entry if exists
154+
if currentEntry != nil && currentTrace != nil {
155+
currentTrace.Entries = append(currentTrace.Entries, *currentEntry)
116156
}
117157

118-
return trace
158+
return traces
119159
}
120160

121161
func (p *Parser) simplifyPath(path string) string {
122-
return filepath.Base(path)
162+
if p.config.Simple {
163+
return filepath.Base(path)
164+
}
165+
return path
123166
}

theme.go

+10-9
Original file line numberDiff line numberDiff line change
@@ -20,23 +20,24 @@ func DefaultTheme() *Theme {
2020
Base: lipgloss.NewStyle().PaddingLeft(2),
2121
Goroutine: lipgloss.NewStyle().
2222
Bold(true).
23-
Foreground(lipgloss.Color("#00ADD8")).
23+
// Foreground(lipgloss.Color("#00ADD8")).
24+
Foreground(lipgloss.Color("#ed8796")).
2425
MarginTop(1).
2526
MarginBottom(1),
2627
Function: lipgloss.NewStyle().
27-
Foreground(lipgloss.Color("#98C379")),
28+
Foreground(lipgloss.Color("#f0c6c6")),
2829
Args: lipgloss.NewStyle().
29-
Foreground(lipgloss.Color("#61AFEF")).
30+
Foreground(lipgloss.Color("#7dc4e4")).
3031
PaddingLeft(2),
3132
File: lipgloss.NewStyle().
32-
Foreground(lipgloss.Color("#C678DD")),
33+
Foreground(lipgloss.Color("#f5a97f")),
3334
Line: lipgloss.NewStyle().
34-
Foreground(lipgloss.Color("#E5C07B")).
35-
PaddingLeft(2),
35+
Foreground(lipgloss.Color("#eed49f")),
36+
// PaddingLeft(2),
3637
CreatedBy: lipgloss.NewStyle().
37-
Foreground(lipgloss.Color("#E06C75")),
38+
Foreground(lipgloss.Color("#ee99a0")),
3839
Repeat: lipgloss.NewStyle().
39-
Foreground(lipgloss.Color("#E06C75")).
40-
Italic(true),
40+
Italic(true).
41+
Faint(true),
4142
}
4243
}

0 commit comments

Comments
 (0)