Fix: highlight shift because of injected tags

This commit is contained in:
Grail Finder
2025-11-30 11:22:48 +03:00
parent a8f942cb0d
commit 5e6bc40853

177
tui.go
View File

@@ -31,7 +31,6 @@ var (
defaultImage = "sysprompts/llama.png" defaultImage = "sysprompts/llama.png"
indexPickWindow *tview.InputField indexPickWindow *tview.InputField
renameWindow *tview.InputField renameWindow *tview.InputField
searchWindow *tview.InputField
fullscreenMode bool fullscreenMode bool
// pages // pages
historyPage = "historyPage" historyPage = "historyPage"
@@ -50,7 +49,6 @@ var (
// For overlay search functionality // For overlay search functionality
searchField *tview.InputField searchField *tview.InputField
isSearching bool
searchPageName = "searchOverlay" searchPageName = "searchOverlay"
// help text // help text
helpText = ` helpText = `
@@ -241,22 +239,17 @@ func executeCommandAndDisplay(cmdText string) {
colorText() colorText()
return return
} }
command := cmdParts[0] command := cmdParts[0]
args := []string{} args := []string{}
if len(cmdParts) > 1 { if len(cmdParts) > 1 {
args = cmdParts[1:] args = cmdParts[1:]
} }
// Create the command execution // Create the command execution
cmd := exec.Command(command, args...) cmd := exec.Command(command, args...)
// Execute the command and get output // Execute the command and get output
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
// Add the command being executed to the chat // Add the command being executed to the chat
fmt.Fprintf(textView, "\n[yellow]$ %s[-:-:-]\n", cmdText) fmt.Fprintf(textView, "\n[yellow]$ %s[-:-:-]\n", cmdText)
if err != nil { if err != nil {
// Include both output and error // Include both output and error
fmt.Fprintf(textView, "[red]Error: %s[-:-:-]\n", err.Error()) fmt.Fprintf(textView, "[red]Error: %s[-:-:-]\n", err.Error())
@@ -271,7 +264,6 @@ func executeCommandAndDisplay(cmdText string) {
fmt.Fprintf(textView, "[green]Command executed successfully (no output)[-:-:-]\n") fmt.Fprintf(textView, "[green]Command executed successfully (no output)[-:-:-]\n")
} }
} }
// Scroll to end and update colors // Scroll to end and update colors
textView.ScrollToEnd() textView.ScrollToEnd()
colorText() colorText()
@@ -283,7 +275,6 @@ func parseCommand(cmd string) []string {
var current string var current string
var inQuotes bool var inQuotes bool
var quoteChar rune var quoteChar rune
for _, r := range cmd { for _, r := range cmd {
switch r { switch r {
case '"', '\'': case '"', '\'':
@@ -308,11 +299,9 @@ func parseCommand(cmd string) []string {
current += string(r) current += string(r)
} }
} }
if current != "" { if current != "" {
args = append(args, current) args = append(args, current)
} }
return args return args
} }
@@ -321,62 +310,9 @@ var searchResults []int
var searchResultLengths []int // To store the length of each match in the formatted string var searchResultLengths []int // To store the length of each match in the formatted string
var searchIndex int var searchIndex int
var searchText string var searchText string
var originalTextForSearch string
// stripTags creates a plain text version of a tview formatted string and a mapping
// from plain text indices to formatted text indices.
func stripTags(formatted string) (string, []int) {
var plain strings.Builder
// The mapping will store the byte index in the formatted string for each byte in the plain string.
mapping := make([]int, 0, len(formatted))
i := 0
for i < len(formatted) {
if formatted[i] != '[' {
mapping = append(mapping, i)
plain.WriteByte(formatted[i])
i++
continue
}
// We are at a '['
if i+1 < len(formatted) && formatted[i+1] == '[' { // Escaped '[['
mapping = append(mapping, i)
plain.WriteByte('[')
i += 2
continue
}
// It's a tag. Find its end.
end := -1
// Region tags are of the form ["..."]
if i+1 < len(formatted) && formatted[i+1] == '"' {
// Find `"]`
for j := i + 2; j < len(formatted)-1; j++ {
if formatted[j] == '"' && formatted[j+1] == ']' {
end = j + 1
break
}
}
} else {
// Color/attr tag [...]
closeBracket := strings.IndexRune(formatted[i:], ']')
if closeBracket != -1 {
end = i + closeBracket
}
}
if end == -1 {
// Unterminated tag. Treat as literal.
mapping = append(mapping, i)
plain.WriteByte(formatted[i])
i++
} else {
// Skip tag
i = end + 1
}
}
return plain.String(), mapping
}
// performSearch searches for the given term in the textView content and highlights matches // performSearch searches for the given term in the textView content and highlights matches
func performSearch(term string) { func performSearch(term string) {
@@ -384,6 +320,7 @@ func performSearch(term string) {
if searchText == "" { if searchText == "" {
searchResults = nil searchResults = nil
searchResultLengths = nil searchResultLengths = nil
originalTextForSearch = ""
// Re-render text without highlights // Re-render text without highlights
textView.SetText(chatToText(cfg.ShowSys)) textView.SetText(chatToText(cfg.ShowSys))
colorText() colorText()
@@ -391,6 +328,7 @@ func performSearch(term string) {
} }
// Get formatted text and search directly in it to avoid mapping issues // Get formatted text and search directly in it to avoid mapping issues
formattedText := textView.GetText(true) formattedText := textView.GetText(true)
originalTextForSearch = formattedText
searchTermLower := strings.ToLower(searchText) searchTermLower := strings.ToLower(searchText)
formattedTextLower := strings.ToLower(formattedText) formattedTextLower := strings.ToLower(formattedText)
// Find all occurrences of the search term in the formatted text directly // Find all occurrences of the search term in the formatted text directly
@@ -409,7 +347,7 @@ func performSearch(term string) {
// No matches found // No matches found
searchResults = nil searchResults = nil
searchResultLengths = nil searchResultLengths = nil
notification := fmt.Sprintf("Pattern not found: %s", term) notification := "Pattern not found: " + term
if err := notifyUser("search", notification); err != nil { if err := notifyUser("search", notification); err != nil {
logger.Error("failed to send notification", "error", err) logger.Error("failed to send notification", "error", err)
} }
@@ -431,23 +369,18 @@ func highlightCurrentMatch() {
if len(searchResults) == 0 || searchIndex >= len(searchResults) { if len(searchResults) == 0 || searchIndex >= len(searchResults) {
return return
} }
// Get the stored formatted text // Get the stored formatted text
formattedText := textView.GetText(true) formattedText := originalTextForSearch
// For tview to properly support highlighting and scrolling, we need to work with its region system // For tview to properly support highlighting and scrolling, we need to work with its region system
// Instead of just applying highlights, we need to add region tags to the text // Instead of just applying highlights, we need to add region tags to the text
highlightedText := addRegionTags(formattedText, searchResults, searchResultLengths, searchIndex, searchText) highlightedText := addRegionTags(formattedText, searchResults, searchResultLengths, searchIndex, searchText)
// Update the text view with the text that includes region tags // Update the text view with the text that includes region tags
textView.SetText(highlightedText) textView.SetText(highlightedText)
// Highlight the current region and scroll to it // Highlight the current region and scroll to it
// Need to identify which position in the results array corresponds to the current match // Need to identify which position in the results array corresponds to the current match
// The region ID will be search_<position>_<index> // The region ID will be search_<position>_<index>
currentRegion := fmt.Sprintf("search_%d_%d", searchResults[searchIndex], searchIndex) currentRegion := fmt.Sprintf("search_%d_%d", searchResults[searchIndex], searchIndex)
textView.Highlight(currentRegion).ScrollToHighlight() textView.Highlight(currentRegion).ScrollToHighlight()
// Send notification about which match we're at // Send notification about which match we're at
notification := fmt.Sprintf("Match %d of %d", searchIndex+1, len(searchResults)) notification := fmt.Sprintf("Match %d of %d", searchIndex+1, len(searchResults))
if err := notifyUser("search", notification); err != nil { if err := notifyUser("search", notification); err != nil {
@@ -455,67 +388,10 @@ func highlightCurrentMatch() {
} }
} }
// applyAllHighlights applies highlighting to all search matches in the text
func applyAllHighlights(text string, positions []int, currentIdx int, searchTerm string) string {
if len(positions) == 0 {
return text
}
// For performance and to avoid freezing, use a simpler approach just highlighting the positions
// that were found in the initial search (even if not perfectly mapped to formatted text)
var result strings.Builder
// For simplicity and to prevent freezing, don't do complex recalculations
// Instead, we'll just highlight based on the initial search results
lastEnd := 0
// Since positions come from plain text search, they may not align with formatted text
// For robustness, only process positions that are within bounds
for i, pos := range positions {
// Only process if within text bounds
if pos >= len(text) {
continue
}
endPos := pos + len(searchTerm)
if endPos > len(text) {
continue
}
// Check if the actual text matches the search term (case insensitive)
actualText := text[pos:endPos]
if strings.ToLower(actualText) != strings.ToLower(searchTerm) {
continue // Skip if the text doesn't actually match at this position
}
// Add text before this match
if pos > lastEnd {
result.WriteString(text[lastEnd:pos])
}
// Highlight this match
highlight := `[gold:red:u]` // All matches - gold on red
if i == currentIdx {
highlight = `[yellow:blue:b]` // Current match - yellow on blue bold
}
result.WriteString(highlight)
result.WriteString(actualText)
result.WriteString(`[-:-:-]`) // Reset formatting
lastEnd = endPos
}
// Add the rest of the text after the last processed match
if lastEnd < len(text) {
result.WriteString(text[lastEnd:])
}
return result.String()
}
// showSearchBar shows the search input field as an overlay // showSearchBar shows the search input field as an overlay
func showSearchBar() { func showSearchBar() {
isSearching = true
// Create a temporary flex to combine search and main content // Create a temporary flex to combine search and main content
updatedFlex := tview.NewFlex().SetDirection(tview.FlexRow). updatedFlex := tview.NewFlex().SetDirection(tview.FlexRow).
AddItem(searchField, 3, 0, true). // Search field at top AddItem(searchField, 3, 0, true). // Search field at top
@@ -528,7 +404,6 @@ func showSearchBar() {
// hideSearchBar hides the search input field // hideSearchBar hides the search input field
func hideSearchBar() { func hideSearchBar() {
isSearching = false
pages.RemovePage(searchPageName) pages.RemovePage(searchPageName)
// Return focus to the text view // Return focus to the text view
app.SetFocus(textView) app.SetFocus(textView)
@@ -541,21 +416,16 @@ func addRegionTags(text string, positions []int, lengths []int, currentIdx int,
if len(positions) == 0 { if len(positions) == 0 {
return text return text
} }
var result strings.Builder var result strings.Builder
lastEnd := 0 lastEnd := 0
for i, pos := range positions { for i, pos := range positions {
endPos := pos + lengths[i] endPos := pos + lengths[i]
// Add text before this match // Add text before this match
if pos > lastEnd { if pos > lastEnd {
result.WriteString(text[lastEnd:pos]) result.WriteString(text[lastEnd:pos])
} }
// The matched text, which may contain its own formatting tags // The matched text, which may contain its own formatting tags
actualText := text[pos:endPos] actualText := text[pos:endPos]
// Add region tag and highlighting for this match // Add region tag and highlighting for this match
// Use a unique region id that includes the match index to avoid conflicts // Use a unique region id that includes the match index to avoid conflicts
regionId := fmt.Sprintf("search_%d_%d", pos, i) // position + index to ensure uniqueness regionId := fmt.Sprintf("search_%d_%d", pos, i) // position + index to ensure uniqueness
@@ -569,45 +439,20 @@ func addRegionTags(text string, positions []int, lengths []int, currentIdx int,
highlightStart = fmt.Sprintf(`["%s"][gold:red:u]`, regionId) // Other matches with region and highlight highlightStart = fmt.Sprintf(`["%s"][gold:red:u]`, regionId) // Other matches with region and highlight
highlightEnd = `[-:-:-][""]` // Reset formatting and close region highlightEnd = `[-:-:-][""]` // Reset formatting and close region
} }
result.WriteString(highlightStart) result.WriteString(highlightStart)
result.WriteString(actualText) result.WriteString(actualText)
result.WriteString(highlightEnd) result.WriteString(highlightEnd)
lastEnd = endPos lastEnd = endPos
} }
// Add the rest of the text after the last processed match // Add the rest of the text after the last processed match
if lastEnd < len(text) { if lastEnd < len(text) {
result.WriteString(text[lastEnd:]) result.WriteString(text[lastEnd:])
} }
return result.String() return result.String()
} }
// insertHighlightAtPosition inserts highlight tags around a specific position in the text
func insertHighlightAtPosition(originalText string, pos int, length int) string {
if pos < 0 || pos >= len(originalText) || pos+length > len(originalText) {
return originalText
}
// Insert highlight tags around the match
var result strings.Builder
result.WriteString(originalText[:pos])
result.WriteString(`[gold:red:u]`) // Highlight with gold text on red background and underline
result.WriteString(originalText[pos : pos+length])
result.WriteString(`[-]`) // Reset to default formatting
result.WriteString(originalText[pos+length:])
return result.String()
}
// highlightTextWithRegions adds region tags to highlight search matches
func highlightTextWithRegions(originalText string, matchStart int, matchLength int) string {
// For now, we'll return the original text and use tview's highlight system differently
// The highlighting will be applied via the textView.Highlight() method
return originalText
}
// searchNext finds the next occurrence of the search term // searchNext finds the next occurrence of the search term
func searchNext() { func searchNext() {
@@ -617,7 +462,6 @@ func searchNext() {
} }
return return
} }
searchIndex = (searchIndex + 1) % len(searchResults) searchIndex = (searchIndex + 1) % len(searchResults)
highlightCurrentMatch() highlightCurrentMatch()
} }
@@ -630,7 +474,6 @@ func searchPrev() {
} }
return return
} }
if searchIndex == 0 { if searchIndex == 0 {
searchIndex = len(searchResults) - 1 searchIndex = len(searchResults) - 1
} else { } else {
@@ -649,7 +492,7 @@ func init() {
textView = tview.NewTextView(). textView = tview.NewTextView().
SetDynamicColors(true). SetDynamicColors(true).
SetRegions(true) SetRegions(true)
//
flex = tview.NewFlex().SetDirection(tview.FlexRow). flex = tview.NewFlex().SetDirection(tview.FlexRow).
AddItem(textView, 0, 40, false). AddItem(textView, 0, 40, false).
AddItem(textArea, 0, 10, true). // Restore original height AddItem(textArea, 0, 10, true). // Restore original height
@@ -658,9 +501,10 @@ func init() {
textView.SetDoneFunc(func(key tcell.Key) { textView.SetDoneFunc(func(key tcell.Key) {
if key == tcell.KeyEnter { if key == tcell.KeyEnter {
if len(searchResults) > 0 { // Check if a search is active if len(searchResults) > 0 { // Check if a search is active
hideSearchBar() // Hide the search bar if visible hideSearchBar() // Hide the search bar if visible
searchResults = nil // Clear search results searchResults = nil // Clear search results
searchResultLengths = nil // Clear search result lengths searchResultLengths = nil // Clear search result lengths
originalTextForSearch = ""
textView.SetText(chatToText(cfg.ShowSys)) // Reset text without search regions textView.SetText(chatToText(cfg.ShowSys)) // Reset text without search regions
colorText() // Apply normal chat coloring colorText() // Apply normal chat coloring
} else { } else {
@@ -846,6 +690,7 @@ func init() {
hideSearchBar() hideSearchBar()
searchResults = nil searchResults = nil
searchResultLengths = nil searchResultLengths = nil
originalTextForSearch = ""
textView.SetText(chatToText(cfg.ShowSys)) textView.SetText(chatToText(cfg.ShowSys))
colorText() colorText()
return return