package editor import ( "github.com/gdamore/tcell/v2" "codeberg.org/turbo-editors/turbo-core/buffer" ) // HandleKey turns a key press into a movement, a selection or an edit. // // The bindings follow Turbo C where it still makes sense — Ctrl-Insert to // copy, Shift-Insert to paste — and modern habits where it does not, since // Ctrl-Z for undo is what fingers now expect. func (v *View) HandleKey(ev *tcell.EventKey) bool { if v.handleMovement(ev) || v.handleClipboard(ev) || v.handleEditing(ev) { return true } return v.handleTyping(ev) } // handleMovement deals with the keys that only move the cursor, extending the // selection when Shift is held. func (v *View) handleMovement(ev *tcell.EventKey) bool { move := v.movementFor(ev) if move == nil { return false } if ev.Modifiers()&tcell.ModShift != 0 { v.buf.Extend(move) } else { move() v.buf.ClearSelection() } v.EnsureCursorVisible() v.notifyCursor() return true } // movementFor returns the cursor movement a key asks for, or nil when the key // is not a movement at all. // // Ctrl widens three of them from a character to a word or to the whole file, // which is why the table is built here rather than being a package variable. func (v *View) movementFor(ev *tcell.EventKey) func() { ctrl := ev.Modifiers()&tcell.ModCtrl != 0 var move func() switch ev.Key() { case tcell.KeyLeft: move = pick(ctrl, v.buf.MoveWordLeft, v.buf.MoveLeft) case tcell.KeyRight: move = pick(ctrl, v.buf.MoveWordRight, v.buf.MoveRight) case tcell.KeyUp: move = v.buf.MoveUp case tcell.KeyDown: move = v.buf.MoveDown case tcell.KeyHome: move = pick(ctrl, v.buf.MoveBufferStart, v.buf.MoveLineStart) case tcell.KeyEnd: move = pick(ctrl, v.buf.MoveBufferEnd, v.buf.MoveLineEnd) case tcell.KeyPgUp: move = func() { v.movePage(-1) } case tcell.KeyPgDn: move = func() { v.movePage(+1) } } return move } // pick returns whichever of two movements the modifier calls for. func pick(modified bool, withModifier, plain func()) func() { if modified { return withModifier } return plain } // movePage moves the cursor and the view by one screenful. func (v *View) movePage(direction int) { page := max(v.VisibleLines(), 1) cursor := v.buf.Cursor() v.buf.SetCursor(buffer.Position{Line: cursor.Line + direction*page, Col: cursor.Col}) v.ScrollBy(direction * page) } // handleClipboard deals with cut, copy, paste and select-all, in both their // Turbo C and their modern spellings. func (v *View) handleClipboard(ev *tcell.EventKey) bool { shift := ev.Modifiers()&tcell.ModShift != 0 ctrl := ev.Modifiers()&tcell.ModCtrl != 0 switch { case ev.Key() == tcell.KeyCtrlC, ev.Key() == tcell.KeyInsert && ctrl: v.Copy() case ev.Key() == tcell.KeyCtrlX, ev.Key() == tcell.KeyDelete && shift: v.Cut() case ev.Key() == tcell.KeyCtrlV, ev.Key() == tcell.KeyInsert && shift: v.Paste() case ev.Key() == tcell.KeyCtrlA: v.SelectAll() default: return false } v.EnsureCursorVisible() return true } // handleEditing deals with the keys that change the text without inserting a // printable character. func (v *View) handleEditing(ev *tcell.EventKey) bool { if v.handleCommand(ev) { return true } switch ev.Key() { case tcell.KeyEnter: v.buf.InsertNewlineAndIndent() case tcell.KeyBackspace, tcell.KeyBackspace2: v.buf.Backspace() case tcell.KeyDelete: v.buf.Delete() default: return false } v.EnsureCursorVisible() v.notifyChange() return true } // handleCommand deals with the keys that ask the view to do something rather // than to change the text under the cursor. Each of them reports its own // change, so none of them falls through to the notification below. func (v *View) handleCommand(ev *tcell.EventKey) bool { switch ev.Key() { case tcell.KeyTab: v.Indent() case tcell.KeyBacktab: v.Unindent() case tcell.KeyCtrlZ: v.Undo() case tcell.KeyCtrlR: v.Redo() // Ctrl-Y deletes a line and Ctrl-N inserts one, as they did in Turbo C and // Turbo Pascal. Ctrl-Y is why redo moved to Ctrl-R: it was the redo key // here for a while, and this is the one place the two claims collide. // Ctrl-Shift-Z, the obvious alternative for redo, is not a key a terminal // can deliver — it arrives as plain Ctrl-Z. case tcell.KeyCtrlY: v.DeleteLine() case tcell.KeyCtrlN: v.InsertLine() case tcell.KeyCtrlSpace: v.requestCompletion() default: return false } v.EnsureCursorVisible() return true } // handleTyping inserts a printable character. func (v *View) handleTyping(ev *tcell.EventKey) bool { if ev.Key() != tcell.KeyRune || ev.Modifiers()&(tcell.ModAlt|tcell.ModCtrl) != 0 { return false } v.buf.InsertRune(ev.Rune()) v.EnsureCursorVisible() v.notifyChange() // A dot is where a completion list is most useful, so asking for one is // automatic there and manual everywhere else. if ev.Rune() == '.' { v.requestCompletion() } return true } // requestCompletion asks the owner to offer a completion list. func (v *View) requestCompletion() { if v.OnCompletionRequest != nil { v.OnCompletionRequest() } } // HandleMouse places the cursor, extends the selection by dragging, and // scrolls with the wheel. func (v *View) HandleMouse(ev *tcell.EventMouse) bool { x, y := ev.Position() switch ev.Buttons() { case tcell.WheelUp: v.ScrollBy(-wheelStep) case tcell.WheelDown: v.ScrollBy(+wheelStep) case tcell.Button1: v.pressOrDrag(x, y) case tcell.ButtonNone: v.selecting = false return false default: return false } return true } // wheelStep is how many lines one notch of the mouse wheel scrolls. const wheelStep = 3 // pressOrDrag puts the cursor where the mouse is, starting a selection on the // first event of a drag and extending it on the ones that follow. // // A press is only a *press* when nothing is being dragged: tcell sends Button1 // for every event of a drag too, and counting those as clicks would turn a // slow drag into a double click. func (v *View) pressOrDrag(x, y int) { position := v.positionAt(x, y) switch { case v.selecting: v.buf.SetCursorKeepingSelection(position) case v.countClick(position) >= 2: // A second press on the same cell selects the word under it, and the // drag carries on from there: holding and moving after a double click // extends from the **start of the word**, by character. That is not // the word-by-word drag a large editor does — it is the cheap half of // it, and it beats the alternative, which is the word collapsing back // to a single character the moment the pointer twitches. v.buf.SelectWord(position) v.selecting = true default: v.buf.SetCursor(position) v.buf.StartSelection() v.selecting = true } v.EnsureCursorVisible() v.notifyCursor() }