mirror of
https://github.com/walles/moar.git
synced 2024-11-22 21:50:43 +03:00
277 lines
7.5 KiB
Go
277 lines
7.5 KiB
Go
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
|
|
}
|
|
|
|
lineNumber := p.scrollPosition.lineNumber(p)
|
|
if lineNumber == nil {
|
|
// No lines to search
|
|
return
|
|
}
|
|
|
|
firstHitPosition := p.findFirstHit(*lineNumber, nil, false)
|
|
if firstHitPosition == nil && (*lineNumber != linenumbers.LineNumber{}) {
|
|
// Try again from the top
|
|
firstHitPosition = p.findFirstHit(linenumbers.LineNumber{}, lineNumber, 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 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)
|
|
if line == nil {
|
|
// No match, give up
|
|
return nil
|
|
}
|
|
|
|
lineText := line.Plain(&searchPosition)
|
|
if p.searchPattern.MatchString(lineText) {
|
|
return scrollPositionFromLineNumber("findFirstHit", searchPosition)
|
|
}
|
|
|
|
if backwards {
|
|
if (searchPosition == linenumbers.LineNumber{}) {
|
|
// 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
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *Pager) isViewing() bool {
|
|
_, isViewing := p.mode.(PagerModeViewing)
|
|
return isViewing
|
|
}
|
|
|
|
func (p *Pager) isNotFound() bool {
|
|
_, isNotFound := p.mode.(PagerModeNotFound)
|
|
return isNotFound
|
|
}
|
|
|
|
func (p *Pager) scrollToNextSearchHit() {
|
|
if p.searchPattern == nil {
|
|
// Nothing to search for, never mind
|
|
return
|
|
}
|
|
|
|
if p.reader.GetLineCount() == 0 {
|
|
// Nothing to search in, never mind
|
|
return
|
|
}
|
|
|
|
if p.isViewing() && p.isScrolledToEnd() {
|
|
p.mode = PagerModeNotFound{pager: p}
|
|
return
|
|
}
|
|
|
|
var firstSearchPosition linenumbers.LineNumber
|
|
|
|
switch {
|
|
case p.isViewing():
|
|
// Start searching on the first line below the bottom of the screen
|
|
position := p.getLastVisiblePosition().NextLine(1)
|
|
firstSearchPosition = *position.lineNumber(p)
|
|
|
|
case p.isNotFound():
|
|
// Restart searching from the top
|
|
p.mode = PagerModeViewing{pager: p}
|
|
firstSearchPosition = linenumbers.LineNumber{}
|
|
|
|
default:
|
|
panic(fmt.Sprint("Unknown search mode when finding next: ", p.mode))
|
|
}
|
|
|
|
firstHitPosition := p.findFirstHit(firstSearchPosition, nil, false)
|
|
if firstHitPosition == nil {
|
|
p.mode = PagerModeNotFound{pager: p}
|
|
return
|
|
}
|
|
p.scrollPosition = *firstHitPosition
|
|
|
|
// Don't let any search hit scroll out of sight
|
|
p.TargetLineNumber = nil
|
|
}
|
|
|
|
func (p *Pager) scrollToPreviousSearchHit() {
|
|
if p.searchPattern == nil {
|
|
// Nothing to search for, never mind
|
|
return
|
|
}
|
|
|
|
if p.reader.GetLineCount() == 0 {
|
|
// Nothing to search in, never mind
|
|
return
|
|
}
|
|
|
|
var firstSearchPosition linenumbers.LineNumber
|
|
|
|
switch {
|
|
case p.isViewing():
|
|
// Start searching on the first line above the top of the screen
|
|
position := p.scrollPosition.PreviousLine(1)
|
|
firstSearchPosition = *position.lineNumber(p)
|
|
|
|
case p.isNotFound():
|
|
// Restart searching from the bottom
|
|
p.mode = PagerModeViewing{pager: p}
|
|
firstSearchPosition = *linenumbers.LineNumberFromLength(p.reader.GetLineCount())
|
|
|
|
default:
|
|
panic(fmt.Sprint("Unknown search mode when finding previous: ", p.mode))
|
|
}
|
|
|
|
firstHitPosition := p.findFirstHit(firstSearchPosition, nil, true)
|
|
if firstHitPosition == nil {
|
|
p.mode = PagerModeNotFound{pager: p}
|
|
return
|
|
}
|
|
p.scrollPosition = *firstHitPosition
|
|
|
|
// Don't let any search hit scroll out of sight
|
|
p.TargetLineNumber = nil
|
|
}
|