src

Go monorepo.
Log | Files | Refs

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:
Mpkg/editor/buffer/cursor.go | 1-
Mpkg/editor/buffer/edit.go | 1-
Mpkg/editor/buffer/line/line.go | 6++++++
Mpkg/editor/buffer/line/line_test.go | 6++++++
Mpkg/editor/buffer/save.go | 22++++++++++++++++++++++
Mpkg/editor/buffers.go | 40+++++++++++++++++++++++-----------------
Mpkg/editor/canvas/render.go | 42+++++++++++++++++++++++++++++++-----------
Mpkg/editor/editor.go | 11++++++-----
Apkg/editor/event/error.go | 19+++++++++++++++++++
Apkg/editor/event/event.go | 7+++++++
Apkg/editor/event/message.go | 23+++++++++++++++++++++++
Apkg/editor/event/quit.go | 15+++++++++++++++
Mpkg/editor/input.go | 74+++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Dpkg/editor/message/message.go | 15---------------
Apkg/editor/prompt.go | 33+++++++++++++++++++++++++++++++++
Apkg/editor/prompt/prompt.go | 163+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dpkg/editor/quit.go | 11-----------
Mpkg/editor/run.go | 63++++++++++++++++++++++++++++++++++++++++++++++-----------------
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) } }