Refactor and clean up

This commit puts in place the ability for multiple views (splits).
This commit also removes the editor bindings so that all bindings can be
rebound by the user.
I also added some more comments

This fixes #109
This commit is contained in:
Zachary Yedidia 2016-05-28 11:32:09 -04:00
parent d9d0af4a99
commit e8d8da1443
9 changed files with 311 additions and 248 deletions

View file

@ -71,9 +71,13 @@ func InitBindings() {
"HalfPageDown": (*View).HalfPageDown,
"StartOfLine": (*View).StartOfLine,
"EndOfLine": (*View).EndOfLine,
"ToggleHelp": (*View).ToggleHelp,
"ToggleRuler": (*View).ToggleRuler,
"JumpLine": (*View).JumpLine,
"ClearStatus": (*View).ClearStatus,
"ShellMode": (*View).ShellMode,
"CommandMode": (*View).CommandMode,
"Quit": (*View).Quit,
}
keys := map[string]Key{
@ -303,13 +307,14 @@ func DefaultBindings() map[string]string {
"End": "End",
"PgUp": "PageUp",
"PgDn": "PageDown",
// Find alternative key
// "CtrlU": "HalfPageUp",
// "CtrlD": "HalfPageDown",
"CtrlR": "ToggleRuler",
"CtrlL": "JumpLine",
"Delete": "Delete",
"Esc": "ClearStatus",
"CtrlG": "ToggleHelp",
"CtrlR": "ToggleRuler",
"CtrlL": "JumpLine",
"Delete": "Delete",
"Esc": "ClearStatus",
"CtrlB": "ShellMode",
"CtrlQ": "Quit",
"CtrlE": "CommandMode",
// Emacs-style keybindings
"Alt-f": "WordRight",
@ -625,6 +630,10 @@ func (v *View) InsertTab() bool {
// Save the buffer to disk
func (v *View) Save() bool {
if v.helpOpen {
// We can't save the help text
return false
}
// If this is an empty buffer, ask for a filename
if v.Buf.Path == "" {
filename, canceled := messenger.Prompt("Filename: ")
@ -894,6 +903,53 @@ func (v *View) ClearStatus() bool {
return false
}
// ToggleHelp toggles the help screen
func (v *View) ToggleHelp() bool {
if !v.helpOpen {
v.lastBuffer = v.Buf
helpBuffer := NewBuffer(helpTxt, "help.md")
helpBuffer.Name = "Help"
v.helpOpen = true
v.OpenBuffer(helpBuffer)
} else {
v.OpenBuffer(v.lastBuffer)
v.helpOpen = false
}
return true
}
// ShellMode opens a terminal to run a shell command
func (v *View) ShellMode() bool {
input, canceled := messenger.Prompt("$ ")
if !canceled {
// The true here is for openTerm to make the command interactive
HandleShellCommand(input, true)
}
return false
}
// CommandMode lets the user enter a command
func (v *View) CommandMode() bool {
input, canceled := messenger.Prompt("> ")
if !canceled {
HandleCommand(input)
}
return false
}
// Quit quits the editor
// This behavior needs to be changed and should really only quit the editor if this
// is the last view
// However, since micro only supports one view for now, it doesn't really matter
func (v *View) Quit() bool {
// Make sure not to quit if there are unsaved changes
if views[mainView].CanClose("Quit anyway? (yes, no, save) ") {
screen.Fini()
os.Exit(0)
}
return false
}
// None is no action
func None() bool {
return false

View file

@ -24,10 +24,13 @@ func RunShellCommand(input string) (string, error) {
return outstring, err
}
// HandleShellCommand runs the shell command and outputs to DisplayBlock
func HandleShellCommand(input string, view *View, openTerm bool) {
// HandleShellCommand runs the shell command
// The openTerm argument specifies whether a terminal should be opened (for viewing output
// or interacting with stdin)
func HandleShellCommand(input string, openTerm bool) {
inputCmd := strings.Split(input, " ")[0]
if !openTerm {
// Simply run the command in the background and notify the user when it's done
messenger.Message("Running...")
go func() {
output, err := RunShellCommand(input)
@ -42,19 +45,24 @@ func HandleShellCommand(input string, view *View, openTerm bool) {
} else {
messenger.Message(output)
}
Redraw(view)
// We have to make sure to redraw
RedrawAll()
}()
} else {
// Shut down the screen because we're going to interact directly with the shell
screen.Fini()
screen = nil
args := strings.Split(input, " ")[1:]
// Set up everything for the command
cmd := exec.Command(inputCmd, args...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
// This is a trap for Ctrl-C so that it doesn't kill micro
// Instead we trap Ctrl-C to kill the program we're running
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func() {
@ -63,51 +71,52 @@ func HandleShellCommand(input string, view *View, openTerm bool) {
}
}()
// Start the command
cmd.Start()
cmd.Wait()
// This is just so we don't return right away and let the user press enter to return
TermMessage("")
// Start the screen back up
InitScreen()
}
}
// HandleCommand handles input from the user
func HandleCommand(input string, view *View) {
func HandleCommand(input string) {
inputCmd := strings.Split(input, " ")[0]
args := strings.Split(input, " ")[1:]
commands := []string{"set", "quit", "save", "replace", "run"}
i := 0
cmd := inputCmd
for _, c := range commands {
if strings.HasPrefix(c, inputCmd) {
i++
cmd = c
}
}
if i == 1 {
inputCmd = cmd
}
switch inputCmd {
case "set":
SetOption(view, args)
// Set an option and we have to set it for every view
for _, view := range views {
SetOption(view, args)
}
case "run":
HandleShellCommand(strings.Join(args, " "), view, false)
// Run a shell command in the background (openTerm is false)
HandleShellCommand(strings.Join(args, " "), false)
case "quit":
if view.CanClose("Quit anyway? (yes, no, save) ") {
// This is a bit weird because micro only has one view for now so there is no way to close
// a single view
// Currently if multiple views were open, it would close all of them, and not check the non-mainviews
// for unsaved changes. This, and the behavior of Ctrl-Q need to be changed when splits are implemented
if views[mainView].CanClose("Quit anyway? (yes, no, save) ") {
screen.Fini()
os.Exit(0)
}
case "save":
view.Save()
// Save the main view
views[mainView].Save()
case "replace":
// This is a regex to parse the replace expression
// We allow no quotes if there are no spaces, but if you want to search
// for or replace an expression with spaces, you can add double quotes
r := regexp.MustCompile(`"[^"\\]*(?:\\.[^"\\]*)*"|[^\s]*`)
replaceCmd := r.FindAllString(strings.Join(args, " "), -1)
if len(replaceCmd) < 2 {
// We need to find both a search and replace expression
messenger.Error("Invalid replace statement: " + strings.Join(args, " "))
return
}
@ -121,6 +130,7 @@ func HandleCommand(input string, view *View) {
search := string(replaceCmd[0])
replace := string(replaceCmd[1])
// If the search and replace expressions have quotes, we need to remove those
if strings.HasPrefix(search, `"`) && strings.HasSuffix(search, `"`) {
search = search[1 : len(search)-1]
}
@ -128,17 +138,19 @@ func HandleCommand(input string, view *View) {
replace = replace[1 : len(replace)-1]
}
// We replace all escaped double quotes to real double quotes
search = strings.Replace(search, `\"`, `"`, -1)
replace = strings.Replace(replace, `\"`, `"`, -1)
// messenger.Error(search + " -> " + replace)
regex, err := regexp.Compile(search)
if err != nil {
// There was an error with the user's regex
messenger.Error(err.Error())
return
}
view := views[mainView]
found := false
for {
match := regex.FindStringIndex(view.Buf.String())
@ -150,7 +162,7 @@ func HandleCommand(input string, view *View) {
// The 'check' flag was used
Search(search, view, true)
view.Relocate()
Redraw(view)
RedrawAll()
choice, canceled := messenger.YesNoPrompt("Perform replacement? (y,n)")
if canceled {
if view.Cursor.HasSelection() {

View file

@ -30,7 +30,8 @@ var (
// Object to send messages and prompts to the user
messenger *Messenger
// The default style
// The default highlighting style
// This simply defines the default foreground and background colors
defStyle tcell.Style
// Where the user's configuration is
@ -38,20 +39,25 @@ var (
// If $XDG_CONFIG_HOME is not set, it is ~/.config/micro
configDir string
// Version is the version number.
// Version is the version number or commit hash
// This should be set by the linker
Version = "Unknown"
// Is the help screen open
helpOpen = false
// L is the lua state
// This is the VM that runs the plugins
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
)
// LoadInput loads the file input for the editor
func LoadInput() (string, []byte, error) {
// 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
// 2. If there is no input file and the input is not a terminal, that means
@ -85,15 +91,15 @@ func LoadInput() (string, []byte, error) {
return filename, input, err
}
// InitConfigDir finds the configuration directory for micro according to the
// XDG spec.
// InitConfigDir finds the configuration directory for micro according to the XDG spec.
// If no directory is found, it creates one.
func InitConfigDir() {
xdgHome := os.Getenv("XDG_CONFIG_HOME")
if xdgHome == "" {
// The user has not set $XDG_CONFIG_HOME so we should act like it was set to ~/.config
home, err := homedir.Dir()
if err != nil {
TermMessage("Error finding your home directory\nCan't load syntax files")
TermMessage("Error finding your home directory\nCan't load config files")
return
}
xdgHome = home + "/.config"
@ -101,6 +107,7 @@ func InitConfigDir() {
configDir = xdgHome + "/micro"
if _, err := os.Stat(xdgHome); os.IsNotExist(err) {
// If the xdgHome doesn't exist we should create it
err = os.Mkdir(xdgHome, os.ModePerm)
if err != nil {
TermMessage("Error creating XDG_CONFIG_HOME directory: " + err.Error())
@ -108,6 +115,7 @@ func InitConfigDir() {
}
if _, err := os.Stat(configDir); os.IsNotExist(err) {
// If the micro specific config directory doesn't exist we should create that too
err = os.Mkdir(configDir, os.ModePerm)
if err != nil {
TermMessage("Error creating configuration directory: " + err.Error())
@ -150,6 +158,7 @@ func InitScreen() {
Background(tcell.ColorDefault)
// There may be another default style defined in the colorscheme
// In that case we should use that one
if style, ok := colorscheme["default"]; ok {
defStyle = style
}
@ -158,10 +167,12 @@ func InitScreen() {
screen.EnableMouse()
}
// Redraw redraws the screen and the given view
func Redraw(view *View) {
// RedrawAll redraws everything -- all the views and the messenger
func RedrawAll() {
screen.Clear()
view.Display()
for _, v := range views {
v.Display()
}
messenger.Display()
screen.Show()
}
@ -184,6 +195,7 @@ func main() {
L = lua.NewState()
defer L.Close()
// Some encoding stuff in case the user isn't using UTF-8
encoding.Register()
tcell.SetEncodingFallback(tcell.EncodingFallbackASCII)
@ -203,6 +215,7 @@ func main() {
// This is just so if we have an error, we can exit cleanly and not completely
// mess up the terminal being worked in
// In other words we need to shut down tcell before the program crashes
defer func() {
if err := recover(); err != nil {
screen.Fini()
@ -214,10 +227,12 @@ func main() {
}()
messenger = new(Messenger)
view := NewView(buf)
views = make([]*View, 1)
views[0] = NewView(buf)
L.SetGlobal("OS", luar.New(L, runtime.GOOS))
L.SetGlobal("view", luar.New(L, view))
L.SetGlobal("views", luar.New(L, views))
L.SetGlobal("mainView", luar.New(L, mainView))
L.SetGlobal("messenger", luar.New(L, messenger))
L.SetGlobal("GetOption", luar.New(L, GetOption))
L.SetGlobal("AddOption", luar.New(L, AddOption))
@ -226,54 +241,18 @@ func main() {
for {
// Display everything
Redraw(view)
RedrawAll()
// Wait for the user's action
event := screen.PollEvent()
if searching {
HandleSearchEvent(event, view)
// 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])
} else {
// Check if we should quit
switch e := event.(type) {
case *tcell.EventKey:
switch e.Key() {
case tcell.KeyCtrlQ:
// Make sure not to quit if there are unsaved changes
if helpOpen {
view.OpenBuffer(buf)
helpOpen = false
} else {
if view.CanClose("Quit anyway? (yes, no, save) ") {
screen.Fini()
os.Exit(0)
}
}
case tcell.KeyCtrlE:
input, canceled := messenger.Prompt("> ")
if !canceled {
HandleCommand(input, view)
}
case tcell.KeyCtrlB:
input, canceled := messenger.Prompt("$ ")
if !canceled {
HandleShellCommand(input, view, true)
}
case tcell.KeyCtrlG:
if !helpOpen {
helpBuffer := NewBuffer(helpTxt, "help.md")
helpBuffer.Name = "Help"
helpOpen = true
view.OpenBuffer(helpBuffer)
} else {
view.OpenBuffer(buf)
helpOpen = false
}
}
}
// Send it to the view
view.HandleEvent(event)
views[mainView].HandleEvent(event)
}
}
}

File diff suppressed because one or more lines are too long

View file

@ -41,7 +41,7 @@ func (sline *Statusline) Display() {
file += " " + sline.view.Buf.FileType
rightText := "Ctrl-g for help "
if helpOpen {
if sline.view.helpOpen {
rightText = "Ctrl-g to close help "
}

View file

@ -37,8 +37,17 @@ type View struct {
// Holds the list of gutter messages
messages map[string][]GutterMessage
// Is the help text opened in this view
helpOpen bool
// Is this view modifiable?
Modifiable bool
// The buffer
Buf *Buffer
// This is the buffer that was last opened
// This is used to open help, and then go back to the previously opened buffer
lastBuffer *Buffer
// The statusline
sline Statusline

View file

@ -21,15 +21,10 @@ You can move the cursor around with the arrow keys and mouse.
These are the default keybindings, along with their actions.
#### Editor bindings
* Ctrl-q: Quit
* Ctrl-e: Execute a command
* Ctrl-g: Toggle help text
* Ctrl-b: Run a shell command
#### Buffer bindings
* Ctrl-s: Save
* Ctrl-o: Open file
* Ctrl-z: Undo
@ -55,7 +50,7 @@ ctrl up and down move the cursor the start and end of the buffer.
You can hold shift with all of these movement actions to select while moving.
The buffer bindings may be rebound using the `~/.config/micro/bindings.json`
The bindings may be rebound using the `~/.config/micro/bindings.json`
file. Each key is bound to an action.
For example, to bind `Ctrl-y` to undo and `Ctrl-z` to redo, you could put the
@ -72,52 +67,64 @@ Here are the defaults:
```json
{
"Up": "CursorUp",
"Down": "CursorDown",
"Right": "CursorRight",
"Left": "CursorLeft",
"ShiftUp": "SelectUp",
"ShiftDown": "SelectDown",
"ShiftLeft": "SelectLeft",
"ShiftRight": "SelectRight",
"AltLeft": "WordLeft",
"AltRight": "WordRight",
"AltShiftRight": "SelectWordRight",
"AltShiftLeft": "SelectWordLeft",
"CtrlLeft": "StartOfLine",
"CtrlRight": "EndOfLine",
"CtrlShiftLeft": "SelectToStartOfLine",
"CtrlShiftRight": "SelectToEndOfLine",
"CtrlUp": "CursorStart",
"CtrlDown": "CursorEnd",
"CtrlShiftUp": "SelectToStart",
"CtrlShiftDown": "SelectToEnd",
"Enter": "InsertEnter",
"Space": "InsertSpace",
"Backspace": "Backspace",
"Backspace2": "Backspace",
"Tab": "InsertTab",
"CtrlO": "OpenFile",
"CtrlS": "Save",
"CtrlF": "Find",
"CtrlN": "FindNext",
"CtrlP": "FindPrevious",
"CtrlZ": "Undo",
"CtrlY": "Redo",
"CtrlC": "Copy",
"CtrlX": "Cut",
"CtrlK": "CutLine",
"CtrlD": "DuplicateLine",
"CtrlV": "Paste",
"CtrlA": "SelectAll",
"Home": "Start",
"End": "End",
"PgUp": "PageUp",
"PgDn": "PageDown",
"CtrlU": "HalfPageUp",
"CtrlD": "HalfPageDown",
"CtrlR": "ToggleRuler",
"Delete": "Delete"
"Up": "CursorUp",
"Down": "CursorDown",
"Right": "CursorRight",
"Left": "CursorLeft",
"ShiftUp": "SelectUp",
"ShiftDown": "SelectDown",
"ShiftLeft": "SelectLeft",
"ShiftRight": "SelectRight",
"AltLeft": "WordLeft",
"AltRight": "WordRight",
"AltShiftRight": "SelectWordRight",
"AltShiftLeft": "SelectWordLeft",
"CtrlLeft": "StartOfLine",
"CtrlRight": "EndOfLine",
"CtrlShiftLeft": "SelectToStartOfLine",
"CtrlShiftRight": "SelectToEndOfLine",
"CtrlUp": "CursorStart",
"CtrlDown": "CursorEnd",
"CtrlShiftUp": "SelectToStart",
"CtrlShiftDown": "SelectToEnd",
"Enter": "InsertEnter",
"Space": "InsertSpace",
"Backspace": "Backspace",
"Backspace2": "Backspace",
"Tab": "InsertTab",
"CtrlO": "OpenFile",
"CtrlS": "Save",
"CtrlF": "Find",
"CtrlN": "FindNext",
"CtrlP": "FindPrevious",
"CtrlZ": "Undo",
"CtrlY": "Redo",
"CtrlC": "Copy",
"CtrlX": "Cut",
"CtrlK": "CutLine",
"CtrlD": "DuplicateLine",
"CtrlV": "Paste",
"CtrlA": "SelectAll",
"Home": "Start",
"End": "End",
"PgUp": "PageUp",
"PgDn": "PageDown",
"CtrlG": "ToggleHelp",
"CtrlR": "ToggleRuler",
"CtrlL": "JumpLine",
"Delete": "Delete",
"Esc": "ClearStatus",
"CtrlB": "ShellMode",
"CtrlQ": "Quit",
"CtrlE": "CommandMode",
// Emacs-style keybindings
"Alt-f": "WordRight",
"Alt-b": "WordLeft",
"Alt-a": "StartOfLine",
"Alt-e": "EndOfLine",
"Alt-p": "CursorUp",
"Alt-n": "CursorDown",
}
```
@ -211,7 +218,7 @@ Here are the options that you can set:
default value: `3`
* `scrollspeed`: amount of lines to scroll
* `scrollspeed`: amount of lines to scroll for one scroll event
default value: `2`

View file

@ -6,25 +6,25 @@ if GetOption("gofmt") == nil then
end
function go_onSave()
if view.Buf.FileType == "Go" then
if views[mainView+1].Buf.FileType == "Go" then
if GetOption("goimports") then
go_goimports()
elseif GetOption("gofmt") then
go_gofmt()
end
view:ReOpen()
views[mainView+1]:ReOpen()
end
end
function go_gofmt()
local handle = io.popen("gofmt -w " .. view.Buf.Path)
local handle = io.popen("gofmt -w " .. views[mainView+1].Buf.Path)
local result = handle:read("*a")
handle:close()
end
function go_goimports()
local handle = io.popen("goimports -w " .. view.Buf.Path)
local handle = io.popen("goimports -w " .. views[mainView+1].Buf.Path)
local result = go_split(handle:read("*a"), ":")
handle:close()
end

View file

@ -4,15 +4,15 @@ end
function linter_onSave()
if GetOption("linter") then
local ft = view.Buf.FileType
local file = view.Buf.Path
local ft = views[mainView+1].Buf.FileType
local file = views[mainView+1].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 " .. view.Buf.Path, "%f:%l:%d+: %m")
linter_lint("golint", "golint " .. views[mainView+1].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
view:ClearAllGutterMessages()
views[mainView+1]:ClearAllGutterMessages()
end
end
function linter_lint(linter, cmd, errorformat)
view:ClearGutterMessages(linter)
views[mainView+1]: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(view.Buf.Path) == linter_basename(file) then
view:GutterMessage(linter, tonumber(line), msg, 2)
if linter_basename(views[mainView+1].Buf.Path) == linter_basename(file) then
views[mainView+1]:GutterMessage(linter, tonumber(line), msg, 2)
end
end
end