diff --git a/cmd/micro/bindings.go b/cmd/micro/bindings.go index 613b80fb..8ef5ada4 100644 --- a/cmd/micro/bindings.go +++ b/cmd/micro/bindings.go @@ -76,6 +76,8 @@ var bindingActions = map[string]func(*View) bool{ "AddTab": (*View).AddTab, "PreviousTab": (*View).PreviousTab, "NextTab": (*View).NextTab, + "NextSplit": (*View).NextSplit, + "PreviousSplit": (*View).PreviousSplit, } var bindingKeys = map[string]tcell.Key{ @@ -398,6 +400,7 @@ func DefaultBindings() map[string]string { "CtrlB": "ShellMode", "CtrlQ": "Quit", "CtrlE": "CommandMode", + "CtrlW": "NextSplit", // Emacs-style keybindings "Alt-f": "WordRight", @@ -915,11 +918,13 @@ func (v *View) OpenFile() bool { filename = strings.Replace(filename, "~", home, 1) file, err := ioutil.ReadFile(filename) + var buf *Buffer if err != nil { - messenger.Error(err.Error()) - return false + // File does not exist -- create an empty buffer with that name + buf = NewBuffer([]byte{}, filename) + } else { + buf = NewBuffer(file, filename) } - buf := NewBuffer(file, filename) v.OpenBuffer(buf) return true } @@ -1090,7 +1095,27 @@ func (v *View) Quit() bool { // Make sure not to quit if there are unsaved changes if v.CanClose("Quit anyway? (yes, no, save) ") { v.CloseBuffer() - if len(tabs) > 1 { + if len(tabs[curTab].views) > 1 { + var view *View + if v.splitChild != nil { + view = v.splitChild + view.splitParent = v.splitParent + } else if v.splitParent != nil { + view = v.splitParent + v.splitParent.splitChild = nil + } + view.x, view.y = view.splitOrigPos[0], view.splitOrigPos[1] + view.widthPercent, view.heightPercent = view.splitOrigDimensions[0], view.splitOrigDimensions[1] + view.Resize(screen.Size()) + if settings["syntax"].(bool) { + view.matches = Match(view) + } + tabs[curTab].views = tabs[curTab].views[:v.Num+copy(tabs[curTab].views[v.Num:], tabs[curTab].views[v.Num+1:])] + for i, v := range tabs[curTab].views { + v.Num = i + } + tabs[curTab].curView = view.Num + } else if len(tabs) > 1 { if len(tabs[v.TabNum].views) == 1 { tabs = tabs[:v.TabNum+copy(tabs[v.TabNum:], tabs[v.TabNum+1:])] for i, t := range tabs { @@ -1148,6 +1173,28 @@ func (v *View) NextTab() bool { return false } +// Changes the view to the next split +func (v *View) NextSplit() bool { + tab := tabs[curTab] + if tab.curView < len(tab.views)-1 { + tab.curView++ + } else { + tab.curView = 0 + } + return false +} + +// Changes the view to the previous split +func (v *View) PreviousSplit() bool { + tab := tabs[curTab] + if tab.curView > 0 { + tab.curView-- + } else { + tab.curView = len(tab.views) - 1 + } + return false +} + // None is no action func None() bool { return false diff --git a/cmd/micro/command.go b/cmd/micro/command.go index 6b1fc8ad..54c2aa3d 100644 --- a/cmd/micro/command.go +++ b/cmd/micro/command.go @@ -2,11 +2,14 @@ package main import ( "bytes" + "io/ioutil" "os" "os/exec" "os/signal" "regexp" "strings" + + "github.com/mitchellh/go-homedir" ) var commands map[string]func([]string) @@ -18,6 +21,8 @@ var commandActions = map[string]func([]string){ "Quit": Quit, "Save": Save, "Replace": Replace, + "VSplit": VSplit, + "HSplit": HSplit, } // InitCommands initializes the default commands @@ -56,6 +61,52 @@ func DefaultCommands() map[string]string { "quit": "Quit", "save": "Save", "replace": "Replace", + "vsplit": "VSplit", + "hsplit": "HSplit", + } +} + +// VSplit opens a vertical split with file given in the first argument +// If no file is given, it opens an empty buffer in a new split +func VSplit(args []string) { + if len(args) == 0 { + CurView().VSplit(NewBuffer([]byte{}, "")) + } else { + filename := args[0] + home, _ := homedir.Dir() + filename = strings.Replace(filename, "~", home, 1) + file, err := ioutil.ReadFile(filename) + + var buf *Buffer + if err != nil { + // File does not exist -- create an empty buffer with that name + buf = NewBuffer([]byte{}, filename) + } else { + buf = NewBuffer(file, filename) + } + CurView().VSplit(buf) + } +} + +// HSplit opens a horizontal split with file given in the first argument +// If no file is given, it opens an empty buffer in a new split +func HSplit(args []string) { + if len(args) == 0 { + CurView().HSplit(NewBuffer([]byte{}, "")) + } else { + filename := args[0] + home, _ := homedir.Dir() + filename = strings.Replace(filename, "~", home, 1) + file, err := ioutil.ReadFile(filename) + + var buf *Buffer + if err != nil { + // File does not exist -- create an empty buffer with that name + buf = NewBuffer([]byte{}, filename) + } else { + buf = NewBuffer(file, filename) + } + CurView().HSplit(buf) } } diff --git a/cmd/micro/highlighter.go b/cmd/micro/highlighter.go index 03c85e2f..32fe6563 100644 --- a/cmd/micro/highlighter.go +++ b/cmd/micro/highlighter.go @@ -152,6 +152,9 @@ func LoadSyntaxFilesFromDir(dir string) { if style, ok := colorscheme["default"]; ok { defStyle = style } + if screen != nil { + screen.SetStyle(defStyle) + } syntaxFiles = make(map[[2]*regexp.Regexp]FileTypeRules) files, _ := ioutil.ReadDir(dir) diff --git a/cmd/micro/micro.go b/cmd/micro/micro.go index 69ac0827..e62c93fa 100644 --- a/cmd/micro/micro.go +++ b/cmd/micro/micro.go @@ -174,11 +174,11 @@ func InitScreen() { // RedrawAll redraws everything -- all the views and the messenger func RedrawAll() { messenger.Clear() + DisplayTabs() + messenger.Display() for _, v := range tabs[curTab].views { v.Display() } - DisplayTabs() - messenger.Display() screen.Show() } @@ -281,11 +281,17 @@ func main() { case *tcell.EventMouse: if e.Buttons() == tcell.Button1 { _, h := screen.Size() - _, y := e.Position() + x, y := e.Position() if y == h-1 && messenger.message != "" { clipboard.WriteAll(messenger.message) continue } + + for _, v := range tabs[curTab].views { + if x >= v.x && x < v.x+v.width && y >= v.y && y < v.y+v.height { + tabs[curTab].curView = v.Num + } + } } } diff --git a/cmd/micro/statusline.go b/cmd/micro/statusline.go index af1ad973..0c72f517 100644 --- a/cmd/micro/statusline.go +++ b/cmd/micro/statusline.go @@ -48,13 +48,18 @@ func (sline *Statusline) Display() { // Maybe there is a unicode filename? fileRunes := []rune(file) + viewX := sline.view.x + if viewX != 0 { + screen.SetContent(viewX, y, ' ', nil, statusLineStyle) + viewX++ + } for x := 0; x < sline.view.width; x++ { if x < len(fileRunes) { - screen.SetContent(x, y, fileRunes[x], nil, statusLineStyle) + screen.SetContent(viewX+x, y, fileRunes[x], nil, statusLineStyle) } else if x >= sline.view.width-len(rightText) && x < len(rightText)+sline.view.width-len(rightText) { - screen.SetContent(x, y, []rune(rightText)[x-sline.view.width+len(rightText)], nil, statusLineStyle) + screen.SetContent(viewX+x, y, []rune(rightText)[x-sline.view.width+len(rightText)], nil, statusLineStyle) } else { - screen.SetContent(x, y, ' ', nil, statusLineStyle) + screen.SetContent(viewX+x, y, ' ', nil, statusLineStyle) } } } diff --git a/cmd/micro/view.go b/cmd/micro/view.go index a3b4b176..79047f4a 100644 --- a/cmd/micro/view.go +++ b/cmd/micro/view.go @@ -87,6 +87,11 @@ type View struct { matches SyntaxMatches // The matches from the last frame lastMatches SyntaxMatches + + splitParent *View + splitChild *View + splitOrigDimensions [2]int + splitOrigPos [2]int } // NewView returns a new fullscreen view @@ -124,15 +129,26 @@ func (v *View) Resize(w, h int) { // Always include 1 line for the command line at the bottom h-- if len(tabs) > 1 { - // Include one line for the tab bar at the top - h-- - v.y = 1 + if v.y == 0 { + // Include one line for the tab bar at the top + h-- + v.y = 1 + } } else { - v.y = 0 + if v.y == 1 { + v.y = 0 + } } v.width = int(float32(w) * float32(v.widthPercent) / 100) // We subtract 1 for the statusline v.height = int(float32(h) * float32(v.heightPercent) / 100) + if w%2 == 0 && v.x > 1 && v.widthPercent < 100 { + v.width++ + } + + if h%2 == 1 && v.y > 1 && v.heightPercent < 100 { + v.height++ + } if settings["statusline"].(bool) { // Make room for the status line if it is enabled v.height-- @@ -218,6 +234,62 @@ func (v *View) ReOpen() { } } +// HSplit opens a horizontal split with the given buffer +func (v *View) HSplit(buf *Buffer) bool { + origDimensions := [2]int{v.widthPercent, v.heightPercent} + origPos := [2]int{v.x, v.y} + + v.heightPercent /= 2 + v.Resize(screen.Size()) + + newView := NewViewWidthHeight(buf, v.widthPercent, v.heightPercent) + + v.splitOrigDimensions = origDimensions + v.splitOrigPos = origPos + newView.splitOrigDimensions = origDimensions + newView.splitOrigPos = origPos + + newView.TabNum = v.TabNum + newView.y = v.y + v.height + 1 + newView.x = v.x + tab := tabs[v.TabNum] + tab.curView++ + newView.Num = len(tab.views) + newView.splitParent = v + v.splitChild = newView + tab.views = append(tab.views, newView) + newView.Resize(screen.Size()) + return false +} + +// VSplit opens a vertical split with the given buffer +func (v *View) VSplit(buf *Buffer) bool { + origDimensions := [2]int{v.widthPercent, v.heightPercent} + origPos := [2]int{v.x, v.y} + + v.widthPercent /= 2 + v.Resize(screen.Size()) + + newView := NewViewWidthHeight(buf, v.widthPercent, v.heightPercent) + + v.splitOrigDimensions = origDimensions + v.splitOrigPos = origPos + newView.splitOrigDimensions = origDimensions + newView.splitOrigPos = origPos + + newView.TabNum = v.TabNum + newView.y = v.y + newView.x = v.x + v.width + tab := tabs[v.TabNum] + tab.curView++ + newView.Num = len(tab.views) + newView.splitParent = v + v.splitChild = newView + tab.views = append(tab.views, newView) + newView.Resize(screen.Size()) + return false +} + // 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 { @@ -340,7 +412,7 @@ func (v *View) HandleEvent(event tcell.Event) { v.freshClip = false case *tcell.EventMouse: x, y := e.Position() - x -= v.lineNumOffset - v.leftCol + x -= v.lineNumOffset - v.leftCol + v.x y += v.Topline - v.y // Don't relocate for mouse events relocate = false @@ -457,8 +529,15 @@ func (v *View) ClearAllGutterMessages() { } } +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) + } +} + // DisplayView renders the view to the screen func (v *View) DisplayView() { + // The character number of the character in the top left of the screen charNum := Loc{0, v.Topline} @@ -483,13 +562,23 @@ func (v *View) DisplayView() { v.lineNumOffset += 2 } + if v.x != 0 { + // One space for the extra split divider + v.lineNumOffset++ + } + for lineN := 0; lineN < v.height; lineN++ { x := v.x + if v.x != 0 { + // Draw the split divider + v.drawCell(x, lineN+v.y, ' ', nil, defStyle.Reverse(true)) + x++ + } // If the buffer is smaller than the view height if lineN+v.Topline >= v.Buf.NumLines { // We have to clear all this space - for i := 0; i < v.width; i++ { - screen.SetContent(i, lineN+v.y, ' ', nil, defStyle) + for i := x; i < v.x+v.width; i++ { + v.drawCell(i, lineN+v.y, ' ', nil, defStyle) } continue @@ -517,9 +606,9 @@ func (v *View) DisplayView() { gutterStyle = style } } - screen.SetContent(x, lineN+v.y, '>', nil, gutterStyle) + v.drawCell(x, lineN+v.y, '>', nil, gutterStyle) x++ - screen.SetContent(x, lineN+v.y, '>', nil, gutterStyle) + v.drawCell(x, lineN+v.y, '>', nil, gutterStyle) x++ if v.Cursor.Y == lineN+v.Topline { messenger.Message(msg.msg) @@ -529,9 +618,9 @@ func (v *View) DisplayView() { } } if !msgOnLine { - screen.SetContent(x, lineN+v.y, ' ', nil, defStyle) + v.drawCell(x, lineN+v.y, ' ', nil, defStyle) x++ - screen.SetContent(x, lineN+v.y, ' ', nil, defStyle) + v.drawCell(x, lineN+v.y, ' ', nil, defStyle) x++ if v.Cursor.Y == lineN+v.Topline && messenger.gutterMessage { messenger.Reset() @@ -550,18 +639,18 @@ func (v *View) DisplayView() { if settings["ruler"] == true { lineNum = strconv.Itoa(lineN + v.Topline + 1) for i := 0; i < maxLineLength-len(lineNum); i++ { - screen.SetContent(x, lineN+v.y, ' ', nil, lineNumStyle) + v.drawCell(x, lineN+v.y, ' ', nil, lineNumStyle) x++ } // Write the actual line number for _, ch := range lineNum { - screen.SetContent(x, lineN+v.y, ch, nil, lineNumStyle) + v.drawCell(x, lineN+v.y, ch, nil, lineNumStyle) x++ } if settings["ruler"] == true { // Write the extra space - screen.SetContent(x, lineN+v.y, ' ', nil, lineNumStyle) + v.drawCell(x, lineN+v.y, ' ', nil, lineNumStyle) x++ } } @@ -587,7 +676,7 @@ func (v *View) DisplayView() { lineStyle = highlightStyle } - if settings["cursorline"].(bool) && !v.Cursor.HasSelection() && v.Cursor.Y == lineN+v.Topline { + if settings["cursorline"].(bool) && tabs[curTab].curView == v.Num && !v.Cursor.HasSelection() && v.Cursor.Y == lineN+v.Topline { if style, ok := colorscheme["cursor-line"]; ok { fg, _, _ := style.Decompose() lineStyle = lineStyle.Background(fg) @@ -609,7 +698,7 @@ func (v *View) DisplayView() { lineIndentStyle = style } } - if settings["cursorline"].(bool) && !v.Cursor.HasSelection() && v.Cursor.Y == lineN+v.Topline { + if settings["cursorline"].(bool) && tabs[curTab].curView == v.Num && !v.Cursor.HasSelection() && v.Cursor.Y == lineN+v.Topline { if style, ok := colorscheme["cursor-line"]; ok { fg, _, _ := style.Decompose() lineIndentStyle = lineIndentStyle.Background(fg) @@ -617,28 +706,28 @@ func (v *View) DisplayView() { } indentChar := []rune(settings["indentchar"].(string)) if x-v.leftCol >= v.lineNumOffset { - screen.SetContent(x-v.leftCol, lineN+v.y, indentChar[0], nil, lineIndentStyle) + v.drawCell(x-v.leftCol, lineN+v.y, indentChar[0], nil, lineIndentStyle) } tabSize := int(settings["tabsize"].(float64)) for i := 0; i < tabSize-1; i++ { x++ if x-v.leftCol >= v.lineNumOffset { - screen.SetContent(x-v.leftCol, lineN+v.y, ' ', nil, lineStyle) + v.drawCell(x-v.leftCol, lineN+v.y, ' ', nil, lineStyle) } } } else if runewidth.RuneWidth(ch) > 1 { if x-v.leftCol >= v.lineNumOffset { - screen.SetContent(x-v.leftCol, lineN, ch, nil, lineStyle) + v.drawCell(x-v.leftCol, lineN+v.y, ch, nil, lineStyle) } for i := 0; i < runewidth.RuneWidth(ch)-1; i++ { x++ if x-v.leftCol >= v.lineNumOffset { - screen.SetContent(x-v.leftCol, lineN, ' ', nil, lineStyle) + v.drawCell(x-v.leftCol, lineN+v.y, ' ', nil, lineStyle) } } } else { if x-v.leftCol >= v.lineNumOffset { - screen.SetContent(x-v.leftCol, lineN+v.y, ch, nil, lineStyle) + v.drawCell(x-v.leftCol, lineN+v.y, ch, nil, lineStyle) } } charNum = charNum.Move(1, v.Buf) @@ -657,22 +746,22 @@ func (v *View) DisplayView() { if style, ok := colorscheme["selection"]; ok { selectStyle = style } - screen.SetContent(x-v.leftCol, lineN+v.y, ' ', nil, selectStyle) + v.drawCell(x-v.leftCol, lineN+v.y, ' ', nil, selectStyle) x++ } charNum = charNum.Move(1, v.Buf) - for i := 0; i < v.width-(x-v.leftCol); i++ { + for i := 0; i < v.width-((x-v.x)-v.leftCol); i++ { lineStyle := defStyle - if settings["cursorline"].(bool) && !v.Cursor.HasSelection() && v.Cursor.Y == lineN+v.Topline { + if settings["cursorline"].(bool) && tabs[curTab].curView == v.Num && !v.Cursor.HasSelection() && v.Cursor.Y == lineN+v.Topline { if style, ok := colorscheme["cursor-line"]; ok { fg, _, _ := style.Decompose() lineStyle = lineStyle.Background(fg) } } if !(x-v.leftCol < v.lineNumOffset) { - screen.SetContent(x+i, lineN+v.y, ' ', nil, lineStyle) + v.drawCell(x+i, lineN+v.y, ' ', nil, lineStyle) } } } @@ -691,7 +780,9 @@ func (v *View) DisplayCursor() { // Display renders the view, the cursor, and statusline func (v *View) Display() { v.DisplayView() - v.DisplayCursor() + if v.Num == tabs[curTab].curView { + v.DisplayCursor() + } if settings["statusline"].(bool) { v.sline.Display() } diff --git a/runtime/help/help.md b/runtime/help/help.md index 031382b8..446e1713 100644 --- a/runtime/help/help.md +++ b/runtime/help/help.md @@ -78,6 +78,7 @@ you can rebind them to your liking. "CtrlB": "ShellMode", "CtrlQ": "Quit", "CtrlE": "CommandMode", + "CtrlW": "NextSplit", // Emacs-style keybindings "Alt-f": "WordRight", @@ -114,6 +115,7 @@ You can execute an editor command by pressing `Ctrl-e` followed by the command. Here are the possible commands that you can use. * `quit`: Quits micro. + * `save`: Saves the current buffer. * `replace "search" "value" flags`: This will replace `search` with `value`. @@ -133,6 +135,12 @@ Here are the possible commands that you can use. * `bind key action`: creates a keybinding from key to action. See the sections on keybindings above for more info about what keys and actions are available. +* `vsplit filename`: opens a vertical split with `filename`. If no filename is + provided, a vertical split is opened with an empty buffer + +* `hsplit filename`: same as `vsplit` but opens a horizontal split instead of + a vertical split + ### Options Micro stores all of the user configuration in its configuration directory.