commit 68b2d7381b8b28b4063a81ea01e6b8d8ca7e71c3
parent cec155286bfbbbc676d1aac72e0b557b52a1ef82
Author: dwrz <dwrz@dwrz.net>
Date: Wed, 25 Jan 2023 14:53:58 +0000
Add editor prompt
Squashed commit of the following:
commit 542a1071ccaea792834fedb71a88c77307a95676
Author: dwrz <dwrz@dwrz.net>
Date: Wed Jan 25 14:53:02 2023 +0000
Refactor prompt
commit 1e88dbb12c89b99ed85fea2454438275c41b47ec
Author: dwrz <dwrz@dwrz.net>
Date: Tue Jan 24 16:17:04 2023 +0000
Fix find glyph index edge case
commit bf64de6ef1bc91d604af3757707d898277aab471
Author: dwrz <dwrz@dwrz.net>
Date: Mon Jan 23 19:22:37 2023 +0000
Refactor event loop
commit 1e4c541dbf1cf07b5b4e3e0dce314213a9460bf6
Author: dwrz <dwrz@dwrz.net>
Date: Mon Jan 23 16:49:17 2023 +0000
Add prompt
Diffstat:
18 files changed, 451 insertions(+), 101 deletions(-)
diff --git a/pkg/editor/buffer/cursor.go b/pkg/editor/buffer/cursor.go
@@ -8,7 +8,6 @@ import (
type Cursor struct {
glyph int
-
index int
line int
}
diff --git a/pkg/editor/buffer/edit.go b/pkg/editor/buffer/edit.go
@@ -44,7 +44,6 @@ func (b *Buffer) Backspace() {
b.CursorLeft()
b.CursorLine().DeleteRune(b.cursor.index)
}
-
}
func (b *Buffer) Insert(r rune) {
diff --git a/pkg/editor/buffer/line/line.go b/pkg/editor/buffer/line/line.go
@@ -25,6 +25,7 @@ func (l *Line) FindGlyphIndex(target int) (index, g int) {
for i, r := range l.data {
// We've reached the target.
if g == target {
+
return i, g
}
// We've gone too far.
@@ -41,6 +42,11 @@ func (l *Line) FindGlyphIndex(target int) (index, g int) {
// Then increment the glyph.
g += glyph.Width(r)
}
+ // Target falls in-between the last glyph and the end of the line.
+ // Use the last glyph.
+ if target >= lg && target < g {
+ return li, lg
+ }
// We weren't able to find the glyph.
// Return the last possible index and glyph.
diff --git a/pkg/editor/buffer/line/line_test.go b/pkg/editor/buffer/line/line_test.go
@@ -68,6 +68,12 @@ func TestGlyphIndex(t *testing.T) {
glyph: 8, // 针
target: 8, // Doesn't exist; return end of last rune.
},
+ {
+ data: "你好!",
+ index: 6, // !
+ glyph: 4, // !
+ target: 5,
+ },
}
for n, test := range tests {
diff --git a/pkg/editor/buffer/save.go b/pkg/editor/buffer/save.go
@@ -50,3 +50,25 @@ func (b *Buffer) Save() error {
return nil
}
+
+func (b *Buffer) SaveAs(name string) error {
+ f, err := os.Create(name)
+ if err != nil {
+ return fmt.Errorf("failed to create file: %w", err)
+ }
+
+ // Close the previous file.
+ if err := b.file.Close(); err != nil {
+ b.log.Error.Printf("failed close file: %v", err)
+ }
+
+ // Set the new file and name.
+ b.file = f
+ b.name = name
+
+ if err := b.Save(); err != nil {
+ return fmt.Errorf("failed to save file: %w", err)
+ }
+
+ return nil
+}
diff --git a/pkg/editor/buffers.go b/pkg/editor/buffers.go
@@ -5,10 +5,9 @@ import (
"fmt"
"os"
"path/filepath"
- "time"
"code.dwrz.net/src/pkg/editor/buffer"
- "code.dwrz.net/src/pkg/editor/message"
+ "code.dwrz.net/src/pkg/editor/event"
)
// load files into editor buffers.
@@ -45,7 +44,7 @@ func (e *Editor) load(files []string) {
}
// If there was an error, report it to the user.
if err != nil {
- e.messages <- message.New(fmt.Sprintf(
+ e.events <- event.NewMessage(fmt.Sprintf(
"failed to load buffer %s: %v",
name, err,
))
@@ -61,21 +60,21 @@ func (e *Editor) load(files []string) {
}
}
- e.messages <- message.New("loaded buffers")
-
- // Set the initial message.
- go func() {
- time.Sleep(1 * time.Second)
- e.messages <- message.New("Ctrl-Q: Quit")
- }()
+ e.events <- event.NewMessage("loaded buffers")
}
-// setBuffer stores a buffer in the editor's buffer map.
-func (e *Editor) setBuffer(b *buffer.Buffer) {
+func (e *Editor) renameBuffer(b *buffer.Buffer, name string) error {
e.mu.Lock()
defer e.mu.Unlock()
- e.buffers[b.Name()] = b
+ delete(e.buffers, b.Name())
+ e.buffers[name] = b
+
+ if err := b.SaveAs(name); err != nil {
+ return fmt.Errorf("failed to save as %s: %v", name, err)
+ }
+
+ return nil
}
// setActiveBuffer sets the named buffer as the active buffer.
@@ -85,12 +84,19 @@ func (e *Editor) setActiveBuffer(name string) {
b, exists := e.buffers[name]
if !exists {
- e.errs <- fmt.Errorf(
- "failed to set active buffer: buffer %s does not exist",
- name,
- )
+ e.events <- event.NewError(fmt.Errorf(
+ "failed to set active buffer: %s does not exist", name,
+ ))
}
e.active = b
e.log.Debug.Printf("set active buffer %s", b.Name())
}
+
+// setBuffer stores a buffer in the editor's buffer map.
+func (e *Editor) setBuffer(b *buffer.Buffer) {
+ e.mu.Lock()
+ defer e.mu.Unlock()
+
+ e.buffers[b.Name()] = b
+}
diff --git a/pkg/editor/canvas/render.go b/pkg/editor/canvas/render.go
@@ -8,11 +8,11 @@ import (
"code.dwrz.net/src/pkg/color"
"code.dwrz.net/src/pkg/editor/buffer"
- "code.dwrz.net/src/pkg/editor/message"
+ "code.dwrz.net/src/pkg/editor/prompt"
"code.dwrz.net/src/pkg/terminal"
)
-func (c *Canvas) Render(b *buffer.Buffer, msg *message.Message) error {
+func (c *Canvas) Render(b *buffer.Buffer, prompt *prompt.Prompt) error {
c.mu.Lock()
defer c.mu.Unlock()
@@ -44,12 +44,21 @@ func (c *Canvas) Render(b *buffer.Buffer, msg *message.Message) error {
c.statusBar(b, width, cursor.Line(), cursor.Glyph())
// Draw the message bar.
- c.messageBar(msg, width)
+ c.messageBar(prompt, width)
// Set the cursor.
- c.buf.WriteString(fmt.Sprintf(
- terminal.SetCursorFmt, output.Line, output.Glyph,
- ))
+ if prompt.IsPrompt() {
+ // FIX: +1's.
+ c.buf.WriteString(fmt.Sprintf(
+ terminal.SetCursorFmt, size.Rows+1,
+ runewidth.StringWidth(prompt.Text())+prompt.Cursor()+2,
+ ))
+ } else {
+ c.buf.WriteString(fmt.Sprintf(
+ terminal.SetCursorFmt, output.Line, output.Glyph,
+ ))
+ }
+
c.buf.WriteString(terminal.CursorShow)
c.out.Write(c.buf.Bytes())
@@ -105,16 +114,27 @@ func (c *Canvas) statusBar(b *buffer.Buffer, width, y, x int) {
}
// TODO: handle messages that are too long for one line.
-func (c *Canvas) messageBar(msg *message.Message, width int) {
+func (c *Canvas) messageBar(p *prompt.Prompt, width int) {
c.buf.WriteString(terminal.EraseLine)
+
+ var (
+ text = p.Text()
+ line = p.Line(0, width) // TODO: handle offset.
+ )
switch {
- case msg == nil:
+ case text == "" && line == "":
c.buf.WriteString(fmt.Sprintf("%*s", width, " "))
- case len(msg.Text) > width:
- c.buf.WriteString(fmt.Sprintf("%*s", width, msg.Text))
+
+ // TODO: handle text + line; offset.
+ case len(text) > width:
+ c.buf.WriteString(fmt.Sprintf("%*s", width, text))
+
default:
+ output := fmt.Sprintf("%s %s", text, line)
+
c.buf.WriteString(fmt.Sprintf(
- "%s %*s", msg.Text, width-len(msg.Text), " ",
+ "%s %*s",
+ output, width-len(output)-1, " ", // FIX -1
))
}
}
diff --git a/pkg/editor/editor.go b/pkg/editor/editor.go
@@ -7,7 +7,8 @@ import (
"code.dwrz.net/src/pkg/editor/buffer"
"code.dwrz.net/src/pkg/editor/canvas"
- "code.dwrz.net/src/pkg/editor/message"
+ "code.dwrz.net/src/pkg/editor/event"
+ "code.dwrz.net/src/pkg/editor/prompt"
"code.dwrz.net/src/pkg/log"
"code.dwrz.net/src/pkg/terminal"
"code.dwrz.net/src/pkg/terminal/input"
@@ -15,10 +16,9 @@ import (
type Editor struct {
canvas *canvas.Canvas
- errs chan error
input chan *input.Event
log *log.Logger
- messages chan *message.Message
+ events chan event.Event
reader *input.Reader
terminal *terminal.Terminal
tmpdir string
@@ -26,6 +26,7 @@ type Editor struct {
mu sync.Mutex
active *buffer.Buffer
buffers map[string]*buffer.Buffer
+ prompt *prompt.Prompt
}
type Parameters struct {
@@ -39,12 +40,12 @@ type Parameters struct {
func New(p Parameters) (*Editor, error) {
var editor = &Editor{
buffers: map[string]*buffer.Buffer{},
- errs: make(chan error),
+ events: make(chan event.Event),
input: make(chan *input.Event),
log: p.Log,
- messages: make(chan *message.Message),
terminal: p.Terminal,
tmpdir: p.TempDir,
+ prompt: prompt.New(prompt.Parameters{Log: p.Log}),
}
// Setup user input.
diff --git a/pkg/editor/event/error.go b/pkg/editor/event/error.go
@@ -0,0 +1,19 @@
+package event
+
+import "time"
+
+type Error struct {
+ error
+ t time.Time
+}
+
+func (e *Error) Time() time.Time {
+ return e.t
+}
+
+func NewError(err error) *Error {
+ return &Error{
+ error: err,
+ t: time.Now(),
+ }
+}
diff --git a/pkg/editor/event/event.go b/pkg/editor/event/event.go
@@ -0,0 +1,7 @@
+package event
+
+import "time"
+
+type Event interface {
+ Time() time.Time
+}
diff --git a/pkg/editor/event/message.go b/pkg/editor/event/message.go
@@ -0,0 +1,23 @@
+package event
+
+import "time"
+
+type Message struct {
+ t time.Time
+ text string
+}
+
+func (e *Message) Time() time.Time {
+ return e.t
+}
+
+func (e *Message) Text() string {
+ return e.text
+}
+
+func NewMessage(s string) *Message {
+ return &Message{
+ t: time.Now(),
+ text: s,
+ }
+}
diff --git a/pkg/editor/event/quit.go b/pkg/editor/event/quit.go
@@ -0,0 +1,15 @@
+package event
+
+import "time"
+
+type Quit struct {
+ t time.Time
+}
+
+func (e *Quit) Time() time.Time {
+ return e.t
+}
+
+func NewQuit() *Quit {
+ return &Quit{}
+}
diff --git a/pkg/editor/input.go b/pkg/editor/input.go
@@ -3,42 +3,48 @@ package editor
import (
"fmt"
- "code.dwrz.net/src/pkg/editor/message"
+ "code.dwrz.net/src/pkg/editor/event"
"code.dwrz.net/src/pkg/terminal/input"
)
-func (e *Editor) bufferInput(event *input.Event) error {
+func (e *Editor) bufferInput(in *input.Event) error {
size, err := e.terminal.Size()
if err != nil {
return fmt.Errorf("failed to get terminal size: %w", err)
}
- if event.Rune != input.Null {
- switch event.Rune {
+ if in.Rune != input.Null {
+ switch in.Rune {
case input.Delete:
e.active.Backspace()
+ case 'q' & input.Control:
+ e.prompt.Prompt("Quit? (y/n)", e.quit)
+
case 's' & input.Control:
- // Get the filename.
- if err := e.active.Save(); err != nil {
- go func() {
- e.messages <- message.New(fmt.Sprintf(
- "failed to save: %v", err,
- ))
- }()
- }
go func() {
- e.messages <- message.New("saved file")
+ if err := e.active.Save(); err != nil {
+ e.events <- event.NewMessage(
+ fmt.Sprintf(
+ "failed to save: %v",
+ err,
+ ),
+ )
+ }
+ e.events <- event.NewMessage("saved file")
}()
+ case 'x' & input.Control:
+ e.prompt.Prompt("Save as:", e.saveAs)
+
default:
- e.active.Insert(event.Rune)
+ e.active.Insert(in.Rune)
}
return nil
}
- switch event.Key {
+ switch in.Key {
case input.Down:
e.active.CursorDown()
@@ -51,8 +57,6 @@ func (e *Editor) bufferInput(event *input.Event) error {
case input.Up:
e.active.CursorUp()
- case input.Insert:
-
case input.End:
e.active.CursorEnd()
@@ -66,23 +70,47 @@ func (e *Editor) bufferInput(event *input.Event) error {
e.active.PageUp(int(size.Rows))
default:
- e.log.Debug.Printf("unrecognized input: %#v", event)
+ e.log.Debug.Printf("unrecognized input: %#v", in)
}
return nil
}
func (e *Editor) promptInput(event *input.Event) error {
+ if event.Rune != input.Null {
+ switch event.Rune {
+ case input.Escape:
+ e.prompt.Escape()
+
+ case input.CarriageReturn:
+ e.prompt.Complete()
+
+ case input.Delete:
+ e.prompt.Backspace()
+
+ default:
+ e.prompt.Insert(event.Rune)
+ }
+
+ return nil
+ }
+
switch event.Key {
+ case input.Left:
+ e.prompt.CursorLeft()
+
+ case input.Right:
+ e.prompt.CursorRight()
+
+ case input.End:
+ e.prompt.CursorEnd()
+
+ case input.Home:
+ e.prompt.CursorHome()
default:
e.log.Debug.Printf("unrecognized input: %#v", event)
}
- // If a newline was received, take the input.
- // Pass it back to the caller.
- // Make it the caller's job to set the prompt again if not happy
- // with return value.
-
return nil
}
diff --git a/pkg/editor/message/message.go b/pkg/editor/message/message.go
@@ -1,15 +0,0 @@
-package message
-
-import "time"
-
-type Message struct {
- Text string
- Time time.Time
-}
-
-func New(text string) *Message {
- return &Message{
- Text: text,
- Time: time.Now(),
- }
-}
diff --git a/pkg/editor/prompt.go b/pkg/editor/prompt.go
@@ -0,0 +1,33 @@
+package editor
+
+import (
+ "fmt"
+
+ "code.dwrz.net/src/pkg/editor/buffer/line"
+ "code.dwrz.net/src/pkg/editor/event"
+)
+
+func (e *Editor) saveAs(l line.Line) error {
+ if err := e.active.SaveAs(l.String()); err != nil {
+ return fmt.Errorf("failed to save: %v", err)
+ }
+
+ return nil
+}
+
+func (e *Editor) quit(l line.Line) error {
+ switch l.String() {
+ case "y":
+ go func() {
+ e.events <- event.NewQuit()
+ }()
+
+ return nil
+
+ case "n":
+ return nil
+
+ default:
+ return fmt.Errorf("invalid selection")
+ }
+}
diff --git a/pkg/editor/prompt/prompt.go b/pkg/editor/prompt/prompt.go
@@ -0,0 +1,163 @@
+package prompt
+
+import (
+ "fmt"
+ "sync"
+ "unicode"
+ "unicode/utf8"
+
+ "code.dwrz.net/src/pkg/editor/buffer/glyph"
+ "code.dwrz.net/src/pkg/editor/buffer/line"
+ "code.dwrz.net/src/pkg/log"
+)
+
+type Prompt struct {
+ log *log.Logger
+
+ mu sync.Mutex
+ err error
+ cb CB
+ cursor cursor
+ line *line.Line
+ text string
+}
+
+type CB func(l line.Line) error
+
+type cursor struct {
+ glyph int
+ index int
+}
+
+var NoOp CB = func(l line.Line) error { return nil }
+
+type Parameters struct {
+ Log *log.Logger
+}
+
+func New(p Parameters) *Prompt {
+ return &Prompt{log: p.Log}
+}
+
+func (p *Prompt) Complete() {
+ if err := p.cb(*p.line); err != nil {
+ p.log.Error.Printf("callback error: %v", err)
+ p.err = err
+
+ return
+ }
+
+ // No error -- we should be done.
+ p.cb = nil
+ p.err = nil
+ p.line = nil
+ p.text = ""
+ p.cursor = cursor{}
+}
+
+func (p *Prompt) Cursor() int {
+ return p.cursor.glyph
+}
+
+func (p *Prompt) Escape() {
+ // No error -- we should be done.
+ p.cb = nil
+ p.err = nil
+ p.line = nil
+ p.text = ""
+ p.cursor = cursor{}
+}
+
+func (p *Prompt) IsPrompt() bool {
+ return p.line != nil
+}
+
+func (p *Prompt) Prompt(s string, cb CB) {
+ p.text = s
+ p.line = &line.Line{}
+ p.cb = cb
+
+ p.log.Debug.Printf("new prompt: %#v", p)
+}
+
+// TODO: mutex? Channel? Better canvas library?
+func (p *Prompt) SetText(s string) {
+ p.text = s
+}
+
+func (p *Prompt) Text() string {
+ if p.err != nil {
+ return fmt.Sprintf("[%v] %s", p.err, p.text)
+ }
+
+ return p.text
+}
+
+func (p *Prompt) Line(offset, width int) string {
+ if p.line == nil {
+ return ""
+ }
+
+ return p.line.Render(offset, width)
+}
+
+func (p *Prompt) CursorLeft() {
+ if p.cursor.index > 0 {
+ var (
+ r rune
+ size int
+ )
+ // Reverse until we hit the start of a rune.
+ for i := p.cursor.index - 1; i >= 0; i-- {
+ r, size = p.line.DecodeRune(i)
+ if r != utf8.RuneError {
+ p.cursor.index -= size
+ p.cursor.glyph -= glyph.Width(r)
+ break
+ }
+ }
+ }
+}
+
+func (p *Prompt) CursorRight() {
+ if p.cursor.index < p.line.Length() {
+ r, size := p.line.DecodeRune(p.cursor.index)
+ if r == utf8.RuneError {
+ p.cursor.index++
+ p.cursor.glyph += glyph.Width(r)
+ }
+
+ p.cursor.index += size
+ p.cursor.glyph += glyph.Width(r)
+ }
+}
+
+func (p *Prompt) CursorHome() {
+ p.cursor.glyph = 0
+ p.cursor.index = 0
+}
+
+func (p *Prompt) CursorEnd() {
+ p.cursor.index = p.line.Length()
+ p.cursor.glyph = p.line.Width()
+}
+
+func (p *Prompt) Backspace() {
+ if p.cursor.index > 0 {
+ p.CursorLeft()
+ p.line.DeleteRune(p.cursor.index)
+ }
+}
+
+func (p *Prompt) Insert(r rune) {
+ switch {
+ // Ignore all other non-printable characters.
+ case !unicode.IsPrint(r):
+ return
+
+ default:
+ p.line.Insert(p.cursor.index, r)
+ p.cursor.index += utf8.RuneLen(r)
+ p.cursor.glyph += glyph.Width(r)
+ }
+}
diff --git a/pkg/editor/quit.go b/pkg/editor/quit.go
@@ -1,11 +0,0 @@
-package editor
-
-func (e *Editor) quit() {
- e.canvas.Reset()
-
- if err := e.terminal.Reset(); err != nil {
- e.log.Error.Printf(
- "failed to reset terminal attributes: %v", err,
- )
- }
-}
diff --git a/pkg/editor/run.go b/pkg/editor/run.go
@@ -5,7 +5,7 @@ import (
"fmt"
"code.dwrz.net/src/pkg/build"
- "code.dwrz.net/src/pkg/terminal/input"
+ "code.dwrz.net/src/pkg/editor/event"
)
func (e *Editor) Run(ctx context.Context, files []string) error {
@@ -21,12 +21,20 @@ func (e *Editor) Run(ctx context.Context, files []string) error {
)
// Reset the terminal before exiting.
- defer e.quit()
+ defer func() {
+ e.canvas.Reset()
+
+ if err := e.terminal.Reset(); err != nil {
+ e.log.Error.Printf(
+ "failed to reset terminal attributes: %v", err,
+ )
+ }
+ }()
// Start reading user input.
go func() {
if err := e.reader.Run(ctx); err != nil {
- e.errs <- err
+ e.events <- event.NewError(err)
}
}()
@@ -40,25 +48,46 @@ func (e *Editor) Run(ctx context.Context, files []string) error {
e.log.Debug.Printf("context done: stopping")
return nil
- case err := <-e.errs:
- return err
+ case ev := <-e.events:
+ switch t := ev.(type) {
+ case *event.Error:
+ return t
- case msg := <-e.messages:
- e.log.Debug.Printf("%s", msg.Text)
- if err := e.canvas.Render(e.active, msg); err != nil {
- return fmt.Errorf("failed to render: %w", err)
- }
+ case *event.Message:
+ e.prompt.SetText(t.Text())
- case event := <-e.input:
- if event.Rune == 'q'&input.Control {
+ if err := e.canvas.Render(
+ e.active, e.prompt,
+ ); err != nil {
+ return fmt.Errorf(
+ "failed to render: %w", err,
+ )
+ }
+
+ case *event.Quit:
return nil
}
- if err := e.bufferInput(event); err != nil {
- return fmt.Errorf(
- "failed to process input: %w", err,
- )
+
+ case event := <-e.input:
+ if e.prompt.IsPrompt() {
+ if err := e.promptInput(event); err != nil {
+ return fmt.Errorf(
+ "failed to process input: %w",
+ err,
+ )
+ }
+ } else {
+ if err := e.bufferInput(event); err != nil {
+ return fmt.Errorf(
+ "failed to process input: %w",
+ err,
+ )
+ }
}
- if err := e.canvas.Render(e.active, nil); err != nil {
+
+ if err := e.canvas.Render(
+ e.active, e.prompt,
+ ); err != nil {
return fmt.Errorf("failed to render: %w", err)
}
}