~adnano/astronaut

93cad79670b644def1d36ea74f54b3bec6b23b1c — Adnan Maolood a month ago 5b2ba7d
browser: Implement support for jumping to headings

Resolves #18
4 files changed, 98 insertions(+), 1 deletions(-)

M browser.go
M command.go
M text.go
M ui/view.go
M browser.go => browser.go +26 -1
@@ 10,6 10,7 @@ import (
	"net/url"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"sync"



@@ 268,11 269,11 @@ func (b *Browser) Event(event tcell.Event) {
				b.hint += string(r)
				if hint, ok := b.hintMap[b.hint]; ok {
					text.focus = hint.Index
					b.Message("=> " + text.LinkURL(hint.Index))
					b.mode = ModeNormal
					b.hint = ""
					b.hints = nil
					b.hintMap = nil
					b.Message("=> " + text.LinkURL(hint.Index))
				}
				b.view.Invalidate()



@@ 660,6 661,30 @@ func (b *Browser) FollowMode() error {
	return nil
}

func (b *Browser) Jump() {
	tab := b.tabs[b.tab]
	text := tab.Text()
	if text == nil {
		return
	}

	toc := text.TableOfContents()
	if toc == nil {
		return
	}
	toc.action = func(action string) {
		idx, err := strconv.Atoi(action)
		if err != nil {
			panic(err)
		}
		tab.setText(text)
		tab.view.SetScrollY(text.pos[idx])
		tab.view.Invalidate()
	}
	tab.setText(toc)
	b.view.Invalidate()
}

// Based on Qutebrowser's 'scattered' hints algorithm.
func makeHintLabels(chars string, length int) []string {
	needed := ceilLog(length, len(chars))

M command.go => command.go +4 -0
@@ 23,6 23,10 @@ var commands = map[string]Command{
	"follow": func(b *Browser, args ...string) error {
		return b.FollowMode()
	},
	"jump": func(b *Browser, args ...string) error {
		b.Jump()
		return nil
	},
	"open": func(b *Browser, args ...string) error {
		if len(args) == 0 {
			return errors.New("usage: open <url>")

M text.go => text.go +60 -0
@@ 5,6 5,7 @@ import (
	"astronaut/ui"
	"bufio"
	"io"
	"strconv"
	"strings"
	"sync"



@@ 28,6 29,7 @@ type Text struct {
	mu        sync.RWMutex
	lines     []Line    // parsed lines
	tabindex  []int     // list of tabbable lines
	headings  []int     // list of heading lines
	processed int       // number of processed lines
	wrapped   []Wrapped // wrapped lines
	pos       []int     // list of line positions


@@ 68,6 70,9 @@ func (t *Text) Parse() error {

// Close stops the parsing of the text document.
func (t *Text) Close() error {
	if t.rc == nil {
		return nil
	}
	return t.rc.Close()
}



@@ 151,6 156,12 @@ func (t *Text) Update() {
			t.view.Invalidate()
		}

		// Populate headings
		switch line.Type {
		case LineHeading1, LineHeading2, LineHeading3:
			t.headings = append(t.headings, t.processed)
		}

		// Populate tab index
		if line.Tabindex != 0 {
			t.tabindex = append(t.tabindex, t.processed)


@@ 278,6 289,55 @@ func (t *Text) Draw() {
	t.view.SetContentSize(w, len(t.wrapped)+h-1, true)
}

// TableOfContents returns the table of contents of the document.
// Returns nil if there are no headings in the document.
func (t *Text) TableOfContents() *Text {
	t.mu.RLock()
	defer t.mu.RUnlock()

	if len(t.headings) == 0 {
		return nil
	}

	toc := &Text{
		view:  t.view,
		cfg:   t.cfg,
		focus: -1,
	}

	toc.lines = append(toc.lines, Line{})
	toc.lines = append(toc.lines, Line{
		Type: LineHeading1,
		Text: "Table of contents",
	})
	toc.lines = append(toc.lines, Line{})

	tabindex := 1
	for _, idx := range t.headings {
		line := t.lines[idx]

		var prefix string
		switch line.Type {
		case LineHeading1:
			prefix = "# "
		case LineHeading2:
			prefix = "## "
		case LineHeading3:
			prefix = "### "
		}

		toc.lines = append(toc.lines, Line{
			Type:     LineLink,
			URL:      strconv.Itoa(idx),
			Text:     prefix + line.Text,
			Tabindex: tabindex,
		})
		tabindex++
	}

	return toc
}

// Invalidate invalidates the text document, forcing a redraw of all the text.
func (t *Text) Invalidate() {
	t.wrapped = nil

M ui/view.go => ui/view.go +8 -0
@@ 175,6 175,14 @@ func (v *View) ScrollY() int {
	return v.scrolly
}

func (v *View) SetScrollX(scrollx int) {
	v.scrollx = scrollx
}

func (v *View) SetScrollY(scrolly int) {
	v.scrolly = scrolly
}

// SetContentSize sets the size of the content area; this is used to limit
// scrolling and view moment.  If locked is true, then the content size will
// not automatically grow even if content is placed outside of this area