Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/firstrow/wig/config"
"github.com/firstrow/wig/metrics"
"github.com/firstrow/wig/render"
"github.com/firstrow/wig/ui"
)

func main() {
Expand Down Expand Up @@ -51,6 +52,10 @@ func main() {
}
}

wig.MarksPopupFactory = func(ctx wig.Context, marks map[rune]wig.Cursor) {
ui.MarksPopupInit(ctx, marks)
}

editor := wig.NewEditor(
render.NewMView(tscreen, 0, 0, w, h),
keys,
Expand Down
3 changes: 3 additions & 0 deletions commands/definitions.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ func init() {
wig.AllCommands["CmdDeleteLine"] = wig.CmdDefinition{Desc: "Delete line", Fn: wig.CmdDeleteLine}
wig.AllCommands["CmdGitHunkPreview"] = wig.CmdDefinition{Desc: "Preview git hunk", Fn: CmdGitHunkPreview}
wig.AllCommands["CmdMRUBufferPicker"] = wig.CmdDefinition{Desc: "MRU Buffer Picker", Fn: CmdMRUBufferPicker}
wig.AllCommands["CmdSetMark"] = wig.CmdDefinition{Desc: "Set mark", Fn: wig.CmdSetMark}
wig.AllCommands["CmdGotoMark"] = wig.CmdDefinition{Desc: "Go to mark", Fn: wig.CmdGotoMark}
wig.AllCommands["CmdJumpToggle"] = wig.CmdDefinition{Desc: "Toggle jump", Fn: wig.CmdJumpToggle}
wig.AllCommands["CmdCheckHealth"] = wig.CmdDefinition{Desc: "Check health of dependencies", Fn: CmdCheckHealth}
wig.AllCommands["checkhealth"] = wig.CmdDefinition{Desc: "Check health of dependencies", Fn: CmdCheckHealth}

Expand Down
3 changes: 3 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,9 @@ func DefaultKeyMap() wig.ModeKeyMap {
"q": wig.CmdMacroRecord,
"@": wig.CmdMacroPlay,
".": wig.CmdMacroRepeat,
"m": wig.CmdSetMark,
"`": wig.CmdGotoMark,
"'": wig.CmdGotoMark,
"c": wig.KeyMap{
"$": wig.CmdChangeEndOfLine,
"c": wig.CmdChangeLine,
Expand Down
38 changes: 38 additions & 0 deletions core.go
Original file line number Diff line number Diff line change
Expand Up @@ -787,3 +787,41 @@ func CmdBufferLast(ctx Context) {
func CmdPaste(ctx Context) {
panic(1)
}

// CmdSetMark waits for a character input and sets a mark at the current cursor position.
func CmdSetMark(ctx Context) func(Context) {
return func(ctx Context) {
charStr := ctx.Char
if strings.HasPrefix(charStr, "shift+") {
charStr = strings.TrimPrefix(charStr, "shift+")
}
if len(charStr) == 0 {
return
}
r := []rune(charStr)[0]
win := ctx.Win
if win == nil {
win = ctx.Editor.ActiveWindow()
}
if win == nil {
return
}
if win.Marks == nil {
win.Marks = make(map[rune]Cursor)
}
cur := ContextCursorGet(ctx)
win.Marks[r] = *cur
ctx.Editor.EchoMessage("Mark '" + string(r) + "' set")
}
}

// CmdGotoMark opens the marks popup legend.
func CmdGotoMark(ctx Context) {
win := ctx.Win
if win == nil {
win = ctx.Editor.ActiveWindow()
}
if MarksPopupFactory != nil && win != nil {
MarksPopupFactory(ctx, win.Marks)
}
}
4 changes: 4 additions & 0 deletions editor.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ type Context struct {

type AutocompleteFn func(Context) bool

// MarksPopupFactory allows the `ui` package to register a popup for marks
// without causing a circular import.
var MarksPopupFactory func(ctx Context, marks map[rune]Cursor)

var EditorInst *Editor

type Layout int
Expand Down
27 changes: 27 additions & 0 deletions movements_cmds.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ func CmdGotoLine0(ctx Context) {
count := max(ctx.Count, 1)
cur := ContextCursorGet(ctx)
defer CmdEnsureCursorVisible(ctx)
ctx.Editor.ActiveWindow().Jumps.Push(ctx.Buf, cur)
cur.Line = min(int(count)-1, ctx.Buf.Lines.Len-1)
ctx.Editor.ActiveWindow().Jumps.Push(ctx.Buf, cur)
}
Expand Down Expand Up @@ -251,6 +252,7 @@ func ParseFileLocation(text string, cursor int) (filename string, line, ch int)
func CmdGotoLineEndOfFile(ctx Context) {
cur := ContextCursorGet(ctx)
defer CmdEnsureCursorVisible(ctx)
ctx.Editor.ActiveWindow().Jumps.Push(ctx.Buf, cur)
cur.Line = ctx.Buf.Lines.Len - 1
ctx.Editor.ActiveWindow().Jumps.Push(ctx.Buf, cur)
}
Expand Down Expand Up @@ -580,6 +582,31 @@ func CmdJumpForward(ctx Context) {
CmdCursorCenter(ctx)
}

// CmdJumpToggle toggles between the previous and current jump locations (pingpong).
func CmdJumpToggle(ctx Context) {
win := ctx.Editor.ActiveWindow()
if win == nil || win.Jumps == nil {
return
}

// If the user moved around on the current jump point, push the position before toggling
if win.buf != nil {
cur := WindowCursorGet(win, win.buf)
if win.Jumps.List.Last() != nil && win.Jumps.current == win.Jumps.List.Last() {
if win.Jumps.List.Last().Value.Cursor.Line != cur.Line {
win.Jumps.Push(win.buf, cur)
}
}
}

if win.Jumps.current != nil && win.Jumps.current != win.Jumps.List.Last() {
win.Jumps.JumpForward()
} else {
win.Jumps.JumpBack()
}
CmdCursorCenter(ctx)
}

// Cycle between last two buffers in jump list
func CmdBufferCycle(ctx Context) {
last := ctx.Editor.ActiveWindow().Jumps.List.Last()
Expand Down
171 changes: 171 additions & 0 deletions ui/marks_popup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package ui

import (
"fmt"
"sort"
"strings"

"github.com/firstrow/wig"
"github.com/gdamore/tcell/v2"
)

type MarksPopupWidget struct {
e *wig.Editor
keymap *wig.KeyHandler
items []markItem
}

type markItem struct {
Mark rune
Line int
Text string
}

func (u *MarksPopupWidget) Plane() wig.RenderPlane { return wig.PlaneEditor }
func (u *MarksPopupWidget) Mode() wig.Mode { return wig.MODE_NORMAL }
func (u *MarksPopupWidget) Keymap() *wig.KeyHandler { return u.keymap }

func MarksPopupInit(ctx wig.Context, marks map[rune]wig.Cursor) {
widget := &MarksPopupWidget{
e: ctx.Editor,
}

var keys []rune
for k := range marks {
keys = append(keys, k)
}
sort.Slice(keys, func(i, j int) bool { return keys[i] < keys[j] })

km := wig.KeyMap{
"Esc": func(ctx wig.Context) {
ctx.Editor.PopUi()
},
// Double backtick / quote (pingpong): jump back and forward
"`": func(ctx wig.Context) {
ctx.Editor.PopUi()
wig.CmdJumpToggle(ctx)
},
"'": func(ctx wig.Context) {
ctx.Editor.PopUi()
wig.CmdJumpToggle(ctx)
},
}

for _, k := range keys {
cur := marks[k]
line := wig.CursorLineByNum(ctx.Buf, cur.Line)
text := ""
if line != nil {
text = strings.TrimRight(line.Value.String(), "\n")
text = strings.TrimSpace(text)
if len(text) > 60 {
text = text[:60] + "..."
}
}

mark := k
targetCur := cur
widget.items = append(widget.items, markItem{
Mark: mark,
Line: cur.Line + 1,
Text: text,
})

jumpFn := func(ctx wig.Context) {
ctx.Editor.PopUi()
win := ctx.Win
if win == nil {
win = ctx.Editor.ActiveWindow()
}
win.VisitBuffer(ctx, targetCur)
wig.CmdEnsureCursorVisible(ctx)
}

markStr := string(mark)
km[markStr] = jumpFn
km["shift+"+markStr] = jumpFn
}

widget.keymap = wig.NewKeyHandler(wig.ModeKeyMap{wig.MODE_NORMAL: km})
ctx.Editor.PushUi(widget)
}

func (u *MarksPopupWidget) Render(view wig.View) {
vw, vh := view.Size()

lines := []string{}
if len(u.items) == 0 {
lines = append(lines, " No marks set. Press ` for pingpong, Esc to close. ")
} else {
for _, item := range u.items {
s := fmt.Sprintf(" '%c' line %-4d │ %s ", item.Mark, item.Line, item.Text)
lines = append(lines, s)
}
}

boxH := len(lines) + 1
boxW := int(float32(vw) * 0.85)
if boxW > vw-4 {
boxW = vw - 4
}
if boxW < 40 {
boxW = min(vw, 40)
}

x := (vw - boxW) / 2
if x < 0 {
x = 0
}
y := vh - boxH - 2
if y < 0 {
y = 0
}

style := wig.Color("default")
drawBox(view, x, y, x+boxW, y+boxH, style)

titleStyle := wig.Color("ui.popup.title")
if titleStyle == style {
titleStyle = wig.Color("ui.linenr.selected")
}
view.SetContent(x+2, y, " Marks (` or ' for pingpong) ", titleStyle)

textStyle := wig.Color("default")
markStyle := wig.Color("ui.mark")
if markStyle == style {
markStyle = style.Foreground(tcell.ColorYellow).Bold(true)
}
lineNrStyle := wig.Color("ui.linenr")

if len(u.items) == 0 {
for i, line := range lines {
view.SetContent(x+2, y+1+i, line, textStyle)
}
} else {
for i, item := range u.items {
cx := x + 2
row := y + 1 + i

view.SetContent(cx, row, "'", textStyle)
cx++

view.SetContent(cx, row, string(item.Mark), markStyle)
cx += len([]rune(string(item.Mark)))

view.SetContent(cx, row, "' line ", textStyle)
cx += 8

lnStr := fmt.Sprintf("%-4d", item.Line)
view.SetContent(cx, row, lnStr, lineNrStyle)
cx += len(lnStr)

view.SetContent(cx, row, " │ ", textStyle)
cx += 3

avail := (x + boxW - 1) - cx
if avail > 0 {
view.SetContent(cx, row, truncate(item.Text, avail), textStyle)
}
}
}
}
29 changes: 25 additions & 4 deletions ui/window.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func WindowRender(e *wig.Editor, view wig.View, win *wig.Window) {
signColWidth := 2
lineNumWidth := 0
if e.Config.ShowLineNumbers {
lineNumWidth = len(fmt.Sprintf("%d", buf.CountLines())) + 1
lineNumWidth = len(fmt.Sprintf("%d", buf.CountLines())) + 2
}
blameColWidth := 0
if buf.BlameEnabled && len(buf.BlameLines) > 0 {
Expand All @@ -66,6 +66,13 @@ func WindowRender(e *wig.Editor, view wig.View, win *wig.Window) {
tsNodeCursor = buf.Highlighter.ForRange(uint32(startLine), startLine+uint32(termHeight))
}

lineMarks := make(map[int]rune)
if win != nil && win.Marks != nil {
for r, mCur := range win.Marks {
lineMarks[mCur.Line] = r
}
}

// Precalculate visual block bounds for efficient rendering
var minVisCol, maxVisCol int
isVisualBlock := buf.Mode() == wig.MODE_VISUAL_BLOCK && buf.Selection != nil
Expand Down Expand Up @@ -129,10 +136,24 @@ func WindowRender(e *wig.Editor, view wig.View, win *wig.Window) {
}

if xCur >= 0 && xCur < termWidth && y >= 0 && y < termHeight {
style := lineNumTextStyle
if lineNum == cur.Line {
view.SetContent(xCur, y, fmt.Sprintf("%d", lnNum), lineNumTextStyleSelected)
style = lineNumTextStyleSelected
}
markRune, hasMark := lineMarks[lineNum]
if hasMark {
markStyle := wig.Color("ui.mark")
if markStyle == wig.Color("default") {
markStyle = wig.Color("diff.plus")
}
if lineNum == cur.Line {
markStyle = wig.ApplyBg("ui.cursorline", markStyle)
}
view.SetContent(xCur, y, string(markRune), markStyle)
view.SetContent(xCur+1, y, fmt.Sprintf("%d", lnNum), style)
} else {
view.SetContent(xCur, y, fmt.Sprintf("%d", lnNum), lineNumTextStyle)
view.SetContent(xCur, y, " ", style)
view.SetContent(xCur+1, y, fmt.Sprintf("%d", lnNum), style)
}
}
}
Expand Down Expand Up @@ -317,7 +338,7 @@ func WindowTextPadding(e *wig.Editor, buf *wig.Buffer) int {

lineNumWidth := 0
if e.Config.ShowLineNumbers {
lineNumWidth = len(fmt.Sprintf("%d", buf.CountLines())) + 1
lineNumWidth = len(fmt.Sprintf("%d", buf.CountLines())) + 2
}

blameColWidth := 0
Expand Down
Loading