diff --git a/cmd/micro/action/actions.go b/cmd/micro/action/actions.go index eaf5a8e2..6afa78ac 100644 --- a/cmd/micro/action/actions.go +++ b/cmd/micro/action/actions.go @@ -1,6 +1,7 @@ package action import ( + "log" "os" "strings" "time" @@ -560,6 +561,37 @@ func (h *BufHandler) SaveAs() bool { // Find opens a prompt and searches forward for the input func (h *BufHandler) Find() bool { + InfoBar.Prompt("Find: ", "", func(resp string) { + match, found, _ := h.Buf.FindNext(resp, h.Cursor.Loc, true) + if found { + h.Cursor.SetSelectionStart(match[0]) + h.Cursor.SetSelectionEnd(match[1]) + h.Cursor.OrigSelection[0] = h.Cursor.CurSelection[0] + h.Cursor.OrigSelection[1] = h.Cursor.CurSelection[1] + } else { + log.Println("RESET") + h.Cursor.ResetSelection() + } + }, func(resp string, canceled bool) { + if !canceled { + match, found, err := h.Buf.FindNext(resp, h.Cursor.Loc, true) + if err != nil { + InfoBar.Error(err) + } + if found { + h.Cursor.SetSelectionStart(match[0]) + h.Cursor.SetSelectionEnd(match[1]) + h.Cursor.OrigSelection[0] = h.Cursor.CurSelection[0] + h.Cursor.OrigSelection[1] = h.Cursor.CurSelection[1] + h.Cursor.Loc = h.Cursor.CurSelection[1] + } else { + h.Cursor.ResetSelection() + } + } else { + h.Cursor.ResetSelection() + } + }) + return true } @@ -725,7 +757,7 @@ func (h *BufHandler) SelectAll() bool { // OpenFile opens a new file in the buffer func (h *BufHandler) OpenFile() bool { - InfoBar.Prompt("> ", "open ", func(resp string, canceled bool) { + InfoBar.Prompt("> ", "open ", nil, func(resp string, canceled bool) { if !canceled { HandleCommand(resp) } @@ -889,7 +921,7 @@ func (h *BufHandler) ShellMode() bool { // CommandMode lets the user enter a command func (h *BufHandler) CommandMode() bool { - InfoBar.Prompt("> ", "", func(resp string, canceled bool) { + InfoBar.Prompt("> ", "", nil, func(resp string, canceled bool) { if !canceled { HandleCommand(resp) } diff --git a/cmd/micro/action/bindings.go b/cmd/micro/action/bindings.go index 3ed86dc0..2b5173a3 100644 --- a/cmd/micro/action/bindings.go +++ b/cmd/micro/action/bindings.go @@ -103,7 +103,8 @@ modSearch: k = string(unicode.ToUpper(rune(k[0]))) + k[1:] if code, ok := keyEvents["Ctrl"+k]; ok { var r tcell.Key - if code < 256 { + // Special case for escape, for some reason tcell doesn't send it with the esc character + if code < 256 && code != 27 { r = code } // It is, we're done. @@ -118,7 +119,8 @@ modSearch: // See if we can find the key in bindingKeys if code, ok := keyEvents[k]; ok { var r tcell.Key - if code < 256 { + // Special case for escape, for some reason tcell doesn't send it with the esc character + if code < 256 && code != 27 { r = code } return KeyEvent{ diff --git a/cmd/micro/action/bufhandler.go b/cmd/micro/action/bufhandler.go index 7469a609..b602ab78 100644 --- a/cmd/micro/action/bufhandler.go +++ b/cmd/micro/action/bufhandler.go @@ -22,6 +22,7 @@ func init() { BufMouseBindings = make(map[MouseEvent]BufMouseAction) } +// BufMapKey maps a key event to an action func BufMapKey(k KeyEvent, action string) { if f, ok := BufKeyActions[action]; ok { BufKeyStrings[k] = action @@ -30,6 +31,8 @@ func BufMapKey(k KeyEvent, action string) { util.TermMessage("Error:", action, "does not exist") } } + +// BufMapMouse maps a mouse event to an action func BufMapMouse(k MouseEvent, action string) { if f, ok := BufMouseActions[action]; ok { BufMouseBindings[k] = f @@ -147,10 +150,13 @@ func (h *BufHandler) HandleEvent(event tcell.Event) { } } +// DoKeyEvent executes a key event by finding the action it is bound +// to and executing it (possibly multiple times for multiple cursors) func (h *BufHandler) DoKeyEvent(e KeyEvent) bool { if action, ok := BufKeyBindings[e]; ok { - for _, a := range MultiActions { - if a == BufKeyStrings[e] { + estr := BufKeyStrings[e] + for _, s := range MultiActions { + if s == estr { cursors := h.Buf.GetCursors() for _, c := range cursors { h.Buf.SetCurCursor(c.Num) @@ -170,6 +176,8 @@ func (h *BufHandler) DoKeyEvent(e KeyEvent) bool { return false } +// DoMouseEvent executes a mouse event by finding the action it is bound +// to and executing it func (h *BufHandler) DoMouseEvent(e MouseEvent, te *tcell.EventMouse) bool { if action, ok := BufMouseBindings[e]; ok { if action(h, te) { @@ -180,6 +188,8 @@ func (h *BufHandler) DoMouseEvent(e MouseEvent, te *tcell.EventMouse) bool { return false } +// DoRuneInsert inserts a given rune into the current buffer +// (possibly multiple times for multiple cursors) func (h *BufHandler) DoRuneInsert(r rune) { cursors := h.Buf.GetCursors() for _, c := range cursors { @@ -199,6 +209,7 @@ func (h *BufHandler) DoRuneInsert(r rune) { } } +// BufKeyActions contains the list of all possible key actions the bufhandler could execute var BufKeyActions = map[string]BufKeyAction{ "CursorUp": (*BufHandler).CursorUp, "CursorDown": (*BufHandler).CursorDown, @@ -297,13 +308,13 @@ var BufKeyActions = map[string]BufKeyAction{ // This was changed to InsertNewline but I don't want to break backwards compatibility "InsertEnter": (*BufHandler).InsertNewline, } + +// BufMouseActions contains the list of all possible mouse actions the bufhandler could execute var BufMouseActions = map[string]BufMouseAction{ "MousePress": (*BufHandler).MousePress, "MouseMultiCursor": (*BufHandler).MouseMultiCursor, } -const funcPrefixLen = 21 // length of "action.(*BufHandler)." - // MultiActions is a list of actions that should be executed multiple // times if there are multiple cursors (one per cursor) // Generally actions that modify global editor state like quitting or diff --git a/cmd/micro/action/command.go b/cmd/micro/action/command.go index 62a1a73b..644fe675 100644 --- a/cmd/micro/action/command.go +++ b/cmd/micro/action/command.go @@ -119,7 +119,7 @@ func DefaultCommands() map[string]StrCommand { // enter func CommandEditAction(prompt string) BufKeyAction { return func(h *BufHandler) bool { - InfoBar.Prompt("> ", prompt, func(resp string, canceled bool) { + InfoBar.Prompt("> ", prompt, nil, func(resp string, canceled bool) { if !canceled { HandleCommand(resp) } diff --git a/cmd/micro/action/editpane.go b/cmd/micro/action/editpane.go index 60eab596..92cc929c 100644 --- a/cmd/micro/action/editpane.go +++ b/cmd/micro/action/editpane.go @@ -8,12 +8,12 @@ import ( type EditPane struct { display.Window - Handler + *BufHandler } type InfoPane struct { display.Window - Handler + *InfoHandler *info.InfoBuf } @@ -22,7 +22,7 @@ func NewBufEditPane(x, y, width, height int, b *buffer.Buffer) *EditPane { // TODO: can probably replace editpane with bufhandler entirely w := display.NewBufWindow(x, y, width, height, b) e.Window = w - e.Handler = NewBufHandler(b, w) + e.BufHandler = NewBufHandler(b, w) return e } @@ -32,7 +32,7 @@ func NewInfoBar() *InfoPane { ib := info.NewBuffer() w := display.NewInfoWindow(ib) e.Window = w - e.Handler = NewBufHandler(ib.Buffer, w) + e.InfoHandler = NewInfoHandler(ib, w) e.InfoBuf = ib return e diff --git a/cmd/micro/action/infohandler.go b/cmd/micro/action/infohandler.go new file mode 100644 index 00000000..c50247de --- /dev/null +++ b/cmd/micro/action/infohandler.go @@ -0,0 +1,158 @@ +package action + +import ( + "strings" + + "github.com/zyedidia/micro/cmd/micro/display" + "github.com/zyedidia/micro/cmd/micro/info" + "github.com/zyedidia/tcell" +) + +type InfoKeyAction func(*InfoHandler) + +type InfoHandler struct { + *BufHandler + *info.InfoBuf +} + +func NewInfoHandler(ib *info.InfoBuf, w display.Window) *InfoHandler { + ih := new(InfoHandler) + ih.InfoBuf = ib + ih.BufHandler = NewBufHandler(ib.Buffer, w) + + return ih +} + +func (h *InfoHandler) HandleEvent(event tcell.Event) { + switch e := event.(type) { + case *tcell.EventKey: + ke := KeyEvent{ + code: e.Key(), + mod: e.Modifiers(), + r: e.Rune(), + } + + done := h.DoKeyEvent(ke) + if !done && e.Key() == tcell.KeyRune { + h.DoRuneInsert(e.Rune()) + } + case *tcell.EventMouse: + h.BufHandler.HandleEvent(event) + } +} + +func (h *InfoHandler) DoKeyEvent(e KeyEvent) bool { + done := false + if action, ok := BufKeyBindings[e]; ok { + estr := BufKeyStrings[e] + for _, s := range InfoNones { + if s == estr { + return false + } + } + for s, a := range InfoOverrides { + if s == estr { + done = true + a(h) + break + } + } + if !done { + done = action(h.BufHandler) + } + } + if done && h.EventCallback != nil { + h.EventCallback(strings.TrimSpace(string(h.LineBytes(0)))) + } + return done +} + +func (h *InfoHandler) DoRuneInsert(r rune) { + h.BufHandler.DoRuneInsert(r) + if h.EventCallback != nil { + h.EventCallback(strings.TrimSpace(string(h.LineBytes(0)))) + } +} + +// InfoNones is a list of actions that should have no effect when executed +// by an infohandler +var InfoNones = []string{ + "Save", + "SaveAll", + "SaveAs", + "Find", + "FindNext", + "FindPrevious", + "Center", + "DuplicateLine", + "MoveLinesUp", + "MoveLinesDown", + "OpenFile", + "Start", + "End", + "PageUp", + "PageDown", + "SelectPageUp", + "SelectPageDown", + "HalfPageUp", + "HalfPageDown", + "ToggleHelp", + "ToggleKeyMenu", + "ToggleRuler", + "JumpLine", + "ClearStatus", + "ShellMode", + "CommandMode", + "AddTab", + "PreviousTab", + "NextTab", + "NextSplit", + "PreviousSplit", + "Unsplit", + "VSplit", + "HSplit", + "ToggleMacro", + "PlayMacro", + "Suspend", + "ScrollUp", + "ScrollDown", + "SpawnMultiCursor", + "SpawnMultiCursorSelect", + "RemoveMultiCursor", + "RemoveAllMultiCursors", + "SkipMultiCursor", +} + +// InfoOverrides is the list of actions which have been overriden +// by the infohandler +var InfoOverrides = map[string]InfoKeyAction{ + "CursorUp": (*InfoHandler).CursorUp, + "CursorDown": (*InfoHandler).CursorDown, + "InsertNewline": (*InfoHandler).InsertNewline, + "InsertTab": (*InfoHandler).InsertTab, + "Escape": (*InfoHandler).Escape, + "Quit": (*InfoHandler).Quit, + "QuitAll": (*InfoHandler).QuitAll, +} + +func (h *InfoHandler) CursorUp() { + // TODO: history +} +func (h *InfoHandler) CursorDown() { + // TODO: history +} +func (h *InfoHandler) InsertTab() { + // TODO: autocomplete +} +func (h *InfoHandler) InsertNewline() { + h.DonePrompt(false) +} +func (h *InfoHandler) Quit() { + h.DonePrompt(true) +} +func (h *InfoHandler) QuitAll() { + h.DonePrompt(true) +} +func (h *InfoHandler) Escape() { + h.DonePrompt(true) +} diff --git a/cmd/micro/buffer/search.go b/cmd/micro/buffer/search.go new file mode 100644 index 00000000..a44b91ff --- /dev/null +++ b/cmd/micro/buffer/search.go @@ -0,0 +1,93 @@ +package buffer + +import ( + "regexp" + "unicode/utf8" + + "github.com/zyedidia/micro/cmd/micro/util" +) + +func (b *Buffer) findDown(r *regexp.Regexp, start, end Loc) ([2]Loc, bool) { + start.Y = util.Clamp(start.Y, 0, b.LinesNum()-1) + + for i := start.Y; i <= end.Y; i++ { + l := b.LineBytes(i) + charpos := 0 + + if i == start.Y { + nchars := utf8.RuneCount(l) + start.X = util.Clamp(start.X, 0, nchars-1) + l = util.SliceEnd(l, start.X) + charpos = start.X + } + + match := r.FindIndex(l) + + if match != nil { + start := Loc{charpos + util.RunePos(l, match[0]), i} + end := Loc{charpos + util.RunePos(l, match[1]), i} + return [2]Loc{start, end}, true + } + } + return [2]Loc{}, false +} + +func (b *Buffer) findUp(r *regexp.Regexp, start, end Loc) ([2]Loc, bool) { + start.Y = util.Clamp(start.Y, 0, b.LinesNum()-1) + + for i := start.Y; i >= end.Y; i-- { + l := b.LineBytes(i) + + if i == start.Y { + nchars := utf8.RuneCount(l) + start.X = util.Clamp(start.X, 0, nchars-1) + l = util.SliceStart(l, start.X) + } + + match := r.FindIndex(l) + + if match != nil { + start := Loc{util.RunePos(l, match[0]), i} + end := Loc{util.RunePos(l, match[1]), i} + return [2]Loc{start, end}, true + } + } + return [2]Loc{}, false +} + +// FindNext finds the next occurrence of a given string in the buffer +// It returns the start and end location of the match (if found) and +// a boolean indicating if it was found +// May also return an error if the search regex is invalid +func (b *Buffer) FindNext(s string, from Loc, down bool) ([2]Loc, bool, error) { + if s == "" { + return [2]Loc{}, false, nil + } + + var r *regexp.Regexp + var err error + if b.Settings["ignorecase"].(bool) { + r, err = regexp.Compile("(?i)" + s) + } else { + r, err = regexp.Compile(s) + } + + if err != nil { + return [2]Loc{}, false, err + } + + found := false + var l [2]Loc + if down { + l, found = b.findDown(r, from, b.End()) + if !found { + l, found = b.findDown(r, b.Start(), from) + } + } else { + l, found = b.findUp(r, from, b.Start()) + if !found { + l, found = b.findUp(r, b.End(), from) + } + } + return l, found, nil +} diff --git a/cmd/micro/display/window.go b/cmd/micro/display/window.go index a188b203..5aec83b9 100644 --- a/cmd/micro/display/window.go +++ b/cmd/micro/display/window.go @@ -317,7 +317,7 @@ func (w *BufWindow) getStyle(style tcell.Style, bloc buffer.Loc, r rune) (tcell. } func (w *BufWindow) showCursor(x, y int, main bool) { - if !main { + if main { screen.Screen.ShowCursor(x, y) } else { r, _, _, _ := screen.Screen.GetContent(x, y) @@ -421,8 +421,8 @@ func (w *BufWindow) displayBuffer() { if showcursor { for _, c := range cursors { - if c.X == bloc.X && c.Y == bloc.Y { - w.showCursor(w.X+vloc.X, w.Y+vloc.Y, true) + if c.X == bloc.X && c.Y == bloc.Y && !c.HasSelection() { + w.showCursor(w.X+vloc.X, w.Y+vloc.Y, c.Num == 0) } } } @@ -495,8 +495,8 @@ func (w *BufWindow) displayBuffer() { } for _, c := range cursors { - if c.X == bloc.X && c.Y == bloc.Y { - w.showCursor(w.X+vloc.X, w.Y+vloc.Y, true) + if c.X == bloc.X && c.Y == bloc.Y && !c.HasSelection() { + w.showCursor(w.X+vloc.X, w.Y+vloc.Y, c.Num == 0) } } diff --git a/cmd/micro/info/infobuffer.go b/cmd/micro/info/infobuffer.go index 6e670305..21b1aee5 100644 --- a/cmd/micro/info/infobuffer.go +++ b/cmd/micro/info/infobuffer.go @@ -27,8 +27,10 @@ type InfoBuf struct { GutterMessage bool PromptCallback func(resp string, canceled bool) + EventCallback func(resp string) } +// NewBuffer returns a new infobuffer func NewBuffer() *InfoBuf { ib := new(InfoBuf) ib.History = make(map[string][]string) @@ -62,7 +64,11 @@ func (i *InfoBuf) Error(msg ...interface{}) { // TODO: add to log? } -func (i *InfoBuf) Prompt(prompt string, msg string, callback func(string, bool)) { +// Prompt starts a prompt for the user, it takes a prompt, a possibly partially filled in msg +// and callbacks executed when the user executes an event and when the user finishes the prompt +// The eventcb passes the current user response as the argument and donecb passes the user's message +// and a boolean indicating if the prompt was canceled +func (i *InfoBuf) Prompt(prompt string, msg string, eventcb func(string), donecb func(string, bool)) { // If we get another prompt mid-prompt we cancel the one getting overwritten if i.HasPrompt { i.DonePrompt(true) @@ -71,21 +77,27 @@ func (i *InfoBuf) Prompt(prompt string, msg string, callback func(string, bool)) i.Msg = prompt i.HasPrompt = true i.HasMessage, i.HasError = false, false - i.PromptCallback = callback + i.PromptCallback = donecb + i.EventCallback = eventcb i.Buffer.Insert(i.Buffer.Start(), msg) } +// DonePrompt finishes the current prompt and indicates whether or not it was canceled func (i *InfoBuf) DonePrompt(canceled bool) { i.HasPrompt = false - if canceled { - i.PromptCallback("", true) - } else { - i.PromptCallback(strings.TrimSpace(string(i.LineBytes(0))), false) + if i.PromptCallback != nil { + if canceled { + i.PromptCallback("", true) + } else { + i.PromptCallback(strings.TrimSpace(string(i.LineBytes(0))), false) + } } + i.PromptCallback = nil + i.EventCallback = nil i.Replace(i.Start(), i.End(), "") } -// Reset resets the messenger's cursor, message and response +// Reset resets the infobuffer's msg and info func (i *InfoBuf) Reset() { i.Msg = "" i.HasPrompt, i.HasMessage, i.HasError = false, false, false diff --git a/cmd/micro/util/util.go b/cmd/micro/util/util.go index 4d4d97ab..f64305a9 100644 --- a/cmd/micro/util/util.go +++ b/cmd/micro/util/util.go @@ -188,6 +188,12 @@ func IsStrWhitespace(str string) bool { return true } +// RunePos returns the rune index of a given byte index +// Make sure the byte index is not between code points +func RunePos(b []byte, i int) int { + return utf8.RuneCount(b[:i]) +} + // TODO: consider changing because of snap segfault // ReplaceHome takes a path as input and replaces ~ at the start of the path with the user's // home directory. Does nothing if the path does not start with '~'. @@ -302,3 +308,12 @@ func GetCharPosInLine(b []byte, visualPos int, tabsize int) int { return i } + +func Clamp(val, min, max int) int { + if val < min { + val = min + } else if val > max { + val = max + } + return val +}