Skip to content

Commit

Permalink
Add filter search and related packages
Browse files Browse the repository at this point in the history
Add filter search feature to search documents by filter.
The 'biomap' package, which provides a bidirectional map,
is also added to support this feature.

The filter function filters only the lines containing word
in the current document and creates a new document.

Link the line numbers of the new document
to the line numbers of the original document.
  • Loading branch information
noborus committed Mar 27, 2024
1 parent 4c84f0b commit e1ddb45
Show file tree
Hide file tree
Showing 17 changed files with 354 additions and 35 deletions.
59 changes: 59 additions & 0 deletions biomap/biomap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package biomap

import "sync"

type Map[k comparable, v comparable] struct {
s sync.RWMutex
Forward map[k]v
Backward map[v]k
}

func NewMap[k comparable, v comparable]() *Map[k, v] {
return &Map[k, v]{
Forward: make(map[k]v),
Backward: make(map[v]k),
}
}

func (m *Map[k, v]) Store(key k, value v) {
m.s.Lock()
defer m.s.Unlock()
m.Forward[key] = value
m.Backward[value] = key
}

func (m *Map[k, v]) LoadForward(key k) (value v, ok bool) {
m.s.RLock()
defer m.s.RUnlock()
value, ok = m.Forward[key]
return
}

func (m *Map[k, v]) LoadBackward(value v) (key k, ok bool) {
m.s.RLock()
defer m.s.RUnlock()
key, ok = m.Backward[value]
return
}

func (m *Map[k, v]) DeleteForward(key k) {
m.s.Lock()
defer m.s.Unlock()
value, ok := m.Forward[key]
if !ok {
return
}
delete(m.Forward, key)
delete(m.Backward, value)
}

func (m *Map[k, v]) DeleteBackward(value v) {
m.s.Lock()
defer m.s.Unlock()
key, ok := m.Backward[value]
if !ok {
return
}
delete(m.Forward, key)
delete(m.Backward, value)
}
36 changes: 36 additions & 0 deletions biomap/biomap_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package biomap

import (
"testing"
)

func TestMap(t *testing.T) {
m := NewMap[int, string]()

// Test Store and LoadForward
m.Store(1, "one")
value, ok := m.LoadForward(1)
if !ok || value != "one" {
t.Errorf("LoadForward failed. Expected value: %s, got: %s", "one", value)
}

// Test LoadBackward
key, ok := m.LoadBackward("one")
if !ok || key != 1 {
t.Errorf("LoadBackward failed. Expected key: %d, got: %d", 1, key)
}

// Test DeleteForward
m.DeleteForward(1)
_, ok = m.LoadForward(1)
if ok {
t.Errorf("DeleteForward failed. Key still exists in Forward map")
}

// Test DeleteBackward
m.DeleteBackward("one")
_, ok = m.LoadBackward("one")
if ok {
t.Errorf("DeleteBackward failed. Value still exists in Backward map")
}
}
4 changes: 4 additions & 0 deletions oviewer/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,10 @@ func (root *Root) tailSection() {
func (root *Root) prepareStartX() {
root.scr.startX = 0
if root.Doc.LineNumMode {
if root.Doc.parent != nil {
root.scr.startX = len(fmt.Sprintf("%d", root.Doc.parent.BufEndNum())) + 1
return
}
root.scr.startX = len(fmt.Sprintf("%d", root.Doc.BufEndNum())) + 1
}
}
Expand Down
3 changes: 3 additions & 0 deletions oviewer/control.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,9 @@ func (m *Document) controlReader(sc controlSpecifier, reader *bufio.Reader, relo
reader = reload()
m.requestStart()
}
case requestClose:
log.Println("close")
return reader, nil
default:
panic(fmt.Sprintf("unexpected %s", sc.request))
}
Expand Down
22 changes: 21 additions & 1 deletion oviewer/doclist.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ func (root *Root) DocumentLen() int {
return len(root.DocList)
}

