From 8c0983a36cf9fea90f06f37890c33db44d431dec Mon Sep 17 00:00:00 2001 From: Zachary Yedidia Date: Sun, 10 Jul 2016 13:26:05 -0400 Subject: [PATCH] Add some more comments --- cmd/micro/buffer.go | 7 ++++++ cmd/micro/job.go | 20 +++++++++++++++ cmd/micro/lineArray.go | 16 ++++++++++++ cmd/micro/loc.go | 9 ++++--- cmd/micro/micro.go | 55 ++++++++++++++++++++++++++++++------------ cmd/micro/tab.go | 12 +++++++++ cmd/micro/util.go | 5 ++++ 7 files changed, 105 insertions(+), 19 deletions(-) diff --git a/cmd/micro/buffer.go b/cmd/micro/buffer.go index 67175de5..9e40f55e 100644 --- a/cmd/micro/buffer.go +++ b/cmd/micro/buffer.go @@ -18,6 +18,7 @@ import ( type Buffer struct { // The eventhandler for undo/redo *EventHandler + // This stores all the text in the buffer as an array of lines *LineArray Cursor Cursor @@ -27,6 +28,7 @@ type Buffer struct { // Name of the buffer on the status line Name string + // Whether or not the buffer has been modified since it was opened IsModified bool // Stores the last modification time of the file the buffer is pointing to @@ -41,6 +43,7 @@ type Buffer struct { } // The SerializedBuffer holds the types that get serialized when a buffer is saved +// These are used for the savecursor and saveundo options type SerializedBuffer struct { EventHandler *EventHandler Cursor Cursor @@ -55,10 +58,12 @@ func NewBuffer(txt []byte, path string) *Buffer { b.Path = path b.Name = path + // If the file doesn't have a path to disk then we give it no name if path == "" { b.Name = "No name" } + // The last time this file was modified b.ModTime, _ = GetModTime(b.Path) b.EventHandler = NewEventHandler(b) @@ -80,6 +85,8 @@ func NewBuffer(txt []byte, path string) *Buffer { } if settings["savecursor"].(bool) || settings["saveundo"].(bool) { + // If either savecursor or saveundo is turned on, we need to load the serialized information + // from ~/.config/micro/buffers absPath, _ := filepath.Abs(b.Path) file, err := os.Open(configDir + "/buffers/" + EscapePath(absPath)) if err == nil { diff --git a/cmd/micro/job.go b/cmd/micro/job.go index 1486706c..316aa636 100644 --- a/cmd/micro/job.go +++ b/cmd/micro/job.go @@ -7,12 +7,24 @@ import ( "strings" ) +// Jobs are the way plugins can run processes in the background +// A job is simply a process that gets executed asynchronously +// There are callbacks for when the job exits, when the job creates stdout +// and when the job creates stderr + +// These jobs run in a separate goroutine but the lua callbacks need to be +// executed in the main thread (where the Lua VM is running) so they are +// put into the jobs channel which gets read by the main loop + +// JobFunction is a representation of a job (this data structure is what is loaded +// into the jobs channel) type JobFunction struct { function func(string, ...string) output string args []string } +// A CallbackFile is the data structure that makes it possible to catch stderr and stdout write events type CallbackFile struct { io.Writer @@ -21,16 +33,21 @@ type CallbackFile struct { } func (f *CallbackFile) Write(data []byte) (int, error) { + // This is either stderr or stdout + // In either case we create a new job function callback and put it in the jobs channel jobFunc := JobFunction{f.callback, string(data), f.args} jobs <- jobFunc return f.Writer.Write(data) } +// JobStart starts a process in the background with the given callbacks +// It returns an *exec.Cmd as the job id func JobStart(cmd string, onStdout, onStderr, onExit string, userargs ...string) *exec.Cmd { split := strings.Split(cmd, " ") args := split[1:] cmdName := split[0] + // Set up everything correctly if the functions have been provided proc := exec.Command(cmdName, args...) var outbuf bytes.Buffer if onStdout != "" { @@ -45,6 +62,7 @@ func JobStart(cmd string, onStdout, onStderr, onExit string, userargs ...string) } go func() { + // Run the process in the background and create the onExit callback proc.Run() jobFunc := JobFunction{LuaFunctionJob(onExit), string(outbuf.Bytes()), userargs} jobs <- jobFunc @@ -53,10 +71,12 @@ func JobStart(cmd string, onStdout, onStderr, onExit string, userargs ...string) return proc } +// JobStop kills a job func JobStop(cmd *exec.Cmd) { cmd.Process.Kill() } +// JobSend sends the given data into the job's stdin stream func JobSend(cmd *exec.Cmd, data string) { stdin, err := cmd.StdinPipe() if err != nil { diff --git a/cmd/micro/lineArray.go b/cmd/micro/lineArray.go index c6e093ab..87f7395a 100644 --- a/cmd/micro/lineArray.go +++ b/cmd/micro/lineArray.go @@ -26,12 +26,16 @@ func runeToByteIndex(n int, txt []byte) int { return count } +// A LineArray simply stores and array of lines and makes it easy to insert +// and delete in it type LineArray struct { lines [][]byte } +// NewLineArray returns a new line array from an array of bytes func NewLineArray(text []byte) *LineArray { la := new(LineArray) + // Split the bytes into lines split := bytes.Split(text, []byte("\n")) la.lines = make([][]byte, len(split)) for i := range split { @@ -42,16 +46,19 @@ func NewLineArray(text []byte) *LineArray { return la } +// Returns the String representation of the LineArray func (la *LineArray) String() string { return string(bytes.Join(la.lines, []byte("\n"))) } +// NewlineBelow adds a newline below the given line number func (la *LineArray) NewlineBelow(y int) { la.lines = append(la.lines, []byte(" ")) copy(la.lines[y+2:], la.lines[y+1:]) la.lines[y+1] = []byte("") } +// inserts a byte array at a given location func (la *LineArray) insert(pos Loc, value []byte) { x, y := runeToByteIndex(pos.X, la.lines[pos.Y]), pos.Y // x, y := pos.x, pos.y @@ -67,23 +74,27 @@ func (la *LineArray) insert(pos Loc, value []byte) { } } +// inserts a byte at a given location func (la *LineArray) insertByte(pos Loc, value byte) { la.lines[pos.Y] = append(la.lines[pos.Y], 0) copy(la.lines[pos.Y][pos.X+1:], la.lines[pos.Y][pos.X:]) la.lines[pos.Y][pos.X] = value } +// JoinLines joins the two lines a and b func (la *LineArray) JoinLines(a, b int) { la.insert(Loc{len(la.lines[a]), a}, la.lines[b]) la.DeleteLine(b) } +// Split splits a line at a given position func (la *LineArray) Split(pos Loc) { la.NewlineBelow(pos.Y) la.insert(Loc{0, pos.Y + 1}, la.lines[pos.Y][pos.X:]) la.DeleteToEnd(Loc{pos.X, pos.Y}) } +// removes from start to end func (la *LineArray) remove(start, end Loc) string { sub := la.Substr(start, end) startX := runeToByteIndex(start.X, la.lines[start.Y]) @@ -101,22 +112,27 @@ func (la *LineArray) remove(start, end Loc) string { return sub } +// DeleteToEnd deletes from the end of a line to the position func (la *LineArray) DeleteToEnd(pos Loc) { la.lines[pos.Y] = la.lines[pos.Y][:pos.X] } +// DeleteFromStart deletes from the start of a line to the position func (la *LineArray) DeleteFromStart(pos Loc) { la.lines[pos.Y] = la.lines[pos.Y][pos.X+1:] } +// DeleteLine deletes the line number func (la *LineArray) DeleteLine(y int) { la.lines = la.lines[:y+copy(la.lines[y:], la.lines[y+1:])] } +// DeleteByte deletes the byte at a position func (la *LineArray) DeleteByte(pos Loc) { la.lines[pos.Y] = la.lines[pos.Y][:pos.X+copy(la.lines[pos.Y][pos.X:], la.lines[pos.Y][pos.X+1:])] } +// Substr returns the string representation between two locations func (la *LineArray) Substr(start, end Loc) string { startX := runeToByteIndex(start.X, la.lines[start.Y]) endX := runeToByteIndex(end.X, la.lines[end.Y]) diff --git a/cmd/micro/loc.go b/cmd/micro/loc.go index d0bee09f..5d3a032a 100644 --- a/cmd/micro/loc.go +++ b/cmd/micro/loc.go @@ -83,6 +83,7 @@ func (l Loc) LessEqual(b Loc) bool { return false } +// This moves the location one character to the right func (l Loc) right(buf *Buffer) Loc { if l == buf.End() { return Loc{l.X + 1, l.Y} @@ -95,6 +96,8 @@ func (l Loc) right(buf *Buffer) Loc { } return res } + +// This moves the given location one character to the left func (l Loc) left(buf *Buffer) Loc { if l == buf.Start() { return Loc{l.X - 1, l.Y} @@ -108,6 +111,8 @@ func (l Loc) left(buf *Buffer) Loc { return res } +// Move moves the cursor n characters to the left or right +// It moves the cursor left if n is negative func (l Loc) Move(n int, buf *Buffer) Loc { if n > 0 { for i := 0; i < n; i++ { @@ -120,7 +125,3 @@ func (l Loc) Move(n int, buf *Buffer) Loc { } return l } - -// func (l Loc) DistanceTo(b Loc, buf *Buffer) int { -// -// } diff --git a/cmd/micro/micro.go b/cmd/micro/micro.go index e62c93fa..80c4db39 100644 --- a/cmd/micro/micro.go +++ b/cmd/micro/micro.go @@ -17,13 +17,6 @@ import ( "github.com/zyedidia/tcell/encoding" ) -const ( - synLinesUp = 75 // How many lines up to look to do syntax highlighting - synLinesDown = 75 // How many lines down to look to do syntax highlighting - doubleClickThreshold = 400 // How many milliseconds to wait before a second click is not a double click - undoThreshold = 500 // If two events are less than n milliseconds apart, undo both of them -) - var ( // The main screen screen tcell.Screen @@ -41,7 +34,7 @@ var ( configDir string // Version is the version number or commit hash - // This should be set by the linker + // This should be set by the linker when compiling Version = "Unknown" // L is the lua state @@ -54,15 +47,18 @@ var ( // It's just an index to the tab in the tabs array curTab int - jobs chan JobFunction + // Channel of jobs running in the background + jobs chan JobFunction + // Event channel events chan tcell.Event ) -// LoadInput loads the file input for the editor +// LoadInput determines which files should be loaded into buffers +// based on the input stored in os.Args 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 + // 1. If it is given a files in os.Args, it should open those // 2. If there is no input file and the input is not a terminal, that means // something is being piped in and the stdin should be opened in an @@ -71,8 +67,6 @@ func LoadInput() []*Buffer { // 3. If there is no input file and the input is a terminal, an empty buffer // should be opened - // These are empty by default so if we get to option 3, we can just returns the - // default values var filename string var input []byte var err error @@ -80,16 +74,19 @@ func LoadInput() []*Buffer { if len(os.Args) > 1 { // Option 1 + // We go through each file and load it for i := 1; i < len(os.Args); i++ { filename = os.Args[i] // Check that the file exists if _, e := os.Stat(filename); e == nil { + // If it exists we load it into a buffer input, err = ioutil.ReadFile(filename) if err != nil { TermMessage(err) continue } } + // If the file didn't exist, input will be empty, and we'll open an empty buffer buffers = append(buffers, NewBuffer(input, filename)) } } else if !isatty.IsTerminal(os.Stdin.Fd()) { @@ -182,15 +179,18 @@ func RedrawAll() { screen.Show() } -var flagVersion = flag.Bool("version", false, "Show version number") +// Passing -version as a flag will have micro print out the version number +var flagVersion = flag.Bool("version", false, "Show the version number") func main() { flag.Parse() if *flagVersion { + // If -version was passed fmt.Println("Micro version:", Version) os.Exit(0) } + // Start the Lua VM for running plugins L = lua.NewState() defer L.Close() @@ -200,15 +200,19 @@ func main() { // Find the user's configuration directory (probably $XDG_CONFIG_HOME/micro) InitConfigDir() + // Load the user's settings InitSettings() InitCommands() InitBindings() + // Load the syntax files, including the colorscheme LoadSyntaxFiles() + // Load the help files LoadHelp() + // Start the screen InitScreen() // This is just so if we have an error, we can exit cleanly and not completely @@ -224,11 +228,15 @@ func main() { } }() + // Create a new messenger + // This is used for sending the user messages in the bottom of the editor messenger = new(Messenger) messenger.history = make(map[string][]string) + // Now we load the input buffers := LoadInput() for _, buf := range buffers { + // For each buffer we create a new tab and place the view in that tab tab := NewTabFromView(NewView(buf)) tab.SetNum(len(tabs)) tabs = append(tabs, tab) @@ -239,6 +247,8 @@ func main() { } } + // Load all the plugin stuff + // We give plugins access to a bunch of variables here which could be useful to them L.SetGlobal("OS", luar.New(L, runtime.GOOS)) L.SetGlobal("tabs", luar.New(L, tabs)) L.SetGlobal("curTab", luar.New(L, curTab)) @@ -250,6 +260,7 @@ func main() { L.SetGlobal("CurView", luar.New(L, CurView)) L.SetGlobal("IsWordChar", luar.New(L, IsWordChar)) + // Used for asynchronous jobs L.SetGlobal("JobStart", luar.New(L, JobStart)) L.SetGlobal("JobSend", luar.New(L, JobSend)) L.SetGlobal("JobStop", luar.New(L, JobStop)) @@ -259,6 +270,7 @@ func main() { jobs = make(chan JobFunction, 100) events = make(chan tcell.Event) + // Here is the event loop which runs in a separate thread go func() { for { events <- screen.PollEvent() @@ -270,8 +282,11 @@ func main() { RedrawAll() var event tcell.Event + + // Check for new events select { case f := <-jobs: + // If a new job has finished while running in the background we should execute the callback f.function(f.output, f.args...) continue case event = <-events: @@ -280,13 +295,20 @@ func main() { switch e := event.(type) { case *tcell.EventMouse: if e.Buttons() == tcell.Button1 { + // If the user left clicked we check a couple things _, h := screen.Size() x, y := e.Position() if y == h-1 && messenger.message != "" { + // If the user clicked in the bottom bar, and there is a message down there + // we copy it to the clipboard. + // Often error messages are displayed down there so it can be useful to easily + // copy the message clipboard.WriteAll(messenger.message) continue } + // We loop through each view in the current tab and make sure the current view + // it the one being clicked in 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 @@ -295,13 +317,16 @@ func main() { } } + // This function checks the mouse event for the possibility of changing the current tab + // If the tab was changed it returns true 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 + // there is a new event in the search bar so we need a special function + // to run instead of the standard HandleEvent. HandleSearchEvent(event, CurView()) } else { // Send it to the view diff --git a/cmd/micro/tab.go b/cmd/micro/tab.go index 370a5ed0..c32bd4b4 100644 --- a/cmd/micro/tab.go +++ b/cmd/micro/tab.go @@ -17,6 +17,7 @@ type Tab struct { name string } +// NewTabFromView creates a new tab and puts the given view in the tab func NewTabFromView(v *View) *Tab { t := new(Tab) t.views = append(t.views, v) @@ -24,6 +25,7 @@ func NewTabFromView(v *View) *Tab { return t } +// SetNum sets all this tab's views to have the correct tab number func (t *Tab) SetNum(num int) { for _, v := range t.views { v.TabNum = num @@ -36,6 +38,10 @@ func CurView() *View { return curTab.views[curTab.curView] } +// TabbarString returns the string that should be displayed in the tabbar +// It also returns a map containing which indicies correspond to which tab number +// This is useful when we know that the mouse click has occured at an x location +// but need to know which tab that corresponds to to accurately change the tab func TabbarString() (string, map[int]int) { str := "" indicies := make(map[int]int) @@ -57,7 +63,11 @@ func TabbarString() (string, map[int]int) { return str, indicies } +// TabbarHandleMouseEvent checks the given mouse event if it is clicking on the tabbar +// If it is it changes the current tab accordingly +// This function returns true if the tab is changed func TabbarHandleMouseEvent(event tcell.Event) bool { + // There is no tabbar displayed if there are less than 2 tabs if len(tabs) <= 1 { return false } @@ -65,6 +75,7 @@ func TabbarHandleMouseEvent(event tcell.Event) bool { switch e := event.(type) { case *tcell.EventMouse: button := e.Buttons() + // Must be a left click if button == tcell.Button1 { x, y := e.Position() if y != 0 { @@ -94,6 +105,7 @@ func TabbarHandleMouseEvent(event tcell.Event) bool { return false } +// DisplayTabs displays the tabbar at the top of the editor if there are multiple tabs func DisplayTabs() { if len(tabs) <= 1 { return diff --git a/cmd/micro/util.go b/cmd/micro/util.go index db452e06..f5d77abc 100644 --- a/cmd/micro/util.go +++ b/cmd/micro/util.go @@ -140,12 +140,15 @@ func GetModTime(path string) (time.Time, bool) { return info.ModTime(), true } +// StringWidth returns the width of a string where tabs count as `tabsize` width func StringWidth(str string) int { sw := runewidth.StringWidth(str) sw += NumOccurences(str, '\t') * (int(settings["tabsize"].(float64)) - 1) return sw } +// WidthOfLargeRunes searches all the runes in a string and counts up all the widths of runes +// that have a width larger than 1 (this also counts tabs as `tabsize` width) func WidthOfLargeRunes(str string) int { count := 0 for _, ch := range str { @@ -162,6 +165,8 @@ func WidthOfLargeRunes(str string) int { return count } +// RunePos returns the rune index of a given byte index +// This could cause problems if the byte index is between code points func runePos(p int, str string) int { return utf8.RuneCountInString(str[:p]) }