X-Git-Url: https://git.lizzy.rs/?a=blobdiff_plain;f=cmd%2Fmicro%2Fview.go;h=c05d83344cea8d37dea84fd00ee24731720699c0;hb=55add69fa01025c37c8a6dbd2014ced24d612e09;hp=af40fb95a4ad6d4063188297d39aa31132a23b7e;hpb=61536326ca3ea8337a57856619d059e58491b9e3;p=micro.git diff --git a/cmd/micro/view.go b/cmd/micro/view.go index af40fb95..c05d8334 100644 --- a/cmd/micro/view.go +++ b/cmd/micro/view.go @@ -1,17 +1,24 @@ package main import ( - "reflect" - "runtime" + "io/ioutil" "strconv" "strings" "time" "github.com/mattn/go-runewidth" - "github.com/yuin/gopher-lua" + "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. @@ -24,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 @@ -80,8 +83,6 @@ type View struct { // Syntax highlighting matches matches SyntaxMatches - // The matches from the last frame - lastMatches SyntaxMatches splitNode *LeafNode } @@ -89,11 +90,11 @@ 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 percentages -// Note that w and h are percentages not actual values +// NewViewWidthHeight returns a new view with the specified width and height +// Note that w and h are raw column and row values func NewViewWidthHeight(buf *Buffer, w, h int) *View { v := new(View) @@ -112,21 +113,31 @@ func NewViewWidthHeight(buf *Buffer, w, h int) *View { view: v, } - if settings["statusline"].(bool) { + if v.Buf.Settings["statusline"].(bool) { v.height-- } + for _, pl := range loadedPlugins { + _, err := Call(pl+".onViewOpen", v) + if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") { + TermMessage(err) + continue + } + } + return v } +// ToggleStatusLine creates an extra row for the statusline if necessary func (v *View) ToggleStatusLine() { - if settings["statusline"].(bool) { + if v.Buf.Settings["statusline"].(bool) { v.height-- } else { v.height++ } } +// ToggleTabbar creates an extra row for the tabbar if necessary func (v *View) ToggleTabbar() { if len(tabs) > 1 { if v.y == 0 { @@ -142,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 @@ -165,15 +190,20 @@ 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) bool { - if v.Buf.IsModified { - quit, canceled := messenger.Prompt("You have unsaved changes. "+msg, "Unsaved", NoCompletion) +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 strings.ToLower(quit) == "yes" || strings.ToLower(quit) == "y" { + if char == 'y' { + v.Save(true) return true - } else if strings.ToLower(quit) == "save" || strings.ToLower(quit) == "s" { - v.Save() + } else if char == 'n' { return true } } @@ -194,6 +224,7 @@ func (v *View) OpenBuffer(buf *Buffer) { v.leftCol = 0 v.Cursor.ResetSelection() v.Relocate() + v.Center(false) v.messages = make(map[string][]GutterMessage) v.matches = Match(v) @@ -204,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 { @@ -213,7 +261,7 @@ func (v *View) CloseBuffer() { // ReOpen reloads the current buffer func (v *View) ReOpen() { - if v.CanClose("Continue? (yes, no, save) ") { + if v.CanClose() { screen.Clear() v.Buf.ReOpen() v.Relocate() @@ -235,12 +283,83 @@ 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(settings["scrollmargin"].(float64)) + scrollmargin := int(v.Buf.Settings["scrollmargin"].(float64)) if cy < v.Topline+scrollmargin && cy > scrollmargin-1 { v.Topline = cy - scrollmargin ret = true @@ -248,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 } @@ -285,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)) } @@ -307,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{string(e.Rune())}) - 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 { @@ -332,43 +441,49 @@ func (v *View) HandleEvent(event tcell.Event) { } if e.Modifiers() == key.modifiers { relocate = false + isBinding = true for _, action := range actions { - executeAction := true - funcName := strings.Split(runtime.FuncForPC(reflect.ValueOf(action).Pointer()).Name(), ".") - for _, pl := range loadedPlugins { - ret, err := Call(pl+".pre"+funcName[len(funcName)-1], nil) - if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") { - TermMessage(err) - continue - } - if ret == lua.LFalse { - executeAction = false - } - } - if executeAction { - relocate = action(v) || relocate - for _, pl := range loadedPlugins { - _, err := Call(pl+".on"+funcName[len(funcName)-1], nil) - if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") { - TermMessage(err) - continue - } + 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 v.Cursor.HasSelection() { - v.Cursor.DeleteSelection() - v.Cursor.ResetSelection() + if !PreActionCall("Paste", v) { + break } - clip := e.Text() - v.Buf.Insert(v.Cursor.Loc, clip) - v.Cursor.Loc = v.Cursor.Loc.Move(Count(clip), v.Buf) - v.freshClip = false + + v.paste(e.Text()) + + PostActionCall("Paste", v) case *tcell.EventMouse: x, y := e.Position() x -= v.lineNumOffset - v.leftCol + v.x @@ -418,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 { @@ -434,17 +553,17 @@ 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 } case tcell.WheelUp: // Scroll up - scrollspeed := int(settings["scrollspeed"].(float64)) + scrollspeed := int(v.Buf.Settings["scrollspeed"].(float64)) v.ScrollUp(scrollspeed) case tcell.WheelDown: // Scroll down - scrollspeed := int(settings["scrollspeed"].(float64)) + scrollspeed := int(v.Buf.Settings["scrollspeed"].(float64)) v.ScrollDown(scrollspeed) } } @@ -452,9 +571,6 @@ func (v *View) HandleEvent(event tcell.Event) { if relocate { v.Relocate() } - if settings["syntax"].(bool) { - v.matches = Match(v) - } } // GutterMessage creates a message in this view's gutter @@ -488,6 +604,23 @@ func (v *View) ClearAllGutterMessages() { } } +// Opens the given help page in a new horizontal split +func (v *View) openHelp(helpPage string) { + if data, err := FindRuntimeFile(RTHelp, helpPage).Data(); err != nil { + TermMessage("Unable to load help text", helpPage, "\n", err) + } else { + helpBuffer := NewBuffer(data, helpPage+".md") + helpBuffer.Name = "Help" + + if v.Type == vtHelp { + v.OpenBuffer(helpBuffer) + } else { + v.HSplit(helpBuffer) + CurView().Type = vtHelp + } + } +} + func (v *View) drawCell(x, y int, ch rune, combc []rune, style tcell.Style) { if x >= v.x && x < v.x+v.width && y >= v.y && y < v.y+v.height { screen.SetContent(x, y, ch, combc, style) @@ -496,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} @@ -504,7 +646,7 @@ func (v *View) DisplayView() { // We are going to have to offset by that amount maxLineLength := len(strconv.Itoa(v.Buf.NumLines)) - if settings["ruler"] == true { + if v.Buf.Settings["ruler"] == true { // + 1 for the little space after the line number v.lineNumOffset = maxLineLength + 1 } else { @@ -528,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 @@ -583,7 +730,7 @@ func (v *View) DisplayView() { screenX++ v.drawCell(screenX, screenY, '>', nil, gutterStyle) screenX++ - if v.Cursor.Y == curLineN { + if v.Cursor.Y == curLineN && !messenger.hasPrompt { messenger.Message(msg.msg) messenger.gutterMessage = true } @@ -603,12 +750,17 @@ func (v *View) DisplayView() { } } - if settings["ruler"] == true { + lineNumStyle := defStyle + if v.Buf.Settings["ruler"] == true { // Write the line number - lineNumStyle := defStyle if style, ok := colorscheme["line-number"]; ok { lineNumStyle = style } + if style, ok := colorscheme["current-line-number"]; ok { + if curLineN == v.Cursor.Y && tabs[curTab].curView == v.Num && !v.Cursor.HasSelection() { + lineNumStyle = style + } + } lineNum := strconv.Itoa(curLineN + 1) @@ -629,10 +781,27 @@ func (v *View) DisplayView() { } // Now we actually draw the line - for colN, ch := range 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 settings["syntax"].(bool) { + if v.Buf.Settings["syntax"].(bool) { // Syntax highlighting is enabled highlightStyle = v.matches[viewLine][colN] } @@ -652,7 +821,7 @@ func (v *View) DisplayView() { // We need to display the background of the linestyle with the correct color if cursorline is enabled // and this is the current view and there is no selection on this line and the cursor is on this line - if settings["cursorline"].(bool) && tabs[curTab].curView == v.Num && !v.Cursor.HasSelection() && v.Cursor.Y == curLineN { + if v.Buf.Settings["cursorline"].(bool) && tabs[curTab].curView == v.Num && !v.Cursor.HasSelection() && v.Cursor.Y == curLineN { if style, ok := colorscheme["cursor-line"]; ok { fg, _, _ := style.Decompose() lineStyle = lineStyle.Background(fg) @@ -665,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() && @@ -678,20 +847,21 @@ func (v *View) DisplayView() { lineIndentStyle = style } } - if settings["cursorline"].(bool) && tabs[curTab].curView == v.Num && !v.Cursor.HasSelection() && v.Cursor.Y == curLineN { + if v.Buf.Settings["cursorline"].(bool) && tabs[curTab].curView == v.Num && !v.Cursor.HasSelection() && v.Cursor.Y == curLineN { if style, ok := colorscheme["cursor-line"]; ok { fg, _, _ := style.Decompose() lineIndentStyle = lineIndentStyle.Background(fg) } } // Here we get the indent char - indentChar := []rune(settings["indentchar"].(string)) + indentChar := []rune(v.Buf.Settings["indentchar"].(string)) if screenX-v.x-v.leftCol >= v.lineNumOffset { v.drawCell(screenX-v.leftCol, screenY, indentChar[0], nil, lineIndentStyle) } // Now the tab has to be displayed as a bunch of spaces - tabSize := int(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) @@ -714,9 +884,15 @@ 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() && @@ -736,13 +912,20 @@ func (v *View) DisplayView() { for i := 0; i < v.width; i++ { lineStyle := defStyle - if settings["cursorline"].(bool) && tabs[curTab].curView == v.Num && !v.Cursor.HasSelection() && v.Cursor.Y == curLineN { + if v.Buf.Settings["cursorline"].(bool) && tabs[curTab].curView == v.Num && !v.Cursor.HasSelection() && v.Cursor.Y == curLineN { if style, ok := colorscheme["cursor-line"]; ok { fg, _, _ := style.Decompose() lineStyle = lineStyle.Background(fg) } } 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) } } @@ -750,23 +933,20 @@ 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 settings["statusline"].(bool) { + if v.Buf.Settings["statusline"].(bool) { v.sline.Display() } else if (v.y + v.height) != screenH-1 { for x := 0; x < v.width; x++ {