func (root *Root) getDocument(docNum int) *Document {
root.mu.RLock()
defer root.mu.RUnlock()
if docNum < 0 || docNum >= len(root.DocList) {
return nil
}
return root.DocList[docNum]
}

// hasDocChanged() returns if doc has changed.
func (root *Root) hasDocChanged() bool {
root.mu.RLock()
Expand Down Expand Up @@ -68,8 +77,19 @@ func (root *Root) nextDoc() {

// previousDoc displays the previous document.
func (root *Root) previousDoc() {
lineNum := 0
targetNum := root.CurrentDoc - 1
targetDoc := root.getDocument(targetNum)
if targetDoc == root.Doc.parent && root.Doc.lineNumMap != nil {
if n, ok := root.Doc.lineNumMap.LoadForward(root.Doc.topLN + root.Doc.firstLine()); ok {
lineNum = n
}
}
root.setDocumentNum(root.CurrentDoc - 1)
root.input.Event = normal()
if lineNum > 0 {
root.sendGoto(lineNum - root.Doc.firstLine() + 1)
}
root.debugMessage("previous document")
}

Expand Down Expand Up @@ -115,7 +135,7 @@ func (root *Root) logDisplay() {
root.toNormal()
return
}
root.setDocument(root.logDoc)
root.setDocument(root.logDoc.Document)
root.screenMode = LogDoc
}

Expand Down
5 changes: 5 additions & 0 deletions oviewer/document.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
lru "github.com/hashicorp/golang-lru/v2"
"github.com/jwalton/gchalk"
"github.com/noborus/guesswidth"
"github.com/noborus/ov/biomap"
)

