X-Git-Url: https://git.lizzy.rs/?a=blobdiff_plain;ds=sidebyside;f=cmd%2Fmicro%2Fview.go;h=e53c66180b7fec021d8a1d419bcea078c299cd7a;hb=f58c5412a81135b39a36a02d46b1c17ec2b0554c;hp=11f7e0cf7748e62598a3c48688d2afbf9712fad4;hpb=eeb2aaf9aec6335902f0d5b7339aac2727d00347;p=micro.git diff --git a/cmd/micro/view.go b/cmd/micro/view.go index 11f7e0cf..e53c6618 100644 --- a/cmd/micro/view.go +++ b/cmd/micro/view.go @@ -1,19 +1,21 @@ package main import ( + "fmt" "os" + "reflect" "strconv" "strings" "time" - "github.com/mitchellh/go-homedir" "github.com/zyedidia/tcell" ) +// The ViewType defines what kind of view this is type ViewType struct { - kind int - readonly bool // The file cannot be edited - scratch bool // The file cannot be saved + Kind int + Readonly bool // The file cannot be edited + Scratch bool // The file cannot be saved } var ( @@ -21,6 +23,7 @@ var ( vtHelp = ViewType{1, true, true} vtLog = ViewType{2, true, true} vtScratch = ViewType{3, false, true} + vtRaw = ViewType{4, true, true} ) // The View struct stores information about a view into a buffer. @@ -62,7 +65,7 @@ type View struct { // The buffer Buf *Buffer // The statusline - sline Statusline + sline *Statusline // Since tcell doesn't differentiate between a mouse release event // and a mouse move event with no keys pressed, we need to keep @@ -70,9 +73,12 @@ type View struct { // mouse release events mouseReleased bool + // We need to keep track of insert key press toggle + isOverwriteMode bool // This stores when the last click was // This is useful for detecting double and triple clicks lastClickTime time.Time + lastLoc Loc // lastCutTime stores when the last ctrl+k was issued. // It is used for clearing the clipboard to replace it with fresh cut lines. @@ -91,6 +97,8 @@ type View struct { cellview *CellView splitNode *LeafNode + + scrollbar *ScrollBar } // NewView returns a new fullscreen view @@ -116,7 +124,11 @@ func NewViewWidthHeight(buf *Buffer, w, h int) *View { v.messages = make(map[string][]GutterMessage) - v.sline = Statusline{ + v.sline = &Statusline{ + view: v, + } + + v.scrollbar = &ScrollBar{ view: v, } @@ -169,7 +181,7 @@ func (v *View) paste(clip string) { } clip = strings.Replace(clip, "\n", "\n"+leadingWS, -1) v.Buf.Insert(v.Cursor.Loc, clip) - v.Cursor.Loc = v.Cursor.Loc.Move(Count(clip), v.Buf) + // v.Cursor.Loc = v.Cursor.Loc.Move(Count(clip), v.Buf) v.freshClip = false messenger.Message("Pasted clipboard") } @@ -198,7 +210,7 @@ func (v *View) ScrollDown(n int) { // If there are unsaved changes, the user will be asked if the view can be closed // causing them to lose the unsaved changes func (v *View) CanClose() bool { - if v.Type == vtDefault && v.Buf.IsModified { + if v.Type == vtDefault && v.Buf.Modified() { var choice bool var canceled bool if v.Buf.Settings["autosave"].(bool) { @@ -210,15 +222,12 @@ func (v *View) CanClose() bool { //if char == 'y' { if choice { v.Save(true) - return true - } else { - return true } + } else { + return false } - } else { - return true } - return false + return true } // OpenBuffer opens a new buffer in this view. @@ -238,13 +247,15 @@ func (v *View) OpenBuffer(buf *Buffer) { // Set mouseReleased to true because we assume the mouse is not being pressed when // the editor is opened v.mouseReleased = true + // Set isOverwriteMode to false, because we assume we are in the default mode when editor + // is opened + v.isOverwriteMode = false v.lastClickTime = time.Time{} } // Open opens the given file in the view func (v *View) Open(filename string) { - home, _ := homedir.Dir() - filename = strings.Replace(filename, "~", home, 1) + filename = ReplaceHome(filename) file, err := os.Open(filename) fileInfo, _ := os.Stat(filename) @@ -285,7 +296,7 @@ func (v *View) ReOpen() { // HSplit opens a horizontal split with the given buffer func (v *View) HSplit(buf *Buffer) { i := 0 - if v.Buf.Settings["splitBottom"].(bool) { + if v.Buf.Settings["splitbottom"].(bool) { i = 1 } v.splitNode.HSplit(buf, v.Num+i) @@ -294,7 +305,7 @@ func (v *View) HSplit(buf *Buffer) { // VSplit opens a vertical split with the given buffer func (v *View) VSplit(buf *Buffer) { i := 0 - if v.Buf.Settings["splitRight"].(bool) { + if v.Buf.Settings["splitright"].(bool) { i = 1 } v.splitNode.VSplit(buf, v.Num+i) @@ -450,8 +461,63 @@ func (v *View) MoveToMouseClick(x, y int) { v.Cursor.LastVisualX = v.Cursor.GetVisualX() } +// Execute actions executes the supplied actions +func (v *View) ExecuteActions(actions []func(*View, bool) bool) bool { + relocate := false + readonlyBindingsList := []string{"Delete", "Insert", "Backspace", "Cut", "Play", "Paste", "Move", "Add", "DuplicateLine", "Macro"} + for _, action := range actions { + readonlyBindingsResult := false + funcName := ShortFuncName(action) + if v.Type.Readonly == true { + // check for readonly and if true only let key bindings get called if they do not change the contents. + for _, readonlyBindings := range readonlyBindingsList { + if strings.Contains(funcName, readonlyBindings) { + readonlyBindingsResult = true + } + } + } + if !readonlyBindingsResult { + // call the key binding + relocate = action(v, true) || relocate + // Macro + if funcName != "ToggleMacro" && funcName != "PlayMacro" { + if recordingMacro { + curMacro = append(curMacro, action) + } + } + } + } + + return relocate +} + +// SetCursor sets the view's and buffer's cursor +func (v *View) SetCursor(c *Cursor) bool { + if c == nil { + return false + } + v.Cursor = c + v.Buf.curCursor = c.Num + + return true +} + // HandleEvent handles an event passed by the main loop func (v *View) HandleEvent(event tcell.Event) { + if v.Type == vtRaw { + v.Buf.Insert(v.Cursor.Loc, reflect.TypeOf(event).String()[7:]) + v.Buf.Insert(v.Cursor.Loc, fmt.Sprintf(": %q\n", event.EscSeq())) + + switch e := event.(type) { + case *tcell.EventKey: + if e.Key() == tcell.KeyCtrlQ { + v.Quit(true) + } + } + + return + } + // This bool determines whether the view is relocated at the end of the function // By default it's true because most events should cause a relocate relocate := true @@ -459,149 +525,138 @@ func (v *View) HandleEvent(event tcell.Event) { v.Buf.CheckModTime() switch e := event.(type) { + case *tcell.EventRaw: + for key, actions := range bindings { + if key.keyCode == -1 { + if e.EscSeq() == key.escape { + for _, c := range v.Buf.cursors { + ok := v.SetCursor(c) + if !ok { + break + } + relocate = false + relocate = v.ExecuteActions(actions) || relocate + } + v.SetCursor(&v.Buf.Cursor) + v.Buf.MergeCursors() + break + } + } + } case *tcell.EventKey: // Check first if input is a key binding, if it is we 'eat' the input and don't insert a rune isBinding := false - readonlyBindingsList := []string{"Delete", "Insert", "Backspace", "Cut", "Play", "Paste", "Move", "Add", "DuplicateLine", "Macro"} - if e.Key() != tcell.KeyRune || e.Modifiers() != 0 { - for key, actions := range bindings { - if e.Key() == key.keyCode { - if e.Key() == tcell.KeyRune { - if e.Rune() != key.r { - continue - } + for key, actions := range bindings { + if e.Key() == key.keyCode { + if e.Key() == tcell.KeyRune { + if e.Rune() != key.r { + continue } - if e.Modifiers() == key.modifiers { + } + if e.Modifiers() == key.modifiers { + for _, c := range v.Buf.cursors { + ok := v.SetCursor(c) + if !ok { + break + } relocate = false isBinding = true - for _, action := range actions { - readonlyBindingsResult := false - funcName := ShortFuncName(action) - if v.Type.readonly == true { - // check for readonly and if true only let key bindings get called if they do not change the contents. - for _, readonlyBindings := range readonlyBindingsList { - if strings.Contains(funcName, readonlyBindings) { - readonlyBindingsResult = true - } - } - } - if !readonlyBindingsResult { - // call the key binding - relocate = action(v, true) || relocate - // Macro - if funcName != "ToggleMacro" && funcName != "PlayMacro" { - if recordingMacro { - curMacro = append(curMacro, action) - } - } - } - } - break + relocate = v.ExecuteActions(actions) || relocate } + v.SetCursor(&v.Buf.Cursor) + v.Buf.MergeCursors() + break } } } + if !isBinding && e.Key() == tcell.KeyRune { // Check viewtype if readonly don't insert a rune (readonly help and log view etc.) - if v.Type.readonly == false { - // Insert a character - if v.Cursor.HasSelection() { - v.Cursor.DeleteSelection() - v.Cursor.ResetSelection() - } - v.Buf.Insert(v.Cursor.Loc, string(e.Rune())) - v.Cursor.Right() + if v.Type.Readonly == false { + for _, c := range v.Buf.cursors { + v.SetCursor(c) + + // Insert a character + if v.Cursor.HasSelection() { + v.Cursor.DeleteSelection() + v.Cursor.ResetSelection() + } - for pl := range loadedPlugins { - _, err := Call(pl+".onRune", string(e.Rune()), v) - if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") { - TermMessage(err) + if v.isOverwriteMode { + next := v.Cursor.Loc + next.X++ + v.Buf.Replace(v.Cursor.Loc, next, string(e.Rune())) + } else { + v.Buf.Insert(v.Cursor.Loc, string(e.Rune())) } - } - if recordingMacro { - curMacro = append(curMacro, e.Rune()) + for pl := range loadedPlugins { + _, err := Call(pl+".onRune", string(e.Rune()), v) + if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") { + TermMessage(err) + } + } + + if recordingMacro { + curMacro = append(curMacro, e.Rune()) + } } + v.SetCursor(&v.Buf.Cursor) } } case *tcell.EventPaste: // Check viewtype if readonly don't paste (readonly help and log view etc.) - if v.Type.readonly == false { + if v.Type.Readonly == false { if !PreActionCall("Paste", v) { break } - v.paste(e.Text()) + for _, c := range v.Buf.cursors { + v.SetCursor(c) + v.paste(e.Text()) + } + v.SetCursor(&v.Buf.Cursor) PostActionCall("Paste", v) } case *tcell.EventMouse: - x, y := e.Position() - x -= v.lineNumOffset - v.leftCol + v.x - y += v.Topline - v.y // Don't relocate for mouse events relocate = false button := e.Buttons() - switch button { - case tcell.Button1: - // Left click - if v.mouseReleased { - v.MoveToMouseClick(x, y) - if time.Since(v.lastClickTime)/time.Millisecond < doubleClickThreshold { - if v.doubleClick { - // Triple click - v.lastClickTime = time.Now() - - v.tripleClick = true - v.doubleClick = false - - v.Cursor.SelectLine() - v.Cursor.CopySelection("primary") - } else { - // Double click - v.lastClickTime = time.Now() - - v.doubleClick = true - v.tripleClick = false - - v.Cursor.SelectWord() - v.Cursor.CopySelection("primary") + for key, actions := range bindings { + if button == key.buttons && e.Modifiers() == key.modifiers { + for _, c := range v.Buf.cursors { + ok := v.SetCursor(c) + if !ok { + break } - } else { - v.doubleClick = false - v.tripleClick = false - v.lastClickTime = time.Now() - - v.Cursor.OrigSelection[0] = v.Cursor.Loc - v.Cursor.CurSelection[0] = v.Cursor.Loc - v.Cursor.CurSelection[1] = v.Cursor.Loc - } - v.mouseReleased = false - } else if !v.mouseReleased { - v.MoveToMouseClick(x, y) - if v.tripleClick { - v.Cursor.AddLineToSelection() - } else if v.doubleClick { - v.Cursor.AddWordToSelection() - } else { - v.Cursor.SetSelectionEnd(v.Cursor.Loc) - v.Cursor.CopySelection("primary") + relocate = v.ExecuteActions(actions) || relocate } + v.SetCursor(&v.Buf.Cursor) + v.Buf.MergeCursors() } - case tcell.Button2: - // Check viewtype if readonly don't paste (readonly help and log view etc.) - if v.Type.readonly == false { - // Middle mouse button was clicked, - // We should paste primary - v.PastePrimary(true) + } + + for key, actions := range mouseBindings { + if button == key.buttons && e.Modifiers() == key.modifiers { + for _, action := range actions { + action(v, true, e) + } } + } + + switch button { case tcell.ButtonNone: // Mouse event with no click if !v.mouseReleased { // Mouse was just released + x, y := e.Position() + x -= v.lineNumOffset - v.leftCol + v.x + y += v.Topline - v.y + // Relocating here isn't really necessary because the cursor will // be in the right place from the last mouse event // However, if we are running in a terminal that doesn't support mouse motion @@ -615,14 +670,6 @@ func (v *View) HandleEvent(event tcell.Event) { } v.mouseReleased = true } - case tcell.WheelUp: - // Scroll up - scrollspeed := int(v.Buf.Settings["scrollspeed"].(float64)) - v.ScrollUp(scrollspeed) - case tcell.WheelDown: - // Scroll down - scrollspeed := int(v.Buf.Settings["scrollspeed"].(float64)) - v.ScrollDown(scrollspeed) } } @@ -637,6 +684,10 @@ func (v *View) HandleEvent(event tcell.Event) { } } +func (v *View) mainCursor() bool { + return v.Buf.curCursor == len(v.Buf.cursors)-1 +} + // GutterMessage creates a message in this view's gutter func (v *View) GutterMessage(section string, lineN int, msg string, kind int) { lineN-- @@ -685,13 +736,14 @@ func (v *View) openHelp(helpPage string) { } } +// DisplayView draws the view to the screen func (v *View) DisplayView() { if v.Buf.Settings["softwrap"].(bool) && v.leftCol != 0 { v.leftCol = 0 } - if v.Type == vtLog { - // Log views should always follow the cursor... + if v.Type == vtLog || v.Type == vtRaw { + // Log or raw views should always follow the cursor... v.Relocate() } @@ -863,22 +915,20 @@ func (v *View) DisplayView() { } charLoc := char.realLoc - if v.Cursor.HasSelection() && - (charLoc.GreaterEqual(v.Cursor.CurSelection[0]) && charLoc.LessThan(v.Cursor.CurSelection[1]) || - charLoc.LessThan(v.Cursor.CurSelection[0]) && charLoc.GreaterEqual(v.Cursor.CurSelection[1])) { - // The current character is selected - lineStyle = defStyle.Reverse(true) - - if style, ok := colorscheme["selection"]; ok { - lineStyle = style + for _, c := range v.Buf.cursors { + v.SetCursor(c) + if v.Cursor.HasSelection() && + (charLoc.GreaterEqual(v.Cursor.CurSelection[0]) && charLoc.LessThan(v.Cursor.CurSelection[1]) || + charLoc.LessThan(v.Cursor.CurSelection[0]) && charLoc.GreaterEqual(v.Cursor.CurSelection[1])) { + // The current character is selected + lineStyle = defStyle.Reverse(true) + + if style, ok := colorscheme["selection"]; ok { + lineStyle = style + } } } - - if tabs[curTab].CurView == v.Num && !v.Cursor.HasSelection() && - v.Cursor.Y == char.realLoc.Y && v.Cursor.X == char.realLoc.X && !cursorSet { - screen.ShowCursor(xOffset+char.visualLoc.X, yOffset+char.visualLoc.Y) - cursorSet = true - } + v.SetCursor(&v.Buf.Cursor) if v.Buf.Settings["cursorline"].(bool) && tabs[curTab].CurView == v.Num && !v.Cursor.HasSelection() && v.Cursor.Y == realLineN { @@ -889,6 +939,16 @@ func (v *View) DisplayView() { screen.SetContent(xOffset+char.visualLoc.X, yOffset+char.visualLoc.Y, char.drawChar, nil, lineStyle) + for i, c := range v.Buf.cursors { + v.SetCursor(c) + if tabs[curTab].CurView == v.Num && !v.Cursor.HasSelection() && + v.Cursor.Y == char.realLoc.Y && v.Cursor.X == char.realLoc.X && (!cursorSet || i != 0) { + ShowMultiCursor(xOffset+char.visualLoc.X, yOffset+char.visualLoc.Y, i) + cursorSet = true + } + } + v.SetCursor(&v.Buf.Cursor) + lastChar = char } } @@ -899,19 +959,27 @@ func (v *View) DisplayView() { var cx, cy int if lastChar != nil { lastX = xOffset + lastChar.visualLoc.X + lastChar.width - if tabs[curTab].CurView == v.Num && !v.Cursor.HasSelection() && - v.Cursor.Y == lastChar.realLoc.Y && v.Cursor.X == lastChar.realLoc.X+1 { - screen.ShowCursor(lastX, yOffset+lastChar.visualLoc.Y) - cx, cy = lastX, yOffset+lastChar.visualLoc.Y + for i, c := range v.Buf.cursors { + v.SetCursor(c) + if tabs[curTab].CurView == v.Num && !v.Cursor.HasSelection() && + v.Cursor.Y == lastChar.realLoc.Y && v.Cursor.X == lastChar.realLoc.X+1 { + ShowMultiCursor(lastX, yOffset+lastChar.visualLoc.Y, i) + cx, cy = lastX, yOffset+lastChar.visualLoc.Y + } } - realLoc = Loc{lastChar.realLoc.X, realLineN} + v.SetCursor(&v.Buf.Cursor) + realLoc = Loc{lastChar.realLoc.X + 1, realLineN} visualLoc = Loc{lastX - xOffset, lastChar.visualLoc.Y} } else if len(line) == 0 { - if tabs[curTab].CurView == v.Num && !v.Cursor.HasSelection() && - v.Cursor.Y == realLineN { - screen.ShowCursor(xOffset, yOffset+visualLineN) - cx, cy = xOffset, yOffset+visualLineN + for i, c := range v.Buf.cursors { + v.SetCursor(c) + if tabs[curTab].CurView == v.Num && !v.Cursor.HasSelection() && + v.Cursor.Y == realLineN { + ShowMultiCursor(xOffset, yOffset+visualLineN, i) + cx, cy = xOffset, yOffset+visualLineN + } } + v.SetCursor(&v.Buf.Cursor) lastX = xOffset realLoc = Loc{0, realLineN} visualLoc = Loc{0, visualLineN} @@ -931,7 +999,7 @@ func (v *View) DisplayView() { if v.Buf.Settings["cursorline"].(bool) && tabs[curTab].CurView == v.Num && !v.Cursor.HasSelection() && v.Cursor.Y == realLineN { - for i := lastX; i < xOffset+v.Width; i++ { + for i := lastX; i < xOffset+v.Width-v.lineNumOffset; i++ { style := GetColor("cursor-line") fg, _, _ := style.Decompose() style = style.Background(fg) @@ -953,6 +1021,18 @@ func (v *View) DisplayView() { } } +// ShowMultiCursor will display a cursor at a location +// If i == 0 then the terminal cursor will be used +// Otherwise a fake cursor will be drawn at the position +func ShowMultiCursor(x, y, i int) { + if i == 0 { + screen.ShowCursor(x, y) + } else { + r, _, _, _ := screen.GetContent(x, y) + screen.SetContent(x, y, r, nil, defStyle.Reverse(true)) + } +} + // Display renders the view, the cursor, and statusline func (v *View) Display() { if globalSettings["termtitle"].(bool) { @@ -964,6 +1044,11 @@ func (v *View) Display() { screen.HideCursor() } _, screenH := screen.Size() + + if v.Buf.Settings["scrollbar"].(bool) { + v.scrollbar.Display() + } + if v.Buf.Settings["statusline"].(bool) { v.sline.Display() } else if (v.y + v.Height) != screenH-1 {