Fix: highlight shift because of injected tags
This commit is contained in:
171
tui.go
171
tui.go
@@ -31,7 +31,6 @@ var (
|
||||
defaultImage = "sysprompts/llama.png"
|
||||
indexPickWindow *tview.InputField
|
||||
renameWindow *tview.InputField
|
||||
searchWindow *tview.InputField
|
||||
fullscreenMode bool
|
||||
// pages
|
||||
historyPage = "historyPage"
|
||||
@@ -50,7 +49,6 @@ var (
|
||||
|
||||
// For overlay search functionality
|
||||
searchField *tview.InputField
|
||||
isSearching bool
|
||||
searchPageName = "searchOverlay"
|
||||
// help text
|
||||
helpText = `
|
||||
@@ -241,22 +239,17 @@ func executeCommandAndDisplay(cmdText string) {
|
||||
colorText()
|
||||
return
|
||||
}
|
||||
|
||||
command := cmdParts[0]
|
||||
args := []string{}
|
||||
if len(cmdParts) > 1 {
|
||||
args = cmdParts[1:]
|
||||
}
|
||||
|
||||
// Create the command execution
|
||||
cmd := exec.Command(command, args...)
|
||||
|
||||
// Execute the command and get output
|
||||
output, err := cmd.CombinedOutput()
|
||||
|
||||
// Add the command being executed to the chat
|
||||
fmt.Fprintf(textView, "\n[yellow]$ %s[-:-:-]\n", cmdText)
|
||||
|
||||
if err != nil {
|
||||
// Include both output and 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")
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll to end and update colors
|
||||
textView.ScrollToEnd()
|
||||
colorText()
|
||||
@@ -283,7 +275,6 @@ func parseCommand(cmd string) []string {
|
||||
var current string
|
||||
var inQuotes bool
|
||||
var quoteChar rune
|
||||
|
||||
for _, r := range cmd {
|
||||
switch r {
|
||||
case '"', '\'':
|
||||
@@ -308,11 +299,9 @@ func parseCommand(cmd string) []string {
|
||||
current += string(r)
|
||||
}
|
||||
}
|
||||
|
||||
if current != "" {
|
||||
args = append(args, current)
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
@@ -321,62 +310,9 @@ var searchResults []int
|
||||
var searchResultLengths []int // To store the length of each match in the formatted string
|
||||
var searchIndex int
|
||||
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
|
||||
func performSearch(term string) {
|
||||
@@ -384,6 +320,7 @@ func performSearch(term string) {
|
||||
if searchText == "" {
|
||||
searchResults = nil
|
||||
searchResultLengths = nil
|
||||
originalTextForSearch = ""
|
||||
// Re-render text without highlights
|
||||
textView.SetText(chatToText(cfg.ShowSys))
|
||||
colorText()
|
||||
@@ -391,6 +328,7 @@ func performSearch(term string) {
|
||||
}
|
||||
// Get formatted text and search directly in it to avoid mapping issues
|
||||
formattedText := textView.GetText(true)
|
||||
originalTextForSearch = formattedText
|
||||
searchTermLower := strings.ToLower(searchText)
|
||||
formattedTextLower := strings.ToLower(formattedText)
|
||||
// Find all occurrences of the search term in the formatted text directly
|
||||
@@ -409,7 +347,7 @@ func performSearch(term string) {
|
||||
// No matches found
|
||||
searchResults = nil
|
||||
searchResultLengths = nil
|
||||
notification := fmt.Sprintf("Pattern not found: %s", term)
|
||||
notification := "Pattern not found: " + term
|
||||
if err := notifyUser("search", notification); err != nil {
|
||||
logger.Error("failed to send notification", "error", err)
|
||||
}
|
||||
@@ -431,23 +369,18 @@ func highlightCurrentMatch() {
|
||||
if len(searchResults) == 0 || searchIndex >= len(searchResults) {
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
// Instead of just applying highlights, we need to add region tags to the text
|
||||
highlightedText := addRegionTags(formattedText, searchResults, searchResultLengths, searchIndex, searchText)
|
||||
|
||||
// Update the text view with the text that includes region tags
|
||||
textView.SetText(highlightedText)
|
||||
|
||||
// Highlight the current region and scroll to it
|
||||
// Need to identify which position in the results array corresponds to the current match
|
||||
// The region ID will be search_<position>_<index>
|
||||
currentRegion := fmt.Sprintf("search_%d_%d", searchResults[searchIndex], searchIndex)
|
||||
textView.Highlight(currentRegion).ScrollToHighlight()
|
||||
|
||||
// Send notification about which match we're at
|
||||
notification := fmt.Sprintf("Match %d of %d", searchIndex+1, len(searchResults))
|
||||
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
|
||||
func showSearchBar() {
|
||||
isSearching = true
|
||||
// Create a temporary flex to combine search and main content
|
||||
updatedFlex := tview.NewFlex().SetDirection(tview.FlexRow).
|
||||
AddItem(searchField, 3, 0, true). // Search field at top
|
||||
@@ -528,7 +404,6 @@ func showSearchBar() {
|
||||
|
||||
// hideSearchBar hides the search input field
|
||||
func hideSearchBar() {
|
||||
isSearching = false
|
||||
pages.RemovePage(searchPageName)
|
||||
// Return focus to the text view
|
||||
app.SetFocus(textView)
|
||||
@@ -541,21 +416,16 @@ func addRegionTags(text string, positions []int, lengths []int, currentIdx int,
|
||||
if len(positions) == 0 {
|
||||
return text
|
||||
}
|
||||
|
||||
var result strings.Builder
|
||||
lastEnd := 0
|
||||
|
||||
for i, pos := range positions {
|
||||
endPos := pos + lengths[i]
|
||||
|
||||
// Add text before this match
|
||||
if pos > lastEnd {
|
||||
result.WriteString(text[lastEnd:pos])
|
||||
}
|
||||
|
||||
// The matched text, which may contain its own formatting tags
|
||||
actualText := text[pos:endPos]
|
||||
|
||||
// Add region tag and highlighting for this match
|
||||
// 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
|
||||
@@ -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
|
||||
highlightEnd = `[-:-:-][""]` // Reset formatting and close region
|
||||
}
|
||||
|
||||
result.WriteString(highlightStart)
|
||||
result.WriteString(actualText)
|
||||
result.WriteString(highlightEnd)
|
||||
|
||||
lastEnd = endPos
|
||||
}
|
||||
|
||||
// Add the rest of the text after the last processed match
|
||||
if lastEnd < len(text) {
|
||||
result.WriteString(text[lastEnd:])
|
||||
}
|
||||
|
||||
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
|
||||
func searchNext() {
|
||||
@@ -617,7 +462,6 @@ func searchNext() {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
searchIndex = (searchIndex + 1) % len(searchResults)
|
||||
highlightCurrentMatch()
|
||||
}
|
||||
@@ -630,7 +474,6 @@ func searchPrev() {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if searchIndex == 0 {
|
||||
searchIndex = len(searchResults) - 1
|
||||
} else {
|
||||
@@ -649,7 +492,7 @@ func init() {
|
||||
textView = tview.NewTextView().
|
||||
SetDynamicColors(true).
|
||||
SetRegions(true)
|
||||
|
||||
//
|
||||
flex = tview.NewFlex().SetDirection(tview.FlexRow).
|
||||
AddItem(textView, 0, 40, false).
|
||||
AddItem(textArea, 0, 10, true). // Restore original height
|
||||
@@ -661,6 +504,7 @@ func init() {
|
||||
hideSearchBar() // Hide the search bar if visible
|
||||
searchResults = nil // Clear search results
|
||||
searchResultLengths = nil // Clear search result lengths
|
||||
originalTextForSearch = ""
|
||||
textView.SetText(chatToText(cfg.ShowSys)) // Reset text without search regions
|
||||
colorText() // Apply normal chat coloring
|
||||
} else {
|
||||
@@ -846,6 +690,7 @@ func init() {
|
||||
hideSearchBar()
|
||||
searchResults = nil
|
||||
searchResultLengths = nil
|
||||
originalTextForSearch = ""
|
||||
textView.SetText(chatToText(cfg.ShowSys))
|
||||
colorText()
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user