Add support for tabs

Merge branch 'tabbar'

This branch adds support for having multiple tabs open, each viewing one
file. Use CtrlT to open a new tab empty tab and then CtrlO to open a
file in that tab. Use can also just open multiple files from the command
line: `micro file1.txt file2.txt ...`. Use Ctrl-] and Ctrl-\ to move
between the tabs, or simply click them with the mouse.
This commit is contained in:
Zachary Yedidia 2016-06-08 18:49:08 -04:00
commit 82c7994e68
11 changed files with 295 additions and 88 deletions

View file

@ -73,6 +73,9 @@ var bindingActions = map[string]func(*View) bool{
"ShellMode": (*View).ShellMode,
"CommandMode": (*View).CommandMode,
"Quit": (*View).Quit,
"AddTab": (*View).AddTab,
"PreviousTab": (*View).PreviousTab,
"NextTab": (*View).NextTab,
}
var bindingKeys = map[string]tcell.Key{
@ -380,6 +383,9 @@ func DefaultBindings() map[string]string {
"CtrlD": "DuplicateLine",
"CtrlV": "Paste",
"CtrlA": "SelectAll",
"CtrlT": "AddTab",
"CtrlRightSq": "PreviousTab",
"CtrlBackslash": "NextTab",
"Home": "Start",
"End": "End",
"PageUp": "CursorPageUp",
@ -902,7 +908,7 @@ func (v *View) OpenFile() bool {
if v.CanClose("Continue? (yes, no, save) ") {
filename, canceled := messenger.Prompt("File to open: ", "Open")
if canceled {
return true
return false
}
home, _ := homedir.Dir()
filename = strings.Replace(filename, "~", home, 1)
@ -910,12 +916,13 @@ func (v *View) OpenFile() bool {
if err != nil {
messenger.Error(err.Error())
return true
return false
}
buf := NewBuffer(file, filename)
v.OpenBuffer(buf)
return true
}
return true
return false
}
// Start moves the viewport to the start of the buffer
@ -1080,10 +1087,55 @@ func (v *View) Quit() bool {
return v.ToggleHelp()
}
// Make sure not to quit if there are unsaved changes
if views[mainView].CanClose("Quit anyway? (yes, no, save) ") {
views[mainView].CloseBuffer()
screen.Fini()
os.Exit(0)
if v.CanClose("Quit anyway? (yes, no, save) ") {
v.CloseBuffer()
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 {
t.SetNum(i)
}
if curTab >= len(tabs) {
curTab--
}
if curTab == 0 {
CurView().Resize(screen.Size())
CurView().matches = Match(CurView())
}
}
} else {
screen.Fini()
os.Exit(0)
}
}
return false
}
func (v *View) AddTab() bool {
tab := NewTabFromView(NewView(NewBuffer([]byte{}, "")))
tab.SetNum(len(tabs))
tabs = append(tabs, tab)
curTab++
if len(tabs) == 2 {
for _, t := range tabs {
for _, v := range t.views {
v.Resize(screen.Size())
}
}
}
return true
}
func (v *View) PreviousTab() bool {
if curTab > 0 {
curTab--
}
return false
}
func (v *View) NextTab() bool {
if curTab < len(tabs)-1 {
curTab++
}
return false
}

View file

@ -51,9 +51,14 @@ type SerializedBuffer struct {
func NewBuffer(txt []byte, path string) *Buffer {
b := new(Buffer)
b.LineArray = NewLineArray(txt)
b.Path = path
b.Name = path
if path == "" {
b.Name = "No name"
}
b.ModTime, _ = GetModTime(b.Path)
b.EventHandler = NewEventHandler(b)

View file

@ -62,8 +62,10 @@ func DefaultCommands() map[string]string {
// Set sets an option
func Set(args []string) {
// Set an option and we have to set it for every view
for _, view := range views {
SetOption(view, args)
for _, tab := range tabs {
for _, view := range tab.views {
SetOption(view, args)
}
}
}
@ -85,13 +87,13 @@ func Run(args []string) {
// Quit closes the main view
func Quit(args []string) {
// Close the main view
views[mainView].Quit()
CurView().Quit()
}
// Save saves the buffer in the main view
func Save(args []string) {
// Save the main view
views[mainView].Save()
CurView().Save()
}
// Replace runs search and replace
@ -138,7 +140,7 @@ func Replace(args []string) {
return
}
view := views[mainView]
view := CurView()
found := false
for {

View file

@ -48,14 +48,14 @@ var (
L *lua.LState
// The list of views
views []*View
// This is the currently open view
// It's just an index to the view in the views array
mainView int
tabs []*Tab
// This is the currently open tab
// It's just an index to the tab in the tabs array
curTab int
)
// LoadInput loads the file input for the editor
func LoadInput() (string, []byte, error) {
func LoadInput() []*Buffer {
// There are a number of ways micro should start given its input
// 1. If it is given a file in os.Args, it should open that
@ -72,23 +72,34 @@ func LoadInput() (string, []byte, error) {
var filename string
var input []byte
var err error
var buffers []*Buffer
if len(os.Args) > 1 {
// Option 1
filename = os.Args[1]
// Check that the file exists
if _, e := os.Stat(filename); e == nil {
input, err = ioutil.ReadFile(filename)
for i := 1; i < len(os.Args); i++ {
filename = os.Args[i]
// Check that the file exists
if _, e := os.Stat(filename); e == nil {
input, err = ioutil.ReadFile(filename)
if err != nil {
TermMessage(err)
continue
}
}
buffers = append(buffers, NewBuffer(input, filename))
}
} else if !isatty.IsTerminal(os.Stdin.Fd()) {
// Option 2
// The input is not a terminal, so something is being piped in
// and we should read from stdin
input, err = ioutil.ReadAll(os.Stdin)
buffers = append(buffers, NewBuffer(input, filename))
} else {
// Option 3, just open an empty buffer
buffers = append(buffers, NewBuffer(input, filename))
}
// Option 3, or just return whatever we got
return filename, input, err
return buffers
}
// InitConfigDir finds the configuration directory for micro according to the XDG spec.
@ -170,9 +181,10 @@ func InitScreen() {
// RedrawAll redraws everything -- all the views and the messenger
func RedrawAll() {
messenger.Clear()
for _, v := range views {
for _, v := range tabs[curTab].views {
v.Display()
}
DisplayTabs()
messenger.Display()
screen.Show()
}
@ -186,12 +198,6 @@ func main() {
os.Exit(0)
}
filename, input, err := LoadInput()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
L = lua.NewState()
defer L.Close()
@ -210,8 +216,6 @@ func main() {
// Load the help files
LoadHelp()
buf := NewBuffer(input, filename)
InitScreen()
// This is just so if we have an error, we can exit cleanly and not completely
@ -229,17 +233,21 @@ func main() {
messenger = new(Messenger)
messenger.history = make(map[string][]string)
views = make([]*View, 1)
views[0] = NewView(buf)
buffers := LoadInput()
for _, buf := range buffers {
tabs = append(tabs, NewTabFromView(NewView(buf)))
}
L.SetGlobal("OS", luar.New(L, runtime.GOOS))
L.SetGlobal("views", luar.New(L, views))
L.SetGlobal("mainView", luar.New(L, mainView))
L.SetGlobal("tabs", luar.New(L, tabs))
L.SetGlobal("curTab", luar.New(L, curTab))
L.SetGlobal("messenger", luar.New(L, messenger))
L.SetGlobal("GetOption", luar.New(L, GetOption))
L.SetGlobal("AddOption", luar.New(L, AddOption))
L.SetGlobal("BindKey", luar.New(L, BindKey))
L.SetGlobal("MakeCommand", luar.New(L, MakeCommand))
L.SetGlobal("CurView", luar.New(L, CurView))
LoadPlugins()
@ -249,14 +257,17 @@ func main() {
// Wait for the user's action
event := screen.PollEvent()
if TabbarHandleMouseEvent(event) {
continue
}
if searching {
// Since searching is done in real time, we need to redraw every time
// there is a new event in the search bar
HandleSearchEvent(event, views[mainView])
HandleSearchEvent(event, CurView())
} else {
// Send it to the view
views[mainView].HandleEvent(event)
CurView().HandleEvent(event)
}
}
}

File diff suppressed because one or more lines are too long

View file

@ -15,13 +15,9 @@ type Statusline struct {
// Display draws the statusline to the screen
func (sline *Statusline) Display() {
// We'll draw the line at the lowest line in the view
y := sline.view.height
y := sline.view.height + sline.view.y
file := sline.view.Buf.Name
// If the name is empty, use 'No name'
if file == "" {
file = "No name"
}
// If the buffer is dirty (has been modified) write a little '+'
if sline.view.Buf.IsModified {

120
cmd/micro/tab.go Normal file
View file

@ -0,0 +1,120 @@
package main
import (
"sort"
"github.com/zyedidia/tcell"
)
type Tab struct {
// This contains all the views in this tab
// There is generally only one view per tab, but you can have
// multiple views with splits
views []*View
// This is the current view for this tab
curView int
// Generally this is the name of the current view's buffer
name string
}
func NewTabFromView(v *View) *Tab {
t := new(Tab)
t.views = append(t.views, v)
t.views[0].Num = 0
return t
}
func (t *Tab) SetNum(num int) {
for _, v := range t.views {
v.TabNum = num
}
}
// CurView returns the current view
func CurView() *View {
curTab := tabs[curTab]
return curTab.views[curTab.curView]
}
func TabbarString() (string, map[int]int) {
str := ""
indicies := make(map[int]int)
indicies[0] = 0
for i, t := range tabs {
if i == curTab {
str += "["
} else {
str += " "
}
str += t.views[t.curView].Buf.Name
if i == curTab {
str += "]"
} else {
str += " "
}
indicies[len(str)-1] = i + 1
str += " "
}
return str, indicies
}
func TabbarHandleMouseEvent(event tcell.Event) bool {
if len(tabs) <= 1 {
return false
}
switch e := event.(type) {
case *tcell.EventMouse:
button := e.Buttons()
if button == tcell.Button1 {
x, y := e.Position()
if y != 0 {
return false
}
str, indicies := TabbarString()
if x >= len(str) {
return false
}
var tabnum int
var keys []int
for k := range indicies {
keys = append(keys, k)
}
sort.Ints(keys)
for _, k := range keys {
if x <= k {
tabnum = indicies[k] - 1
break
}
}
curTab = tabnum
return true
}
}
return false
}
func DisplayTabs() {
if len(tabs) <= 1 {
return
}
str, _ := TabbarString()
tabBarStyle := defStyle.Reverse(true)
if style, ok := colorscheme["tabbar"]; ok {
tabBarStyle = style
}
// Maybe there is a unicode filename?
fileRunes := []rune(str)
w, _ := screen.Size()
for x := 0; x < w; x++ {
if x < len(fileRunes) {
screen.SetContent(x, 0, fileRunes[x], nil, tabBarStyle)
} else {
screen.SetContent(x, 0, ' ', nil, tabBarStyle)
}
}
}

View file

@ -31,6 +31,9 @@ type View struct {
width int
height int
// Where this view is located
x, y int
// How much to offset because of line numbers
lineNumOffset int
@ -40,6 +43,11 @@ type View struct {
// Is the help text opened in this view
helpOpen bool
// This is the index of this view in the views array
Num int
// What tab is this view stored in
TabNum int
// Is this view modifiable?
Modifiable bool
@ -91,6 +99,8 @@ func NewView(buf *Buffer) *View {
func NewViewWidthHeight(buf *Buffer, w, h int) *View {
v := new(View)
v.x, v.y = 0, 0
v.widthPercent = w
v.heightPercent = h
v.Resize(screen.Size())
@ -113,6 +123,13 @@ func NewViewWidthHeight(buf *Buffer, w, h int) *View {
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
} else {
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)
@ -173,6 +190,7 @@ func (v *View) OpenBuffer(buf *Buffer) {
v.Topline = 0
v.leftCol = 0
v.Cursor.ResetSelection()
v.Relocate()
v.messages = make(map[string][]GutterMessage)
v.matches = Match(v)
@ -459,12 +477,12 @@ func (v *View) DisplayView() {
}
for lineN := 0; lineN < v.height; lineN++ {
var x int
x := v.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, ' ', nil, defStyle)
screen.SetContent(i, lineN+v.y, ' ', nil, defStyle)
}
continue
@ -492,9 +510,9 @@ func (v *View) DisplayView() {
gutterStyle = style
}
}
screen.SetContent(x, lineN, '>', nil, gutterStyle)
screen.SetContent(x, lineN+v.y, '>', nil, gutterStyle)
x++
screen.SetContent(x, lineN, '>', nil, gutterStyle)
screen.SetContent(x, lineN+v.y, '>', nil, gutterStyle)
x++
if v.Cursor.Y == lineN+v.Topline {
messenger.Message(msg.msg)
@ -504,9 +522,9 @@ func (v *View) DisplayView() {
}
}
if !msgOnLine {
screen.SetContent(x, lineN, ' ', nil, tcell.StyleDefault)
screen.SetContent(x, lineN+v.y, ' ', nil, tcell.StyleDefault)
x++
screen.SetContent(x, lineN, ' ', nil, tcell.StyleDefault)
screen.SetContent(x, lineN+v.y, ' ', nil, tcell.StyleDefault)
x++
if v.Cursor.Y == lineN+v.Topline && messenger.gutterMessage {
messenger.Reset()
@ -525,18 +543,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, ' ', nil, lineNumStyle)
screen.SetContent(x, lineN+v.y, ' ', nil, lineNumStyle)
x++
}
// Write the actual line number
for _, ch := range lineNum {
screen.SetContent(x, lineN, ch, nil, lineNumStyle)
screen.SetContent(x, lineN+v.y, ch, nil, lineNumStyle)
x++
}
if settings["ruler"] == true {
// Write the extra space
screen.SetContent(x, lineN, ' ', nil, lineNumStyle)
screen.SetContent(x, lineN+v.y, ' ', nil, lineNumStyle)
x++
}
}
@ -592,13 +610,13 @@ func (v *View) DisplayView() {
}
indentChar := []rune(settings["indentchar"].(string))
if x-v.leftCol >= v.lineNumOffset {
screen.SetContent(x-v.leftCol, lineN, indentChar[0], nil, lineIndentStyle)
screen.SetContent(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, ' ', nil, lineStyle)
screen.SetContent(x-v.leftCol, lineN+v.y, ' ', nil, lineStyle)
}
}
} else if runewidth.RuneWidth(ch) > 1 {
@ -613,7 +631,7 @@ func (v *View) DisplayView() {
}
} else {
if x-v.leftCol >= v.lineNumOffset {
screen.SetContent(x-v.leftCol, lineN, ch, nil, lineStyle)
screen.SetContent(x-v.leftCol, lineN+v.y, ch, nil, lineStyle)
}
}
charNum = charNum.Move(1, v.Buf)
@ -632,7 +650,7 @@ func (v *View) DisplayView() {
if style, ok := colorscheme["selection"]; ok {
selectStyle = style
}
screen.SetContent(x-v.leftCol, lineN, ' ', nil, selectStyle)
screen.SetContent(x-v.leftCol, lineN+v.y, ' ', nil, selectStyle)
x++
}
@ -647,7 +665,7 @@ func (v *View) DisplayView() {
}
}
if !(x-v.leftCol < v.lineNumOffset) {
screen.SetContent(x-v.leftCol+i, lineN, ' ', nil, lineStyle)
screen.SetContent(x+i, lineN+v.y, ' ', nil, lineStyle)
}
}
}
@ -659,7 +677,7 @@ func (v *View) DisplayCursor() {
if (v.Cursor.Y-v.Topline < 0 || v.Cursor.Y-v.Topline > v.height-1) || v.Cursor.HasSelection() {
screen.HideCursor()
} else {
screen.ShowCursor(v.Cursor.GetVisualX()+v.lineNumOffset-v.leftCol, v.Cursor.Y-v.Topline)
screen.ShowCursor(v.x+v.Cursor.GetVisualX()+v.lineNumOffset-v.leftCol, v.Cursor.Y-v.Topline+v.y)
}
}

View file

@ -63,6 +63,9 @@ you can rebind them to your liking.
"CtrlD": "DuplicateLine",
"CtrlV": "Paste",
"CtrlA": "SelectAll",
"CtrlT": "AddTab"
"CtrlRightSq": "PreviousTab",
"CtrlBackslash": "NextTab",
"Home": "Start",
"End": "End",
"PageUp": "CursorPageUp",

View file

@ -9,7 +9,7 @@ MakeCommand("goimports", "go_goimports")
MakeCommand("gofmt", "go_gofmt")
function go_onSave()
if views[mainView+1].Buf.FileType == "Go" then
if CurView().Buf.FileType == "Go" then
if GetOption("goimports") then
go_goimports()
elseif GetOption("gofmt") then
@ -19,21 +19,21 @@ function go_onSave()
end
function go_gofmt()
views[mainView+1]:Save()
local handle = io.popen("gofmt -w " .. views[mainView+1].Buf.Path)
CurView():Save()
local handle = io.popen("gofmt -w " .. CurView().Buf.Path)
local result = handle:read("*a")
handle:close()
views[mainView+1]:ReOpen()
CurView():ReOpen()
end
function go_goimports()
views[mainView+1]:Save()
local handle = io.popen("goimports -w " .. views[mainView+1].Buf.Path)
CurView():Save()
local handle = io.popen("goimports -w " .. CurView().Buf.Path)
local result = go_split(handle:read("*a"), ":")
handle:close()
views[mainView+1]:ReOpen()
CurView():ReOpen()
end
function go_split(str, sep)

View file

@ -4,15 +4,15 @@ end
function linter_onSave()
if GetOption("linter") then
local ft = views[mainView+1].Buf.FileType
local file = views[mainView+1].Buf.Path
local ft = CurView().Buf.FileType
local file = CurView().Buf.Path
local devnull = "/dev/null"
if OS == "windows" then
devnull = "NUL"
end
if ft == "Go" then
linter_lint("gobuild", "go build -o " .. devnull, "%f:%l: %m")
linter_lint("golint", "golint " .. views[mainView+1].Buf.Path, "%f:%l:%d+: %m")
linter_lint("golint", "golint " .. CurView().Buf.Path, "%f:%l:%d+: %m")
elseif ft == "Lua" then
linter_lint("luacheck", "luacheck --no-color " .. file, "%f:%l:%d+: %m")
elseif ft == "Python" then
@ -27,12 +27,12 @@ function linter_onSave()
linter_lint("jshint", "jshint " .. file, "%f: line %l,.+, %m")
end
else
views[mainView+1]:ClearAllGutterMessages()
CurView():ClearAllGutterMessages()
end
end
function linter_lint(linter, cmd, errorformat)
views[mainView+1]:ClearGutterMessages(linter)
CurView():ClearGutterMessages(linter)
local handle = io.popen("(" .. cmd .. ")" .. " 2>&1")
local lines = linter_split(handle:read("*a"), "\n")
@ -44,8 +44,8 @@ function linter_lint(linter, cmd, errorformat)
line = line:match("^%s*(.+)%s*$")
if string.find(line, regex) then
local file, line, msg = string.match(line, regex)
if linter_basename(views[mainView+1].Buf.Path) == linter_basename(file) then
views[mainView+1]:GutterMessage(linter, tonumber(line), msg, 2)
if linter_basename(CurView().Buf.Path) == linter_basename(file) then
CurView():GutterMessage(linter, tonumber(line), msg, 2)
end
end
end