From 1c1469d5522c7c7d7c03232d9c2711c5d96d3c18 Mon Sep 17 00:00:00 2001 From: Petter Rodhelind Date: Sat, 24 Feb 2018 23:43:22 +0100 Subject: Bulk. Move fast and break things. Whatever. Fixed scrolling and mouse selection in regards to soft wrap and tabstop. Fixed empty buffer windir setting which will make new buffers start in correct working dir. Fixed automatic dot expansion on Open and Run. Made commands not need a prepended ! to be considered an external command. --- commands.go | 12 ++--- style.go | 12 +++-- text.go | 9 ++++ todo | 13 +++-- view.go | 176 ++++++++++++++++++++++++++++++++++++++++++++++-------------- window.go | 20 ++++++- 6 files changed, 182 insertions(+), 60 deletions(-) diff --git a/commands.go b/commands.go index 023a112..9d31614 100644 --- a/commands.go +++ b/commands.go @@ -37,15 +37,11 @@ func RunCommand(input string) { // Edit shortcuts for external commands and piping switch input[0] { - case '!': - fallthrough - case '<': - fallthrough - case '>': - fallthrough - case '|': + case '!', '<', '>', '|': CmdEdit(input) } + + CmdEdit("!" + input) } func CmdExit(args string) { @@ -116,7 +112,7 @@ func CmdEdit(args string) { // if command produced output, print it outstr := string(out) if outstr != "" { - printMsg("%s\n", outstr) + printMsg("%s", outstr) } default: printMsg("?\n") diff --git a/style.go b/style.go index 57debbf..eaff897 100644 --- a/style.go +++ b/style.go @@ -9,9 +9,11 @@ var ( bodyHilightStyle tcell.Style // tag is the window tag line above the body - tagStyle tcell.Style - tagCursorStyle tcell.Style - tagHilightStyle tcell.Style + tagStyle tcell.Style + tagCursorStyle tcell.Style + tagHilightStyle tcell.Style + tagSquareStyle tcell.Style + tagSquareModifiedStyle tcell.Style // vertline is the vertical line separating columns vertlineStyle tcell.Style @@ -40,6 +42,10 @@ func InitStyles() { Foreground(tcell.ColorBlack) tagHilightStyle = tagStyle. Background(tcell.NewHexColor(0x8888cc)) + tagSquareStyle = tagStyle. + Background(tcell.NewHexColor(0x8888cc)) + tagSquareModifiedStyle = tagStyle. + Background(tcell.NewHexColor(0x0)) vertlineStyle = bodyStyle } diff --git a/text.go b/text.go index 5b50be9..b9398ed 100644 --- a/text.go +++ b/text.go @@ -74,6 +74,11 @@ func (t *Text) Len() int { return t.buf.Len() } +// String returns the entire text buffer as a string. +func (t *Text) String() string { + return string(t.buf.Bytes()) +} + // ReadRune reads a rune from buffer and advances the internal offset. This could be called in sequence to get all runes from buffer. This populates LastRune(). func (t *Text) ReadRune() (r rune, size int, err error) { r, size, err = t.ReadRuneAt(t.off) @@ -261,6 +266,10 @@ func (t *Text) Select(offset int) { offset, _ = t.Seek(offset, io.SeekStart) start, end := offset, offset + // space + //start -= t.PrevSpace(start) + //end += t.NextSpace(end) + // word start -= t.PrevWord(start) end += t.NextWord(end) diff --git a/todo b/todo index b79f2f5..bffdcd4 100644 --- a/todo +++ b/todo @@ -1,15 +1,13 @@ jobs / commands - async extern exekvering via !<>| + async execution via !<>| file - spara tom buffer med nytt filnamn + save empty buffer with tag name check for overwrite overwrite when hash-verify fails on save backup files on disk window - hide - command Get -view - scroll must account for soft wraps + hide / collapse + command Get (update buffer content) text int64 as default in case of large files undvika in-ram buffer - swapfiles? @@ -18,6 +16,7 @@ text undo unlimited for real (undo undo) cache write Change{} until next action + refactor all Next/Prev-funcs --- visual calculate view percentage on screen @@ -29,4 +28,4 @@ visual another style for top menu do not change CurWin on mousepressed scroll on mpressed at bottom line - unprintableChar styling \ No newline at end of file + unprintableChar styling diff --git a/view.go b/view.go index d0e6bba..7bf1860 100644 --- a/view.go +++ b/view.go @@ -3,6 +3,7 @@ package main import ( "io" "path/filepath" + "strings" "time" "unicode/utf8" @@ -92,79 +93,106 @@ func (v *View) SetCursor(pos, whence int) { } } -// XYToOffset translates mouse coordinates in a 2D terminal to the correct byte offset in buffer, accounting for rune length and width. -func (b *View) XYToOffset(x, y int) int { - offset := b.scrollpos +// XYToOffset translates mouse coordinates in a 2D terminal to the correct byte offset in buffer, accounting for rune length, width and tabstops. +func (v *View) XYToOffset(x, y int) int { + offset := v.scrollpos - // vertical (number of lines) - for y-b.y > 0 { - r, _, _ := b.text.ReadRuneAt(offset) + // vertical (number of visual lines) + for y-v.y > 0 { + r, _, _ := v.text.ReadRuneAt(offset) if r == '\n' { offset++ y-- continue } - // loop until next line - xw := 0 - for r != '\n' && xw < b.x+b.w { + // loop until next line, either new line or soft wrap at end of window width + xw := v.x + for r != '\n' && xw <= v.x+v.w { var n int - r, n, _ = b.text.ReadRuneAt(offset) + r, n, _ = v.text.ReadRuneAt(offset) offset += n rw := RuneWidth(r) - if rw == 0 { + if r == '\t' { + rw = v.tabstop - (xw-v.x)%v.tabstop + } else if rw == 0 { rw = 1 } + xw += rw } y-- } // horizontal - for x-b.x > 0 { - r, n, _ := b.text.ReadRuneAt(offset) + xw := v.x // for tabstop count + for x-v.x > 0 { + r, n, _ := v.text.ReadRuneAt(offset) if r == '\n' { break } - if r == '\t' { - x -= b.tabstop - 1 // TODO: modulo count - } offset += n rw := RuneWidth(r) - if rw == 0 { + if r == '\t' { + rw = v.tabstop - (xw-v.x)%v.tabstop + } else if rw == 0 { rw = 1 } + xw += rw // keep track of tabstop modulo x -= rw } return offset } -// Scroll will move the visible part of the buffer in number of lines. Negative means upwards. +// Scroll will move the visible part of the buffer in number of lines, accounting for soft wraps and tabstops. Negative means upwards. func (v *View) Scroll(n int) { offset := 0 - if n > 0 { + + xw := v.x // for tabstop count and soft wrap + switch { + case n > 0: // downwards, next line for n > 0 { - if r, _, _ := v.text.ReadRuneAt(v.scrollpos + offset); r == '\n' { - offset++ - } else { - offset += v.text.NextDelim('\n', v.scrollpos+offset) + 1 + r, size, err := v.text.ReadRuneAt(v.scrollpos + offset) + if err != nil { + break // hit EOF, stop scrolling + } + offset += size + + rw := RuneWidth(r) + if r == '\t' { + rw = v.tabstop - (xw-v.x)%v.tabstop + } else if rw == 0 { + rw = 1 + } + xw += rw + + if r == '\n' || xw > v.x+v.w { // new line or soft wrap + n-- // move down + xw = v.x // reset soft wrap } - n-- } v.scrollpos += offset - } - - if n < 0 { - offset += v.text.PrevDelim('\n', v.scrollpos) + case n < 0: // upwards, previous line + // This is kind of ugly, but it relies on the soft wrap + // counting in positive scrolling. It will scroll back to the + // nearest new line character and then scroll forward again + // until the very last iteration, which is the offset for the previous + // softwrap/nl. for n < 0 { - offset += v.text.PrevDelim('\n', v.scrollpos-offset) + start := v.scrollpos // save current offset + v.scrollpos -= v.text.PrevDelim('\n', v.scrollpos) // scroll back + if start-v.scrollpos == 1 { // if it was an empty new line, back up one more + v.scrollpos -= v.text.PrevDelim('\n', v.scrollpos) + } + prevlineoffset := v.scrollpos // previous (or one more) new line, may be way back + + for v.scrollpos < start { // scroll one line forward until we're back at current + prevlineoffset = v.scrollpos // save offset just before we jump forward again + v.Scroll(1) // used for the side effect of setting v.scrollpos + } + v.scrollpos = prevlineoffset n++ } - if v.scrollpos-offset > 0 { - v.scrollpos -= offset - 1 - } else { - v.scrollpos = 0 - } } // boundaries @@ -317,7 +345,7 @@ func (v *View) HandleEvent(ev tcell.Event) { v.mpressed = false } case tcell.Button1: - if v.mpressed { + if v.mpressed { // select text via click-n-drag if pos > v.mclickpos { v.text.SetDot(v.mclickpos, pos) } else { @@ -348,6 +376,45 @@ func (v *View) HandleEvent(ev tcell.Event) { v.Scroll(-1) case tcell.WheelDown: // scrolldown v.Scroll(1) + case tcell.Button2: // middle click + // if we clicked inside a current selection, run that one + q0, q1 := v.text.Dot() + if pos >= q0 && pos <= q1 && q0 != q1 { + RunCommand(v.text.ReadDot()) + return + } + + // otherwise, select non-space chars under mouse and run that + p := pos - v.text.PrevSpace(pos) + n := pos + v.text.NextSpace(pos) + v.text.SetDot(p, n) + fn := strings.Trim(v.text.ReadDot(), "\n\t ") + v.text.SetDot(q0, q1) + RunCommand(fn) + return + case tcell.Button3: // right click + // if we clicked inside a current selection, open that one + q0, q1 := v.text.Dot() + if pos >= q0 && pos <= q1 && q0 != q1 { + CmdOpen(v.text.ReadDot()) + return + } + + // otherwise, select everything inside surround spaces and open that + p := pos - v.text.PrevSpace(pos) + n := pos + v.text.NextSpace(pos) + v.text.SetDot(p, n) + fn := strings.Trim(v.text.ReadDot(), "\n\t ") + v.text.SetDot(q0, q1) + if fn == "" { // if it is still blank, abort + return + } + if fn != "" && fn[0] != filepath.Separator { + fn = CurWin.Dir() + string(filepath.Separator) + fn + fn = filepath.Clean(fn) + } + CmdOpen(fn) + return default: printMsg("%#v", btn) } @@ -390,6 +457,9 @@ func (v *View) HandleEvent(ev tcell.Event) { v.SetCursor(offset, io.SeekCurrent) return case tcell.KeyCtrlU: // delete line backwards + if v.text.ReadDot() != "" { + v.Delete() // delete current selection first + } offset := v.text.PrevDelim('\n', v.Cursor()) if offset > 1 && v.Cursor()-offset != 0 { offset -= 1 @@ -402,6 +472,9 @@ func (v *View) HandleEvent(ev tcell.Event) { v.Delete() return case tcell.KeyCtrlW: // delete word backwards + if v.text.ReadDot() != "" { + v.Delete() // delete current selection first + } startpos := v.Cursor() offset := v.text.PrevWord(v.Cursor()) if offset == 0 { @@ -423,23 +496,44 @@ func (v *View) HandleEvent(ev tcell.Event) { v.Delete() return case tcell.KeyCtrlG: // file info/statistics - printMsg("0x%.4x %q %d,%d/%d\noverflow: %d scroll: %d\ndot: %q\nprev nl: %d\n", + printMsg("0x%.4x %q %d,%d/%d\nbasedir: %s\nwindir: %s\n\nname: %s\nnameabs: %s\ntagname: %s\n", v.Rune(), v.Rune(), v.text.q0, v.text.q1, v.text.Len(), - v.opos, v.scrollpos, - v.text.ReadDot(), - v.text.PrevDelim('\n', v.Cursor())) + baseDir, CurWin.Dir(), CurWin.Name(), CurWin.NameAbs(), CurWin.NameFromTag()) return case tcell.KeyCtrlO: // open file/dir fn := v.text.ReadDot() - if fn != FnEmptyWin && fn[0] != filepath.Separator { + if fn == "" { // select all non-space characters + curpos := v.Cursor() + p := curpos - v.text.PrevSpace(curpos) + n := curpos + v.text.NextSpace(curpos) + v.text.SetDot(p, n) + fn = strings.Trim(v.text.ReadDot(), "\n\t ") + v.SetCursor(curpos, io.SeekStart) + if fn == "" { // if it is still blank, abort + return + } + } + if fn != "" && fn[0] != filepath.Separator { fn = CurWin.Dir() + string(filepath.Separator) + fn fn = filepath.Clean(fn) } CmdOpen(fn) return case tcell.KeyCtrlR: // run command in dot - RunCommand(v.text.ReadDot()) + cmd := v.text.ReadDot() + if cmd == "" { // select all non-space characters + curpos := v.Cursor() + p := curpos - v.text.PrevSpace(curpos) + n := curpos + v.text.NextSpace(curpos) + v.text.SetDot(p, n) + cmd = strings.Trim(v.text.ReadDot(), "\n\t ") + v.SetCursor(curpos, io.SeekStart) + if cmd == "" { // if it is still blank, abort + return + } + } + RunCommand(cmd) return case tcell.KeyCtrlC: // copy to clipboard if err := clipboard.WriteAll(v.text.ReadDot()); err != nil { diff --git a/window.go b/window.go index c3a5c72..9560ec6 100644 --- a/window.go +++ b/window.go @@ -68,7 +68,7 @@ func NewWindow(fn string) *Window { file: &File{name: fnabs}, } - fmt.Fprintf(win.tagline, "⌘ %s%s Del", + fmt.Fprintf(win.tagline, "%s%s Del ", win.Flags(), win.Name(), ) @@ -171,6 +171,10 @@ func (win *Window) SaveFile() (int, error) { return 0, nil } + if win.isdir { + return 0, nil + } + f, err := os.OpenFile(win.NameAbs(), os.O_RDWR|os.O_CREATE, 0644) if err != nil { return 0, err @@ -221,15 +225,29 @@ func (win *Window) Name() string { } func (win *Window) NameAbs() string { + if win.file.name == FnEmptyWin { + return "" + } s, _ := filepath.Abs(win.file.name) return s } +func (win *Window) NameFromTag() string { + tstr := win.tagline.text.String() + if tstr == "" { + return "" + } + return strings.Split(tstr, " ")[0] +} + // Dir returns the working directory of current window, without trailing path separator. func (win *Window) Dir() string { if win.isdir { return win.NameAbs() } + if win.Name() == FnEmptyWin { + return baseDir + } return filepath.Dir(win.NameAbs()) } -- cgit v1.2.3