mirror of
https://github.com/charmbracelet/gum
synced 2024-06-08 16:52:17 +02:00
c8710071ad
* Added initial search functionality * Added a handler for the key presses * Added a searchbar at the bottom of the screen * Made search results cycleable by pressing n * correct start pos and ignore keys while searching * fix out of bound error when pressing n * made the matching pattern relative to the current pos * added p for searching for previous match * added highlighting to search matches * dynamically replaced all matches * fixed string highlight issue * fixed truncation issue * small simplifaction in ypos logic * made prev and next behave the same atBottom * simplified logic and fixed linter errors * updated help text * style changes * added comments * fixed truncation issue * fixes infinte loop on very long lines * added simple lipgloss truncate function * updated colors for better contrast * lint fix * initial commit for soft-wrap functionality * linter corrections and added for pager with new model * added generic functions to a utility package * fix soft lint errors * made N match previous as well as p * replaced help text when search is active * ran gofmt -w * reimplemented search and next to enabled support for dynamic highlights * made the highlight move as you progress through the search * simplified highlighter * improvements to the clean up of the highlight function * semi working reverse search * reverse search without highlight * added semi working highlight for reverser search * working version of previous match * fixed issue with single letter matches in next * added support for softwrapping * respond to soft lint warnings * removed unused function * lint * simplified matchers and fixed duplicate highlights * optimisations and change in matching pattern * fixed bug in lipglosspadding and allowed matching 1 etc. * make highlight respect user settings * fixed logic error in slice * made prev match wrap around * fix: show next/prev match help when active * updated how view port line is set * avoid crashes when regex doesn't compile * fix: spelling previous --------- Co-authored-by: Maas Lalani <maas@lalani.dev>
164 lines
4.4 KiB
Go
164 lines
4.4 KiB
Go
package pager
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/charmbracelet/bubbles/textinput"
|
|
"github.com/charmbracelet/gum/internal/utils"
|
|
"github.com/charmbracelet/lipgloss"
|
|
)
|
|
|
|
type search struct {
|
|
active bool
|
|
input textinput.Model
|
|
query *regexp.Regexp
|
|
matchIndex int
|
|
matchLipglossStr string
|
|
matchString string
|
|
}
|
|
|
|
func (s *search) new() {
|
|
input := textinput.New()
|
|
input.Placeholder = "search"
|
|
input.Prompt = "/"
|
|
input.PromptStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
|
|
s.input = input
|
|
}
|
|
|
|
func (s *search) Begin() {
|
|
s.new()
|
|
s.active = true
|
|
s.input.Focus()
|
|
}
|
|
|
|
// Execute find all lines in the model with a match.
|
|
func (s *search) Execute(m *model) {
|
|
defer s.Done()
|
|
if s.input.Value() == "" {
|
|
s.query = nil
|
|
return
|
|
}
|
|
|
|
var err error
|
|
s.query, err = regexp.Compile(s.input.Value())
|
|
if err != nil {
|
|
s.query = nil
|
|
return
|
|
}
|
|
query := regexp.MustCompile(fmt.Sprintf("(%s)", s.query.String()))
|
|
m.content = query.ReplaceAllString(m.content, m.matchStyle.Render("$1"))
|
|
|
|
// Recompile the regex to match the an replace the highlights.
|
|
leftPad, _ := utils.LipglossPadding(m.matchStyle)
|
|
matchingString := regexp.QuoteMeta(m.matchStyle.Render()[:leftPad]) + s.query.String() + regexp.QuoteMeta(m.matchStyle.Render()[leftPad:])
|
|
s.query, err = regexp.Compile(matchingString)
|
|
if err != nil {
|
|
s.query = nil
|
|
}
|
|
}
|
|
|
|
func (s *search) Done() {
|
|
s.active = false
|
|
|
|
// To account for the first match is always executed.
|
|
s.matchIndex = -1
|
|
}
|
|
|
|
func (s *search) NextMatch(m *model) {
|
|
// Check that we are within bounds.
|
|
if s.query == nil {
|
|
return
|
|
}
|
|
|
|
// Remove previous highlight.
|
|
m.content = strings.Replace(m.content, s.matchLipglossStr, s.matchString, 1)
|
|
|
|
// Highlight the next match.
|
|
allMatches := s.query.FindAllStringIndex(m.content, -1)
|
|
if len(allMatches) == 0 {
|
|
return
|
|
}
|
|
|
|
leftPad, rightPad := utils.LipglossPadding(m.matchStyle)
|
|
s.matchIndex = (s.matchIndex + 1) % len(allMatches)
|
|
match := allMatches[s.matchIndex]
|
|
lhs := m.content[:match[0]]
|
|
rhs := m.content[match[0]:]
|
|
s.matchString = m.content[match[0]:match[1]]
|
|
s.matchLipglossStr = m.matchHighlightStyle.Render(s.matchString[leftPad : len(s.matchString)-rightPad])
|
|
m.content = lhs + strings.Replace(rhs, m.content[match[0]:match[1]], s.matchLipglossStr, 1)
|
|
|
|
// Update the viewport position.
|
|
var line int
|
|
formatStr := softWrapEm(m.content, m.maxWidth, m.softWrap)
|
|
index := strings.Index(formatStr, s.matchLipglossStr)
|
|
if index != -1 {
|
|
line = strings.Count(formatStr[:index], "\n")
|
|
}
|
|
|
|
// Only update if the match is not within the viewport.
|
|
if index != -1 && (line > m.viewport.YOffset-1+m.viewport.VisibleLineCount()-1 || line < m.viewport.YOffset) {
|
|
m.viewport.SetYOffset(line)
|
|
}
|
|
}
|
|
|
|
func (s *search) PrevMatch(m *model) {
|
|
// Check that we are within bounds.
|
|
if s.query == nil {
|
|
return
|
|
}
|
|
|
|
// Remove previous highlight.
|
|
m.content = strings.Replace(m.content, s.matchLipglossStr, s.matchString, 1)
|
|
|
|
// Highlight the previous match.
|
|
allMatches := s.query.FindAllStringIndex(m.content, -1)
|
|
if len(allMatches) == 0 {
|
|
return
|
|
}
|
|
|
|
s.matchIndex = (s.matchIndex - 1) % len(allMatches)
|
|
if s.matchIndex < 0 {
|
|
s.matchIndex = len(allMatches) - 1
|
|
}
|
|
|
|
leftPad, rightPad := utils.LipglossPadding(m.matchStyle)
|
|
match := allMatches[s.matchIndex]
|
|
lhs := m.content[:match[0]]
|
|
rhs := m.content[match[0]:]
|
|
s.matchString = m.content[match[0]:match[1]]
|
|
s.matchLipglossStr = m.matchHighlightStyle.Render(s.matchString[leftPad : len(s.matchString)-rightPad])
|
|
m.content = lhs + strings.Replace(rhs, m.content[match[0]:match[1]], s.matchLipglossStr, 1)
|
|
|
|
// Update the viewport position.
|
|
var line int
|
|
formatStr := softWrapEm(m.content, m.maxWidth, m.softWrap)
|
|
index := strings.Index(formatStr, s.matchLipglossStr)
|
|
if index != -1 {
|
|
line = strings.Count(formatStr[:index], "\n")
|
|
}
|
|
|
|
// Only update if the match is not within the viewport.
|
|
if index != -1 && (line > m.viewport.YOffset-1+m.viewport.VisibleLineCount()-1 || line < m.viewport.YOffset) {
|
|
m.viewport.SetYOffset(line)
|
|
}
|
|
}
|
|
|
|
func softWrapEm(str string, maxWidth int, softWrap bool) string {
|
|
var text strings.Builder
|
|
for _, line := range strings.Split(str, "\n") {
|
|
for softWrap && lipgloss.Width(line) > maxWidth {
|
|
truncatedLine := utils.LipglossTruncate(line, maxWidth)
|
|
text.WriteString(truncatedLine)
|
|
text.WriteString("\n")
|
|
line = strings.Replace(line, truncatedLine, "", 1)
|
|
}
|
|
text.WriteString(utils.LipglossTruncate(line, maxWidth))
|
|
text.WriteString("\n")
|
|
}
|
|
|
|
return text.String()
|
|
}
|