X-Git-Url: https://git.lizzy.rs/?a=blobdiff_plain;f=cmd%2Fmicro%2Fview.go;h=c05d83344cea8d37dea84fd00ee24731720699c0;hb=55add69fa01025c37c8a6dbd2014ced24d612e09;hp=e977e2a5370e1884540a109c5d632f447b36e70c;hpb=f8612d1572946e1308ed6792e61b2ce02f3c6634;p=micro.git diff --git a/cmd/micro/view.go b/cmd/micro/view.go index e977e2a5..c05d8334 100644 --- a/cmd/micro/view.go +++ b/cmd/micro/view.go @@ -1,14 +1,24 @@ package main import ( + "io/ioutil" "strconv" "strings" "time" "github.com/mattn/go-runewidth" + "github.com/mitchellh/go-homedir" "github.com/zyedidia/tcell" ) +type ViewType int + +const ( + vtDefault ViewType = iota + vtHelp + vtLog +) + // The View struct stores information about a view into a buffer. // It stores information about the cursor, and the viewport // that the user sees the buffer from. @@ -21,14 +31,10 @@ type View struct { // The leftmost column, used for horizontal scrolling leftCol int - // Percentage of the terminal window that this view takes up (from 0 to 100) - widthPercent int - heightPercent int - // Specifies whether or not this view holds a help buffer - Help bool + Type ViewType - // Actual with and height + // Actual width and height width int height int @@ -84,7 +90,7 @@ type View struct { // NewView returns a new fullscreen view func NewView(buf *Buffer) *View { screenW, screenH := screen.Size() - return NewViewWidthHeight(buf, screenW, screenH-1) + return NewViewWidthHeight(buf, screenW, screenH) } // NewViewWidthHeight returns a new view with the specified width and height @@ -122,6 +128,7 @@ func NewViewWidthHeight(buf *Buffer, w, h int) *View { return v } +// ToggleStatusLine creates an extra row for the statusline if necessary func (v *View) ToggleStatusLine() { if v.Buf.Settings["statusline"].(bool) { v.height-- @@ -130,6 +137,7 @@ func (v *View) ToggleStatusLine() { } } +// ToggleTabbar creates an extra row for the tabbar if necessary func (v *View) ToggleTabbar() { if len(tabs) > 1 { if v.y == 0 { @@ -145,6 +153,20 @@ func (v *View) ToggleTabbar() { } } +func (v *View) paste(clip string) { + leadingWS := GetLeadingWhitespace(v.Buf.Line(v.Cursor.Y)) + + if v.Cursor.HasSelection() { + v.Cursor.DeleteSelection() + v.Cursor.ResetSelection() + } + 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.freshClip = false + messenger.Message("Pasted clipboard") +} + // ScrollUp scrolls the view up n lines (if possible) func (v *View) ScrollUp(n int) { // Try to scroll by n but if it would overflow, scroll by 1 @@ -168,16 +190,21 @@ func (v *View) ScrollDown(n int) { // CanClose returns whether or not the view can be closed // If there are unsaved changes, the user will be asked if the view can be closed // causing them to lose the unsaved changes -// The message is what to print after saying "You have unsaved changes. " -func (v *View) CanClose(msg string, responses ...rune) bool { - if v.Buf.IsModified { - char, canceled := messenger.LetterPrompt("You have unsaved changes. "+msg, responses...) +func (v *View) CanClose() bool { + if v.Type == vtDefault && v.Buf.IsModified { + var char rune + var canceled bool + if v.Buf.Settings["autosave"].(bool) { + char = 'y' + } else { + char, canceled = messenger.LetterPrompt("Save changes to "+v.Buf.Name+" before closing? (y,n,esc) ", 'y', 'n') + } if !canceled { if char == 'y' { - return true - } else if char == 's' { v.Save(true) return true + } else if char == 'n' { + return true } } } else { @@ -208,6 +235,23 @@ func (v *View) OpenBuffer(buf *Buffer) { 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) + file, err := ioutil.ReadFile(filename) + + var buf *Buffer + if err != nil { + messenger.Message(err.Error()) + // File does not exist -- create an empty buffer with that name + buf = NewBuffer([]byte{}, filename) + } else { + buf = NewBuffer(file, filename) + } + v.OpenBuffer(buf) +} + // CloseBuffer performs any closing functions on the buffer func (v *View) CloseBuffer() { if v.Buf != nil { @@ -217,7 +261,7 @@ func (v *View) CloseBuffer() { // ReOpen reloads the current buffer func (v *View) ReOpen() { - if v.CanClose("Continue? (y,n,s) ", 'y', 'n', 's') { + if v.CanClose() { screen.Clear() v.Buf.ReOpen() v.Relocate() @@ -239,9 +283,80 @@ func (v *View) VSplit(buf *Buffer) bool { return false } +// GetSoftWrapLocation gets the location of a visual click on the screen and converts it to col,line +func (v *View) GetSoftWrapLocation(vx, vy int) (int, int) { + if !v.Buf.Settings["softwrap"].(bool) { + vx = v.Cursor.GetCharPosInLine(vy, vx) + return vx, vy + } + + screenX, screenY := 0, v.Topline + for lineN := v.Topline; lineN < v.Bottomline(); lineN++ { + line := v.Buf.Line(lineN) + + colN := 0 + for _, ch := range line { + if screenX >= v.width-v.lineNumOffset { + screenX = 0 + screenY++ + } + + if screenX == vx && screenY == vy { + return colN, lineN + } + + if ch == '\t' { + screenX += int(v.Buf.Settings["tabsize"].(float64)) - 1 + } + + screenX++ + colN++ + } + if screenY == vy { + return colN, lineN + } + screenX = 0 + screenY++ + } + + return 0, 0 +} + +func (v *View) Bottomline() int { + screenX, screenY := 0, 0 + numLines := 0 + for lineN := v.Topline; lineN < v.Topline+v.height; lineN++ { + line := v.Buf.Line(lineN) + + colN := 0 + for _, ch := range line { + if screenX >= v.width-v.lineNumOffset { + screenX = 0 + screenY++ + } + + if ch == '\t' { + screenX += int(v.Buf.Settings["tabsize"].(float64)) - 1 + } + + screenX++ + colN++ + } + screenX = 0 + screenY++ + numLines++ + + if screenY >= v.height { + break + } + } + return numLines + v.Topline +} + // Relocate moves the view window so that the cursor is in view // This is useful if the user has scrolled far away, and then starts typing func (v *View) Relocate() bool { + height := v.Bottomline() - v.Topline ret := false cy := v.Cursor.Y scrollmargin := int(v.Buf.Settings["scrollmargin"].(float64)) @@ -252,22 +367,24 @@ func (v *View) Relocate() bool { v.Topline = cy ret = true } - if cy > v.Topline+v.height-1-scrollmargin && cy < v.Buf.NumLines-scrollmargin { - v.Topline = cy - v.height + 1 + scrollmargin + if cy > v.Topline+height-1-scrollmargin && cy < v.Buf.NumLines-scrollmargin { + v.Topline = cy - height + 1 + scrollmargin ret = true - } else if cy >= v.Buf.NumLines-scrollmargin && cy > v.height { - v.Topline = v.Buf.NumLines - v.height + } else if cy >= v.Buf.NumLines-scrollmargin && cy > height { + v.Topline = v.Buf.NumLines - height ret = true } - cx := v.Cursor.GetVisualX() - if cx < v.leftCol { - v.leftCol = cx - ret = true - } - if cx+v.lineNumOffset+1 > v.leftCol+v.width { - v.leftCol = cx - v.width + v.lineNumOffset + 1 - ret = true + if !v.Buf.Settings["softwrap"].(bool) { + cx := v.Cursor.GetVisualX() + if cx < v.leftCol { + v.leftCol = cx + ret = true + } + if cx+v.lineNumOffset+1 > v.leftCol+v.width { + v.leftCol = cx - v.width + v.lineNumOffset + 1 + ret = true + } } return ret } @@ -289,7 +406,8 @@ func (v *View) MoveToMouseClick(x, y int) { x = 0 } - x = v.Cursor.GetCharPosInLine(y, x) + x, y = v.GetSoftWrapLocation(x, y) + // x = v.Cursor.GetCharPosInLine(y, x) if x > Count(v.Buf.Line(y)) { x = Count(v.Buf.Line(y)) } @@ -311,22 +429,9 @@ func (v *View) HandleEvent(event tcell.Event) { // Window resized tabs[v.TabNum].Resize() case *tcell.EventKey: - if e.Key() == tcell.KeyRune && (e.Modifiers() == 0 || e.Modifiers() == tcell.ModShift) { - // 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() - - 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) - } - } - } else { + // Check first if input is a key binding, if it is we 'eat' the input and don't insert a rune + isBinding := false + if e.Key() != tcell.KeyRune || e.Modifiers() != 0 { for key, actions := range bindings { if e.Key() == key.keyCode { if e.Key() == tcell.KeyRune { @@ -336,30 +441,47 @@ func (v *View) HandleEvent(event tcell.Event) { } if e.Modifiers() == key.modifiers { relocate = false + isBinding = true for _, action := range actions { relocate = action(v, true) || relocate + funcName := FuncName(action) + if funcName != "main.(*View).ToggleMacro" && funcName != "main.(*View).PlayMacro" { + if recordingMacro { + curMacro = append(curMacro, action) + } + } } + break } } } } + if !isBinding && e.Key() == tcell.KeyRune { + // 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() + + 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()) + } + } case *tcell.EventPaste: if !PreActionCall("Paste", v) { break } - leadingWS := GetLeadingWhitespace(v.Buf.Line(v.Cursor.Y)) - - if v.Cursor.HasSelection() { - v.Cursor.DeleteSelection() - v.Cursor.ResetSelection() - } - clip := e.Text() - 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.freshClip = false - messenger.Message("Pasted clipboard") + v.paste(e.Text()) PostActionCall("Paste", v) case *tcell.EventMouse: @@ -411,9 +533,13 @@ func (v *View) HandleEvent(event tcell.Event) { } else if v.doubleClick { v.Cursor.AddWordToSelection() } else { - v.Cursor.CurSelection[1] = v.Cursor.Loc + v.Cursor.SetSelectionEnd(v.Cursor.Loc) } } + case tcell.Button2: + // Middle mouse button was clicked, + // We should paste primary + v.PastePrimary(true) case tcell.ButtonNone: // Mouse event with no click if !v.mouseReleased { @@ -427,7 +553,7 @@ func (v *View) HandleEvent(event tcell.Event) { if !v.doubleClick && !v.tripleClick { v.MoveToMouseClick(x, y) - v.Cursor.CurSelection[1] = v.Cursor.Loc + v.Cursor.SetSelectionEnd(v.Cursor.Loc) } v.mouseReleased = true } @@ -445,9 +571,6 @@ func (v *View) HandleEvent(event tcell.Event) { if relocate { v.Relocate() } - if v.Buf.Settings["syntax"].(bool) { - v.matches = Match(v) - } } // GutterMessage creates a message in this view's gutter @@ -483,15 +606,18 @@ func (v *View) ClearAllGutterMessages() { // Opens the given help page in a new horizontal split func (v *View) openHelp(helpPage string) { - if v.Help { - helpBuffer := NewBuffer([]byte(helpPages[helpPage]), helpPage+".md") - helpBuffer.Name = "Help" - v.OpenBuffer(helpBuffer) + if data, err := FindRuntimeFile(RTHelp, helpPage).Data(); err != nil { + TermMessage("Unable to load help text", helpPage, "\n", err) } else { - helpBuffer := NewBuffer([]byte(helpPages[helpPage]), helpPage+".md") + helpBuffer := NewBuffer(data, helpPage+".md") helpBuffer.Name = "Help" - v.HSplit(helpBuffer) - CurView().Help = true + + if v.Type == vtHelp { + v.OpenBuffer(helpBuffer) + } else { + v.HSplit(helpBuffer) + CurView().Type = vtHelp + } } } @@ -503,6 +629,15 @@ func (v *View) drawCell(x, y int, ch rune, combc []rune, style tcell.Style) { // DisplayView renders the view to the screen func (v *View) DisplayView() { + if v.Type == vtLog { + // Log views should always follow the cursor... + v.Relocate() + } + + if v.Buf.Settings["syntax"].(bool) { + v.matches = Match(v) + } + // The charNum we are currently displaying // starts at the start of the viewport charNum := Loc{0, v.Topline} @@ -535,17 +670,22 @@ func (v *View) DisplayView() { } // These represent the current screen coordinates - screenX, screenY := 0, 0 + screenX, screenY := v.x, v.y-1 highlightStyle := defStyle + curLineN := 0 // ViewLine is the current line from the top of the viewport for viewLine := 0; viewLine < v.height; viewLine++ { - screenY = v.y + viewLine + screenY++ screenX = v.x // This is the current line number of the buffer that we are drawing - curLineN := viewLine + v.Topline + curLineN = viewLine + v.Topline + + if screenY-v.y >= v.height { + break + } if v.x != 0 { // Draw the split divider @@ -610,9 +750,9 @@ func (v *View) DisplayView() { } } + lineNumStyle := defStyle if v.Buf.Settings["ruler"] == true { // Write the line number - lineNumStyle := defStyle if style, ok := colorscheme["line-number"]; ok { lineNumStyle = style } @@ -642,7 +782,23 @@ func (v *View) DisplayView() { // Now we actually draw the line colN := 0 + strWidth := 0 + tabSize := int(v.Buf.Settings["tabsize"].(float64)) for _, ch := range line { + if v.Buf.Settings["softwrap"].(bool) { + if screenX-v.x >= v.width { + screenY++ + for i := 0; i < v.lineNumOffset; i++ { + screen.SetContent(v.x+i, screenY, ' ', nil, lineNumStyle) + } + screenX = v.x + v.lineNumOffset + } + } + + if tabs[curTab].curView == v.Num && !v.Cursor.HasSelection() && v.Cursor.Y == curLineN && colN == v.Cursor.X { + v.DisplayCursor(screenX-v.leftCol, screenY) + } + lineStyle := defStyle if v.Buf.Settings["syntax"].(bool) { @@ -678,7 +834,7 @@ func (v *View) DisplayView() { // First the user may have configured an `indent-char` to be displayed to show that this // is a tab character lineIndentStyle := defStyle - if style, ok := colorscheme["indent-char"]; ok { + if style, ok := colorscheme["indent-char"]; ok && v.Buf.Settings["indentchar"].(string) != " " { lineIndentStyle = style } if v.Cursor.HasSelection() && @@ -703,8 +859,9 @@ func (v *View) DisplayView() { v.drawCell(screenX-v.leftCol, screenY, indentChar[0], nil, lineIndentStyle) } // Now the tab has to be displayed as a bunch of spaces - tabSize := int(v.Buf.Settings["tabsize"].(float64)) - for i := 0; i < tabSize-1; i++ { + visLoc := strWidth + remainder := tabSize - (visLoc % tabSize) + for i := 0; i < remainder-1; i++ { screenX++ if screenX-v.x-v.leftCol >= v.lineNumOffset { v.drawCell(screenX-v.leftCol, screenY, ' ', nil, lineStyle) @@ -728,9 +885,14 @@ func (v *View) DisplayView() { charNum = charNum.Move(1, v.Buf) screenX++ colN++ + strWidth += StringWidth(string(ch), tabSize) } // Here we are at a newline + if tabs[curTab].curView == v.Num && !v.Cursor.HasSelection() && v.Cursor.Y == curLineN && colN == v.Cursor.X { + v.DisplayCursor(screenX-v.leftCol, screenY) + } + // The newline may be selected, in which case we should draw the selection style // with a space to represent it if v.Cursor.HasSelection() && @@ -757,6 +919,13 @@ func (v *View) DisplayView() { } } if screenX-v.x-v.leftCol+i >= v.lineNumOffset { + colorcolumn := int(v.Buf.Settings["colorcolumn"].(float64)) + if colorcolumn != 0 && screenX-v.lineNumOffset+i == colorcolumn-1 { + if style, ok := colorscheme["color-column"]; ok { + fg, _, _ := style.Decompose() + lineStyle = lineStyle.Background(fg) + } + } v.drawCell(screenX-v.leftCol+i, screenY, ' ', nil, lineStyle) } } @@ -764,20 +933,17 @@ func (v *View) DisplayView() { } // DisplayCursor draws the current buffer's cursor to the screen -func (v *View) DisplayCursor() { - // Don't draw the cursor if it is out of the viewport or if it has a selection - if (v.Cursor.Y-v.Topline < 0 || v.Cursor.Y-v.Topline > v.height-1) || v.Cursor.HasSelection() { - screen.HideCursor() - } else { - screen.ShowCursor(v.x+v.Cursor.GetVisualX()+v.lineNumOffset-v.leftCol, v.Cursor.Y-v.Topline+v.y) - } +func (v *View) DisplayCursor(x, y int) { + // screen.ShowCursor(v.x+v.Cursor.GetVisualX()+v.lineNumOffset-v.leftCol, y) + screen.ShowCursor(x, y) } // Display renders the view, the cursor, and statusline func (v *View) Display() { v.DisplayView() - if v.Num == tabs[curTab].curView { - v.DisplayCursor() + // Don't draw the cursor if it is out of the viewport or if it has a selection + if (v.Cursor.Y-v.Topline < 0 || v.Cursor.Y-v.Topline > v.height-1) || v.Cursor.HasSelection() { + screen.HideCursor() } _, screenH := screen.Size() if v.Buf.Settings["statusline"].(bool) {