mirror of
https://github.com/walles/moar.git
synced 2024-08-16 15:30:34 +03:00
Compare commits
22 Commits
19b54159a3
...
c8b4ecf38a
Author | SHA1 | Date | |
---|---|---|---|
|
c8b4ecf38a | ||
|
e9bc0dffda | ||
|
5eb548a126 | ||
|
6325a1cd95 | ||
|
c1641c8456 | ||
|
6bb2919459 | ||
|
eae1915b5e | ||
|
11095496b2 | ||
|
d1fac66a23 | ||
|
f434895eef | ||
|
7b032b2fa4 | ||
|
686882ffcd | ||
|
60d3577b59 | ||
|
64ae443e1f | ||
|
856574c289 | ||
|
ee35c6b166 | ||
|
625193933d | ||
|
387fb348c7 | ||
|
84cf4a9cd9 | ||
|
aa342d4325 | ||
|
27d03630a5 | ||
|
0ba0415a50 |
2
build.sh
2
build.sh
@ -28,4 +28,6 @@ fi
|
||||
go build -trimpath -ldflags="-s -w -X main.versionString=${VERSION}" -o "${BINARY}"
|
||||
|
||||
# Alternative build line, if you want to attach to the running process in the Go debugger:
|
||||
#
|
||||
# -gcflags='-N -l' disables optimizations and inlining, which makes debugging easier.
|
||||
# go build -ldflags="-X main.versionString=${VERSION}" -gcflags='-N -l' -o "${BINARY}"
|
||||
|
@ -445,6 +445,12 @@ func (p *Pager) StartPaging(screen twin.Screen, chromaStyle *chroma.Style, chrom
|
||||
case twin.EventTerminalBackgroundDetected:
|
||||
// Do nothing, we don't care about background color updates
|
||||
|
||||
case eventGoToLine:
|
||||
line := NewScrollPositionFromLineNumber(event.lineNumber, "goToLine")
|
||||
if !line.isVisible(p) {
|
||||
p.scrollPosition = line
|
||||
}
|
||||
|
||||
default:
|
||||
log.Warnf("Unhandled event type: %v", event)
|
||||
}
|
||||
|
@ -2,18 +2,37 @@ package m
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"sync"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/walles/moar/m/linenumbers"
|
||||
"github.com/walles/moar/twin"
|
||||
)
|
||||
|
||||
type PagerModeSearch struct {
|
||||
pager *Pager
|
||||
type searchCommand int
|
||||
|
||||
const (
|
||||
searchCommandSearch searchCommand = iota
|
||||
searchCommandDone
|
||||
)
|
||||
|
||||
type eventGoToLine struct {
|
||||
lineNumber linenumbers.LineNumber
|
||||
}
|
||||
|
||||
func (m PagerModeSearch) drawFooter(_ string, _ string) {
|
||||
type PagerModeSearch struct {
|
||||
pager *Pager
|
||||
|
||||
pattern *regexp.Regexp
|
||||
startLine linenumbers.LineNumber
|
||||
lock sync.Mutex
|
||||
|
||||
searcher chan searchCommand
|
||||
}
|
||||
|
||||
func (m *PagerModeSearch) drawFooter(_ string, _ string) {
|
||||
width, height := m.pager.screen.Size()
|
||||
|
||||
pos := 0
|
||||
@ -33,12 +52,77 @@ func (m PagerModeSearch) drawFooter(_ string, _ string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *PagerModeSearch) searcherSearch() *linenumbers.LineNumber {
|
||||
// Search to the end
|
||||
for position := m.startLine; ; position = position.NonWrappingAdd(1) {
|
||||
line := m.pager.reader.GetLine(position)
|
||||
if line == nil {
|
||||
// Reached end of input without any match, give up
|
||||
break
|
||||
}
|
||||
|
||||
if m.pattern.MatchString(line.Plain(&position)) {
|
||||
return &position
|
||||
}
|
||||
}
|
||||
|
||||
// Search from the beginning
|
||||
for position := m.startLine; position != m.startLine; position = position.NonWrappingAdd(1) {
|
||||
line := m.pager.reader.GetLine(position)
|
||||
if m.pattern.MatchString(line.Plain(&position)) {
|
||||
return &position
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *PagerModeSearch) initSearcher() {
|
||||
m.searcher = make(chan searchCommand, 1)
|
||||
|
||||
go func() {
|
||||
for command := range m.searcher {
|
||||
switch command {
|
||||
case searchCommandSearch:
|
||||
found := m.searcherSearch()
|
||||
if found != nil {
|
||||
m.pager.screen.Events() <- eventGoToLine{*found}
|
||||
}
|
||||
case searchCommandDone:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (m *PagerModeSearch) updateSearchPattern() {
|
||||
// For highlighting
|
||||
m.pager.searchPattern = toPattern(m.pager.searchString)
|
||||
if m.pager.searchPattern == nil {
|
||||
// Nothing to search for, never mind
|
||||
return
|
||||
}
|
||||
startLine := m.pager.scrollPosition.lineNumber(m.pager)
|
||||
if startLine == nil {
|
||||
// Nothing to search in, never mind
|
||||
return
|
||||
}
|
||||
|
||||
m.pager.scrollToSearchHits()
|
||||
if m.searcher == nil {
|
||||
m.initSearcher()
|
||||
}
|
||||
|
||||
// FIXME: If the user is typing, indicate to user if we didn't find anything
|
||||
// Give the searcher the new pattern
|
||||
m.lock.Lock()
|
||||
m.pattern = m.pager.searchPattern
|
||||
m.startLine = *startLine
|
||||
m.lock.Unlock()
|
||||
|
||||
// Tell the searcher there's a new pattern to look for
|
||||
select {
|
||||
case m.searcher <- searchCommandSearch:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// toPattern compiles a search string into a pattern.
|
||||
@ -92,9 +176,12 @@ func removeLastChar(s string) string {
|
||||
return s[:len(s)-size]
|
||||
}
|
||||
|
||||
func (m PagerModeSearch) onKey(key twin.KeyCode) {
|
||||
func (m *PagerModeSearch) onKey(key twin.KeyCode) {
|
||||
switch key {
|
||||
case twin.KeyEscape, twin.KeyEnter:
|
||||
if m.searcher != nil {
|
||||
m.searcher <- searchCommandDone
|
||||
}
|
||||
//nolint:gosimple // The linter's advice is just wrong here
|
||||
m.pager.mode = PagerModeViewing{pager: m.pager}
|
||||
|
||||
@ -107,6 +194,9 @@ func (m PagerModeSearch) onKey(key twin.KeyCode) {
|
||||
m.updateSearchPattern()
|
||||
|
||||
case twin.KeyUp, twin.KeyDown, twin.KeyPgUp, twin.KeyPgDown:
|
||||
if m.searcher != nil {
|
||||
m.searcher <- searchCommandDone
|
||||
}
|
||||
//nolint:gosimple // The linter's advice is just wrong here
|
||||
m.pager.mode = PagerModeViewing{pager: m.pager}
|
||||
m.pager.mode.onKey(key)
|
||||
@ -116,7 +206,7 @@ func (m PagerModeSearch) onKey(key twin.KeyCode) {
|
||||
}
|
||||
}
|
||||
|
||||
func (m PagerModeSearch) onRune(char rune) {
|
||||
func (m *PagerModeSearch) onRune(char rune) {
|
||||
m.pager.searchString = m.pager.searchString + string(char)
|
||||
m.updateSearchPattern()
|
||||
}
|
||||
|
@ -144,7 +144,7 @@ func (m PagerModeViewing) onRune(char rune) {
|
||||
p.handleScrolledDown()
|
||||
|
||||
case '/':
|
||||
p.mode = PagerModeSearch{pager: p}
|
||||
p.mode = &PagerModeSearch{pager: p}
|
||||
p.TargetLineNumber = nil
|
||||
p.searchString = ""
|
||||
p.searchPattern = nil
|
||||
|
149
m/search.go
149
m/search.go
@ -2,40 +2,125 @@ package m
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/walles/moar/m/linenumbers"
|
||||
)
|
||||
|
||||
func (p *Pager) scrollToSearchHits() {
|
||||
if p.searchPattern == nil {
|
||||
// This is not a search
|
||||
return
|
||||
}
|
||||
|
||||
firstHitPosition := p.findFirstHit(*p.scrollPosition.lineNumber(p), nil, false)
|
||||
if firstHitPosition == nil {
|
||||
// Try again from the top
|
||||
firstHitPosition = p.findFirstHit(linenumbers.LineNumber{}, p.scrollPosition.lineNumber(p), false)
|
||||
}
|
||||
if firstHitPosition == nil {
|
||||
// No match, give up
|
||||
return
|
||||
}
|
||||
|
||||
if firstHitPosition.isVisible(p) {
|
||||
// Already on-screen, never mind
|
||||
return
|
||||
}
|
||||
|
||||
p.scrollPosition = *firstHitPosition
|
||||
}
|
||||
|
||||
// NOTE: When we search, we do that by looping over the *input lines*, not
|
||||
// the screen lines. That's why we're using a line number rather than a
|
||||
// scrollPosition for searching.
|
||||
// NOTE: When we search, we do that by looping over the *input lines*, not the
|
||||
// screen lines. That's why startPosition is a LineNumber rather than a
|
||||
// scrollPosition.
|
||||
//
|
||||
// The `beforePosition` parameter is exclusive, meaning that line will not be
|
||||
// searched.
|
||||
//
|
||||
// For the actual searching, this method will call _findFirstHit() in parallel
|
||||
// on multiple cores, to help large file search performance.
|
||||
//
|
||||
// FIXME: We should take startPosition.deltaScreenLines into account as well!
|
||||
func (p *Pager) findFirstHit(startPosition linenumbers.LineNumber, beforePosition *linenumbers.LineNumber, backwards bool) *scrollPosition {
|
||||
// If the number of lines to search matches the number of cores (or more),
|
||||
// divide the search into chunks. Otherwise use one chunk.
|
||||
chunkCount := runtime.NumCPU()
|
||||
var linesCount int
|
||||
if backwards {
|
||||
// If the startPosition is zero, that should make the count one
|
||||
linesCount = startPosition.AsZeroBased() + 1
|
||||
if beforePosition != nil {
|
||||
// Searching from 1 with before set to 0 should make the count 1
|
||||
linesCount = startPosition.AsZeroBased() - beforePosition.AsZeroBased()
|
||||
}
|
||||
} else {
|
||||
linesCount = p.reader.GetLineCount() - startPosition.AsZeroBased()
|
||||
if beforePosition != nil {
|
||||
// Searching from 1 with before set to 2 should make the count 1
|
||||
linesCount = beforePosition.AsZeroBased() - startPosition.AsZeroBased()
|
||||
}
|
||||
}
|
||||
|
||||
if linesCount < chunkCount {
|
||||
chunkCount = 1
|
||||
}
|
||||
chunkSize := linesCount / chunkCount
|
||||
|
||||
log.Debugf("Searching %d lines across %d cores with %d lines per core...", linesCount, chunkCount, chunkSize)
|
||||
t0 := time.Now()
|
||||
defer func() {
|
||||
linesPerSecond := float64(linesCount) / time.Since(t0).Seconds()
|
||||
linesPerSecondS := fmt.Sprintf("%.0f", linesPerSecond)
|
||||
if linesPerSecond > 7_000_000.0 {
|
||||
linesPerSecondS = fmt.Sprintf("%.0fM", linesPerSecond/1000_000.0)
|
||||
} else if linesPerSecond > 7_000.0 {
|
||||
linesPerSecondS = fmt.Sprintf("%.0fk", linesPerSecond/1000.0)
|
||||
}
|
||||
|
||||
if linesCount > 0 {
|
||||
log.Debugf("Searched %d lines in %s at %slines/s or %s/line",
|
||||
linesCount,
|
||||
time.Since(t0),
|
||||
linesPerSecondS,
|
||||
time.Since(t0)/time.Duration(linesCount))
|
||||
} else {
|
||||
log.Debugf("Searched %d lines in %s at %slines/s", linesCount, time.Since(t0), linesPerSecondS)
|
||||
}
|
||||
}()
|
||||
|
||||
// Each parallel search will start at one of these positions
|
||||
searchStarts := make([]linenumbers.LineNumber, chunkCount)
|
||||
direction := 1
|
||||
if backwards {
|
||||
direction = -1
|
||||
}
|
||||
for i := 0; i < chunkCount; i++ {
|
||||
searchStarts[i] = startPosition.NonWrappingAdd(i * direction * chunkSize)
|
||||
}
|
||||
|
||||
// Make a results array, with one result per chunk
|
||||
findings := make([]chan *scrollPosition, chunkCount)
|
||||
|
||||
// Search all chunks in parallel
|
||||
for i, searchStart := range searchStarts {
|
||||
findings[i] = make(chan *scrollPosition)
|
||||
|
||||
searchEndIndex := i + 1
|
||||
var chunkBefore *linenumbers.LineNumber
|
||||
if searchEndIndex < len(searchStarts) {
|
||||
chunkBefore = &searchStarts[searchEndIndex]
|
||||
} else if beforePosition != nil {
|
||||
chunkBefore = beforePosition
|
||||
}
|
||||
|
||||
go func(i int, searchStart linenumbers.LineNumber, chunkBefore *linenumbers.LineNumber) {
|
||||
findings[i] <- p._findFirstHit(searchStart, chunkBefore, backwards)
|
||||
}(i, searchStart, chunkBefore)
|
||||
}
|
||||
|
||||
// Return the first non-nil result
|
||||
for _, finding := range findings {
|
||||
result := <-finding
|
||||
if result != nil {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NOTE: When we search, we do that by looping over the *input lines*, not the
|
||||
// screen lines. That's why startPosition is a LineNumber rather than a
|
||||
// scrollPosition.
|
||||
//
|
||||
// The `beforePosition` parameter is exclusive, meaning that line will not be
|
||||
// searched.
|
||||
//
|
||||
// This method will run over multiple chunks of the input file in parallel to
|
||||
// help large file search performance.
|
||||
//
|
||||
// FIXME: We should take startPosition.deltaScreenLines into account as well!
|
||||
func (p *Pager) _findFirstHit(startPosition linenumbers.LineNumber, beforePosition *linenumbers.LineNumber, backwards bool) *scrollPosition {
|
||||
searchPosition := startPosition
|
||||
for {
|
||||
line := p.reader.GetLine(searchPosition)
|
||||
@ -51,18 +136,18 @@ func (p *Pager) findFirstHit(startPosition linenumbers.LineNumber, beforePositio
|
||||
|
||||
if backwards {
|
||||
if (searchPosition == linenumbers.LineNumber{}) {
|
||||
// No match, give up
|
||||
// Reached the top without any match, give up
|
||||
return nil
|
||||
}
|
||||
|
||||
searchPosition = searchPosition.NonWrappingAdd(-1)
|
||||
} else {
|
||||
searchPosition = searchPosition.NonWrappingAdd(1)
|
||||
}
|
||||
|
||||
if beforePosition != nil && searchPosition == *beforePosition {
|
||||
// No match, give up
|
||||
return nil
|
||||
}
|
||||
if beforePosition != nil && searchPosition == *beforePosition {
|
||||
// No match, give up
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ func modeName(pager *Pager) string {
|
||||
return "Viewing"
|
||||
case PagerModeNotFound:
|
||||
return "NotFound"
|
||||
case PagerModeSearch:
|
||||
case *PagerModeSearch:
|
||||
return "Search"
|
||||
case *PagerModeGotoLine:
|
||||
return "GotoLine"
|
||||
@ -116,7 +116,7 @@ func Test152(t *testing.T) {
|
||||
|
||||
// Search for the first not-visible hit
|
||||
pager.searchString = "abcde"
|
||||
searchMode := PagerModeSearch{pager: pager}
|
||||
searchMode := &PagerModeSearch{pager: pager}
|
||||
pager.mode = searchMode
|
||||
|
||||
// Scroll to the next search hit
|
||||
|
Loading…
Reference in New Issue
Block a user