// The Document structure contains the values
Expand All @@ -25,6 +26,10 @@ type Document struct {

cache *lru.Cache[int, LineC]

// parent is the parent document.
parent *Document
lineNumMap *biomap.Map[int, int]

ticker *time.Ticker
tickerDone chan struct{}
// ctlCh is the channel for controlling the reader goroutine.
Expand Down
21 changes: 17 additions & 4 deletions oviewer/draw.go
Original file line number Diff line number Diff line change
Expand Up @@ -347,9 +347,20 @@ func (root *Root) drawLineNumber(lN int, y int, valid bool) {
}
if !valid {
root.blankLineNumber(y)
return
}

number := lN
if m.lineNumMap != nil {
n, ok := m.lineNumMap.LoadForward(number)
if ok {
number = n
}
}
number = number - m.firstLine()

// Line numbers start at 1 except for skip and header lines.
numC := StrToContents(fmt.Sprintf("%*d", root.scr.startX-1, lN-m.firstLine()+1), m.TabWidth)
numC := StrToContents(fmt.Sprintf("%*d", root.scr.startX-1, number), m.TabWidth)
for i := 0; i < len(numC); i++ {
numC[i].style = applyStyle(tcell.StyleDefault, root.StyleLineNumber)
}
Expand Down Expand Up @@ -602,12 +613,14 @@ func (root *Root) inputOpts() string {

// The current search mode.
mode := root.input.Event.Mode()
if mode == Search || mode == Backsearch {
if mode == Search || mode == Backsearch || mode == Filter {
if root.Config.RegexpSearch {
opts += "(R)"
}
if root.Config.Incsearch {
opts += "(I)"
if mode != Filter {
if root.Config.Incsearch {
opts += "(I)"
}
}
if root.Config.SmartCaseSensitive {
opts += "(S)"
Expand Down
2 changes: 2 additions & 0 deletions oviewer/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ func (root *Root) eventLoop(ctx context.Context, quitChan chan<- struct{}) {
root.nextBackSearch(ctx, ev.str)
case *eventSearchMove:
root.searchGo(ev.value)
case *eventInputFilter:
root.filter(ctx)
case *eventGoto:
root.goLine(ev.value)
case *eventHeader:
Expand Down
78 changes: 78 additions & 0 deletions oviewer/filter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package oviewer

import (
"context"
"fmt"
"io"
"log"
"strings"
)

func (root *Root) filter(ctx context.Context) {
searcher := root.setSearcher(root.input.value, root.Config.CaseSensitive)
if searcher == nil {
if root.Doc.jumpTargetSection {
root.Doc.jumpTargetNum = 0
}
return
}
word := root.searcher.String()
root.setMessagef("filter:%v (%v)Cancel", word, strings.Join(root.cancelKeys, ","))

m := root.Doc
r, w := io.Pipe()
filterDoc, err := renderDoc(m, r)
if err != nil {
log.Println(err)
return
}
filterDoc.FileName = fmt.Sprintf("filter:%s:%v", m.FileName, word)
filterDoc.Caption = fmt.Sprintf("%s:%v", m.FileName, word)
root.addDocument(filterDoc.Document)

if m.Header > 0 {
for ln := m.SkipLines; ln < m.Header; ln++ {
line, err := m.Line(ln)
if err != nil {
break
}
filterDoc.lineNumMap.Store(ln, ln)
w.Write(line)
w.Write([]byte("\n"))
}
}

filterDoc.writer = w
filterDoc.Header = m.Header
filterDoc.SkipLines = m.SkipLines

go m.searchWriter(ctx, searcher, filterDoc, m.firstLine())
root.setMessagef("filter:%v", word)
}

// searchWriter searches the document and writes the result to w.
func (m *Document) searchWriter(ctx context.Context, searcher Searcher, renderDoc *renderDocument, ln int) {
defer renderDoc.writer.Close()
nextLN := ln
for {
lineNum, err := m.searchLine(ctx, searcher, true, nextLN)
if err != nil {
break
}
line, err := m.Line(lineNum)
if err != nil {
break
}
num := lineNum
if m.lineNumMap != nil {
if n, ok := m.lineNumMap.LoadForward(num); ok {
num = n
}
}
renderDoc.lineNumMap.Store(ln, num)
renderDoc.writer.Write(line)
renderDoc.writer.Write([]byte("\n"))
nextLN = lineNum + 1
ln++
}
}
1 change: 1 addition & 0 deletions oviewer/input.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const (
ViewMode // ViewMode is a view selection input mode.
Search // Search is a search input mode.
Backsearch // Backsearch is a backward search input mode.
Filter // Filter is a filter input mode.
Goline // Goline is a move input mode.
Header // Header is the number of headers input mode.
Delimiter // Delimiter is a delimiter input mode.
Expand Down
57 changes: 57 additions & 0 deletions oviewer/input_search.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,60 @@ func (input *Input) searchCandidates(n int) []string {
start := max(0, listLen-n)
return input.SearchCandidate.list[start:listLen]
}

type eventInputFilter struct {
tcell.EventTime
clist *candidate
value string
}

// setBackSearchMode sets the inputMode to Backsearch.
func (root *Root) setSearchFilterMode() {
input := root.input
input.value = ""
input.cursorX = 0

if root.searcher != nil {
input.SearchCandidate.toLast(root.searcher.String())
}

input.Event = newSearchFilterEvent(input.SearchCandidate)
}

func newSearchFilterEvent(clist *candidate) *eventInputFilter {
return &eventInputFilter{
value: "",
clist: clist,
EventTime: tcell.EventTime{},
}
}

// Mode returns InputMode.
func (e *eventInputFilter) Mode() InputMode {
return Filter
}

// Prompt returns the prompt string in the input field.
func (e *eventInputFilter) Prompt() string {
return "&"
}

// Confirm returns the event when the input is confirmed.
func (e *eventInputFilter) Confirm(str string) tcell.Event {
e.value = str
e.clist.toLast(str)
e.SetEventNow()
return e
}

// Up returns strings when the up key is pressed during input.
func (e *eventInputFilter) Up(str string) string {
e.clist.toAddLast(str)
return e.clist.up()
}

// Down returns strings when the down key is pressed during input.
func (e *eventInputFilter) Down(str string) string {
e.clist.toAddTop(str)
return e.clist.down()
}
Loading

0 comments on commit e1ddb45

Please sign in to comment.