From dc68183fc114ab2911d6ba934cf5df019566d0cf Mon Sep 17 00:00:00 2001 From: Zachary Yedidia Date: Sat, 25 Aug 2018 23:06:44 -0400 Subject: [PATCH] Start refactor --- .gitignore | 3 + Makefile | 9 +- cmd/micro/actionhandler.go | 40 + cmd/micro/actions.go | 2328 --------------------- cmd/micro/actions_other.go | 9 - cmd/micro/actions_posix.go | 37 - cmd/micro/autocomplete.go | 249 --- cmd/micro/bindings.go | 616 ------ cmd/micro/buffer.go | 974 +++------ cmd/micro/buffer_test.go | 117 -- cmd/micro/cellview.go | 238 --- cmd/micro/colorscheme.go | 45 +- cmd/micro/colorscheme_test.go | 62 + cmd/micro/command.go | 694 ------ cmd/micro/cursor.go | 299 +-- cmd/micro/cursor_test.go | 1 + cmd/micro/debug.go | 27 + cmd/micro/eventhandler.go | 77 +- cmd/micro/highlighter.go | 28 - cmd/micro/job.go | 88 - cmd/micro/keymenu.go | 20 - cmd/micro/{lineArray.go => line_array.go} | 139 +- cmd/micro/line_array_test.go | 60 + cmd/micro/loc.go | 147 +- cmd/micro/lua.go | 3 +- cmd/micro/message.go | 38 + cmd/micro/messenger.go | 669 ------ cmd/micro/micro.go | 421 +--- cmd/micro/plugin.go | 184 -- cmd/micro/pluginmanager.go | 622 ------ cmd/micro/pluginmanager_test.go | 56 - cmd/micro/profile.go | 14 + cmd/micro/rtfiles.go | 43 +- cmd/micro/rtfiles_test.go | 42 + cmd/micro/runtime.go | 2 +- cmd/micro/scrollbar.go | 20 - cmd/micro/search.go | 214 -- cmd/micro/settings.go | 626 +++--- cmd/micro/shell.go | 129 -- cmd/micro/shell_supported.go | 18 - cmd/micro/shell_unsupported.go | 11 - cmd/micro/split_tree.go | 317 --- cmd/micro/stack.go | 12 +- cmd/micro/stack_test.go | 35 + cmd/micro/statusline.go | 160 +- cmd/micro/tab.go | 293 --- cmd/micro/terminal.go | 228 -- cmd/micro/util.go | 331 +-- cmd/micro/util_test.go | 344 +-- cmd/micro/view.go | 1117 ---------- cmd/micro/window.go | 187 ++ 51 files changed, 1621 insertions(+), 10822 deletions(-) create mode 100644 cmd/micro/actionhandler.go delete mode 100644 cmd/micro/actions.go delete mode 100644 cmd/micro/actions_other.go delete mode 100644 cmd/micro/actions_posix.go delete mode 100644 cmd/micro/autocomplete.go delete mode 100644 cmd/micro/bindings.go delete mode 100644 cmd/micro/buffer_test.go delete mode 100644 cmd/micro/cellview.go create mode 100644 cmd/micro/colorscheme_test.go delete mode 100644 cmd/micro/command.go create mode 100644 cmd/micro/cursor_test.go create mode 100644 cmd/micro/debug.go delete mode 100644 cmd/micro/highlighter.go delete mode 100644 cmd/micro/job.go delete mode 100644 cmd/micro/keymenu.go rename cmd/micro/{lineArray.go => line_array.go} (60%) create mode 100644 cmd/micro/line_array_test.go create mode 100644 cmd/micro/message.go delete mode 100644 cmd/micro/messenger.go delete mode 100644 cmd/micro/plugin.go delete mode 100644 cmd/micro/pluginmanager.go delete mode 100644 cmd/micro/pluginmanager_test.go create mode 100644 cmd/micro/profile.go create mode 100644 cmd/micro/rtfiles_test.go delete mode 100644 cmd/micro/scrollbar.go delete mode 100644 cmd/micro/search.go delete mode 100644 cmd/micro/shell.go delete mode 100644 cmd/micro/shell_supported.go delete mode 100644 cmd/micro/shell_unsupported.go delete mode 100644 cmd/micro/split_tree.go create mode 100644 cmd/micro/stack_test.go delete mode 100644 cmd/micro/tab.go delete mode 100644 cmd/micro/terminal.go delete mode 100644 cmd/micro/view.go create mode 100644 cmd/micro/window.go diff --git a/.gitignore b/.gitignore index 08c1a639..acfa0730 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ tmp.sh test/ .idea/ packages/ +todo.txt +test.txt +log.txt diff --git a/Makefile b/Makefile index e03ee2fc..08a70768 100644 --- a/Makefile +++ b/Makefile @@ -9,28 +9,29 @@ ADDITIONAL_GO_LINKER_FLAGS := $(shell GOOS=$(shell go env GOHOSTOS) \ GOARCH=$(shell go env GOHOSTARCH) \ go run tools/info-plist.go "$(VERSION)") GOBIN ?= $(shell go env GOPATH)/bin +GOVARS := -X main.Version=$(VERSION) -X main.CommitHash=$(HASH) -X 'main.CompileDate=$(DATE)' -X main.Debug=OFF # Builds micro after checking dependencies but without updating the runtime build: update - go build -ldflags "-s -w -X main.Version=$(VERSION) -X main.CommitHash=$(HASH) -X 'main.CompileDate=$(DATE)' $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro + go build -ldflags "-s -w $(GOVARS) $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro # Builds micro after building the runtime and checking dependencies build-all: runtime build # Builds micro without checking for dependencies build-quick: - go build -ldflags "-s -w -X main.Version=$(VERSION) -X main.CommitHash=$(HASH) -X 'main.CompileDate=$(DATE)' $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro + go build -ldflags "-s -w $(GOVARS) $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro # Same as 'build' but installs to $GOBIN afterward install: update - go install -ldflags "-s -w -X main.Version=$(VERSION) -X main.CommitHash=$(HASH) -X 'main.CompileDate=$(DATE)' $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro + go install -ldflags "-s -w $(GOVARS) $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro # Same as 'build-all' but installs to $GOBIN afterward install-all: runtime install # Same as 'build-quick' but installs to $GOBIN afterward install-quick: - go install -ldflags "-s -w -X main.Version=$(VERSION) -X main.CommitHash=$(HASH) -X 'main.CompileDate=$(DATE)' $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro + go install -ldflags "-s -w $(GOVARS) $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro update: git pull diff --git a/cmd/micro/actionhandler.go b/cmd/micro/actionhandler.go new file mode 100644 index 00000000..3cceabd8 --- /dev/null +++ b/cmd/micro/actionhandler.go @@ -0,0 +1,40 @@ +package main + +import "time" + +// The ActionHandler connects the buffer and the window +// It provides a cursor (or multiple) and defines a set of actions +// that can be taken on the buffer +// The ActionHandler can access the window for necessary info about +// visual positions for mouse clicks and scrolling +type ActionHandler struct { + Buf *Buffer + Win *Window + + // Since tcell doesn't differentiate between a mouse release event + // and a mouse move event with no keys pressed, we need to keep + // track of whether or not the mouse was pressed (or not released) last event to determine + // mouse release events + mouseReleased bool + + // We need to keep track of insert key press toggle + isOverwriteMode bool + // This stores when the last click was + // This is useful for detecting double and triple clicks + lastClickTime time.Time + lastLoc Loc + + // lastCutTime stores when the last ctrl+k was issued. + // It is used for clearing the clipboard to replace it with fresh cut lines. + lastCutTime time.Time + + // freshClip returns true if the clipboard has never been pasted. + freshClip bool + + // Was the last mouse event actually a double click? + // Useful for detecting triple clicks -- if a double click is detected + // but the last mouse event was actually a double click, it's a triple click + doubleClick bool + // Same here, just to keep track for mouse move events + tripleClick bool +} diff --git a/cmd/micro/actions.go b/cmd/micro/actions.go deleted file mode 100644 index 83eec144..00000000 --- a/cmd/micro/actions.go +++ /dev/null @@ -1,2328 +0,0 @@ -package main - -import ( - "fmt" - "os" - "regexp" - "strconv" - "strings" - "time" - "unicode/utf8" - - "github.com/yuin/gopher-lua" - "github.com/zyedidia/clipboard" - "github.com/zyedidia/micro/cmd/micro/shellwords" - "github.com/zyedidia/tcell" -) - -// PreActionCall executes the lua pre callback if possible -func PreActionCall(funcName string, view *View, args ...interface{}) bool { - executeAction := true - for pl := range loadedPlugins { - ret, err := Call(pl+".pre"+funcName, append([]interface{}{view}, args...)...) - if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") { - TermMessage(err) - continue - } - if ret == lua.LFalse { - executeAction = false - } - } - return executeAction -} - -// PostActionCall executes the lua plugin callback if possible -func PostActionCall(funcName string, view *View, args ...interface{}) bool { - relocate := true - for pl := range loadedPlugins { - ret, err := Call(pl+".on"+funcName, append([]interface{}{view}, args...)...) - if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") { - TermMessage(err) - continue - } - if ret == lua.LFalse { - relocate = false - } - } - return relocate -} - -func (v *View) deselect(index int) bool { - if v.Cursor.HasSelection() { - v.Cursor.Loc = v.Cursor.CurSelection[index] - v.Cursor.ResetSelection() - v.Cursor.StoreVisualX() - return true - } - return false -} - -// MousePress is the event that should happen when a normal click happens -// This is almost always bound to left click -func (v *View) MousePress(usePlugin bool, e *tcell.EventMouse) bool { - if usePlugin && !PreActionCall("MousePress", v, e) { - return false - } - - x, y := e.Position() - x -= v.lineNumOffset - v.leftCol + v.x - y += v.Topline - v.y - - // This is usually bound to left click - v.MoveToMouseClick(x, y) - if v.mouseReleased { - if len(v.Buf.cursors) > 1 { - for i := 1; i < len(v.Buf.cursors); i++ { - v.Buf.cursors[i] = nil - } - v.Buf.cursors = v.Buf.cursors[:1] - v.Buf.UpdateCursors() - v.Cursor.ResetSelection() - v.Relocate() - } - if time.Since(v.lastClickTime)/time.Millisecond < doubleClickThreshold && (x == v.lastLoc.X && y == v.lastLoc.Y) { - if v.doubleClick { - // Triple click - v.lastClickTime = time.Now() - - v.tripleClick = true - v.doubleClick = false - - v.Cursor.SelectLine() - v.Cursor.CopySelection("primary") - } else { - // Double click - v.lastClickTime = time.Now() - - v.doubleClick = true - v.tripleClick = false - - v.Cursor.SelectWord() - v.Cursor.CopySelection("primary") - } - } else { - v.doubleClick = false - v.tripleClick = false - v.lastClickTime = time.Now() - - v.Cursor.OrigSelection[0] = v.Cursor.Loc - v.Cursor.CurSelection[0] = v.Cursor.Loc - v.Cursor.CurSelection[1] = v.Cursor.Loc - } - v.mouseReleased = false - } else if !v.mouseReleased { - if v.tripleClick { - v.Cursor.AddLineToSelection() - } else if v.doubleClick { - v.Cursor.AddWordToSelection() - } else { - v.Cursor.SetSelectionEnd(v.Cursor.Loc) - v.Cursor.CopySelection("primary") - } - } - - v.lastLoc = Loc{x, y} - - if usePlugin { - PostActionCall("MousePress", v, e) - } - return false -} - -// ScrollUpAction scrolls the view up -func (v *View) ScrollUpAction(usePlugin bool) bool { - if v.mainCursor() { - if usePlugin && !PreActionCall("ScrollUp", v) { - return false - } - - scrollspeed := int(v.Buf.Settings["scrollspeed"].(float64)) - v.ScrollUp(scrollspeed) - - if usePlugin { - PostActionCall("ScrollUp", v) - } - } - return false -} - -// ScrollDownAction scrolls the view up -func (v *View) ScrollDownAction(usePlugin bool) bool { - if v.mainCursor() { - if usePlugin && !PreActionCall("ScrollDown", v) { - return false - } - - scrollspeed := int(v.Buf.Settings["scrollspeed"].(float64)) - v.ScrollDown(scrollspeed) - - if usePlugin { - PostActionCall("ScrollDown", v) - } - } - return false -} - -// Center centers the view on the cursor -func (v *View) Center(usePlugin bool) bool { - if usePlugin && !PreActionCall("Center", v) { - return false - } - - v.Topline = v.Cursor.Y - v.Height/2 - if v.Topline+v.Height > v.Buf.NumLines { - v.Topline = v.Buf.NumLines - v.Height - } - if v.Topline < 0 { - v.Topline = 0 - } - - if usePlugin { - return PostActionCall("Center", v) - } - return true -} - -// CursorUp moves the cursor up -func (v *View) CursorUp(usePlugin bool) bool { - if usePlugin && !PreActionCall("CursorUp", v) { - return false - } - - v.deselect(0) - v.Cursor.Up() - - if usePlugin { - return PostActionCall("CursorUp", v) - } - return true -} - -// CursorDown moves the cursor down -func (v *View) CursorDown(usePlugin bool) bool { - if usePlugin && !PreActionCall("CursorDown", v) { - return false - } - - v.deselect(1) - v.Cursor.Down() - - if usePlugin { - return PostActionCall("CursorDown", v) - } - return true -} - -// CursorLeft moves the cursor left -func (v *View) CursorLeft(usePlugin bool) bool { - if usePlugin && !PreActionCall("CursorLeft", v) { - return false - } - - if v.Cursor.HasSelection() { - v.Cursor.Loc = v.Cursor.CurSelection[0] - v.Cursor.ResetSelection() - v.Cursor.StoreVisualX() - } else { - tabstospaces := v.Buf.Settings["tabstospaces"].(bool) - tabmovement := v.Buf.Settings["tabmovement"].(bool) - if tabstospaces && tabmovement { - tabsize := int(v.Buf.Settings["tabsize"].(float64)) - line := v.Buf.Line(v.Cursor.Y) - if v.Cursor.X-tabsize >= 0 && line[v.Cursor.X-tabsize:v.Cursor.X] == Spaces(tabsize) && IsStrWhitespace(line[0:v.Cursor.X-tabsize]) { - for i := 0; i < tabsize; i++ { - v.Cursor.Left() - } - } else { - v.Cursor.Left() - } - } else { - v.Cursor.Left() - } - } - - if usePlugin { - return PostActionCall("CursorLeft", v) - } - return true -} - -// CursorRight moves the cursor right -func (v *View) CursorRight(usePlugin bool) bool { - if usePlugin && !PreActionCall("CursorRight", v) { - return false - } - - if v.Cursor.HasSelection() { - v.Cursor.Loc = v.Cursor.CurSelection[1] - v.Cursor.ResetSelection() - v.Cursor.StoreVisualX() - } else { - tabstospaces := v.Buf.Settings["tabstospaces"].(bool) - tabmovement := v.Buf.Settings["tabmovement"].(bool) - if tabstospaces && tabmovement { - tabsize := int(v.Buf.Settings["tabsize"].(float64)) - line := v.Buf.Line(v.Cursor.Y) - if v.Cursor.X+tabsize < Count(line) && line[v.Cursor.X:v.Cursor.X+tabsize] == Spaces(tabsize) && IsStrWhitespace(line[0:v.Cursor.X]) { - for i := 0; i < tabsize; i++ { - v.Cursor.Right() - } - } else { - v.Cursor.Right() - } - } else { - v.Cursor.Right() - } - } - - if usePlugin { - return PostActionCall("CursorRight", v) - } - return true -} - -// WordRight moves the cursor one word to the right -func (v *View) WordRight(usePlugin bool) bool { - if usePlugin && !PreActionCall("WordRight", v) { - return false - } - - v.Cursor.WordRight() - - if usePlugin { - return PostActionCall("WordRight", v) - } - return true -} - -// WordLeft moves the cursor one word to the left -func (v *View) WordLeft(usePlugin bool) bool { - if usePlugin && !PreActionCall("WordLeft", v) { - return false - } - - v.Cursor.WordLeft() - - if usePlugin { - return PostActionCall("WordLeft", v) - } - return true -} - -// SelectUp selects up one line -func (v *View) SelectUp(usePlugin bool) bool { - if usePlugin && !PreActionCall("SelectUp", v) { - return false - } - - if !v.Cursor.HasSelection() { - v.Cursor.OrigSelection[0] = v.Cursor.Loc - } - v.Cursor.Up() - v.Cursor.SelectTo(v.Cursor.Loc) - - if usePlugin { - return PostActionCall("SelectUp", v) - } - return true -} - -// SelectDown selects down one line -func (v *View) SelectDown(usePlugin bool) bool { - if usePlugin && !PreActionCall("SelectDown", v) { - return false - } - - if !v.Cursor.HasSelection() { - v.Cursor.OrigSelection[0] = v.Cursor.Loc - } - v.Cursor.Down() - v.Cursor.SelectTo(v.Cursor.Loc) - - if usePlugin { - return PostActionCall("SelectDown", v) - } - return true -} - -// SelectLeft selects the character to the left of the cursor -func (v *View) SelectLeft(usePlugin bool) bool { - if usePlugin && !PreActionCall("SelectLeft", v) { - return false - } - - loc := v.Cursor.Loc - count := v.Buf.End() - if loc.GreaterThan(count) { - loc = count - } - if !v.Cursor.HasSelection() { - v.Cursor.OrigSelection[0] = loc - } - v.Cursor.Left() - v.Cursor.SelectTo(v.Cursor.Loc) - - if usePlugin { - return PostActionCall("SelectLeft", v) - } - return true -} - -// SelectRight selects the character to the right of the cursor -func (v *View) SelectRight(usePlugin bool) bool { - if usePlugin && !PreActionCall("SelectRight", v) { - return false - } - - loc := v.Cursor.Loc - count := v.Buf.End() - if loc.GreaterThan(count) { - loc = count - } - if !v.Cursor.HasSelection() { - v.Cursor.OrigSelection[0] = loc - } - v.Cursor.Right() - v.Cursor.SelectTo(v.Cursor.Loc) - - if usePlugin { - return PostActionCall("SelectRight", v) - } - return true -} - -// SelectWordRight selects the word to the right of the cursor -func (v *View) SelectWordRight(usePlugin bool) bool { - if usePlugin && !PreActionCall("SelectWordRight", v) { - return false - } - - if !v.Cursor.HasSelection() { - v.Cursor.OrigSelection[0] = v.Cursor.Loc - } - v.Cursor.WordRight() - v.Cursor.SelectTo(v.Cursor.Loc) - - if usePlugin { - return PostActionCall("SelectWordRight", v) - } - return true -} - -// SelectWordLeft selects the word to the left of the cursor -func (v *View) SelectWordLeft(usePlugin bool) bool { - if usePlugin && !PreActionCall("SelectWordLeft", v) { - return false - } - - if !v.Cursor.HasSelection() { - v.Cursor.OrigSelection[0] = v.Cursor.Loc - } - v.Cursor.WordLeft() - v.Cursor.SelectTo(v.Cursor.Loc) - - if usePlugin { - return PostActionCall("SelectWordLeft", v) - } - return true -} - -// StartOfLine moves the cursor to the start of the line -func (v *View) StartOfLine(usePlugin bool) bool { - if usePlugin && !PreActionCall("StartOfLine", v) { - return false - } - - v.deselect(0) - - if v.Cursor.X != 0 { - v.Cursor.Start() - } else { - v.Cursor.StartOfText() - } - - if usePlugin { - return PostActionCall("StartOfLine", v) - } - return true -} - -// EndOfLine moves the cursor to the end of the line -func (v *View) EndOfLine(usePlugin bool) bool { - if usePlugin && !PreActionCall("EndOfLine", v) { - return false - } - - v.deselect(0) - - v.Cursor.End() - - if usePlugin { - return PostActionCall("EndOfLine", v) - } - return true -} - -// SelectLine selects the entire current line -func (v *View) SelectLine(usePlugin bool) bool { - if usePlugin && !PreActionCall("SelectLine", v) { - return false - } - - v.Cursor.SelectLine() - - if usePlugin { - return PostActionCall("SelectLine", v) - } - return true -} - -// SelectToStartOfLine selects to the start of the current line -func (v *View) SelectToStartOfLine(usePlugin bool) bool { - if usePlugin && !PreActionCall("SelectToStartOfLine", v) { - return false - } - - if !v.Cursor.HasSelection() { - v.Cursor.OrigSelection[0] = v.Cursor.Loc - } - v.Cursor.Start() - v.Cursor.SelectTo(v.Cursor.Loc) - - if usePlugin { - return PostActionCall("SelectToStartOfLine", v) - } - return true -} - -// SelectToEndOfLine selects to the end of the current line -func (v *View) SelectToEndOfLine(usePlugin bool) bool { - if usePlugin && !PreActionCall("SelectToEndOfLine", v) { - return false - } - - if !v.Cursor.HasSelection() { - v.Cursor.OrigSelection[0] = v.Cursor.Loc - } - v.Cursor.End() - v.Cursor.SelectTo(v.Cursor.Loc) - - if usePlugin { - return PostActionCall("SelectToEndOfLine", v) - } - return true -} - -// ParagraphPrevious moves the cursor to the previous empty line, or beginning of the buffer if there's none -func (v *View) ParagraphPrevious(usePlugin bool) bool { - if usePlugin && !PreActionCall("ParagraphPrevious", v) { - return false - } - var line int - for line = v.Cursor.Y; line > 0; line-- { - if len(v.Buf.lines[line].data) == 0 && line != v.Cursor.Y { - v.Cursor.X = 0 - v.Cursor.Y = line - break - } - } - // If no empty line found. move cursor to end of buffer - if line == 0 { - v.Cursor.Loc = v.Buf.Start() - } - - if usePlugin { - return PostActionCall("ParagraphPrevious", v) - } - return true -} - -// ParagraphNext moves the cursor to the next empty line, or end of the buffer if there's none -func (v *View) ParagraphNext(usePlugin bool) bool { - if usePlugin && !PreActionCall("ParagraphNext", v) { - return false - } - - var line int - for line = v.Cursor.Y; line < len(v.Buf.lines); line++ { - if len(v.Buf.lines[line].data) == 0 && line != v.Cursor.Y { - v.Cursor.X = 0 - v.Cursor.Y = line - break - } - } - // If no empty line found. move cursor to end of buffer - if line == len(v.Buf.lines) { - v.Cursor.Loc = v.Buf.End() - } - - if usePlugin { - return PostActionCall("ParagraphNext", v) - } - return true -} - -// Retab changes all tabs to spaces or all spaces to tabs depending -// on the user's settings -func (v *View) Retab(usePlugin bool) bool { - if usePlugin && !PreActionCall("Retab", v) { - return false - } - - toSpaces := v.Buf.Settings["tabstospaces"].(bool) - tabsize := int(v.Buf.Settings["tabsize"].(float64)) - dirty := false - - for i := 0; i < v.Buf.NumLines; i++ { - l := v.Buf.Line(i) - - ws := GetLeadingWhitespace(l) - if ws != "" { - if toSpaces { - ws = strings.Replace(ws, "\t", Spaces(tabsize), -1) - } else { - ws = strings.Replace(ws, Spaces(tabsize), "\t", -1) - } - } - - l = strings.TrimLeft(l, " \t") - v.Buf.lines[i].data = []byte(ws + l) - dirty = true - } - - v.Buf.IsModified = dirty - - if usePlugin { - return PostActionCall("Retab", v) - } - return true -} - -// CursorStart moves the cursor to the start of the buffer -func (v *View) CursorStart(usePlugin bool) bool { - if usePlugin && !PreActionCall("CursorStart", v) { - return false - } - - v.deselect(0) - - v.Cursor.X = 0 - v.Cursor.Y = 0 - - if usePlugin { - return PostActionCall("CursorStart", v) - } - return true -} - -// CursorEnd moves the cursor to the end of the buffer -func (v *View) CursorEnd(usePlugin bool) bool { - if usePlugin && !PreActionCall("CursorEnd", v) { - return false - } - - v.deselect(0) - - v.Cursor.Loc = v.Buf.End() - v.Cursor.StoreVisualX() - - if usePlugin { - return PostActionCall("CursorEnd", v) - } - return true -} - -// SelectToStart selects the text from the cursor to the start of the buffer -func (v *View) SelectToStart(usePlugin bool) bool { - if usePlugin && !PreActionCall("SelectToStart", v) { - return false - } - - if !v.Cursor.HasSelection() { - v.Cursor.OrigSelection[0] = v.Cursor.Loc - } - v.CursorStart(false) - v.Cursor.SelectTo(v.Buf.Start()) - - if usePlugin { - return PostActionCall("SelectToStart", v) - } - return true -} - -// SelectToEnd selects the text from the cursor to the end of the buffer -func (v *View) SelectToEnd(usePlugin bool) bool { - if usePlugin && !PreActionCall("SelectToEnd", v) { - return false - } - - if !v.Cursor.HasSelection() { - v.Cursor.OrigSelection[0] = v.Cursor.Loc - } - v.CursorEnd(false) - v.Cursor.SelectTo(v.Buf.End()) - - if usePlugin { - return PostActionCall("SelectToEnd", v) - } - return true -} - -// InsertSpace inserts a space -func (v *View) InsertSpace(usePlugin bool) bool { - if usePlugin && !PreActionCall("InsertSpace", v) { - return false - } - - if v.Cursor.HasSelection() { - v.Cursor.DeleteSelection() - v.Cursor.ResetSelection() - } - v.Buf.Insert(v.Cursor.Loc, " ") - // v.Cursor.Right() - - if usePlugin { - return PostActionCall("InsertSpace", v) - } - return true -} - -// InsertNewline inserts a newline plus possible some whitespace if autoindent is on -func (v *View) InsertNewline(usePlugin bool) bool { - if usePlugin && !PreActionCall("InsertNewline", v) { - return false - } - - // Insert a newline - if v.Cursor.HasSelection() { - v.Cursor.DeleteSelection() - v.Cursor.ResetSelection() - } - - ws := GetLeadingWhitespace(v.Buf.Line(v.Cursor.Y)) - cx := v.Cursor.X - v.Buf.Insert(v.Cursor.Loc, "\n") - // v.Cursor.Right() - - if v.Buf.Settings["autoindent"].(bool) { - if cx < len(ws) { - ws = ws[0:cx] - } - v.Buf.Insert(v.Cursor.Loc, ws) - // for i := 0; i < len(ws); i++ { - // v.Cursor.Right() - // } - - // Remove the whitespaces if keepautoindent setting is off - if IsSpacesOrTabs(v.Buf.Line(v.Cursor.Y-1)) && !v.Buf.Settings["keepautoindent"].(bool) { - line := v.Buf.Line(v.Cursor.Y - 1) - v.Buf.Remove(Loc{0, v.Cursor.Y - 1}, Loc{Count(line), v.Cursor.Y - 1}) - } - } - v.Cursor.LastVisualX = v.Cursor.GetVisualX() - - if usePlugin { - return PostActionCall("InsertNewline", v) - } - return true -} - -// Backspace deletes the previous character -func (v *View) Backspace(usePlugin bool) bool { - if usePlugin && !PreActionCall("Backspace", v) { - return false - } - - // Delete a character - if v.Cursor.HasSelection() { - v.Cursor.DeleteSelection() - v.Cursor.ResetSelection() - } else if v.Cursor.Loc.GreaterThan(v.Buf.Start()) { - // We have to do something a bit hacky here because we want to - // delete the line by first moving left and then deleting backwards - // but the undo redo would place the cursor in the wrong place - // So instead we move left, save the position, move back, delete - // and restore the position - - // If the user is using spaces instead of tabs and they are deleting - // whitespace at the start of the line, we should delete as if it's a - // tab (tabSize number of spaces) - lineStart := sliceEnd(v.Buf.LineBytes(v.Cursor.Y), v.Cursor.X) - tabSize := int(v.Buf.Settings["tabsize"].(float64)) - if v.Buf.Settings["tabstospaces"].(bool) && IsSpaces(lineStart) && utf8.RuneCount(lineStart) != 0 && utf8.RuneCount(lineStart)%tabSize == 0 { - loc := v.Cursor.Loc - v.Buf.Remove(loc.Move(-tabSize, v.Buf), loc) - } else { - loc := v.Cursor.Loc - v.Buf.Remove(loc.Move(-1, v.Buf), loc) - } - } - v.Cursor.LastVisualX = v.Cursor.GetVisualX() - - if usePlugin { - return PostActionCall("Backspace", v) - } - return true -} - -// DeleteWordRight deletes the word to the right of the cursor -func (v *View) DeleteWordRight(usePlugin bool) bool { - if usePlugin && !PreActionCall("DeleteWordRight", v) { - return false - } - - v.SelectWordRight(false) - if v.Cursor.HasSelection() { - v.Cursor.DeleteSelection() - v.Cursor.ResetSelection() - } - - if usePlugin { - return PostActionCall("DeleteWordRight", v) - } - return true -} - -// DeleteWordLeft deletes the word to the left of the cursor -func (v *View) DeleteWordLeft(usePlugin bool) bool { - if usePlugin && !PreActionCall("DeleteWordLeft", v) { - return false - } - - v.SelectWordLeft(false) - if v.Cursor.HasSelection() { - v.Cursor.DeleteSelection() - v.Cursor.ResetSelection() - } - - if usePlugin { - return PostActionCall("DeleteWordLeft", v) - } - return true -} - -// Delete deletes the next character -func (v *View) Delete(usePlugin bool) bool { - if usePlugin && !PreActionCall("Delete", v) { - return false - } - - if v.Cursor.HasSelection() { - v.Cursor.DeleteSelection() - v.Cursor.ResetSelection() - } else { - loc := v.Cursor.Loc - if loc.LessThan(v.Buf.End()) { - v.Buf.Remove(loc, loc.Move(1, v.Buf)) - } - } - - if usePlugin { - return PostActionCall("Delete", v) - } - return true -} - -// IndentSelection indents the current selection -func (v *View) IndentSelection(usePlugin bool) bool { - if usePlugin && !PreActionCall("IndentSelection", v) { - return false - } - - if v.Cursor.HasSelection() { - start := v.Cursor.CurSelection[0] - end := v.Cursor.CurSelection[1] - if end.Y < start.Y { - start, end = end, start - v.Cursor.SetSelectionStart(start) - v.Cursor.SetSelectionEnd(end) - } - - startY := start.Y - endY := end.Move(-1, v.Buf).Y - endX := end.Move(-1, v.Buf).X - tabsize := len(v.Buf.IndentString()) - for y := startY; y <= endY; y++ { - v.Buf.Insert(Loc{0, y}, v.Buf.IndentString()) - if y == startY && start.X > 0 { - v.Cursor.SetSelectionStart(start.Move(tabsize, v.Buf)) - } - if y == endY { - v.Cursor.SetSelectionEnd(Loc{endX + tabsize + 1, endY}) - } - } - v.Cursor.Relocate() - - if usePlugin { - return PostActionCall("IndentSelection", v) - } - return true - } - return false -} - -// OutdentLine moves the current line back one indentation -func (v *View) OutdentLine(usePlugin bool) bool { - if usePlugin && !PreActionCall("OutdentLine", v) { - return false - } - - if v.Cursor.HasSelection() { - return false - } - - for x := 0; x < len(v.Buf.IndentString()); x++ { - if len(GetLeadingWhitespace(v.Buf.Line(v.Cursor.Y))) == 0 { - break - } - v.Buf.Remove(Loc{0, v.Cursor.Y}, Loc{1, v.Cursor.Y}) - } - v.Cursor.Relocate() - - if usePlugin { - return PostActionCall("OutdentLine", v) - } - return true -} - -// OutdentSelection takes the current selection and moves it back one indent level -func (v *View) OutdentSelection(usePlugin bool) bool { - if usePlugin && !PreActionCall("OutdentSelection", v) { - return false - } - - if v.Cursor.HasSelection() { - start := v.Cursor.CurSelection[0] - end := v.Cursor.CurSelection[1] - if end.Y < start.Y { - start, end = end, start - v.Cursor.SetSelectionStart(start) - v.Cursor.SetSelectionEnd(end) - } - - startY := start.Y - endY := end.Move(-1, v.Buf).Y - for y := startY; y <= endY; y++ { - for x := 0; x < len(v.Buf.IndentString()); x++ { - if len(GetLeadingWhitespace(v.Buf.Line(y))) == 0 { - break - } - v.Buf.Remove(Loc{0, y}, Loc{1, y}) - } - } - v.Cursor.Relocate() - - if usePlugin { - return PostActionCall("OutdentSelection", v) - } - return true - } - return false -} - -// InsertTab inserts a tab or spaces -func (v *View) InsertTab(usePlugin bool) bool { - if usePlugin && !PreActionCall("InsertTab", v) { - return false - } - - if v.Cursor.HasSelection() { - return false - } - - tabBytes := len(v.Buf.IndentString()) - bytesUntilIndent := tabBytes - (v.Cursor.GetVisualX() % tabBytes) - v.Buf.Insert(v.Cursor.Loc, v.Buf.IndentString()[:bytesUntilIndent]) - // for i := 0; i < bytesUntilIndent; i++ { - // v.Cursor.Right() - // } - - if usePlugin { - return PostActionCall("InsertTab", v) - } - return true -} - -// SaveAll saves all open buffers -func (v *View) SaveAll(usePlugin bool) bool { - if v.mainCursor() { - if usePlugin && !PreActionCall("SaveAll", v) { - return false - } - - for _, t := range tabs { - for _, v := range t.Views { - v.Save(false) - } - } - - if usePlugin { - return PostActionCall("SaveAll", v) - } - } - return false -} - -// Save the buffer to disk -func (v *View) Save(usePlugin bool) bool { - if v.mainCursor() { - if usePlugin && !PreActionCall("Save", v) { - return false - } - - if v.Type.Scratch == true { - // We can't save any view type with scratch set. eg help and log text - return false - } - // If this is an empty buffer, ask for a filename - if v.Buf.Path == "" { - v.SaveAs(false) - } else { - v.saveToFile(v.Buf.Path) - } - - if usePlugin { - return PostActionCall("Save", v) - } - } - return false -} - -// This function saves the buffer to `filename` and changes the buffer's path and name -// to `filename` if the save is successful -func (v *View) saveToFile(filename string) { - err := v.Buf.SaveAs(filename) - if err != nil { - if strings.HasSuffix(err.Error(), "permission denied") { - choice, _ := messenger.YesNoPrompt("Permission denied. Do you want to save this file using sudo? (y,n)") - if choice { - err = v.Buf.SaveAsWithSudo(filename) - if err != nil { - messenger.Error(err.Error()) - } else { - v.Buf.Path = filename - v.Buf.name = filename - messenger.Message("Saved " + filename) - } - } - messenger.Reset() - messenger.Clear() - } else { - messenger.Error(err.Error()) - } - } else { - v.Buf.Path = filename - v.Buf.name = filename - messenger.Message("Saved " + filename) - } -} - -// SaveAs saves the buffer to disk with the given name -func (v *View) SaveAs(usePlugin bool) bool { - if v.mainCursor() { - if usePlugin && !PreActionCall("SaveAs", v) { - return false - } - - filename, canceled := messenger.Prompt("Filename: ", "", "Save", NoCompletion) - if !canceled { - // the filename might or might not be quoted, so unquote first then join the strings. - args, err := shellwords.Split(filename) - filename = strings.Join(args, " ") - if err != nil { - messenger.Error("Error parsing arguments: ", err) - return false - } - v.saveToFile(filename) - } - - if usePlugin { - PostActionCall("SaveAs", v) - } - } - return false -} - -// Find opens a prompt and searches forward for the input -func (v *View) Find(usePlugin bool) bool { - if v.mainCursor() { - if usePlugin && !PreActionCall("Find", v) { - return false - } - - searchStr := "" - if v.Cursor.HasSelection() { - searchStart = v.Cursor.CurSelection[1] - searchStart = v.Cursor.CurSelection[1] - searchStr = v.Cursor.GetSelection() - } else { - searchStart = v.Cursor.Loc - } - BeginSearch(searchStr) - - if usePlugin { - return PostActionCall("Find", v) - } - } - return true -} - -// FindNext searches forwards for the last used search term -func (v *View) FindNext(usePlugin bool) bool { - if usePlugin && !PreActionCall("FindNext", v) { - return false - } - - if v.Cursor.HasSelection() { - searchStart = v.Cursor.CurSelection[1] - // lastSearch = v.Cursor.GetSelection() - } else { - searchStart = v.Cursor.Loc - } - if lastSearch == "" { - return true - } - messenger.Message("Finding: " + lastSearch) - Search(lastSearch, v, true) - - if usePlugin { - return PostActionCall("FindNext", v) - } - return true -} - -// FindPrevious searches backwards for the last used search term -func (v *View) FindPrevious(usePlugin bool) bool { - if usePlugin && !PreActionCall("FindPrevious", v) { - return false - } - - if v.Cursor.HasSelection() { - searchStart = v.Cursor.CurSelection[0] - } else { - searchStart = v.Cursor.Loc - } - messenger.Message("Finding: " + lastSearch) - Search(lastSearch, v, false) - - if usePlugin { - return PostActionCall("FindPrevious", v) - } - return true -} - -// Undo undoes the last action -func (v *View) Undo(usePlugin bool) bool { - if usePlugin && !PreActionCall("Undo", v) { - return false - } - - if v.Buf.curCursor == 0 { - v.Buf.clearCursors() - } - - v.Buf.Undo() - messenger.Message("Undid action") - - if usePlugin { - return PostActionCall("Undo", v) - } - return true -} - -// Redo redoes the last action -func (v *View) Redo(usePlugin bool) bool { - if usePlugin && !PreActionCall("Redo", v) { - return false - } - - if v.Buf.curCursor == 0 { - v.Buf.clearCursors() - } - - v.Buf.Redo() - messenger.Message("Redid action") - - if usePlugin { - return PostActionCall("Redo", v) - } - return true -} - -// Copy the selection to the system clipboard -func (v *View) Copy(usePlugin bool) bool { - if v.mainCursor() { - if usePlugin && !PreActionCall("Copy", v) { - return false - } - - if v.Cursor.HasSelection() { - v.Cursor.CopySelection("clipboard") - v.freshClip = true - messenger.Message("Copied selection") - } - - if usePlugin { - return PostActionCall("Copy", v) - } - } - return true -} - -// CutLine cuts the current line to the clipboard -func (v *View) CutLine(usePlugin bool) bool { - if usePlugin && !PreActionCall("CutLine", v) { - return false - } - - v.Cursor.SelectLine() - if !v.Cursor.HasSelection() { - return false - } - if v.freshClip == true { - if v.Cursor.HasSelection() { - if clip, err := clipboard.ReadAll("clipboard"); err != nil { - messenger.Error(err) - } else { - clipboard.WriteAll(clip+v.Cursor.GetSelection(), "clipboard") - } - } - } else if time.Since(v.lastCutTime)/time.Second > 10*time.Second || v.freshClip == false { - v.Copy(true) - } - v.freshClip = true - v.lastCutTime = time.Now() - v.Cursor.DeleteSelection() - v.Cursor.ResetSelection() - messenger.Message("Cut line") - - if usePlugin { - return PostActionCall("CutLine", v) - } - return true -} - -// Cut the selection to the system clipboard -func (v *View) Cut(usePlugin bool) bool { - if usePlugin && !PreActionCall("Cut", v) { - return false - } - - if v.Cursor.HasSelection() { - v.Cursor.CopySelection("clipboard") - v.Cursor.DeleteSelection() - v.Cursor.ResetSelection() - v.freshClip = true - messenger.Message("Cut selection") - - if usePlugin { - return PostActionCall("Cut", v) - } - return true - } else { - return v.CutLine(usePlugin) - } -} - -// DuplicateLine duplicates the current line or selection -func (v *View) DuplicateLine(usePlugin bool) bool { - if usePlugin && !PreActionCall("DuplicateLine", v) { - return false - } - - if v.Cursor.HasSelection() { - v.Buf.Insert(v.Cursor.CurSelection[1], v.Cursor.GetSelection()) - } else { - v.Cursor.End() - v.Buf.Insert(v.Cursor.Loc, "\n"+v.Buf.Line(v.Cursor.Y)) - // v.Cursor.Right() - } - - messenger.Message("Duplicated line") - - if usePlugin { - return PostActionCall("DuplicateLine", v) - } - return true -} - -// DeleteLine deletes the current line -func (v *View) DeleteLine(usePlugin bool) bool { - if usePlugin && !PreActionCall("DeleteLine", v) { - return false - } - - v.Cursor.SelectLine() - if !v.Cursor.HasSelection() { - return false - } - v.Cursor.DeleteSelection() - v.Cursor.ResetSelection() - messenger.Message("Deleted line") - - if usePlugin { - return PostActionCall("DeleteLine", v) - } - return true -} - -// MoveLinesUp moves up the current line or selected lines if any -func (v *View) MoveLinesUp(usePlugin bool) bool { - if usePlugin && !PreActionCall("MoveLinesUp", v) { - return false - } - - if v.Cursor.HasSelection() { - if v.Cursor.CurSelection[0].Y == 0 { - messenger.Message("Can not move further up") - return true - } - start := v.Cursor.CurSelection[0].Y - end := v.Cursor.CurSelection[1].Y - if start > end { - end, start = start, end - } - - v.Buf.MoveLinesUp( - start, - end, - ) - v.Cursor.CurSelection[1].Y -= 1 - messenger.Message("Moved up selected line(s)") - } else { - if v.Cursor.Loc.Y == 0 { - messenger.Message("Can not move further up") - return true - } - v.Buf.MoveLinesUp( - v.Cursor.Loc.Y, - v.Cursor.Loc.Y+1, - ) - messenger.Message("Moved up current line") - } - v.Buf.IsModified = true - - if usePlugin { - return PostActionCall("MoveLinesUp", v) - } - return true -} - -// MoveLinesDown moves down the current line or selected lines if any -func (v *View) MoveLinesDown(usePlugin bool) bool { - if usePlugin && !PreActionCall("MoveLinesDown", v) { - return false - } - - if v.Cursor.HasSelection() { - if v.Cursor.CurSelection[1].Y >= len(v.Buf.lines) { - messenger.Message("Can not move further down") - return true - } - start := v.Cursor.CurSelection[0].Y - end := v.Cursor.CurSelection[1].Y - if start > end { - end, start = start, end - } - - v.Buf.MoveLinesDown( - start, - end, - ) - messenger.Message("Moved down selected line(s)") - } else { - if v.Cursor.Loc.Y >= len(v.Buf.lines)-1 { - messenger.Message("Can not move further down") - return true - } - v.Buf.MoveLinesDown( - v.Cursor.Loc.Y, - v.Cursor.Loc.Y+1, - ) - messenger.Message("Moved down current line") - } - v.Buf.IsModified = true - - if usePlugin { - return PostActionCall("MoveLinesDown", v) - } - return true -} - -// Paste whatever is in the system clipboard into the buffer -// Delete and paste if the user has a selection -func (v *View) Paste(usePlugin bool) bool { - if usePlugin && !PreActionCall("Paste", v) { - return false - } - - clip, _ := clipboard.ReadAll("clipboard") - v.paste(clip) - - if usePlugin { - return PostActionCall("Paste", v) - } - return true -} - -// PastePrimary pastes from the primary clipboard (only use on linux) -func (v *View) PastePrimary(usePlugin bool) bool { - if usePlugin && !PreActionCall("Paste", v) { - return false - } - - clip, _ := clipboard.ReadAll("primary") - v.paste(clip) - - if usePlugin { - return PostActionCall("Paste", v) - } - return true -} - -// JumpToMatchingBrace moves the cursor to the matching brace if it is -// currently on a brace -func (v *View) JumpToMatchingBrace(usePlugin bool) bool { - if usePlugin && !PreActionCall("JumpToMatchingBrace", v) { - return false - } - - for _, bp := range bracePairs { - r := v.Cursor.RuneUnder(v.Cursor.X) - if r == bp[0] || r == bp[1] { - matchingBrace := v.Buf.FindMatchingBrace(bp, v.Cursor.Loc) - v.Cursor.GotoLoc(matchingBrace) - } - } - - if usePlugin { - return PostActionCall("JumpToMatchingBrace", v) - } - return true -} - -// SelectAll selects the entire buffer -func (v *View) SelectAll(usePlugin bool) bool { - if usePlugin && !PreActionCall("SelectAll", v) { - return false - } - - v.Cursor.SetSelectionStart(v.Buf.Start()) - v.Cursor.SetSelectionEnd(v.Buf.End()) - // Put the cursor at the beginning - v.Cursor.X = 0 - v.Cursor.Y = 0 - - if usePlugin { - return PostActionCall("SelectAll", v) - } - return true -} - -// OpenFile opens a new file in the buffer -func (v *View) OpenFile(usePlugin bool) bool { - if v.mainCursor() { - if usePlugin && !PreActionCall("OpenFile", v) { - return false - } - - if v.CanClose() { - input, canceled := messenger.Prompt("> ", "open ", "Open", CommandCompletion) - if !canceled { - HandleCommand(input) - if usePlugin { - return PostActionCall("OpenFile", v) - } - } - } - } - return false -} - -// Start moves the viewport to the start of the buffer -func (v *View) Start(usePlugin bool) bool { - if v.mainCursor() { - if usePlugin && !PreActionCall("Start", v) { - return false - } - - v.Topline = 0 - - if usePlugin { - return PostActionCall("Start", v) - } - } - return false -} - -// End moves the viewport to the end of the buffer -func (v *View) End(usePlugin bool) bool { - if v.mainCursor() { - if usePlugin && !PreActionCall("End", v) { - return false - } - - if v.Height > v.Buf.NumLines { - v.Topline = 0 - } else { - v.Topline = v.Buf.NumLines - v.Height - } - - if usePlugin { - return PostActionCall("End", v) - } - } - return false -} - -// PageUp scrolls the view up a page -func (v *View) PageUp(usePlugin bool) bool { - if v.mainCursor() { - if usePlugin && !PreActionCall("PageUp", v) { - return false - } - - if v.Topline > v.Height { - v.ScrollUp(v.Height) - } else { - v.Topline = 0 - } - - if usePlugin { - return PostActionCall("PageUp", v) - } - } - return false -} - -// PageDown scrolls the view down a page -func (v *View) PageDown(usePlugin bool) bool { - if v.mainCursor() { - if usePlugin && !PreActionCall("PageDown", v) { - return false - } - - if v.Buf.NumLines-(v.Topline+v.Height) > v.Height { - v.ScrollDown(v.Height) - } else if v.Buf.NumLines >= v.Height { - v.Topline = v.Buf.NumLines - v.Height - } - - if usePlugin { - return PostActionCall("PageDown", v) - } - } - return false -} - -// SelectPageUp selects up one page -func (v *View) SelectPageUp(usePlugin bool) bool { - if usePlugin && !PreActionCall("SelectPageUp", v) { - return false - } - - if !v.Cursor.HasSelection() { - v.Cursor.OrigSelection[0] = v.Cursor.Loc - } - v.Cursor.UpN(v.Height) - v.Cursor.SelectTo(v.Cursor.Loc) - - if usePlugin { - return PostActionCall("SelectPageUp", v) - } - return true -} - -// SelectPageDown selects down one page -func (v *View) SelectPageDown(usePlugin bool) bool { - if usePlugin && !PreActionCall("SelectPageDown", v) { - return false - } - - if !v.Cursor.HasSelection() { - v.Cursor.OrigSelection[0] = v.Cursor.Loc - } - v.Cursor.DownN(v.Height) - v.Cursor.SelectTo(v.Cursor.Loc) - - if usePlugin { - return PostActionCall("SelectPageDown", v) - } - return true -} - -// CursorPageUp places the cursor a page up -func (v *View) CursorPageUp(usePlugin bool) bool { - if usePlugin && !PreActionCall("CursorPageUp", v) { - return false - } - - v.deselect(0) - - if v.Cursor.HasSelection() { - v.Cursor.Loc = v.Cursor.CurSelection[0] - v.Cursor.ResetSelection() - v.Cursor.StoreVisualX() - } - v.Cursor.UpN(v.Height) - - if usePlugin { - return PostActionCall("CursorPageUp", v) - } - return true -} - -// CursorPageDown places the cursor a page up -func (v *View) CursorPageDown(usePlugin bool) bool { - if usePlugin && !PreActionCall("CursorPageDown", v) { - return false - } - - v.deselect(0) - - if v.Cursor.HasSelection() { - v.Cursor.Loc = v.Cursor.CurSelection[1] - v.Cursor.ResetSelection() - v.Cursor.StoreVisualX() - } - v.Cursor.DownN(v.Height) - - if usePlugin { - return PostActionCall("CursorPageDown", v) - } - return true -} - -// HalfPageUp scrolls the view up half a page -func (v *View) HalfPageUp(usePlugin bool) bool { - if v.mainCursor() { - if usePlugin && !PreActionCall("HalfPageUp", v) { - return false - } - - if v.Topline > v.Height/2 { - v.ScrollUp(v.Height / 2) - } else { - v.Topline = 0 - } - - if usePlugin { - return PostActionCall("HalfPageUp", v) - } - } - return false -} - -// HalfPageDown scrolls the view down half a page -func (v *View) HalfPageDown(usePlugin bool) bool { - if v.mainCursor() { - if usePlugin && !PreActionCall("HalfPageDown", v) { - return false - } - - if v.Buf.NumLines-(v.Topline+v.Height) > v.Height/2 { - v.ScrollDown(v.Height / 2) - } else { - if v.Buf.NumLines >= v.Height { - v.Topline = v.Buf.NumLines - v.Height - } - } - - if usePlugin { - return PostActionCall("HalfPageDown", v) - } - } - return false -} - -// ToggleRuler turns line numbers off and on -func (v *View) ToggleRuler(usePlugin bool) bool { - if v.mainCursor() { - if usePlugin && !PreActionCall("ToggleRuler", v) { - return false - } - - if v.Buf.Settings["ruler"] == false { - v.Buf.Settings["ruler"] = true - messenger.Message("Enabled ruler") - } else { - v.Buf.Settings["ruler"] = false - messenger.Message("Disabled ruler") - } - - if usePlugin { - return PostActionCall("ToggleRuler", v) - } - } - return false -} - -// JumpLine jumps to a line and moves the view accordingly. -func (v *View) JumpLine(usePlugin bool) bool { - if usePlugin && !PreActionCall("JumpLine", v) { - return false - } - - // Prompt for line number - message := fmt.Sprintf("Jump to line:col (1 - %v) # ", v.Buf.NumLines) - input, canceled := messenger.Prompt(message, "", "LineNumber", NoCompletion) - if canceled { - return false - } - var lineInt int - var colInt int - var err error - if strings.Contains(input, ":") { - split := strings.Split(input, ":") - lineInt, err = strconv.Atoi(split[0]) - if err != nil { - messenger.Message("Invalid line number") - return false - } - colInt, err = strconv.Atoi(split[1]) - if err != nil { - messenger.Message("Invalid column number") - return false - } - } else { - lineInt, err = strconv.Atoi(input) - if err != nil { - messenger.Message("Invalid line number") - return false - } - } - lineInt-- - // Move cursor and view if possible. - if lineInt < v.Buf.NumLines && lineInt >= 0 { - v.Cursor.X = colInt - v.Cursor.Y = lineInt - - if usePlugin { - return PostActionCall("JumpLine", v) - } - return true - } - messenger.Error("Only ", v.Buf.NumLines, " lines to jump") - return false -} - -// ClearStatus clears the messenger bar -func (v *View) ClearStatus(usePlugin bool) bool { - if v.mainCursor() { - if usePlugin && !PreActionCall("ClearStatus", v) { - return false - } - - messenger.Message("") - - if usePlugin { - return PostActionCall("ClearStatus", v) - } - } - return false -} - -// ToggleHelp toggles the help screen -func (v *View) ToggleHelp(usePlugin bool) bool { - if v.mainCursor() { - if usePlugin && !PreActionCall("ToggleHelp", v) { - return false - } - - if v.Type != vtHelp { - // Open the default help - v.openHelp("help") - } else { - v.Quit(true) - } - - if usePlugin { - return PostActionCall("ToggleHelp", v) - } - } - return true -} - -// ToggleKeyMenu toggles the keymenu option and resizes all tabs -func (v *View) ToggleKeyMenu(usePlugin bool) bool { - if v.mainCursor() { - if usePlugin && !PreActionCall("ToggleBindings", v) { - return false - } - - globalSettings["keymenu"] = !globalSettings["keymenu"].(bool) - for _, tab := range tabs { - tab.Resize() - } - - if usePlugin { - return PostActionCall("ToggleBindings", v) - } - } - return true -} - -// ShellMode opens a terminal to run a shell command -func (v *View) ShellMode(usePlugin bool) bool { - if v.mainCursor() { - if usePlugin && !PreActionCall("ShellMode", v) { - return false - } - - input, canceled := messenger.Prompt("$ ", "", "Shell", NoCompletion) - if !canceled { - // The true here is for openTerm to make the command interactive - HandleShellCommand(input, true, true) - if usePlugin { - return PostActionCall("ShellMode", v) - } - } - } - return false -} - -// CommandMode lets the user enter a command -func (v *View) CommandMode(usePlugin bool) bool { - if v.mainCursor() { - if usePlugin && !PreActionCall("CommandMode", v) { - return false - } - - input, canceled := messenger.Prompt("> ", "", "Command", CommandCompletion) - if !canceled { - HandleCommand(input) - if usePlugin { - return PostActionCall("CommandMode", v) - } - } - } - - return false -} - -// ToggleOverwriteMode lets the user toggle the text overwrite mode -func (v *View) ToggleOverwriteMode(usePlugin bool) bool { - if v.mainCursor() { - if usePlugin && !PreActionCall("ToggleOverwriteMode", v) { - return false - } - - v.isOverwriteMode = !v.isOverwriteMode - - if usePlugin { - return PostActionCall("ToggleOverwriteMode", v) - } - } - return false -} - -// Escape leaves current mode -func (v *View) Escape(usePlugin bool) bool { - if v.mainCursor() { - // check if user is searching, or the last search is still active - if searching || lastSearch != "" { - ExitSearch(v) - return true - } - // check if a prompt is shown, hide it and don't quit - if messenger.hasPrompt { - messenger.Reset() // FIXME - return true - } - } - - return false -} - -// Quit this will close the current tab or view that is open -func (v *View) Quit(usePlugin bool) bool { - if v.mainCursor() { - if usePlugin && !PreActionCall("Quit", v) { - return false - } - - // Make sure not to quit if there are unsaved changes - if v.CanClose() { - v.CloseBuffer() - if len(tabs[curTab].Views) > 1 { - v.splitNode.Delete() - tabs[v.TabNum].Cleanup() - tabs[v.TabNum].Resize() - } 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 { - t.SetNum(i) - } - if curTab >= len(tabs) { - curTab-- - } - if curTab == 0 { - CurView().ToggleTabbar() - } - } - } else { - if usePlugin { - PostActionCall("Quit", v) - } - - screen.Fini() - messenger.SaveHistory() - os.Exit(0) - } - } - - if usePlugin { - return PostActionCall("Quit", v) - } - } - return false -} - -// QuitAll quits the whole editor; all splits and tabs -func (v *View) QuitAll(usePlugin bool) bool { - if v.mainCursor() { - if usePlugin && !PreActionCall("QuitAll", v) { - return false - } - - closeAll := true - for _, tab := range tabs { - for _, v := range tab.Views { - if !v.CanClose() { - closeAll = false - } - } - } - - if closeAll { - // only quit if all of the buffers can be closed and the user confirms that they actually want to quit everything - shouldQuit, _ := messenger.YesNoPrompt("Do you want to quit micro (all open files will be closed)?") - - if shouldQuit { - for _, tab := range tabs { - for _, v := range tab.Views { - v.CloseBuffer() - } - } - - if usePlugin { - PostActionCall("QuitAll", v) - } - - screen.Fini() - messenger.SaveHistory() - os.Exit(0) - } - } - } - - return false -} - -// AddTab adds a new tab with an empty buffer -func (v *View) AddTab(usePlugin bool) bool { - if v.mainCursor() { - if usePlugin && !PreActionCall("AddTab", v) { - return false - } - - tab := NewTabFromView(NewView(NewBufferFromString("", ""))) - tab.SetNum(len(tabs)) - tabs = append(tabs, tab) - curTab = len(tabs) - 1 - if len(tabs) == 2 { - for _, t := range tabs { - for _, v := range t.Views { - v.ToggleTabbar() - } - } - } - - if usePlugin { - return PostActionCall("AddTab", v) - } - } - return true -} - -// PreviousTab switches to the previous tab in the tab list -func (v *View) PreviousTab(usePlugin bool) bool { - if v.mainCursor() { - if usePlugin && !PreActionCall("PreviousTab", v) { - return false - } - - if curTab > 0 { - curTab-- - } else if curTab == 0 { - curTab = len(tabs) - 1 - } - - if usePlugin { - return PostActionCall("PreviousTab", v) - } - } - return false -} - -// NextTab switches to the next tab in the tab list -func (v *View) NextTab(usePlugin bool) bool { - if v.mainCursor() { - if usePlugin && !PreActionCall("NextTab", v) { - return false - } - - if curTab < len(tabs)-1 { - curTab++ - } else if curTab == len(tabs)-1 { - curTab = 0 - } - - if usePlugin { - return PostActionCall("NextTab", v) - } - } - return false -} - -// VSplitBinding opens an empty vertical split -func (v *View) VSplitBinding(usePlugin bool) bool { - if v.mainCursor() { - if usePlugin && !PreActionCall("VSplit", v) { - return false - } - - v.VSplit(NewBufferFromString("", "")) - - if usePlugin { - return PostActionCall("VSplit", v) - } - } - return false -} - -// HSplitBinding opens an empty horizontal split -func (v *View) HSplitBinding(usePlugin bool) bool { - if v.mainCursor() { - if usePlugin && !PreActionCall("HSplit", v) { - return false - } - - v.HSplit(NewBufferFromString("", "")) - - if usePlugin { - return PostActionCall("HSplit", v) - } - } - return false -} - -// Unsplit closes all splits in the current tab except the active one -func (v *View) Unsplit(usePlugin bool) bool { - if v.mainCursor() { - if usePlugin && !PreActionCall("Unsplit", v) { - return false - } - - curView := tabs[curTab].CurView - for i := len(tabs[curTab].Views) - 1; i >= 0; i-- { - view := tabs[curTab].Views[i] - if view != nil && view.Num != curView { - view.Quit(true) - // messenger.Message("Quit ", view.Buf.Path) - } - } - - if usePlugin { - return PostActionCall("Unsplit", v) - } - } - return false -} - -// NextSplit changes the view to the next split -func (v *View) NextSplit(usePlugin bool) bool { - if v.mainCursor() { - if usePlugin && !PreActionCall("NextSplit", v) { - return false - } - - tab := tabs[curTab] - if tab.CurView < len(tab.Views)-1 { - tab.CurView++ - } else { - tab.CurView = 0 - } - - if usePlugin { - return PostActionCall("NextSplit", v) - } - } - return false -} - -// PreviousSplit changes the view to the previous split -func (v *View) PreviousSplit(usePlugin bool) bool { - if v.mainCursor() { - if usePlugin && !PreActionCall("PreviousSplit", v) { - return false - } - - tab := tabs[curTab] - if tab.CurView > 0 { - tab.CurView-- - } else { - tab.CurView = len(tab.Views) - 1 - } - - if usePlugin { - return PostActionCall("PreviousSplit", v) - } - } - return false -} - -var curMacro []interface{} -var recordingMacro bool - -// ToggleMacro toggles recording of a macro -func (v *View) ToggleMacro(usePlugin bool) bool { - if v.mainCursor() { - if usePlugin && !PreActionCall("ToggleMacro", v) { - return false - } - - recordingMacro = !recordingMacro - - if recordingMacro { - curMacro = []interface{}{} - messenger.Message("Recording") - } else { - messenger.Message("Stopped recording") - } - - if usePlugin { - return PostActionCall("ToggleMacro", v) - } - } - return true -} - -// PlayMacro plays back the most recently recorded macro -func (v *View) PlayMacro(usePlugin bool) bool { - if usePlugin && !PreActionCall("PlayMacro", v) { - return false - } - - for _, action := range curMacro { - switch t := action.(type) { - case rune: - // Insert a character - if v.Cursor.HasSelection() { - v.Cursor.DeleteSelection() - v.Cursor.ResetSelection() - } - v.Buf.Insert(v.Cursor.Loc, string(t)) - // v.Cursor.Right() - - for pl := range loadedPlugins { - _, err := Call(pl+".onRune", string(t), v) - if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") { - TermMessage(err) - } - } - case func(*View, bool) bool: - t(v, true) - } - } - - if usePlugin { - return PostActionCall("PlayMacro", v) - } - return true -} - -// SpawnMultiCursor creates a new multiple cursor at the next occurrence of the current selection or current word -func (v *View) SpawnMultiCursor(usePlugin bool) bool { - spawner := v.Buf.cursors[len(v.Buf.cursors)-1] - // You can only spawn a cursor from the main cursor - if v.Cursor == spawner { - if usePlugin && !PreActionCall("SpawnMultiCursor", v) { - return false - } - - if !spawner.HasSelection() { - spawner.SelectWord() - } else { - c := &Cursor{ - buf: v.Buf, - } - - sel := spawner.GetSelection() - - searchStart = spawner.CurSelection[1] - v.Cursor = c - Search(regexp.QuoteMeta(sel), v, true) - - for _, cur := range v.Buf.cursors { - if c.Loc == cur.Loc { - return false - } - } - v.Buf.cursors = append(v.Buf.cursors, c) - v.Buf.UpdateCursors() - v.Relocate() - v.Cursor = spawner - } - - if usePlugin { - PostActionCall("SpawnMultiCursor", v) - } - } - - return false -} - -// SpawnMultiCursorSelect adds a cursor at the beginning of each line of a selection -func (v *View) SpawnMultiCursorSelect(usePlugin bool) bool { - if v.Cursor == &v.Buf.Cursor { - if usePlugin && !PreActionCall("SpawnMultiCursorSelect", v) { - return false - } - - // Avoid cases where multiple cursors already exist, that would create problems - if len(v.Buf.cursors) > 1 { - return false - } - - var startLine int - var endLine int - - a, b := v.Cursor.CurSelection[0].Y, v.Cursor.CurSelection[1].Y - if a > b { - startLine, endLine = b, a - } else { - startLine, endLine = a, b - } - - if v.Cursor.HasSelection() { - v.Cursor.ResetSelection() - v.Cursor.GotoLoc(Loc{0, startLine}) - - for i := startLine; i <= endLine; i++ { - c := &Cursor{ - buf: v.Buf, - } - c.GotoLoc(Loc{0, i}) - v.Buf.cursors = append(v.Buf.cursors, c) - } - v.Buf.MergeCursors() - v.Buf.UpdateCursors() - } else { - return false - } - - if usePlugin { - PostActionCall("SpawnMultiCursorSelect", v) - } - - messenger.Message("Added cursors from selection") - } - return false -} - -// MouseMultiCursor is a mouse action which puts a new cursor at the mouse position -func (v *View) MouseMultiCursor(usePlugin bool, e *tcell.EventMouse) bool { - if v.Cursor == &v.Buf.Cursor { - if usePlugin && !PreActionCall("SpawnMultiCursorAtMouse", v, e) { - return false - } - x, y := e.Position() - x -= v.lineNumOffset - v.leftCol + v.x - y += v.Topline - v.y - - c := &Cursor{ - buf: v.Buf, - } - v.Cursor = c - v.MoveToMouseClick(x, y) - v.Relocate() - v.Cursor = &v.Buf.Cursor - - v.Buf.cursors = append(v.Buf.cursors, c) - v.Buf.MergeCursors() - v.Buf.UpdateCursors() - - if usePlugin { - PostActionCall("SpawnMultiCursorAtMouse", v) - } - } - return false -} - -// SkipMultiCursor moves the current multiple cursor to the next available position -func (v *View) SkipMultiCursor(usePlugin bool) bool { - cursor := v.Buf.cursors[len(v.Buf.cursors)-1] - - if v.mainCursor() { - if usePlugin && !PreActionCall("SkipMultiCursor", v) { - return false - } - sel := cursor.GetSelection() - - searchStart = cursor.CurSelection[1] - v.Cursor = cursor - Search(regexp.QuoteMeta(sel), v, true) - v.Relocate() - v.Cursor = cursor - - if usePlugin { - PostActionCall("SkipMultiCursor", v) - } - } - return false -} - -// RemoveMultiCursor removes the latest multiple cursor -func (v *View) RemoveMultiCursor(usePlugin bool) bool { - end := len(v.Buf.cursors) - if end > 1 { - if v.mainCursor() { - if usePlugin && !PreActionCall("RemoveMultiCursor", v) { - return false - } - - v.Buf.cursors[end-1] = nil - v.Buf.cursors = v.Buf.cursors[:end-1] - v.Buf.UpdateCursors() - v.Relocate() - - if usePlugin { - return PostActionCall("RemoveMultiCursor", v) - } - return true - } - } else { - v.RemoveAllMultiCursors(usePlugin) - } - return false -} - -// RemoveAllMultiCursors removes all cursors except the base cursor -func (v *View) RemoveAllMultiCursors(usePlugin bool) bool { - if v.mainCursor() { - if usePlugin && !PreActionCall("RemoveAllMultiCursors", v) { - return false - } - - v.Buf.clearCursors() - v.Relocate() - - if usePlugin { - return PostActionCall("RemoveAllMultiCursors", v) - } - return true - } - return false -} diff --git a/cmd/micro/actions_other.go b/cmd/micro/actions_other.go deleted file mode 100644 index 10a099ef..00000000 --- a/cmd/micro/actions_other.go +++ /dev/null @@ -1,9 +0,0 @@ -// +build plan9 nacl windows - -package main - -func (v *View) Suspend(usePlugin bool) bool { - messenger.Error("Suspend is only supported on Posix") - - return false -} diff --git a/cmd/micro/actions_posix.go b/cmd/micro/actions_posix.go deleted file mode 100644 index eb67d689..00000000 --- a/cmd/micro/actions_posix.go +++ /dev/null @@ -1,37 +0,0 @@ -// +build linux darwin dragonfly solaris openbsd netbsd freebsd - -package main - -import "syscall" - -// Suspend sends micro to the background. This is the same as pressing CtrlZ in most unix programs. -// This only works on linux and has no default binding. -// This code was adapted from the suspend code in nsf/godit -func (v *View) Suspend(usePlugin bool) bool { - if usePlugin && !PreActionCall("Suspend", v) { - return false - } - - screenWasNil := screen == nil - - if !screenWasNil { - screen.Fini() - screen = nil - } - - // suspend the process - pid := syscall.Getpid() - err := syscall.Kill(pid, syscall.SIGSTOP) - if err != nil { - TermMessage(err) - } - - if !screenWasNil { - InitScreen() - } - - if usePlugin { - return PostActionCall("Suspend", v) - } - return true -} diff --git a/cmd/micro/autocomplete.go b/cmd/micro/autocomplete.go deleted file mode 100644 index 3ecc3f16..00000000 --- a/cmd/micro/autocomplete.go +++ /dev/null @@ -1,249 +0,0 @@ -package main - -import ( - "io/ioutil" - "os" - "strings" -) - -var pluginCompletions []func(string) []string - -// This file is meant (for now) for autocompletion in command mode, not -// while coding. This helps micro autocomplete commands and then filenames -// for example with `vsplit filename`. - -// FileComplete autocompletes filenames -func FileComplete(input string) (string, []string) { - var sep string = string(os.PathSeparator) - dirs := strings.Split(input, sep) - - var files []os.FileInfo - var err error - if len(dirs) > 1 { - directories := strings.Join(dirs[:len(dirs)-1], sep) + sep - - directories = ReplaceHome(directories) - files, err = ioutil.ReadDir(directories) - } else { - files, err = ioutil.ReadDir(".") - } - - var suggestions []string - if err != nil { - return "", suggestions - } - for _, f := range files { - name := f.Name() - if f.IsDir() { - name += sep - } - if strings.HasPrefix(name, dirs[len(dirs)-1]) { - suggestions = append(suggestions, name) - } - } - - var chosen string - if len(suggestions) == 1 { - if len(dirs) > 1 { - chosen = strings.Join(dirs[:len(dirs)-1], sep) + sep + suggestions[0] - } else { - chosen = suggestions[0] - } - } else { - if len(dirs) > 1 { - chosen = strings.Join(dirs[:len(dirs)-1], sep) + sep - } - } - - return chosen, suggestions -} - -// CommandComplete autocompletes commands -func CommandComplete(input string) (string, []string) { - var suggestions []string - for cmd := range commands { - if strings.HasPrefix(cmd, input) { - suggestions = append(suggestions, cmd) - } - } - - var chosen string - if len(suggestions) == 1 { - chosen = suggestions[0] - } - return chosen, suggestions -} - -// HelpComplete autocompletes help topics -func HelpComplete(input string) (string, []string) { - var suggestions []string - - for _, file := range ListRuntimeFiles(RTHelp) { - topic := file.Name() - if strings.HasPrefix(topic, input) { - suggestions = append(suggestions, topic) - } - } - - var chosen string - if len(suggestions) == 1 { - chosen = suggestions[0] - } - return chosen, suggestions -} - -// ColorschemeComplete tab-completes names of colorschemes. -func ColorschemeComplete(input string) (string, []string) { - var suggestions []string - files := ListRuntimeFiles(RTColorscheme) - - for _, f := range files { - if strings.HasPrefix(f.Name(), input) { - suggestions = append(suggestions, f.Name()) - } - } - - var chosen string - if len(suggestions) == 1 { - chosen = suggestions[0] - } - - return chosen, suggestions -} - -func contains(s []string, e string) bool { - for _, a := range s { - if a == e { - return true - } - } - return false -} - -// OptionComplete autocompletes options -func OptionComplete(input string) (string, []string) { - var suggestions []string - localSettings := DefaultLocalSettings() - for option := range globalSettings { - if strings.HasPrefix(option, input) { - suggestions = append(suggestions, option) - } - } - for option := range localSettings { - if strings.HasPrefix(option, input) && !contains(suggestions, option) { - suggestions = append(suggestions, option) - } - } - - var chosen string - if len(suggestions) == 1 { - chosen = suggestions[0] - } - return chosen, suggestions -} - -// OptionValueComplete completes values for various options -func OptionValueComplete(inputOpt, input string) (string, []string) { - inputOpt = strings.TrimSpace(inputOpt) - var suggestions []string - localSettings := DefaultLocalSettings() - var optionVal interface{} - for k, option := range globalSettings { - if k == inputOpt { - optionVal = option - } - } - for k, option := range localSettings { - if k == inputOpt { - optionVal = option - } - } - - switch optionVal.(type) { - case bool: - if strings.HasPrefix("on", input) { - suggestions = append(suggestions, "on") - } else if strings.HasPrefix("true", input) { - suggestions = append(suggestions, "true") - } - if strings.HasPrefix("off", input) { - suggestions = append(suggestions, "off") - } else if strings.HasPrefix("false", input) { - suggestions = append(suggestions, "false") - } - case string: - switch inputOpt { - case "colorscheme": - _, suggestions = ColorschemeComplete(input) - case "fileformat": - if strings.HasPrefix("unix", input) { - suggestions = append(suggestions, "unix") - } - if strings.HasPrefix("dos", input) { - suggestions = append(suggestions, "dos") - } - case "sucmd": - if strings.HasPrefix("sudo", input) { - suggestions = append(suggestions, "sudo") - } - if strings.HasPrefix("doas", input) { - suggestions = append(suggestions, "doas") - } - } - } - - var chosen string - if len(suggestions) == 1 { - chosen = suggestions[0] - } - return chosen, suggestions -} - -// MakeCompletion registers a function from a plugin for autocomplete commands -func MakeCompletion(function string) Completion { - pluginCompletions = append(pluginCompletions, LuaFunctionComplete(function)) - return Completion(-len(pluginCompletions)) -} - -// PluginComplete autocompletes from plugin function -func PluginComplete(complete Completion, input string) (chosen string, suggestions []string) { - idx := int(-complete) - 1 - - if len(pluginCompletions) <= idx { - return "", nil - } - suggestions = pluginCompletions[idx](input) - - if len(suggestions) == 1 { - chosen = suggestions[0] - } - return -} - -// PluginCmdComplete completes with possible choices for the `> plugin` command -func PluginCmdComplete(input string) (chosen string, suggestions []string) { - for _, cmd := range []string{"install", "remove", "search", "update", "list"} { - if strings.HasPrefix(cmd, input) { - suggestions = append(suggestions, cmd) - } - } - - if len(suggestions) == 1 { - chosen = suggestions[0] - } - return chosen, suggestions -} - -// PluginnameComplete completes with the names of loaded plugins -func PluginNameComplete(input string) (chosen string, suggestions []string) { - for _, pp := range GetAllPluginPackages() { - if strings.HasPrefix(pp.Name, input) { - suggestions = append(suggestions, pp.Name) - } - } - - if len(suggestions) == 1 { - chosen = suggestions[0] - } - return chosen, suggestions -} diff --git a/cmd/micro/bindings.go b/cmd/micro/bindings.go deleted file mode 100644 index e3a02841..00000000 --- a/cmd/micro/bindings.go +++ /dev/null @@ -1,616 +0,0 @@ -package main - -import ( - "fmt" - "io/ioutil" - "os" - "strings" - "unicode" - - "github.com/flynn/json5" - "github.com/zyedidia/tcell" -) - -var bindingsStr map[string]string -var bindings map[Key][]func(*View, bool) bool -var mouseBindings map[Key][]func(*View, bool, *tcell.EventMouse) bool -var helpBinding string -var kmenuBinding string - -var mouseBindingActions = map[string]func(*View, bool, *tcell.EventMouse) bool{ - "MousePress": (*View).MousePress, - "MouseMultiCursor": (*View).MouseMultiCursor, -} - -var bindingActions = map[string]func(*View, bool) bool{ - "CursorUp": (*View).CursorUp, - "CursorDown": (*View).CursorDown, - "CursorPageUp": (*View).CursorPageUp, - "CursorPageDown": (*View).CursorPageDown, - "CursorLeft": (*View).CursorLeft, - "CursorRight": (*View).CursorRight, - "CursorStart": (*View).CursorStart, - "CursorEnd": (*View).CursorEnd, - "SelectToStart": (*View).SelectToStart, - "SelectToEnd": (*View).SelectToEnd, - "SelectUp": (*View).SelectUp, - "SelectDown": (*View).SelectDown, - "SelectLeft": (*View).SelectLeft, - "SelectRight": (*View).SelectRight, - "WordRight": (*View).WordRight, - "WordLeft": (*View).WordLeft, - "SelectWordRight": (*View).SelectWordRight, - "SelectWordLeft": (*View).SelectWordLeft, - "DeleteWordRight": (*View).DeleteWordRight, - "DeleteWordLeft": (*View).DeleteWordLeft, - "SelectLine": (*View).SelectLine, - "SelectToStartOfLine": (*View).SelectToStartOfLine, - "SelectToEndOfLine": (*View).SelectToEndOfLine, - "ParagraphPrevious": (*View).ParagraphPrevious, - "ParagraphNext": (*View).ParagraphNext, - "InsertNewline": (*View).InsertNewline, - "InsertSpace": (*View).InsertSpace, - "Backspace": (*View).Backspace, - "Delete": (*View).Delete, - "InsertTab": (*View).InsertTab, - "Save": (*View).Save, - "SaveAll": (*View).SaveAll, - "SaveAs": (*View).SaveAs, - "Find": (*View).Find, - "FindNext": (*View).FindNext, - "FindPrevious": (*View).FindPrevious, - "Center": (*View).Center, - "Undo": (*View).Undo, - "Redo": (*View).Redo, - "Copy": (*View).Copy, - "Cut": (*View).Cut, - "CutLine": (*View).CutLine, - "DuplicateLine": (*View).DuplicateLine, - "DeleteLine": (*View).DeleteLine, - "MoveLinesUp": (*View).MoveLinesUp, - "MoveLinesDown": (*View).MoveLinesDown, - "IndentSelection": (*View).IndentSelection, - "OutdentSelection": (*View).OutdentSelection, - "OutdentLine": (*View).OutdentLine, - "Paste": (*View).Paste, - "PastePrimary": (*View).PastePrimary, - "SelectAll": (*View).SelectAll, - "OpenFile": (*View).OpenFile, - "Start": (*View).Start, - "End": (*View).End, - "PageUp": (*View).PageUp, - "PageDown": (*View).PageDown, - "SelectPageUp": (*View).SelectPageUp, - "SelectPageDown": (*View).SelectPageDown, - "HalfPageUp": (*View).HalfPageUp, - "HalfPageDown": (*View).HalfPageDown, - "StartOfLine": (*View).StartOfLine, - "EndOfLine": (*View).EndOfLine, - "ToggleHelp": (*View).ToggleHelp, - "ToggleKeyMenu": (*View).ToggleKeyMenu, - "ToggleRuler": (*View).ToggleRuler, - "JumpLine": (*View).JumpLine, - "ClearStatus": (*View).ClearStatus, - "ShellMode": (*View).ShellMode, - "CommandMode": (*View).CommandMode, - "ToggleOverwriteMode": (*View).ToggleOverwriteMode, - "Escape": (*View).Escape, - "Quit": (*View).Quit, - "QuitAll": (*View).QuitAll, - "AddTab": (*View).AddTab, - "PreviousTab": (*View).PreviousTab, - "NextTab": (*View).NextTab, - "NextSplit": (*View).NextSplit, - "PreviousSplit": (*View).PreviousSplit, - "Unsplit": (*View).Unsplit, - "VSplit": (*View).VSplitBinding, - "HSplit": (*View).HSplitBinding, - "ToggleMacro": (*View).ToggleMacro, - "PlayMacro": (*View).PlayMacro, - "Suspend": (*View).Suspend, - "ScrollUp": (*View).ScrollUpAction, - "ScrollDown": (*View).ScrollDownAction, - "SpawnMultiCursor": (*View).SpawnMultiCursor, - "SpawnMultiCursorSelect": (*View).SpawnMultiCursorSelect, - "RemoveMultiCursor": (*View).RemoveMultiCursor, - "RemoveAllMultiCursors": (*View).RemoveAllMultiCursors, - "SkipMultiCursor": (*View).SkipMultiCursor, - "JumpToMatchingBrace": (*View).JumpToMatchingBrace, - - // This was changed to InsertNewline but I don't want to break backwards compatibility - "InsertEnter": (*View).InsertNewline, -} - -var bindingMouse = map[string]tcell.ButtonMask{ - "MouseLeft": tcell.Button1, - "MouseMiddle": tcell.Button2, - "MouseRight": tcell.Button3, - "MouseWheelUp": tcell.WheelUp, - "MouseWheelDown": tcell.WheelDown, - "MouseWheelLeft": tcell.WheelLeft, - "MouseWheelRight": tcell.WheelRight, -} - -var bindingKeys = map[string]tcell.Key{ - "Up": tcell.KeyUp, - "Down": tcell.KeyDown, - "Right": tcell.KeyRight, - "Left": tcell.KeyLeft, - "UpLeft": tcell.KeyUpLeft, - "UpRight": tcell.KeyUpRight, - "DownLeft": tcell.KeyDownLeft, - "DownRight": tcell.KeyDownRight, - "Center": tcell.KeyCenter, - "PageUp": tcell.KeyPgUp, - "PageDown": tcell.KeyPgDn, - "Home": tcell.KeyHome, - "End": tcell.KeyEnd, - "Insert": tcell.KeyInsert, - "Delete": tcell.KeyDelete, - "Help": tcell.KeyHelp, - "Exit": tcell.KeyExit, - "Clear": tcell.KeyClear, - "Cancel": tcell.KeyCancel, - "Print": tcell.KeyPrint, - "Pause": tcell.KeyPause, - "Backtab": tcell.KeyBacktab, - "F1": tcell.KeyF1, - "F2": tcell.KeyF2, - "F3": tcell.KeyF3, - "F4": tcell.KeyF4, - "F5": tcell.KeyF5, - "F6": tcell.KeyF6, - "F7": tcell.KeyF7, - "F8": tcell.KeyF8, - "F9": tcell.KeyF9, - "F10": tcell.KeyF10, - "F11": tcell.KeyF11, - "F12": tcell.KeyF12, - "F13": tcell.KeyF13, - "F14": tcell.KeyF14, - "F15": tcell.KeyF15, - "F16": tcell.KeyF16, - "F17": tcell.KeyF17, - "F18": tcell.KeyF18, - "F19": tcell.KeyF19, - "F20": tcell.KeyF20, - "F21": tcell.KeyF21, - "F22": tcell.KeyF22, - "F23": tcell.KeyF23, - "F24": tcell.KeyF24, - "F25": tcell.KeyF25, - "F26": tcell.KeyF26, - "F27": tcell.KeyF27, - "F28": tcell.KeyF28, - "F29": tcell.KeyF29, - "F30": tcell.KeyF30, - "F31": tcell.KeyF31, - "F32": tcell.KeyF32, - "F33": tcell.KeyF33, - "F34": tcell.KeyF34, - "F35": tcell.KeyF35, - "F36": tcell.KeyF36, - "F37": tcell.KeyF37, - "F38": tcell.KeyF38, - "F39": tcell.KeyF39, - "F40": tcell.KeyF40, - "F41": tcell.KeyF41, - "F42": tcell.KeyF42, - "F43": tcell.KeyF43, - "F44": tcell.KeyF44, - "F45": tcell.KeyF45, - "F46": tcell.KeyF46, - "F47": tcell.KeyF47, - "F48": tcell.KeyF48, - "F49": tcell.KeyF49, - "F50": tcell.KeyF50, - "F51": tcell.KeyF51, - "F52": tcell.KeyF52, - "F53": tcell.KeyF53, - "F54": tcell.KeyF54, - "F55": tcell.KeyF55, - "F56": tcell.KeyF56, - "F57": tcell.KeyF57, - "F58": tcell.KeyF58, - "F59": tcell.KeyF59, - "F60": tcell.KeyF60, - "F61": tcell.KeyF61, - "F62": tcell.KeyF62, - "F63": tcell.KeyF63, - "F64": tcell.KeyF64, - "CtrlSpace": tcell.KeyCtrlSpace, - "CtrlA": tcell.KeyCtrlA, - "CtrlB": tcell.KeyCtrlB, - "CtrlC": tcell.KeyCtrlC, - "CtrlD": tcell.KeyCtrlD, - "CtrlE": tcell.KeyCtrlE, - "CtrlF": tcell.KeyCtrlF, - "CtrlG": tcell.KeyCtrlG, - "CtrlH": tcell.KeyCtrlH, - "CtrlI": tcell.KeyCtrlI, - "CtrlJ": tcell.KeyCtrlJ, - "CtrlK": tcell.KeyCtrlK, - "CtrlL": tcell.KeyCtrlL, - "CtrlM": tcell.KeyCtrlM, - "CtrlN": tcell.KeyCtrlN, - "CtrlO": tcell.KeyCtrlO, - "CtrlP": tcell.KeyCtrlP, - "CtrlQ": tcell.KeyCtrlQ, - "CtrlR": tcell.KeyCtrlR, - "CtrlS": tcell.KeyCtrlS, - "CtrlT": tcell.KeyCtrlT, - "CtrlU": tcell.KeyCtrlU, - "CtrlV": tcell.KeyCtrlV, - "CtrlW": tcell.KeyCtrlW, - "CtrlX": tcell.KeyCtrlX, - "CtrlY": tcell.KeyCtrlY, - "CtrlZ": tcell.KeyCtrlZ, - "CtrlLeftSq": tcell.KeyCtrlLeftSq, - "CtrlBackslash": tcell.KeyCtrlBackslash, - "CtrlRightSq": tcell.KeyCtrlRightSq, - "CtrlCarat": tcell.KeyCtrlCarat, - "CtrlUnderscore": tcell.KeyCtrlUnderscore, - "CtrlPageUp": tcell.KeyCtrlPgUp, - "CtrlPageDown": tcell.KeyCtrlPgDn, - "Tab": tcell.KeyTab, - "Esc": tcell.KeyEsc, - "Escape": tcell.KeyEscape, - "Enter": tcell.KeyEnter, - "Backspace": tcell.KeyBackspace2, - "OldBackspace": tcell.KeyBackspace, - - // I renamed these keys to PageUp and PageDown but I don't want to break someone's keybindings - "PgUp": tcell.KeyPgUp, - "PgDown": tcell.KeyPgDn, -} - -// The Key struct holds the data for a keypress (keycode + modifiers) -type Key struct { - keyCode tcell.Key - modifiers tcell.ModMask - buttons tcell.ButtonMask - r rune - escape string -} - -// InitBindings initializes the keybindings for micro -func InitBindings() { - bindings = make(map[Key][]func(*View, bool) bool) - bindingsStr = make(map[string]string) - mouseBindings = make(map[Key][]func(*View, bool, *tcell.EventMouse) bool) - - var parsed map[string]string - defaults := DefaultBindings() - - filename := configDir + "/bindings.json" - if _, e := os.Stat(filename); e == nil { - input, err := ioutil.ReadFile(filename) - if err != nil { - TermMessage("Error reading bindings.json file: " + err.Error()) - return - } - - err = json5.Unmarshal(input, &parsed) - if err != nil { - TermMessage("Error reading bindings.json:", err.Error()) - } - } - - parseBindings(defaults) - parseBindings(parsed) -} - -func parseBindings(userBindings map[string]string) { - for k, v := range userBindings { - BindKey(k, v) - } -} - -// findKey will find binding Key 'b' using string 'k' -func findKey(k string) (b Key, ok bool) { - modifiers := tcell.ModNone - - // First, we'll strip off all the modifiers in the name and add them to the - // ModMask -modSearch: - for { - switch { - case strings.HasPrefix(k, "-"): - // We optionally support dashes between modifiers - k = k[1:] - case strings.HasPrefix(k, "Ctrl") && k != "CtrlH": - // CtrlH technically does not have a 'Ctrl' modifier because it is really backspace - k = k[4:] - modifiers |= tcell.ModCtrl - case strings.HasPrefix(k, "Alt"): - k = k[3:] - modifiers |= tcell.ModAlt - case strings.HasPrefix(k, "Shift"): - k = k[5:] - modifiers |= tcell.ModShift - case strings.HasPrefix(k, "\x1b"): - return Key{ - keyCode: -1, - modifiers: modifiers, - buttons: -1, - r: 0, - escape: k, - }, true - default: - break modSearch - } - } - - if len(k) == 0 { - return Key{buttons: -1}, false - } - - // Control is handled specially, since some character codes in bindingKeys - // are different when Control is depressed. We should check for Control keys - // first. - if modifiers&tcell.ModCtrl != 0 { - // see if the key is in bindingKeys with the Ctrl prefix. - k = string(unicode.ToUpper(rune(k[0]))) + k[1:] - if code, ok := bindingKeys["Ctrl"+k]; ok { - // It is, we're done. - return Key{ - keyCode: code, - modifiers: modifiers, - buttons: -1, - r: 0, - }, true - } - } - - // See if we can find the key in bindingKeys - if code, ok := bindingKeys[k]; ok { - return Key{ - keyCode: code, - modifiers: modifiers, - buttons: -1, - r: 0, - }, true - } - - // See if we can find the key in bindingMouse - if code, ok := bindingMouse[k]; ok { - return Key{ - modifiers: modifiers, - buttons: code, - r: 0, - }, true - } - - // If we were given one character, then we've got a rune. - if len(k) == 1 { - return Key{ - keyCode: tcell.KeyRune, - modifiers: modifiers, - buttons: -1, - r: rune(k[0]), - }, true - } - - // We don't know what happened. - return Key{buttons: -1}, false -} - -// findAction will find 'action' using string 'v' -func findAction(v string) (action func(*View, bool) bool) { - action, ok := bindingActions[v] - if !ok { - // If the user seems to be binding a function that doesn't exist - // We hope that it's a lua function that exists and bind it to that - action = LuaFunctionBinding(v) - } - return action -} - -func findMouseAction(v string) func(*View, bool, *tcell.EventMouse) bool { - action, ok := mouseBindingActions[v] - if !ok { - // If the user seems to be binding a function that doesn't exist - // We hope that it's a lua function that exists and bind it to that - action = LuaFunctionMouseBinding(v) - } - return action -} - -// TryBindKey tries to bind a key by writing to configDir/bindings.json -// This function is unused for now -func TryBindKey(k, v string) { - filename := configDir + "/bindings.json" - if _, e := os.Stat(filename); e == nil { - input, err := ioutil.ReadFile(filename) - if err != nil { - TermMessage("Error reading bindings.json file: " + err.Error()) - return - } - - conflict := -1 - lines := strings.Split(string(input), "\n") - for i, l := range lines { - parts := strings.Split(l, ":") - if len(parts) >= 2 { - if strings.Contains(parts[0], k) { - conflict = i - TermMessage("Warning: Keybinding conflict:", k, " has been overwritten") - } - } - } - - binding := fmt.Sprintf(" \"%s\": \"%s\",", k, v) - if conflict == -1 { - lines = append([]string{lines[0], binding}, lines[conflict:]...) - } else { - lines = append(append(lines[:conflict], binding), lines[conflict+1:]...) - } - txt := strings.Join(lines, "\n") - err = ioutil.WriteFile(filename, []byte(txt), 0644) - if err != nil { - TermMessage("Error") - } - } -} - -// BindKey takes a key and an action and binds the two together -func BindKey(k, v string) { - key, ok := findKey(k) - if !ok { - TermMessage("Unknown keybinding: " + k) - return - } - if v == "ToggleHelp" { - helpBinding = k - } - if v == "ToggleKeyMenu" { - kmenuBinding = k - } - if helpBinding == k && v != "ToggleHelp" { - helpBinding = "" - } - if kmenuBinding == k && v != "ToggleKeyMenu" { - kmenuBinding = "" - } - - actionNames := strings.Split(v, ",") - if actionNames[0] == "UnbindKey" { - delete(bindings, key) - delete(mouseBindings, key) - delete(bindingsStr, k) - if len(actionNames) == 1 { - return - } - actionNames = append(actionNames[:0], actionNames[1:]...) - } - actions := make([]func(*View, bool) bool, 0, len(actionNames)) - mouseActions := make([]func(*View, bool, *tcell.EventMouse) bool, 0, len(actionNames)) - for _, actionName := range actionNames { - if strings.HasPrefix(actionName, "Mouse") { - mouseActions = append(mouseActions, findMouseAction(actionName)) - } else if strings.HasPrefix(actionName, "command:") { - cmd := strings.SplitN(actionName, ":", 2)[1] - actions = append(actions, CommandAction(cmd)) - } else if strings.HasPrefix(actionName, "command-edit:") { - cmd := strings.SplitN(actionName, ":", 2)[1] - actions = append(actions, CommandEditAction(cmd)) - } else { - actions = append(actions, findAction(actionName)) - } - } - - if len(actions) > 0 { - // Can't have a binding be both mouse and normal - delete(mouseBindings, key) - bindings[key] = actions - bindingsStr[k] = v - } else if len(mouseActions) > 0 { - // Can't have a binding be both mouse and normal - delete(bindings, key) - mouseBindings[key] = mouseActions - } -} - -// DefaultBindings returns a map containing micro's default keybindings -func DefaultBindings() map[string]string { - return map[string]string{ - "Up": "CursorUp", - "Down": "CursorDown", - "Right": "CursorRight", - "Left": "CursorLeft", - "ShiftUp": "SelectUp", - "ShiftDown": "SelectDown", - "ShiftLeft": "SelectLeft", - "ShiftRight": "SelectRight", - "AltLeft": "WordLeft", - "AltRight": "WordRight", - "AltUp": "MoveLinesUp", - "AltDown": "MoveLinesDown", - "AltShiftRight": "SelectWordRight", - "AltShiftLeft": "SelectWordLeft", - "CtrlLeft": "StartOfLine", - "CtrlRight": "EndOfLine", - "CtrlShiftLeft": "SelectToStartOfLine", - "ShiftHome": "SelectToStartOfLine", - "CtrlShiftRight": "SelectToEndOfLine", - "ShiftEnd": "SelectToEndOfLine", - "CtrlUp": "CursorStart", - "CtrlDown": "CursorEnd", - "CtrlShiftUp": "SelectToStart", - "CtrlShiftDown": "SelectToEnd", - "Alt-{": "ParagraphPrevious", - "Alt-}": "ParagraphNext", - "Enter": "InsertNewline", - "CtrlH": "Backspace", - "Backspace": "Backspace", - "Alt-CtrlH": "DeleteWordLeft", - "Alt-Backspace": "DeleteWordLeft", - "Tab": "IndentSelection,InsertTab", - "Backtab": "OutdentSelection,OutdentLine", - "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", - "CtrlT": "AddTab", - "Alt,": "PreviousTab", - "Alt.": "NextTab", - "Home": "StartOfLine", - "End": "EndOfLine", - "CtrlHome": "CursorStart", - "CtrlEnd": "CursorEnd", - "PageUp": "CursorPageUp", - "PageDown": "CursorPageDown", - "CtrlPageUp": "PreviousTab", - "CtrlPageDown": "NextTab", - "CtrlG": "ToggleHelp", - "Alt-g": "ToggleKeyMenu", - "CtrlR": "ToggleRuler", - "CtrlL": "JumpLine", - "Delete": "Delete", - "CtrlB": "ShellMode", - "CtrlQ": "Quit", - "CtrlE": "CommandMode", - "CtrlW": "NextSplit", - "CtrlU": "ToggleMacro", - "CtrlJ": "PlayMacro", - "Insert": "ToggleOverwriteMode", - - // Emacs-style keybindings - "Alt-f": "WordRight", - "Alt-b": "WordLeft", - "Alt-a": "StartOfLine", - "Alt-e": "EndOfLine", - // "Alt-p": "CursorUp", - // "Alt-n": "CursorDown", - - // Integration with file managers - "F2": "Save", - "F3": "Find", - "F4": "Quit", - "F7": "Find", - "F10": "Quit", - "Esc": "Escape", - - // Mouse bindings - "MouseWheelUp": "ScrollUp", - "MouseWheelDown": "ScrollDown", - "MouseLeft": "MousePress", - "MouseMiddle": "PastePrimary", - "Ctrl-MouseLeft": "MouseMultiCursor", - - "Alt-n": "SpawnMultiCursor", - "Alt-m": "SpawnMultiCursorSelect", - "Alt-p": "RemoveMultiCursor", - "Alt-c": "RemoveAllMultiCursors", - "Alt-x": "SkipMultiCursor", - } -} diff --git a/cmd/micro/buffer.go b/cmd/micro/buffer.go index c0161b34..747ace3d 100644 --- a/cmd/micro/buffer.go +++ b/cmd/micro/buffer.go @@ -4,44 +4,43 @@ import ( "bufio" "bytes" "crypto/md5" - "encoding/gob" "errors" "io" "io/ioutil" + "log" "os" "os/exec" "os/signal" "path/filepath" - "strconv" "strings" "time" - "unicode" "unicode/utf8" "github.com/zyedidia/micro/cmd/micro/highlight" ) +// LargeFileThreshold is the number of bytes when fastdirty is forced +// because hashing is too slow const LargeFileThreshold = 50000 +// The BufType defines what kind of buffer this is +type BufType struct { + Kind int + Readonly bool // The file cannot be edited + Scratch bool // The file cannot be saved +} + var ( - // 0 - no line type detected - // 1 - lf detected - // 2 - crlf detected - fileformat = 0 + btDefault = BufType{0, false, false} + btHelp = BufType{1, true, true} + btLog = BufType{2, true, true} + btScratch = BufType{3, false, true} + btRaw = BufType{4, true, true} ) -// Buffer stores the text for files that are loaded into the text editor -// It uses a rope to efficiently store the string and contains some -// simple functions for saving and wrapper functions for modifying the rope 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 - cursors []*Cursor // for multiple cursors - curCursor int // the current cursor + *EventHandler // Path to the file on disk Path string @@ -51,30 +50,22 @@ type Buffer struct { name string // Whether or not the buffer has been modified since it was opened - IsModified bool + isModified bool // Stores the last modification time of the file the buffer is pointing to ModTime time.Time - // NumLines is the number of lines in the buffer - NumLines int - syntaxDef *highlight.Def highlighter *highlight.Highlighter // Hash of the original buffer -- empty if fastdirty is on origHash [md5.Size]byte - // Buffer local settings + // Settings customized by the user Settings map[string]interface{} -} -// 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 - ModTime time.Time + // Type of the buffer (e.g. help, raw, scratch etc..) + Type BufType } // NewBufferFromFile opens a new buffer using the given path @@ -82,8 +73,13 @@ type SerializedBuffer struct { // It will return an empty buffer if the path does not exist // and an error if the file is a directory func NewBufferFromFile(path string) (*Buffer, error) { + var err error filename, cursorPosition := GetPathAndCursorPosition(path) - filename = ReplaceHome(filename) + filename, err = ReplaceHome(filename) + if err != nil { + return nil, err + } + file, err := os.Open(filename) fileInfo, _ := os.Stat(filename) @@ -110,20 +106,10 @@ func NewBufferFromString(text, path string) *Buffer { } // NewBuffer creates a new buffer from a given reader with a given path +// Ensure that ReadSettings and InitGlobalSettings have been called before creating +// a new buffer func NewBuffer(reader io.Reader, size int64, path string, cursorPosition []string) *Buffer { - // check if the file is already open in a tab. If it's open return the buffer to that tab - if path != "" { - for _, tab := range tabs { - for _, view := range tab.Views { - if view.Buf.Path == path { - return view.Buf - } - } - } - } - b := new(Buffer) - b.LineArray = NewLineArray(size, reader) b.Settings = DefaultLocalSettings() for k, v := range globalSettings { @@ -131,12 +117,9 @@ func NewBuffer(reader io.Reader, size int64, path string, cursorPosition []strin b.Settings[k] = v } } + InitLocalSettings(b) - if fileformat == 1 { - b.Settings["fileformat"] = "unix" - } else if fileformat == 2 { - b.Settings["fileformat"] = "dos" - } + b.LineArray = NewLineArray(uint64(size), FFAuto, reader) absPath, _ := filepath.Abs(path) @@ -148,110 +131,21 @@ func NewBuffer(reader io.Reader, size int64, path string, cursorPosition []strin b.EventHandler = NewEventHandler(b) - b.Update() b.UpdateRules() - - if _, err := os.Stat(configDir + "/buffers/"); os.IsNotExist(err) { - os.Mkdir(configDir+"/buffers/", os.ModePerm) - } - - cursorLocation, cursorLocationError := GetBufferCursorLocation(cursorPosition, b) - b.Cursor = Cursor{ - Loc: cursorLocation, - buf: b, - } - - InitLocalSettings(b) - - if cursorLocationError != nil && len(*flagStartPos) == 0 && (b.Settings["savecursor"].(bool) || b.Settings["saveundo"].(bool)) { - // If either savecursor or saveundo is turned on, we need to load the serialized information - // from ~/.config/micro/buffers - file, err := os.Open(configDir + "/buffers/" + EscapePath(b.AbsPath)) - defer file.Close() - if err == nil { - var buffer SerializedBuffer - decoder := gob.NewDecoder(file) - gob.Register(TextEvent{}) - err = decoder.Decode(&buffer) - if err != nil { - TermMessage(err.Error(), "\n", "You may want to remove the files in ~/.config/micro/buffers (these files store the information for the 'saveundo' and 'savecursor' options) if this problem persists.") - } - if b.Settings["savecursor"].(bool) { - b.Cursor = buffer.Cursor - b.Cursor.buf = b - b.Cursor.Relocate() - } - - if b.Settings["saveundo"].(bool) { - // We should only use last time's eventhandler if the file wasn't modified by someone else in the meantime - if b.ModTime == buffer.ModTime { - b.EventHandler = buffer.EventHandler - b.EventHandler.buf = b - } - } - } - } + log.Println("Filetype detected: ", b.Settings["filetype"]) if !b.Settings["fastdirty"].(bool) { if size > LargeFileThreshold { - // If the file is larger than a megabyte fastdirty needs to be on + // If the file is larger than LargeFileThreshold fastdirty needs to be on b.Settings["fastdirty"] = true } else { calcHash(b, &b.origHash) } } - b.cursors = []*Cursor{&b.Cursor} - return b } -func GetBufferCursorLocation(cursorPosition []string, b *Buffer) (Loc, error) { - // parse the cursor position. The cursor location is ALWAYS initialised to 0, 0 even when - // an error occurs due to lack of arguments or because the arguments are not numbers - cursorLocation, cursorLocationError := ParseCursorLocation(cursorPosition) - - // Put the cursor at the first spot. In the logic for cursor position the -startpos - // flag is processed first and will overwrite any line/col parameters with colons after the filename - if len(*flagStartPos) > 0 || cursorLocationError == nil { - var lineNum, colNum int - var errPos1, errPos2 error - - positions := strings.Split(*flagStartPos, ",") - - // if the -startpos flag contains enough args use them for the cursor location. - // In this case args passed at the end of the filename will be ignored - if len(positions) == 2 { - lineNum, errPos1 = strconv.Atoi(positions[0]) - colNum, errPos2 = strconv.Atoi(positions[1]) - } - // if -startpos has invalid arguments, use the arguments from filename. - // This will have a default value (0, 0) even when the filename arguments are invalid - if errPos1 != nil || errPos2 != nil || len(*flagStartPos) == 0 { - // otherwise check if there are any arguments after the filename and use them - lineNum = cursorLocation.Y - colNum = cursorLocation.X - } - - // if some arguments were found make sure they don't go outside the file and cause overflows - cursorLocation.Y = lineNum - 1 - cursorLocation.X = colNum - // Check to avoid line overflow - if cursorLocation.Y > b.NumLines-1 { - cursorLocation.Y = b.NumLines - 1 - } else if cursorLocation.Y < 0 { - cursorLocation.Y = 0 - } - // Check to avoid column overflow - if cursorLocation.X > len(b.Line(cursorLocation.Y)) { - cursorLocation.X = len(b.Line(cursorLocation.Y)) - } else if cursorLocation.X < 0 { - cursorLocation.X = 0 - } - } - return cursorLocation, cursorLocationError -} - // GetName returns the name that should be displayed in the statusline // for this buffer func (b *Buffer) GetName() string { @@ -264,6 +158,288 @@ func (b *Buffer) GetName() string { return b.name } +// FileType returns the buffer's filetype +func (b *Buffer) FileType() string { + return b.Settings["filetype"].(string) +} + +// ReOpen reloads the current buffer from disk +func (b *Buffer) ReOpen() error { + data, err := ioutil.ReadFile(b.Path) + txt := string(data) + + if err != nil { + return err + } + b.EventHandler.ApplyDiff(txt) + + b.ModTime, err = GetModTime(b.Path) + b.isModified = false + return err + // TODO: buffer cursor + // b.Cursor.Relocate() +} + +// Saving + +// Save saves the buffer to its default path +func (b *Buffer) Save() error { + return b.SaveAs(b.Path) +} + +// SaveAs saves the buffer to a specified path (filename), creating the file if it does not exist +func (b *Buffer) SaveAs(filename string) error { + // TODO: rmtrailingws and updaterules + b.UpdateRules() + // if b.Settings["rmtrailingws"].(bool) { + // for i, l := range b.lines { + // pos := len(bytes.TrimRightFunc(l.data, unicode.IsSpace)) + // + // if pos < len(l.data) { + // b.deleteToEnd(Loc{pos, i}) + // } + // } + // + // b.Cursor.Relocate() + // } + + if b.Settings["eofnewline"].(bool) { + end := b.End() + if b.RuneAt(Loc{end.X - 1, end.Y}) != '\n' { + b.Insert(end, "\n") + } + } + + // Update the last time this file was updated after saving + defer func() { + b.ModTime, _ = GetModTime(filename) + }() + + // Removes any tilde and replaces with the absolute path to home + absFilename, _ := ReplaceHome(filename) + + // TODO: save creates parent dirs + // // Get the leading path to the file | "." is returned if there's no leading path provided + // if dirname := filepath.Dir(absFilename); dirname != "." { + // // Check if the parent dirs don't exist + // if _, statErr := os.Stat(dirname); os.IsNotExist(statErr) { + // // Prompt to make sure they want to create the dirs that are missing + // if yes, canceled := messenger.YesNoPrompt("Parent folders \"" + dirname + "\" do not exist. Create them? (y,n)"); yes && !canceled { + // // Create all leading dir(s) since they don't exist + // if mkdirallErr := os.MkdirAll(dirname, os.ModePerm); mkdirallErr != nil { + // // If there was an error creating the dirs + // return mkdirallErr + // } + // } else { + // // If they canceled the creation of leading dirs + // return errors.New("Save aborted") + // } + // } + // } + + var fileSize int + + err := overwriteFile(absFilename, func(file io.Writer) (e error) { + if len(b.lines) == 0 { + return + } + + // end of line + var eol []byte + if b.Settings["fileformat"] == "dos" { + eol = []byte{'\r', '\n'} + } else { + eol = []byte{'\n'} + } + + // write lines + if fileSize, e = file.Write(b.lines[0].data); e != nil { + return + } + + for _, l := range b.lines[1:] { + if _, e = file.Write(eol); e != nil { + return + } + if _, e = file.Write(l.data); e != nil { + return + } + fileSize += len(eol) + len(l.data) + } + return + }) + + if err != nil { + return err + } + + if !b.Settings["fastdirty"].(bool) { + if fileSize > LargeFileThreshold { + // For large files 'fastdirty' needs to be on + b.Settings["fastdirty"] = true + } else { + calcHash(b, &b.origHash) + } + } + + b.Path = filename + absPath, _ := filepath.Abs(filename) + b.AbsPath = absPath + b.isModified = false + // TODO: serialize + // return b.Serialize() + return nil +} + +// overwriteFile opens the given file for writing, truncating if one exists, and then calls +// the supplied function with the file as io.Writer object, also making sure the file is +// closed afterwards. +func overwriteFile(name string, fn func(io.Writer) error) (err error) { + var file *os.File + + if file, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644); err != nil { + return + } + + defer func() { + if e := file.Close(); e != nil && err == nil { + err = e + } + }() + + w := bufio.NewWriter(file) + + if err = fn(w); err != nil { + return + } + + err = w.Flush() + return +} + +// SaveWithSudo saves the buffer to the default path with sudo +func (b *Buffer) SaveWithSudo() error { + return b.SaveAsWithSudo(b.Path) +} + +// SaveAsWithSudo is the same as SaveAs except it uses a neat trick +// with tee to use sudo so the user doesn't have to reopen micro with sudo +func (b *Buffer) SaveAsWithSudo(filename string) error { + b.UpdateRules() + b.Path = filename + absPath, _ := filepath.Abs(filename) + b.AbsPath = absPath + + // Set up everything for the command + cmd := exec.Command(globalSettings["sucmd"].(string), "tee", filename) + cmd.Stdin = bytes.NewBuffer(b.Bytes()) + + // 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() { + for range c { + cmd.Process.Kill() + } + }() + + // Start the command + cmd.Start() + err := cmd.Wait() + + if err == nil { + b.isModified = false + b.ModTime, _ = GetModTime(filename) + // TODO: serialize + } + return err +} + +func (b *Buffer) GetActiveCursor() *Cursor { + return nil +} + +func (b *Buffer) GetCursor(n int) *Cursor { + return nil +} + +func (b *Buffer) GetCursors() []*Cursor { + return nil +} + +func (b *Buffer) NumCursors() int { + return 0 +} + +func (b *Buffer) LineBytes(n int) []byte { + if n >= len(b.lines) || n < 0 { + return []byte{} + } + return b.lines[n].data +} + +func (b *Buffer) LinesNum() int { + return len(b.lines) +} + +func (b *Buffer) Start() Loc { + return Loc{0, 0} +} + +// End returns the location of the last character in the buffer +func (b *Buffer) End() Loc { + numlines := len(b.lines) + return Loc{utf8.RuneCount(b.lines[numlines-1].data), numlines - 1} +} + +// RuneAt returns the rune at a given location in the buffer +func (b *Buffer) RuneAt(loc Loc) rune { + line := b.LineBytes(loc.Y) + if len(line) > 0 { + i := 0 + for len(line) > 0 { + r, size := utf8.DecodeRune(line) + line = line[size:] + i++ + + if i == loc.X { + return r + } + } + } + return '\n' +} + +// Modified returns if this buffer has been modified since +// being opened +func (b *Buffer) Modified() bool { + if b.Settings["fastdirty"].(bool) { + return b.isModified + } + + var buff [md5.Size]byte + + calcHash(b, &buff) + return buff != b.origHash +} + +// calcHash calculates md5 hash of all lines in the buffer +func calcHash(b *Buffer, out *[md5.Size]byte) { + h := md5.New() + + if len(b.lines) > 0 { + h.Write(b.lines[0].data) + + for _, l := range b.lines[1:] { + h.Write([]byte{'\n'}) + h.Write(l.data) + } + } + + h.Sum((*out)[:0]) +} + // UpdateRules updates the syntax rules and filetype for this buffer // This is called when the colorscheme changes func (b *Buffer) UpdateRules() { @@ -329,525 +505,3 @@ func (b *Buffer) UpdateRules() { } } } - -// FileType returns the buffer's filetype -func (b *Buffer) FileType() string { - return b.Settings["filetype"].(string) -} - -// IndentString returns a string representing one level of indentation -func (b *Buffer) IndentString() string { - if b.Settings["tabstospaces"].(bool) { - return Spaces(int(b.Settings["tabsize"].(float64))) - } - return "\t" -} - -// CheckModTime makes sure that the file this buffer points to hasn't been updated -// by an external program since it was last read -// If it has, we ask the user if they would like to reload the file -func (b *Buffer) CheckModTime() { - modTime, ok := GetModTime(b.Path) - if ok { - if modTime != b.ModTime { - choice, canceled := messenger.YesNoPrompt("The file has changed since it was last read. Reload file? (y,n)") - messenger.Reset() - messenger.Clear() - if !choice || canceled { - // Don't load new changes -- do nothing - b.ModTime, _ = GetModTime(b.Path) - } else { - // Load new changes - b.ReOpen() - } - } - } -} - -// ReOpen reloads the current buffer from disk -func (b *Buffer) ReOpen() { - data, err := ioutil.ReadFile(b.Path) - txt := string(data) - - if err != nil { - messenger.Error(err.Error()) - return - } - b.EventHandler.ApplyDiff(txt) - - b.ModTime, _ = GetModTime(b.Path) - b.IsModified = false - b.Update() - b.Cursor.Relocate() -} - -// Update fetches the string from the rope and updates the `text` and `lines` in the buffer -func (b *Buffer) Update() { - b.NumLines = len(b.lines) -} - -// MergeCursors merges any cursors that are at the same position -// into one cursor -func (b *Buffer) MergeCursors() { - var cursors []*Cursor - for i := 0; i < len(b.cursors); i++ { - c1 := b.cursors[i] - if c1 != nil { - for j := 0; j < len(b.cursors); j++ { - c2 := b.cursors[j] - if c2 != nil && i != j && c1.Loc == c2.Loc { - b.cursors[j] = nil - } - } - cursors = append(cursors, c1) - } - } - - b.cursors = cursors - - for i := range b.cursors { - b.cursors[i].Num = i - } - - if b.curCursor >= len(b.cursors) { - b.curCursor = len(b.cursors) - 1 - } -} - -// UpdateCursors updates all the cursors indicies -func (b *Buffer) UpdateCursors() { - for i, c := range b.cursors { - c.Num = i - } -} - -// Save saves the buffer to its default path -func (b *Buffer) Save() error { - return b.SaveAs(b.Path) -} - -// SaveWithSudo saves the buffer to the default path with sudo -func (b *Buffer) SaveWithSudo() error { - return b.SaveAsWithSudo(b.Path) -} - -// Serialize serializes the buffer to configDir/buffers -func (b *Buffer) Serialize() error { - if !b.Settings["savecursor"].(bool) && !b.Settings["saveundo"].(bool) { - return nil - } - - name := configDir + "/buffers/" + EscapePath(b.AbsPath) - - return overwriteFile(name, func(file io.Writer) error { - return gob.NewEncoder(file).Encode(SerializedBuffer{ - b.EventHandler, - b.Cursor, - b.ModTime, - }) - }) -} - -func init() { - gob.Register(TextEvent{}) - gob.Register(SerializedBuffer{}) -} - -// SaveAs saves the buffer to a specified path (filename), creating the file if it does not exist -func (b *Buffer) SaveAs(filename string) error { - b.UpdateRules() - if b.Settings["rmtrailingws"].(bool) { - for i, l := range b.lines { - pos := len(bytes.TrimRightFunc(l.data, unicode.IsSpace)) - - if pos < len(l.data) { - b.deleteToEnd(Loc{pos, i}) - } - } - - b.Cursor.Relocate() - } - - if b.Settings["eofnewline"].(bool) { - end := b.End() - if b.RuneAt(Loc{end.X - 1, end.Y}) != '\n' { - b.Insert(end, "\n") - } - } - - defer func() { - b.ModTime, _ = GetModTime(filename) - }() - - // Removes any tilde and replaces with the absolute path to home - absFilename := ReplaceHome(filename) - - // Get the leading path to the file | "." is returned if there's no leading path provided - if dirname := filepath.Dir(absFilename); dirname != "." { - // Check if the parent dirs don't exist - if _, statErr := os.Stat(dirname); os.IsNotExist(statErr) { - // Prompt to make sure they want to create the dirs that are missing - if yes, canceled := messenger.YesNoPrompt("Parent folders \"" + dirname + "\" do not exist. Create them? (y,n)"); yes && !canceled { - // Create all leading dir(s) since they don't exist - if mkdirallErr := os.MkdirAll(dirname, os.ModePerm); mkdirallErr != nil { - // If there was an error creating the dirs - return mkdirallErr - } - } else { - // If they canceled the creation of leading dirs - return errors.New("Save aborted") - } - } - } - - var fileSize int - - err := overwriteFile(absFilename, func(file io.Writer) (e error) { - if len(b.lines) == 0 { - return - } - - // end of line - var eol []byte - - if b.Settings["fileformat"] == "dos" { - eol = []byte{'\r', '\n'} - } else { - eol = []byte{'\n'} - } - - // write lines - if fileSize, e = file.Write(b.lines[0].data); e != nil { - return - } - - for _, l := range b.lines[1:] { - if _, e = file.Write(eol); e != nil { - return - } - - if _, e = file.Write(l.data); e != nil { - return - } - - fileSize += len(eol) + len(l.data) - } - - return - }) - - if err != nil { - return err - } - - if !b.Settings["fastdirty"].(bool) { - if fileSize > LargeFileThreshold { - // For large files 'fastdirty' needs to be on - b.Settings["fastdirty"] = true - } else { - calcHash(b, &b.origHash) - } - } - - b.Path = filename - b.IsModified = false - return b.Serialize() -} - -// overwriteFile opens the given file for writing, truncating if one exists, and then calls -// the supplied function with the file as io.Writer object, also making sure the file is -// closed afterwards. -func overwriteFile(name string, fn func(io.Writer) error) (err error) { - var file *os.File - - if file, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644); err != nil { - return - } - - defer func() { - if e := file.Close(); e != nil && err == nil { - err = e - } - }() - - w := bufio.NewWriter(file) - - if err = fn(w); err != nil { - return - } - - err = w.Flush() - return -} - -// calcHash calculates md5 hash of all lines in the buffer -func calcHash(b *Buffer, out *[md5.Size]byte) { - h := md5.New() - - if len(b.lines) > 0 { - h.Write(b.lines[0].data) - - for _, l := range b.lines[1:] { - h.Write([]byte{'\n'}) - h.Write(l.data) - } - } - - h.Sum((*out)[:0]) -} - -// SaveAsWithSudo is the same as SaveAs except it uses a neat trick -// with tee to use sudo so the user doesn't have to reopen micro with sudo -func (b *Buffer) SaveAsWithSudo(filename string) error { - b.UpdateRules() - b.Path = filename - - // Shut down the screen because we're going to interact directly with the shell - screen.Fini() - screen = nil - - // Set up everything for the command - cmd := exec.Command(globalSettings["sucmd"].(string), "tee", filename) - cmd.Stdin = bytes.NewBufferString(b.SaveString(b.Settings["fileformat"] == "dos")) - - // 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() { - for range c { - cmd.Process.Kill() - } - }() - - // Start the command - cmd.Start() - err := cmd.Wait() - - // Start the screen back up - InitScreen() - if err == nil { - b.IsModified = false - b.ModTime, _ = GetModTime(filename) - b.Serialize() - } - return err -} - -// Modified returns if this buffer has been modified since -// being opened -func (b *Buffer) Modified() bool { - if b.Settings["fastdirty"].(bool) { - return b.IsModified - } - - var buff [md5.Size]byte - - calcHash(b, &buff) - return buff != b.origHash -} - -func (b *Buffer) insert(pos Loc, value []byte) { - b.IsModified = true - b.LineArray.insert(pos, value) - b.Update() -} -func (b *Buffer) remove(start, end Loc) string { - b.IsModified = true - sub := b.LineArray.remove(start, end) - b.Update() - return sub -} -func (b *Buffer) deleteToEnd(start Loc) { - b.IsModified = true - b.LineArray.DeleteToEnd(start) - b.Update() -} - -// Start returns the location of the first character in the buffer -func (b *Buffer) Start() Loc { - return Loc{0, 0} -} - -// End returns the location of the last character in the buffer -func (b *Buffer) End() Loc { - return Loc{utf8.RuneCount(b.lines[b.NumLines-1].data), b.NumLines - 1} -} - -// RuneAt returns the rune at a given location in the buffer -func (b *Buffer) RuneAt(loc Loc) rune { - line := b.LineRunes(loc.Y) - if len(line) > 0 { - return line[loc.X] - } - return '\n' -} - -// LineBytes returns a single line as an array of runes -func (b *Buffer) LineBytes(n int) []byte { - if n >= len(b.lines) { - return []byte{} - } - return b.lines[n].data -} - -// LineRunes returns a single line as an array of runes -func (b *Buffer) LineRunes(n int) []rune { - if n >= len(b.lines) { - return []rune{} - } - return toRunes(b.lines[n].data) -} - -// Line returns a single line -func (b *Buffer) Line(n int) string { - if n >= len(b.lines) { - return "" - } - return string(b.lines[n].data) -} - -// LinesNum returns the number of lines in the buffer -func (b *Buffer) LinesNum() int { - return len(b.lines) -} - -// Lines returns an array of strings containing the lines from start to end -func (b *Buffer) Lines(start, end int) []string { - lines := b.lines[start:end] - var slice []string - for _, line := range lines { - slice = append(slice, string(line.data)) - } - return slice -} - -// Len gives the length of the buffer -func (b *Buffer) Len() (n int) { - for _, l := range b.lines { - n += utf8.RuneCount(l.data) - } - - if len(b.lines) > 1 { - n += len(b.lines) - 1 // account for newlines - } - - return -} - -// MoveLinesUp moves the range of lines up one row -func (b *Buffer) MoveLinesUp(start int, end int) { - // 0 < start < end <= len(b.lines) - if start < 1 || start >= end || end > len(b.lines) { - return // what to do? FIXME - } - if end == len(b.lines) { - b.Insert( - Loc{ - utf8.RuneCount(b.lines[end-1].data), - end - 1, - }, - "\n"+b.Line(start-1), - ) - } else { - b.Insert( - Loc{0, end}, - b.Line(start-1)+"\n", - ) - } - b.Remove( - Loc{0, start - 1}, - Loc{0, start}, - ) -} - -// MoveLinesDown moves the range of lines down one row -func (b *Buffer) MoveLinesDown(start int, end int) { - // 0 <= start < end < len(b.lines) - // if end == len(b.lines), we can't do anything here because the - // last line is unaccessible, FIXME - if start < 0 || start >= end || end >= len(b.lines)-1 { - return // what to do? FIXME - } - b.Insert( - Loc{0, start}, - b.Line(end)+"\n", - ) - end++ - b.Remove( - Loc{0, end}, - Loc{0, end + 1}, - ) -} - -// ClearMatches clears all of the syntax highlighting for this buffer -func (b *Buffer) ClearMatches() { - for i := range b.lines { - b.SetMatch(i, nil) - b.SetState(i, nil) - } -} - -func (b *Buffer) clearCursors() { - for i := 1; i < len(b.cursors); i++ { - b.cursors[i] = nil - } - b.cursors = b.cursors[:1] - b.UpdateCursors() - b.Cursor.ResetSelection() -} - -var bracePairs = [][2]rune{ - {'(', ')'}, - {'{', '}'}, - {'[', ']'}, -} - -// FindMatchingBrace returns the location in the buffer of the matching bracket -// It is given a brace type containing the open and closing character, (for example -// '{' and '}') as well as the location to match from -func (b *Buffer) FindMatchingBrace(braceType [2]rune, start Loc) Loc { - curLine := b.LineRunes(start.Y) - startChar := curLine[start.X] - var i int - if startChar == braceType[0] { - for y := start.Y; y < b.NumLines; y++ { - l := b.LineRunes(y) - xInit := 0 - if y == start.Y { - xInit = start.X - } - for x := xInit; x < len(l); x++ { - r := l[x] - if r == braceType[0] { - i++ - } else if r == braceType[1] { - i-- - if i == 0 { - return Loc{x, y} - } - } - } - } - } else if startChar == braceType[1] { - for y := start.Y; y >= 0; y-- { - l := []rune(string(b.lines[y].data)) - xInit := len(l) - 1 - if y == start.Y { - xInit = start.X - } - for x := xInit; x >= 0; x-- { - r := l[x] - if r == braceType[0] { - i-- - if i == 0 { - return Loc{x, y} - } - } else if r == braceType[1] { - i++ - } - } - } - } - return start -} diff --git a/cmd/micro/buffer_test.go b/cmd/micro/buffer_test.go deleted file mode 100644 index 7369c786..00000000 --- a/cmd/micro/buffer_test.go +++ /dev/null @@ -1,117 +0,0 @@ -package main - -import ( - "testing" -) - -func TestGetBufferCursorLocationEmptyArgs(t *testing.T) { - buf := NewBufferFromString("this is my\nbuffer\nfile\nhello", "") - - location, err := GetBufferCursorLocation(nil, buf) - - assertEqual(t, 0, location.Y) - assertEqual(t, 0, location.X) - - // an error is present due to the cursorLocation being nil - assertTrue(t, err != nil) - -} - -func TestGetBufferCursorLocationStartposFlag(t *testing.T) { - buf := NewBufferFromString("this is my\nbuffer\nfile\nhello", "") - - *flagStartPos = "1,2" - - location, err := GetBufferCursorLocation(nil, buf) - - // note: 1 is subtracted from the line to get the correct index in the buffer - assertTrue(t, 0 == location.Y) - assertTrue(t, 2 == location.X) - - // an error is present due to the cursorLocation being nil - assertTrue(t, err != nil) -} - -func TestGetBufferCursorLocationInvalidStartposFlag(t *testing.T) { - buf := NewBufferFromString("this is my\nbuffer\nfile\nhello", "") - - *flagStartPos = "apples,2" - - location, err := GetBufferCursorLocation(nil, buf) - // expect to default to the start of the file, which is 0,0 - assertEqual(t, 0, location.Y) - assertEqual(t, 0, location.X) - - // an error is present due to the cursorLocation being nil - assertTrue(t, err != nil) -} -func TestGetBufferCursorLocationStartposFlagAndCursorPosition(t *testing.T) { - text := "this is my\nbuffer\nfile\nhello" - cursorPosition := []string{"3", "1"} - - buf := NewBufferFromString(text, "") - - *flagStartPos = "1,2" - - location, err := GetBufferCursorLocation(cursorPosition, buf) - // expect to have the flag positions, not the cursor position - // note: 1 is subtracted from the line to get the correct index in the buffer - assertEqual(t, 0, location.Y) - assertEqual(t, 2, location.X) - - assertTrue(t, err == nil) -} -func TestGetBufferCursorLocationCursorPositionAndInvalidStartposFlag(t *testing.T) { - text := "this is my\nbuffer\nfile\nhello" - cursorPosition := []string{"3", "1"} - - buf := NewBufferFromString(text, "") - - *flagStartPos = "apples,2" - - location, err := GetBufferCursorLocation(cursorPosition, buf) - // expect to have the flag positions, not the cursor position - // note: 1 is subtracted from the line to get the correct index in the buffer - assertEqual(t, 2, location.Y) - assertEqual(t, 1, location.X) - - // no errors this time as cursorPosition is not nil - assertTrue(t, err == nil) -} - -func TestGetBufferCursorLocationNoErrorWhenOverflowWithStartpos(t *testing.T) { - text := "this is my\nbuffer\nfile\nhello" - - buf := NewBufferFromString(text, "") - - *flagStartPos = "50,50" - - location, err := GetBufferCursorLocation(nil, buf) - // expect to have the flag positions, not the cursor position - assertEqual(t, buf.NumLines-1, location.Y) - assertEqual(t, 5, location.X) - - // error is expected as cursorPosition is nil - assertTrue(t, err != nil) -} -func TestGetBufferCursorLocationNoErrorWhenOverflowWithCursorPosition(t *testing.T) { - text := "this is my\nbuffer\nfile\nhello" - cursorPosition := []string{"50", "2"} - - *flagStartPos = "" - - buf := NewBufferFromString(text, "") - - location, err := GetBufferCursorLocation(cursorPosition, buf) - // expect to have the flag positions, not the cursor position - assertEqual(t, buf.NumLines-1, location.Y) - assertEqual(t, 2, location.X) - - // error is expected as cursorPosition is nil - assertTrue(t, err == nil) -} - -//func TestGetBufferCursorLocationColonArgs(t *testing.T) { -// buf := new(Buffer) - -//} diff --git a/cmd/micro/cellview.go b/cmd/micro/cellview.go deleted file mode 100644 index b0cb6523..00000000 --- a/cmd/micro/cellview.go +++ /dev/null @@ -1,238 +0,0 @@ -package main - -import ( - "github.com/mattn/go-runewidth" - "github.com/zyedidia/tcell" -) - -func min(a, b int) int { - if a <= b { - return a - } - return b -} - -func visualToCharPos(visualIndex int, lineN int, str string, buf *Buffer, tabsize int) (int, int, *tcell.Style) { - charPos := 0 - var lineIdx int - var lastWidth int - var style *tcell.Style - var width int - var rw int - for i, c := range str { - // width := StringWidth(str[:i], tabsize) - - if group, ok := buf.Match(lineN)[charPos]; ok { - s := GetColor(group.String()) - style = &s - } - - if width >= visualIndex { - return charPos, visualIndex - lastWidth, style - } - - if i != 0 { - charPos++ - lineIdx += rw - } - lastWidth = width - rw = 0 - if c == '\t' { - rw = tabsize - (lineIdx % tabsize) - width += rw - } else { - rw = runewidth.RuneWidth(c) - width += rw - } - } - - return -1, -1, style -} - -type Char struct { - visualLoc Loc - realLoc Loc - char rune - // The actual character that is drawn - // This is only different from char if it's for example hidden character - drawChar rune - style tcell.Style - width int -} - -type CellView struct { - lines [][]*Char -} - -func (c *CellView) Draw(buf *Buffer, top, height, left, width int) { - if width <= 0 { - return - } - - matchingBrace := Loc{-1, -1} - // bracePairs is defined in buffer.go - if buf.Settings["matchbrace"].(bool) { - for _, bp := range bracePairs { - curX := buf.Cursor.X - curLoc := buf.Cursor.Loc - if buf.Settings["matchbraceleft"].(bool) { - if curX > 0 { - curX-- - curLoc = curLoc.Move(-1, buf) - } - } - - r := buf.Cursor.RuneUnder(curX) - if r == bp[0] || r == bp[1] { - matchingBrace = buf.FindMatchingBrace(bp, curLoc) - } - } - } - - tabsize := int(buf.Settings["tabsize"].(float64)) - softwrap := buf.Settings["softwrap"].(bool) - indentrunes := []rune(buf.Settings["indentchar"].(string)) - // if empty indentchar settings, use space - if indentrunes == nil || len(indentrunes) == 0 { - indentrunes = []rune{' '} - } - indentchar := indentrunes[0] - - start := buf.Cursor.Y - if buf.Settings["syntax"].(bool) && buf.syntaxDef != nil { - if start > 0 && buf.lines[start-1].rehighlight { - buf.highlighter.ReHighlightLine(buf, start-1) - buf.lines[start-1].rehighlight = false - } - - buf.highlighter.ReHighlightStates(buf, start) - - buf.highlighter.HighlightMatches(buf, top, top+height) - } - - c.lines = make([][]*Char, 0) - - viewLine := 0 - lineN := top - - curStyle := defStyle - for viewLine < height { - if lineN >= len(buf.lines) { - break - } - - lineStr := buf.Line(lineN) - line := []rune(lineStr) - - colN, startOffset, startStyle := visualToCharPos(left, lineN, lineStr, buf, tabsize) - if colN < 0 { - colN = len(line) - } - viewCol := -startOffset - if startStyle != nil { - curStyle = *startStyle - } - - // We'll either draw the length of the line, or the width of the screen - // whichever is smaller - lineLength := min(StringWidth(lineStr, tabsize), width) - c.lines = append(c.lines, make([]*Char, lineLength)) - - wrap := false - // We only need to wrap if the length of the line is greater than the width of the terminal screen - if softwrap && StringWidth(lineStr, tabsize) > width { - wrap = true - // We're going to draw the entire line now - lineLength = StringWidth(lineStr, tabsize) - } - - for viewCol < lineLength { - if colN >= len(line) { - break - } - if group, ok := buf.Match(lineN)[colN]; ok { - curStyle = GetColor(group.String()) - } - - char := line[colN] - - if viewCol >= 0 { - st := curStyle - if colN == matchingBrace.X && lineN == matchingBrace.Y && !buf.Cursor.HasSelection() { - st = curStyle.Reverse(true) - } - if viewCol < len(c.lines[viewLine]) { - c.lines[viewLine][viewCol] = &Char{Loc{viewCol, viewLine}, Loc{colN, lineN}, char, char, st, 1} - } - } - if char == '\t' { - charWidth := tabsize - (viewCol+left)%tabsize - if viewCol >= 0 { - c.lines[viewLine][viewCol].drawChar = indentchar - c.lines[viewLine][viewCol].width = charWidth - - indentStyle := curStyle - ch := buf.Settings["indentchar"].(string) - if group, ok := colorscheme["indent-char"]; ok && !IsStrWhitespace(ch) && ch != "" { - indentStyle = group - } - - c.lines[viewLine][viewCol].style = indentStyle - } - - for i := 1; i < charWidth; i++ { - viewCol++ - if viewCol >= 0 && viewCol < lineLength && viewCol < len(c.lines[viewLine]) { - c.lines[viewLine][viewCol] = &Char{Loc{viewCol, viewLine}, Loc{colN, lineN}, char, ' ', curStyle, 1} - } - } - viewCol++ - } else if runewidth.RuneWidth(char) > 1 { - charWidth := runewidth.RuneWidth(char) - if viewCol >= 0 { - c.lines[viewLine][viewCol].width = charWidth - } - for i := 1; i < charWidth; i++ { - viewCol++ - if viewCol >= 0 && viewCol < lineLength && viewCol < len(c.lines[viewLine]) { - c.lines[viewLine][viewCol] = &Char{Loc{viewCol, viewLine}, Loc{colN, lineN}, char, ' ', curStyle, 1} - } - } - viewCol++ - } else { - viewCol++ - } - colN++ - - if wrap && viewCol >= width { - viewLine++ - - // If we go too far soft wrapping we have to cut off - if viewLine >= height { - break - } - - nextLine := line[colN:] - lineLength := min(StringWidth(string(nextLine), tabsize), width) - c.lines = append(c.lines, make([]*Char, lineLength)) - - viewCol = 0 - } - - } - if group, ok := buf.Match(lineN)[len(line)]; ok { - curStyle = GetColor(group.String()) - } - - // newline - viewLine++ - lineN++ - } - - for i := top; i < top+height; i++ { - if i >= buf.NumLines { - break - } - buf.SetMatch(i, nil) - } -} diff --git a/cmd/micro/colorscheme.go b/cmd/micro/colorscheme.go index 8a649b17..b3c84a2d 100644 --- a/cmd/micro/colorscheme.go +++ b/cmd/micro/colorscheme.go @@ -1,7 +1,7 @@ package main import ( - "fmt" + "errors" "regexp" "strconv" "strings" @@ -9,6 +9,9 @@ import ( "github.com/zyedidia/tcell" ) +// Micro's default style +var defStyle tcell.Style = tcell.StyleDefault + // Colorscheme is a map from string to style -- it represents a colorscheme type Colorscheme map[string]tcell.Style @@ -48,42 +51,41 @@ func ColorschemeExists(colorschemeName string) bool { } // InitColorscheme picks and initializes the colorscheme when micro starts -func InitColorscheme() { +func InitColorscheme() error { colorscheme = make(Colorscheme) - defStyle = tcell.StyleDefault. - Foreground(tcell.ColorDefault). - Background(tcell.ColorDefault) - if screen != nil { - // screen.SetStyle(defStyle) - } + defStyle = tcell.StyleDefault - LoadDefaultColorscheme() + return LoadDefaultColorscheme() } // LoadDefaultColorscheme loads the default colorscheme from $(configDir)/colorschemes -func LoadDefaultColorscheme() { - LoadColorscheme(globalSettings["colorscheme"].(string)) +func LoadDefaultColorscheme() error { + return LoadColorscheme(globalSettings["colorscheme"].(string)) } // LoadColorscheme loads the given colorscheme from a directory -func LoadColorscheme(colorschemeName string) { +func LoadColorscheme(colorschemeName string) error { file := FindRuntimeFile(RTColorscheme, colorschemeName) if file == nil { - TermMessage(colorschemeName, "is not a valid colorscheme") + return errors.New(colorschemeName + " is not a valid colorscheme") + } + if data, err := file.Data(); err != nil { + return errors.New("Error loading colorscheme: " + err.Error()) } else { - if data, err := file.Data(); err != nil { - TermMessage("Error loading colorscheme:", err) - } else { - colorscheme = ParseColorscheme(string(data)) + colorscheme, err = ParseColorscheme(string(data)) + if err != nil { + return err } } + return nil } // ParseColorscheme parses the text definition for a colorscheme and returns the corresponding object // Colorschemes are made up of color-link statements linking a color group to a list of colors // For example, color-link keyword (blue,red) makes all keywords have a blue foreground and // red background -func ParseColorscheme(text string) Colorscheme { +func ParseColorscheme(text string) (Colorscheme, error) { + var err error parser := regexp.MustCompile(`color-link\s+(\S*)\s+"(.*)"`) lines := strings.Split(text, "\n") @@ -108,15 +110,12 @@ func ParseColorscheme(text string) Colorscheme { if link == "default" { defStyle = style } - if screen != nil { - // screen.SetStyle(defStyle) - } } else { - fmt.Println("Color-link statement is not valid:", line) + err = errors.New("Color-link statement is not valid: " + line) } } - return c + return c, err } // StringToStyle returns a style from a string diff --git a/cmd/micro/colorscheme_test.go b/cmd/micro/colorscheme_test.go new file mode 100644 index 00000000..098af941 --- /dev/null +++ b/cmd/micro/colorscheme_test.go @@ -0,0 +1,62 @@ +package main + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/zyedidia/tcell" +) + +func TestSimpleStringToStyle(t *testing.T) { + s := StringToStyle("lightblue,magenta") + + fg, bg, _ := s.Decompose() + + assert.Equal(t, tcell.ColorBlue, fg) + assert.Equal(t, tcell.ColorPurple, bg) +} + +func TestAttributeStringToStyle(t *testing.T) { + s := StringToStyle("bold cyan,brightcyan") + + fg, bg, attr := s.Decompose() + + assert.Equal(t, tcell.ColorTeal, fg) + assert.Equal(t, tcell.ColorAqua, bg) + assert.NotEqual(t, 0, attr&tcell.AttrBold) +} + +func TestColor256StringToStyle(t *testing.T) { + s := StringToStyle("128,60") + + fg, bg, _ := s.Decompose() + + assert.Equal(t, tcell.Color128, fg) + assert.Equal(t, tcell.Color60, bg) +} + +func TestColorHexStringToStyle(t *testing.T) { + s := StringToStyle("#deadbe,#ef1234") + + fg, bg, _ := s.Decompose() + + assert.Equal(t, tcell.NewRGBColor(222, 173, 190), fg) + assert.Equal(t, tcell.NewRGBColor(239, 18, 52), bg) +} + +func TestColorschemeParser(t *testing.T) { + testColorscheme := `color-link default "#F8F8F2,#282828" +color-link comment "#75715E,#282828" +# comment +color-link identifier "#66D9EF,#282828" #comment +color-link constant "#AE81FF,#282828" +color-link constant.string "#E6DB74,#282828" +color-link constant.string.char "#BDE6AD,#282828"` + + c, err := ParseColorscheme(testColorscheme) + assert.Nil(t, err) + + fg, bg, _ := c["comment"].Decompose() + assert.Equal(t, tcell.NewRGBColor(117, 113, 94), fg) + assert.Equal(t, tcell.NewRGBColor(40, 40, 40), bg) +} diff --git a/cmd/micro/command.go b/cmd/micro/command.go deleted file mode 100644 index f3a1b26a..00000000 --- a/cmd/micro/command.go +++ /dev/null @@ -1,694 +0,0 @@ -package main - -import ( - "fmt" - "os" - "path/filepath" - "regexp" - "runtime" - "strconv" - "strings" - "unicode/utf8" - - humanize "github.com/dustin/go-humanize" - "github.com/zyedidia/micro/cmd/micro/shellwords" -) - -// A Command contains a action (a function to call) as well as information about how to autocomplete the command -type Command struct { - action func([]string) - completions []Completion -} - -// A StrCommand is similar to a command but keeps the name of the action -type StrCommand struct { - action string - completions []Completion -} - -var commands map[string]Command - -var commandActions map[string]func([]string) - -func init() { - commandActions = map[string]func([]string){ - "Set": Set, - "SetLocal": SetLocal, - "Show": Show, - "ShowKey": ShowKey, - "Run": Run, - "Bind": Bind, - "Quit": Quit, - "Save": Save, - "Replace": Replace, - "ReplaceAll": ReplaceAll, - "VSplit": VSplit, - "HSplit": HSplit, - "Tab": NewTab, - "Help": Help, - "Eval": Eval, - "ToggleLog": ToggleLog, - "Plugin": PluginCmd, - "Reload": Reload, - "Cd": Cd, - "Pwd": Pwd, - "Open": Open, - "TabSwitch": TabSwitch, - "Term": Term, - "MemUsage": MemUsage, - "Retab": Retab, - "Raw": Raw, - "TextFilter": TextFilter, - } -} - -// InitCommands initializes the default commands -func InitCommands() { - commands = make(map[string]Command) - - defaults := DefaultCommands() - parseCommands(defaults) -} - -func parseCommands(userCommands map[string]StrCommand) { - for k, v := range userCommands { - MakeCommand(k, v.action, v.completions...) - } -} - -// MakeCommand is a function to easily create new commands -// This can be called by plugins in Lua so that plugins can define their own commands -func MakeCommand(name, function string, completions ...Completion) { - action := commandActions[function] - if _, ok := commandActions[function]; !ok { - // If the user seems to be binding a function that doesn't exist - // We hope that it's a lua function that exists and bind it to that - action = LuaFunctionCommand(function) - } - - commands[name] = Command{action, completions} -} - -// DefaultCommands returns a map containing micro's default commands -func DefaultCommands() map[string]StrCommand { - return map[string]StrCommand{ - "set": {"Set", []Completion{OptionCompletion, OptionValueCompletion}}, - "setlocal": {"SetLocal", []Completion{OptionCompletion, OptionValueCompletion}}, - "show": {"Show", []Completion{OptionCompletion, NoCompletion}}, - "showkey": {"ShowKey", []Completion{NoCompletion}}, - "bind": {"Bind", []Completion{NoCompletion}}, - "run": {"Run", []Completion{NoCompletion}}, - "quit": {"Quit", []Completion{NoCompletion}}, - "save": {"Save", []Completion{NoCompletion}}, - "replace": {"Replace", []Completion{NoCompletion}}, - "replaceall": {"ReplaceAll", []Completion{NoCompletion}}, - "vsplit": {"VSplit", []Completion{FileCompletion, NoCompletion}}, - "hsplit": {"HSplit", []Completion{FileCompletion, NoCompletion}}, - "tab": {"Tab", []Completion{FileCompletion, NoCompletion}}, - "help": {"Help", []Completion{HelpCompletion, NoCompletion}}, - "eval": {"Eval", []Completion{NoCompletion}}, - "log": {"ToggleLog", []Completion{NoCompletion}}, - "plugin": {"Plugin", []Completion{PluginCmdCompletion, PluginNameCompletion}}, - "reload": {"Reload", []Completion{NoCompletion}}, - "cd": {"Cd", []Completion{FileCompletion}}, - "pwd": {"Pwd", []Completion{NoCompletion}}, - "open": {"Open", []Completion{FileCompletion}}, - "tabswitch": {"TabSwitch", []Completion{NoCompletion}}, - "term": {"Term", []Completion{NoCompletion}}, - "memusage": {"MemUsage", []Completion{NoCompletion}}, - "retab": {"Retab", []Completion{NoCompletion}}, - "raw": {"Raw", []Completion{NoCompletion}}, - "textfilter": {"TextFilter", []Completion{NoCompletion}}, - } -} - -// CommandEditAction returns a bindable function that opens a prompt with -// the given string and executes the command when the user presses -// enter -func CommandEditAction(prompt string) func(*View, bool) bool { - return func(v *View, usePlugin bool) bool { - input, canceled := messenger.Prompt("> ", prompt, "Command", CommandCompletion) - if !canceled { - HandleCommand(input) - } - return false - } -} - -// CommandAction returns a bindable function which executes the -// given command -func CommandAction(cmd string) func(*View, bool) bool { - return func(v *View, usePlugin bool) bool { - HandleCommand(cmd) - return false - } -} - -// PluginCmd installs, removes, updates, lists, or searches for given plugins -func PluginCmd(args []string) { - if len(args) >= 1 { - switch args[0] { - case "install": - installedVersions := GetInstalledVersions(false) - for _, plugin := range args[1:] { - pp := GetAllPluginPackages().Get(plugin) - if pp == nil { - messenger.Error("Unknown plugin \"" + plugin + "\"") - } else if err := pp.IsInstallable(); err != nil { - messenger.Error("Error installing ", plugin, ": ", err) - } else { - for _, installed := range installedVersions { - if pp.Name == installed.pack.Name { - if pp.Versions[0].Version.Compare(installed.Version) == 1 { - messenger.Error(pp.Name, " is already installed but out-of-date: use 'plugin update ", pp.Name, "' to update") - } else { - messenger.Error(pp.Name, " is already installed") - } - } - } - pp.Install() - } - } - case "remove": - removed := "" - for _, plugin := range args[1:] { - // check if the plugin exists. - if _, ok := loadedPlugins[plugin]; ok { - UninstallPlugin(plugin) - removed += plugin + " " - continue - } - } - if !IsSpaces([]byte(removed)) { - messenger.Message("Removed ", removed) - } else { - messenger.Error("The requested plugins do not exist") - } - case "update": - UpdatePlugins(args[1:]) - case "list": - plugins := GetInstalledVersions(false) - messenger.AddLog("----------------") - messenger.AddLog("The following plugins are currently installed:\n") - for _, p := range plugins { - messenger.AddLog(fmt.Sprintf("%s (%s)", p.pack.Name, p.Version)) - } - messenger.AddLog("----------------") - if len(plugins) > 0 { - if CurView().Type != vtLog { - ToggleLog([]string{}) - } - } - case "search": - plugins := SearchPlugin(args[1:]) - messenger.Message(len(plugins), " plugins found") - for _, p := range plugins { - messenger.AddLog("----------------") - messenger.AddLog(p.String()) - } - messenger.AddLog("----------------") - if len(plugins) > 0 { - if CurView().Type != vtLog { - ToggleLog([]string{}) - } - } - case "available": - packages := GetAllPluginPackages() - messenger.AddLog("Available Plugins:") - for _, pkg := range packages { - messenger.AddLog(pkg.Name) - } - if CurView().Type != vtLog { - ToggleLog([]string{}) - } - } - } else { - messenger.Error("Not enough arguments") - } -} - -// Retab changes all spaces to tabs or all tabs to spaces -// depending on the user's settings -func Retab(args []string) { - CurView().Retab(true) -} - -// Raw opens a new raw view which displays the escape sequences micro -// is receiving in real-time -func Raw(args []string) { - buf := NewBufferFromString("", "Raw events") - - view := NewView(buf) - view.Buf.Insert(view.Cursor.Loc, "Warning: Showing raw event escape codes\n") - view.Buf.Insert(view.Cursor.Loc, "Use CtrlQ to exit\n") - view.Type = vtRaw - tab := NewTabFromView(view) - tab.SetNum(len(tabs)) - tabs = append(tabs, tab) - curTab = len(tabs) - 1 - if len(tabs) == 2 { - for _, t := range tabs { - for _, v := range t.Views { - v.ToggleTabbar() - } - } - } -} - -// TabSwitch switches to a given tab either by name or by number -func TabSwitch(args []string) { - if len(args) > 0 { - num, err := strconv.Atoi(args[0]) - if err != nil { - // Check for tab with this name - - found := false - for _, t := range tabs { - v := t.Views[t.CurView] - if v.Buf.GetName() == args[0] { - curTab = v.TabNum - found = true - } - } - if !found { - messenger.Error("Could not find tab: ", err) - } - } else { - num-- - if num >= 0 && num < len(tabs) { - curTab = num - } else { - messenger.Error("Invalid tab index") - } - } - } -} - -// Cd changes the current working directory -func Cd(args []string) { - if len(args) > 0 { - path := ReplaceHome(args[0]) - err := os.Chdir(path) - if err != nil { - messenger.Error("Error with cd: ", err) - return - } - wd, _ := os.Getwd() - for _, tab := range tabs { - for _, view := range tab.Views { - if len(view.Buf.name) == 0 { - continue - } - - view.Buf.Path, _ = MakeRelative(view.Buf.AbsPath, wd) - if p, _ := filepath.Abs(view.Buf.Path); !strings.Contains(p, wd) { - view.Buf.Path = view.Buf.AbsPath - } - } - } - } -} - -// MemUsage prints micro's memory usage -// Alloc shows how many bytes are currently in use -// Sys shows how many bytes have been requested from the operating system -// NumGC shows how many times the GC has been run -// Note that Go commonly reserves more memory from the OS than is currently in-use/required -// Additionally, even if Go returns memory to the OS, the OS does not always claim it because -// there may be plenty of memory to spare -func MemUsage(args []string) { - var mem runtime.MemStats - runtime.ReadMemStats(&mem) - - messenger.Message(fmt.Sprintf("Alloc: %v, Sys: %v, NumGC: %v", humanize.Bytes(mem.Alloc), humanize.Bytes(mem.Sys), mem.NumGC)) -} - -// Pwd prints the current working directory -func Pwd(args []string) { - wd, err := os.Getwd() - if err != nil { - messenger.Message(err.Error()) - } else { - messenger.Message(wd) - } -} - -// Open opens a new buffer with a given filename -func Open(args []string) { - if len(args) > 0 { - filename := args[0] - // the filename might or might not be quoted, so unquote first then join the strings. - args, err := shellwords.Split(filename) - if err != nil { - messenger.Error("Error parsing args ", err) - return - } - filename = strings.Join(args, " ") - - CurView().Open(filename) - } else { - messenger.Error("No filename") - } -} - -// ToggleLog toggles the log view -func ToggleLog(args []string) { - buffer := messenger.getBuffer() - if CurView().Type != vtLog { - CurView().HSplit(buffer) - CurView().Type = vtLog - RedrawAll() - buffer.Cursor.Loc = buffer.Start() - CurView().Relocate() - buffer.Cursor.Loc = buffer.End() - CurView().Relocate() - } else { - CurView().Quit(true) - } -} - -// Reload reloads all files (syntax files, colorschemes...) -func Reload(args []string) { - LoadAll() -} - -// Help tries to open the given help page in a horizontal split -func Help(args []string) { - if len(args) < 1 { - // Open the default help if the user just typed "> help" - CurView().openHelp("help") - } else { - helpPage := args[0] - if FindRuntimeFile(RTHelp, helpPage) != nil { - CurView().openHelp(helpPage) - } else { - messenger.Error("Sorry, no help for ", helpPage) - } - } -} - -// 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(NewBufferFromString("", "")) - } else { - buf, err := NewBufferFromFile(args[0]) - if err != nil { - messenger.Error(err) - return - } - 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(NewBufferFromString("", "")) - } else { - buf, err := NewBufferFromFile(args[0]) - if err != nil { - messenger.Error(err) - return - } - CurView().HSplit(buf) - } -} - -// Eval evaluates a lua expression -func Eval(args []string) { - if len(args) >= 1 { - err := L.DoString(args[0]) - if err != nil { - messenger.Error(err) - } - } else { - messenger.Error("Not enough arguments") - } -} - -// NewTab opens the given file in a new tab -func NewTab(args []string) { - if len(args) == 0 { - CurView().AddTab(true) - } else { - buf, err := NewBufferFromFile(args[0]) - if err != nil { - messenger.Error(err) - return - } - - tab := NewTabFromView(NewView(buf)) - tab.SetNum(len(tabs)) - tabs = append(tabs, tab) - curTab = len(tabs) - 1 - if len(tabs) == 2 { - for _, t := range tabs { - for _, v := range t.Views { - v.ToggleTabbar() - } - } - } - } -} - -// Set sets an option -func Set(args []string) { - if len(args) < 2 { - messenger.Error("Not enough arguments") - return - } - - option := args[0] - value := args[1] - - SetOptionAndSettings(option, value) -} - -// SetLocal sets an option local to the buffer -func SetLocal(args []string) { - if len(args) < 2 { - messenger.Error("Not enough arguments") - return - } - - option := args[0] - value := args[1] - - err := SetLocalOption(option, value, CurView()) - if err != nil { - messenger.Error(err.Error()) - } -} - -// Show shows the value of the given option -func Show(args []string) { - if len(args) < 1 { - messenger.Error("Please provide an option to show") - return - } - - option := GetOption(args[0]) - - if option == nil { - messenger.Error(args[0], " is not a valid option") - return - } - - messenger.Message(option) -} - -// ShowKey displays the action that a key is bound to -func ShowKey(args []string) { - if len(args) < 1 { - messenger.Error("Please provide a key to show") - return - } - - if action, ok := bindingsStr[args[0]]; ok { - messenger.Message(action) - } else { - messenger.Message(args[0], " has no binding") - } -} - -// Bind creates a new keybinding -func Bind(args []string) { - if len(args) < 2 { - messenger.Error("Not enough arguments") - return - } - BindKey(args[0], args[1]) -} - -// Run runs a shell command in the background -func Run(args []string) { - // Run a shell command in the background (openTerm is false) - HandleShellCommand(shellwords.Join(args...), false, true) -} - -// Quit closes the main view -func Quit(args []string) { - // Close the main view - CurView().Quit(true) -} - -// Save saves the buffer in the main view -func Save(args []string) { - if len(args) == 0 { - // Save the main view - CurView().Save(true) - } else { - CurView().Buf.SaveAs(args[0]) - } -} - -// Replace runs search and replace -func Replace(args []string) { - if len(args) < 2 || len(args) > 4 { - // We need to find both a search and replace expression - messenger.Error("Invalid replace statement: " + strings.Join(args, " ")) - return - } - - all := false - noRegex := false - - if len(args) > 2 { - for _, arg := range args[2:] { - switch arg { - case "-a": - all = true - case "-l": - noRegex = true - default: - messenger.Error("Invalid flag: " + arg) - return - } - } - } - - search := string(args[0]) - - if noRegex { - search = regexp.QuoteMeta(search) - } - - replace := string(args[1]) - replaceBytes := []byte(replace) - - regex, err := regexp.Compile("(?m)" + search) - if err != nil { - // There was an error with the user's regex - messenger.Error(err.Error()) - return - } - - view := CurView() - - found := 0 - replaceAll := func() { - var deltas []Delta - for i := 0; i < view.Buf.LinesNum(); i++ { - newText := regex.ReplaceAllFunc(view.Buf.lines[i].data, func(in []byte) []byte { - found++ - return replaceBytes - }) - - from := Loc{0, i} - to := Loc{utf8.RuneCount(view.Buf.lines[i].data), i} - - deltas = append(deltas, Delta{string(newText), from, to}) - } - view.Buf.MultipleReplace(deltas) - } - - if all { - replaceAll() - } else { - for { - // The 'check' flag was used - Search(search, view, true) - if !view.Cursor.HasSelection() { - break - } - view.Relocate() - RedrawAll() - choice, canceled := messenger.LetterPrompt("Perform replacement? (y,n,a)", 'y', 'n', 'a') - if canceled { - if view.Cursor.HasSelection() { - view.Cursor.Loc = view.Cursor.CurSelection[0] - view.Cursor.ResetSelection() - } - messenger.Reset() - break - } else if choice == 'a' { - if view.Cursor.HasSelection() { - view.Cursor.Loc = view.Cursor.CurSelection[0] - view.Cursor.ResetSelection() - } - messenger.Reset() - replaceAll() - break - } else if choice == 'y' { - view.Cursor.DeleteSelection() - view.Buf.Insert(view.Cursor.Loc, replace) - view.Cursor.ResetSelection() - messenger.Reset() - found++ - } - if view.Cursor.HasSelection() { - searchStart = view.Cursor.CurSelection[1] - } else { - searchStart = view.Cursor.Loc - } - } - } - view.Cursor.Relocate() - - if found > 1 { - messenger.Message("Replaced ", found, " occurrences of ", search) - } else if found == 1 { - messenger.Message("Replaced ", found, " occurrence of ", search) - } else { - messenger.Message("Nothing matched ", search) - } -} - -// ReplaceAll replaces search term all at once -func ReplaceAll(args []string) { - // aliased to Replace command - Replace(append(args, "-a")) -} - -// Term opens a terminal in the current view -func Term(args []string) { - var err error - if len(args) == 0 { - err = CurView().StartTerminal([]string{os.Getenv("SHELL"), "-i"}, true, false, "") - } else { - err = CurView().StartTerminal(args, true, false, "") - } - if err != nil { - messenger.Error(err) - } -} - -// HandleCommand handles input from the user -func HandleCommand(input string) { - args, err := shellwords.Split(input) - if err != nil { - messenger.Error("Error parsing args ", err) - return - } - - inputCmd := args[0] - - if _, ok := commands[inputCmd]; !ok { - messenger.Error("Unknown command ", inputCmd) - } else { - commands[inputCmd].action(args[1:]) - } -} diff --git a/cmd/micro/cursor.go b/cmd/micro/cursor.go index 15dbb1b3..9e9f1d81 100644 --- a/cmd/micro/cursor.go +++ b/cmd/micro/cursor.go @@ -1,15 +1,23 @@ package main import ( + "unicode/utf8" + + runewidth "github.com/mattn/go-runewidth" "github.com/zyedidia/clipboard" ) -// The Cursor struct stores the location of the cursor in the view -// The complicated part about the cursor is storing its location. -// The cursor must be displayed at an x, y location, but since the buffer -// uses a rope to store text, to insert text we must have an index. It -// is also simpler to use character indicies for other tasks such as -// selection. +// InBounds returns whether the given location is a valid character position in the given buffer +func InBounds(pos Loc, buf *Buffer) bool { + if pos.Y < 0 || pos.Y >= len(buf.lines) || pos.X < 0 || pos.X > utf8.RuneCount(buf.LineBytes(pos.Y)) { + return false + } + + return true +} + +// The Cursor struct stores the location of the cursor in the buffer +// as well as the selection type Cursor struct { buf *Buffer Loc @@ -42,12 +50,72 @@ func (c *Cursor) GotoLoc(l Loc) { c.LastVisualX = c.GetVisualX() } +// GetVisualX returns the x value of the cursor in visual spaces +func (c *Cursor) GetVisualX() int { + if c.X <= 0 { + c.X = 0 + return 0 + } + + bytes := c.buf.LineBytes(c.Y) + tabsize := int(c.buf.Settings["tabsize"].(float64)) + if c.X > utf8.RuneCount(bytes) { + c.X = utf8.RuneCount(bytes) - 1 + } + + return StringWidth(bytes, c.X, tabsize) +} + +// GetCharPosInLine gets the char position of a visual x y +// coordinate (this is necessary because tabs are 1 char but +// 4 visual spaces) +func (c *Cursor) GetCharPosInLine(b []byte, visualPos int) int { + tabsize := int(c.buf.Settings["tabsize"].(float64)) + + // Scan rune by rune until we exceed the visual width that we are + // looking for. Then we can return the character position we have found + i := 0 // char pos + width := 0 // string visual width + for len(b) > 0 { + r, size := utf8.DecodeRune(b) + b = b[size:] + + switch r { + case '\t': + ts := tabsize - (width % tabsize) + width += ts + default: + width += runewidth.RuneWidth(r) + } + + i++ + + if width >= visualPos { + break + } + } + + return i +} + +// Start moves the cursor to the start of the line it is on +func (c *Cursor) Start() { + c.X = 0 + c.LastVisualX = c.GetVisualX() +} + +// End moves the cursor to the end of the line it is on +func (c *Cursor) End() { + c.X = utf8.RuneCount(c.buf.LineBytes(c.Y)) + c.LastVisualX = c.GetVisualX() +} + // CopySelection copies the user's selection to either "primary" // or "clipboard" func (c *Cursor) CopySelection(target string) { if c.HasSelection() { if target != "primary" || c.buf.Settings["useprimary"].(bool) { - clipboard.WriteAll(c.GetSelection(), target) + clipboard.WriteAll(string(c.GetSelection()), target) } } } @@ -87,14 +155,14 @@ func (c *Cursor) DeleteSelection() { } // GetSelection returns the cursor's selection -func (c *Cursor) GetSelection() string { +func (c *Cursor) GetSelection() []byte { if InBounds(c.CurSelection[0], c.buf) && InBounds(c.CurSelection[1], c.buf) { if c.CurSelection[0].GreaterThan(c.CurSelection[1]) { return c.buf.Substr(c.CurSelection[1], c.CurSelection[0]) } return c.buf.Substr(c.CurSelection[0], c.CurSelection[1]) } - return "" + return []byte{} } // SelectLine selects the current line @@ -102,7 +170,7 @@ func (c *Cursor) SelectLine() { c.Start() c.SetSelectionStart(c.Loc) c.End() - if c.buf.NumLines-1 > c.Y { + if len(c.buf.lines)-1 > c.Y { c.SetSelectionEnd(c.Loc.Move(1, c.buf)) } else { c.SetSelectionEnd(c.Loc) @@ -129,146 +197,20 @@ func (c *Cursor) AddLineToSelection() { } } -// SelectWord selects the word the cursor is currently on -func (c *Cursor) SelectWord() { - if len(c.buf.Line(c.Y)) == 0 { - return - } - - if !IsWordChar(string(c.RuneUnder(c.X))) { - c.SetSelectionStart(c.Loc) - c.SetSelectionEnd(c.Loc.Move(1, c.buf)) - c.OrigSelection = c.CurSelection - return - } - - forward, backward := c.X, c.X - - for backward > 0 && IsWordChar(string(c.RuneUnder(backward-1))) { - backward-- - } - - c.SetSelectionStart(Loc{backward, c.Y}) - c.OrigSelection[0] = c.CurSelection[0] - - for forward < Count(c.buf.Line(c.Y))-1 && IsWordChar(string(c.RuneUnder(forward+1))) { - forward++ - } - - c.SetSelectionEnd(Loc{forward, c.Y}.Move(1, c.buf)) - c.OrigSelection[1] = c.CurSelection[1] - c.Loc = c.CurSelection[1] -} - -// AddWordToSelection adds the word the cursor is currently on -// to the selection -func (c *Cursor) AddWordToSelection() { - if c.Loc.GreaterThan(c.OrigSelection[0]) && c.Loc.LessThan(c.OrigSelection[1]) { - c.CurSelection = c.OrigSelection - return - } - - if c.Loc.LessThan(c.OrigSelection[0]) { - backward := c.X - - for backward > 0 && IsWordChar(string(c.RuneUnder(backward-1))) { - backward-- - } - - c.SetSelectionStart(Loc{backward, c.Y}) - c.SetSelectionEnd(c.OrigSelection[1]) - } - - if c.Loc.GreaterThan(c.OrigSelection[1]) { - forward := c.X - - for forward < Count(c.buf.Line(c.Y))-1 && IsWordChar(string(c.RuneUnder(forward+1))) { - forward++ - } - - c.SetSelectionEnd(Loc{forward, c.Y}.Move(1, c.buf)) - c.SetSelectionStart(c.OrigSelection[0]) - } - - c.Loc = c.CurSelection[1] -} - -// SelectTo selects from the current cursor location to the given -// location -func (c *Cursor) SelectTo(loc Loc) { - if loc.GreaterThan(c.OrigSelection[0]) { - c.SetSelectionStart(c.OrigSelection[0]) - c.SetSelectionEnd(loc) - } else { - c.SetSelectionStart(loc) - c.SetSelectionEnd(c.OrigSelection[0]) - } -} - -// WordRight moves the cursor one word to the right -func (c *Cursor) WordRight() { - for IsWhitespace(c.RuneUnder(c.X)) { - if c.X == Count(c.buf.Line(c.Y)) { - c.Right() - return - } - c.Right() - } - c.Right() - for IsWordChar(string(c.RuneUnder(c.X))) { - if c.X == Count(c.buf.Line(c.Y)) { - return - } - c.Right() - } -} - -// WordLeft moves the cursor one word to the left -func (c *Cursor) WordLeft() { - c.Left() - for IsWhitespace(c.RuneUnder(c.X)) { - if c.X == 0 { - return - } - c.Left() - } - c.Left() - for IsWordChar(string(c.RuneUnder(c.X))) { - if c.X == 0 { - return - } - c.Left() - } - c.Right() -} - -// RuneUnder returns the rune under the given x position -func (c *Cursor) RuneUnder(x int) rune { - line := []rune(c.buf.Line(c.Y)) - if len(line) == 0 { - return '\n' - } - if x >= len(line) { - return '\n' - } else if x < 0 { - x = 0 - } - return line[x] -} // UpN moves the cursor up N lines (if possible) func (c *Cursor) UpN(amount int) { proposedY := c.Y - amount if proposedY < 0 { proposedY = 0 - c.LastVisualX = 0 - } else if proposedY >= c.buf.NumLines { - proposedY = c.buf.NumLines - 1 + } else if proposedY >= len(c.buf.lines) { + proposedY = len(c.buf.lines) - 1 } - runes := []rune(c.buf.Line(proposedY)) - c.X = c.GetCharPosInLine(proposedY, c.LastVisualX) - if c.X > len(runes) || (amount < 0 && proposedY == c.Y) { - c.X = len(runes) + bytes := c.buf.LineBytes(proposedY) + c.X = c.GetCharPosInLine(bytes, c.LastVisualX) + + if c.X > utf8.RuneCount(bytes) || (amount < 0 && proposedY == c.Y) { + c.X = utf8.RuneCount(bytes) } c.Y = proposedY @@ -310,7 +252,7 @@ func (c *Cursor) Right() { if c.Loc == c.buf.End() { return } - if c.X < Count(c.buf.Line(c.Y)) { + if c.X < utf8.RuneCount(c.buf.LineBytes(c.Y)) { c.X++ } else { c.Down() @@ -319,80 +261,19 @@ func (c *Cursor) Right() { c.LastVisualX = c.GetVisualX() } -// End moves the cursor to the end of the line it is on -func (c *Cursor) End() { - c.X = Count(c.buf.Line(c.Y)) - c.LastVisualX = c.GetVisualX() -} - -// Start moves the cursor to the start of the line it is on -func (c *Cursor) Start() { - c.X = 0 - c.LastVisualX = c.GetVisualX() -} - -// StartOfText moves the cursor to the first non-whitespace rune of -// the line it is on -func (c *Cursor) StartOfText() { - c.Start() - for IsWhitespace(c.RuneUnder(c.X)) { - if c.X == Count(c.buf.Line(c.Y)) { - break - } - c.Right() - } -} - -// GetCharPosInLine gets the char position of a visual x y -// coordinate (this is necessary because tabs are 1 char but -// 4 visual spaces) -func (c *Cursor) GetCharPosInLine(lineNum, visualPos int) int { - // Get the tab size - tabSize := int(c.buf.Settings["tabsize"].(float64)) - visualLineLen := StringWidth(c.buf.Line(lineNum), tabSize) - if visualPos > visualLineLen { - visualPos = visualLineLen - } - width := WidthOfLargeRunes(c.buf.Line(lineNum), tabSize) - if visualPos >= width { - return visualPos - width - } - return visualPos / tabSize -} - -// GetVisualX returns the x value of the cursor in visual spaces -func (c *Cursor) GetVisualX() int { - runes := []rune(c.buf.Line(c.Y)) - tabSize := int(c.buf.Settings["tabsize"].(float64)) - if c.X > len(runes) { - c.X = len(runes) - 1 - } - - if c.X < 0 { - c.X = 0 - } - - return StringWidth(string(runes[:c.X]), tabSize) -} - -// StoreVisualX stores the current visual x value in the cursor -func (c *Cursor) StoreVisualX() { - c.LastVisualX = c.GetVisualX() -} - // Relocate makes sure that the cursor is inside the bounds // of the buffer If it isn't, it moves it to be within the // buffer's lines func (c *Cursor) Relocate() { if c.Y < 0 { c.Y = 0 - } else if c.Y >= c.buf.NumLines { - c.Y = c.buf.NumLines - 1 + } else if c.Y >= len(c.buf.lines) { + c.Y = len(c.buf.lines) - 1 } if c.X < 0 { c.X = 0 - } else if c.X > Count(c.buf.Line(c.Y)) { - c.X = Count(c.buf.Line(c.Y)) + } else if c.X > utf8.RuneCount(c.buf.LineBytes(c.Y)) { + c.X = utf8.RuneCount(c.buf.LineBytes(c.Y)) } } diff --git a/cmd/micro/cursor_test.go b/cmd/micro/cursor_test.go new file mode 100644 index 00000000..06ab7d0f --- /dev/null +++ b/cmd/micro/cursor_test.go @@ -0,0 +1 @@ +package main diff --git a/cmd/micro/debug.go b/cmd/micro/debug.go new file mode 100644 index 00000000..494b0e92 --- /dev/null +++ b/cmd/micro/debug.go @@ -0,0 +1,27 @@ +package main + +import ( + "log" + "os" +) + +type NullWriter struct{} + +func (NullWriter) Write(data []byte) (n int, err error) { + return 0, nil +} + +func InitLog() { + if Debug == "ON" { + f, err := os.OpenFile("log.txt", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) + if err != nil { + log.Fatalf("error opening file: %v", err) + } + + log.SetOutput(f) + log.Println("Micro started") + } else { + log.SetOutput(NullWriter{}) + log.Println("Micro started") + } +} diff --git a/cmd/micro/eventhandler.go b/cmd/micro/eventhandler.go index adf7cba4..5eefa2af 100644 --- a/cmd/micro/eventhandler.go +++ b/cmd/micro/eventhandler.go @@ -1,11 +1,10 @@ package main import ( - "strings" "time" + "unicode/utf8" dmp "github.com/sergi/go-diff/diffmatchpatch" - "github.com/yuin/gopher-lua" ) const ( @@ -30,7 +29,7 @@ type TextEvent struct { // A Delta is a change to the buffer type Delta struct { - Text string + Text []byte Start Loc End Loc } @@ -39,7 +38,7 @@ type Delta struct { func ExecuteTextEvent(t *TextEvent, buf *Buffer) { if t.EventType == TextEventInsert { for _, d := range t.Deltas { - buf.insert(d.Start, []byte(d.Text)) + buf.insert(d.Start, d.Text) } } else if t.EventType == TextEventRemove { for i, d := range t.Deltas { @@ -48,9 +47,9 @@ func ExecuteTextEvent(t *TextEvent, buf *Buffer) { } else if t.EventType == TextEventReplace { for i, d := range t.Deltas { t.Deltas[i].Text = buf.remove(d.Start, d.End) - buf.insert(d.Start, []byte(d.Text)) + buf.insert(d.Start, d.Text) t.Deltas[i].Start = d.Start - t.Deltas[i].End = Loc{d.Start.X + Count(d.Text), d.Start.Y} + t.Deltas[i].End = Loc{d.Start.X + utf8.RuneCount(d.Text), d.Start.Y} } for i, j := 0, len(t.Deltas)-1; i < j; i, j = i+1, j-1 { t.Deltas[i], t.Deltas[j] = t.Deltas[j], t.Deltas[i] @@ -67,15 +66,15 @@ func UndoTextEvent(t *TextEvent, buf *Buffer) { // EventHandler executes text manipulations and allows undoing and redoing type EventHandler struct { buf *Buffer - UndoStack *Stack - RedoStack *Stack + UndoStack *TEStack + RedoStack *TEStack } // NewEventHandler returns a new EventHandler func NewEventHandler(buf *Buffer) *EventHandler { eh := new(EventHandler) - eh.UndoStack = new(Stack) - eh.RedoStack = new(Stack) + eh.UndoStack = new(TEStack) + eh.RedoStack = new(TEStack) eh.buf = buf return eh } @@ -86,38 +85,39 @@ func NewEventHandler(buf *Buffer) *EventHandler { // through insert and delete events func (eh *EventHandler) ApplyDiff(new string) { differ := dmp.New() - diff := differ.DiffMain(eh.buf.String(), new, false) + diff := differ.DiffMain(string(eh.buf.Bytes()), new, false) loc := eh.buf.Start() for _, d := range diff { if d.Type == dmp.DiffDelete { - eh.Remove(loc, loc.Move(Count(d.Text), eh.buf)) + eh.Remove(loc, loc.Move(utf8.RuneCountInString(d.Text), eh.buf)) } else { if d.Type == dmp.DiffInsert { eh.Insert(loc, d.Text) } - loc = loc.Move(Count(d.Text), eh.buf) + loc = loc.Move(utf8.RuneCountInString(d.Text), eh.buf) } } } // Insert creates an insert text event and executes it -func (eh *EventHandler) Insert(start Loc, text string) { +func (eh *EventHandler) Insert(start Loc, textStr string) { + text := []byte(textStr) e := &TextEvent{ - C: *eh.buf.cursors[eh.buf.curCursor], + C: *eh.buf.GetActiveCursor(), EventType: TextEventInsert, Deltas: []Delta{{text, start, Loc{0, 0}}}, Time: time.Now(), } eh.Execute(e) - e.Deltas[0].End = start.Move(Count(text), eh.buf) + e.Deltas[0].End = start.Move(utf8.RuneCount(text), eh.buf) end := e.Deltas[0].End - for _, c := range eh.buf.cursors { + for _, c := range eh.buf.GetCursors() { move := func(loc Loc) Loc { if start.Y != end.Y && loc.GreaterThan(start) { loc.Y += end.Y - start.Y } else if loc.Y == start.Y && loc.GreaterEqual(start) { - loc = loc.Move(Count(text), eh.buf) + loc = loc.Move(utf8.RuneCount(text), eh.buf) } return loc } @@ -133,14 +133,14 @@ func (eh *EventHandler) Insert(start Loc, text string) { // Remove creates a remove text event and executes it func (eh *EventHandler) Remove(start, end Loc) { e := &TextEvent{ - C: *eh.buf.cursors[eh.buf.curCursor], + C: *eh.buf.GetActiveCursor(), EventType: TextEventRemove, - Deltas: []Delta{{"", start, end}}, + Deltas: []Delta{{[]byte{}, start, end}}, Time: time.Now(), } eh.Execute(e) - for _, c := range eh.buf.cursors { + for _, c := range eh.buf.GetCursors() { move := func(loc Loc) Loc { if start.Y != end.Y && loc.GreaterThan(end) { loc.Y -= end.Y - start.Y @@ -161,7 +161,7 @@ func (eh *EventHandler) Remove(start, end Loc) { // MultipleReplace creates an multiple insertions executes them func (eh *EventHandler) MultipleReplace(deltas []Delta) { e := &TextEvent{ - C: *eh.buf.cursors[eh.buf.curCursor], + C: *eh.buf.GetActiveCursor(), EventType: TextEventReplace, Deltas: deltas, Time: time.Now(), @@ -178,19 +178,20 @@ func (eh *EventHandler) Replace(start, end Loc, replace string) { // Execute a textevent and add it to the undo stack func (eh *EventHandler) Execute(t *TextEvent) { if eh.RedoStack.Len() > 0 { - eh.RedoStack = new(Stack) + eh.RedoStack = new(TEStack) } eh.UndoStack.Push(t) - for pl := range loadedPlugins { - ret, err := Call(pl+".onBeforeTextEvent", t) - if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") { - TermMessage(err) - } - if val, ok := ret.(lua.LBool); ok && val == lua.LFalse { - return - } - } + // TODO: Call plugins on text events + // for pl := range loadedPlugins { + // ret, err := Call(pl+".onBeforeTextEvent", t) + // if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") { + // TermMessage(err) + // } + // if val, ok := ret.(lua.LBool); ok && val == lua.LFalse { + // return + // } + // } ExecuteTextEvent(t, eh.buf) } @@ -236,9 +237,9 @@ func (eh *EventHandler) UndoOneEvent() { // Set the cursor in the right place teCursor := t.C - if teCursor.Num >= 0 && teCursor.Num < len(eh.buf.cursors) { - t.C = *eh.buf.cursors[teCursor.Num] - eh.buf.cursors[teCursor.Num].Goto(teCursor) + if teCursor.Num >= 0 && teCursor.Num < eh.buf.NumCursors() { + t.C = *eh.buf.GetCursor(teCursor.Num) + eh.buf.GetCursor(teCursor.Num).Goto(teCursor) } else { teCursor.Num = -1 } @@ -283,9 +284,9 @@ func (eh *EventHandler) RedoOneEvent() { UndoTextEvent(t, eh.buf) teCursor := t.C - if teCursor.Num >= 0 && teCursor.Num < len(eh.buf.cursors) { - t.C = *eh.buf.cursors[teCursor.Num] - eh.buf.cursors[teCursor.Num].Goto(teCursor) + if teCursor.Num >= 0 && teCursor.Num < eh.buf.NumCursors() { + t.C = *eh.buf.GetCursor(teCursor.Num) + eh.buf.GetCursor(teCursor.Num).Goto(teCursor) } else { teCursor.Num = -1 } diff --git a/cmd/micro/highlighter.go b/cmd/micro/highlighter.go deleted file mode 100644 index 53a46555..00000000 --- a/cmd/micro/highlighter.go +++ /dev/null @@ -1,28 +0,0 @@ -package main - -import "github.com/zyedidia/micro/cmd/micro/highlight" - -var syntaxFiles []*highlight.File - -func LoadSyntaxFiles() { - InitColorscheme() - for _, f := range ListRuntimeFiles(RTSyntax) { - data, err := f.Data() - if err != nil { - TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error()) - } else { - LoadSyntaxFile(data, f.Name()) - } - } -} - -func LoadSyntaxFile(text []byte, filename string) { - f, err := highlight.ParseFile(text) - - if err != nil { - TermMessage("Syntax file error: " + filename + ": " + err.Error()) - return - } - - syntaxFiles = append(syntaxFiles, f) -} diff --git a/cmd/micro/job.go b/cmd/micro/job.go deleted file mode 100644 index df9151b5..00000000 --- a/cmd/micro/job.go +++ /dev/null @@ -1,88 +0,0 @@ -package main - -import ( - "bytes" - "io" - "os/exec" -) - -// 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 - - callback func(string, ...string) - args []string -} - -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 shell command 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 { - return JobSpawn("sh", []string{"-c", cmd}, onStdout, onStderr, onExit, userargs...) -} - -// JobSpawn starts a process with args in the background with the given callbacks -// It returns an *exec.Cmd as the job id -func JobSpawn(cmdName string, cmdArgs []string, onStdout, onStderr, onExit string, userargs ...string) *exec.Cmd { - // Set up everything correctly if the functions have been provided - proc := exec.Command(cmdName, cmdArgs...) - var outbuf bytes.Buffer - if onStdout != "" { - proc.Stdout = &CallbackFile{&outbuf, LuaFunctionJob(onStdout), userargs} - } else { - proc.Stdout = &outbuf - } - if onStderr != "" { - proc.Stderr = &CallbackFile{&outbuf, LuaFunctionJob(onStderr), userargs} - } else { - proc.Stderr = &outbuf - } - - 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 - }() - - 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 { - return - } - - stdin.Write([]byte(data)) -} diff --git a/cmd/micro/keymenu.go b/cmd/micro/keymenu.go deleted file mode 100644 index 737cc36f..00000000 --- a/cmd/micro/keymenu.go +++ /dev/null @@ -1,20 +0,0 @@ -package main - -// DisplayKeyMenu displays the nano-style key menu at the bottom of the screen -func DisplayKeyMenu() { - w, h := screen.Size() - - bot := h - 3 - - display := []string{"^Q Quit, ^S Save, ^O Open, ^G Help, ^E Command Bar, ^K Cut Line", "^F Find, ^Z Undo, ^Y Redo, ^A Select All, ^D Duplicate Line, ^T New Tab"} - - for y := 0; y < len(display); y++ { - for x := 0; x < w; x++ { - if x < len(display[y]) { - screen.SetContent(x, bot+y, rune(display[y][x]), nil, defStyle) - } else { - screen.SetContent(x, bot+y, ' ', nil, defStyle) - } - } - } -} diff --git a/cmd/micro/lineArray.go b/cmd/micro/line_array.go similarity index 60% rename from cmd/micro/lineArray.go rename to cmd/micro/line_array.go index 24860378..44907f2a 100644 --- a/cmd/micro/lineArray.go +++ b/cmd/micro/line_array.go @@ -8,6 +8,7 @@ import ( "github.com/zyedidia/micro/cmd/micro/highlight" ) +// Finds the byte index of the nth rune in a byte slice func runeToByteIndex(n int, txt []byte) int { if n == 0 { return 0 @@ -39,10 +40,21 @@ type Line struct { rehighlight bool } +const ( + // Line ending file formats + FFAuto = 0 // Autodetect format + FFUnix = 1 // LF line endings (unix style '\n') + FFDos = 2 // CRLF line endings (dos style '\r\n') +) + +type FileFormat byte + // A LineArray simply stores and array of lines and makes it easy to insert // and delete in it type LineArray struct { - lines []Line + lines []Line + endings FileFormat + initsize uint64 } // Append efficiently appends lines together @@ -52,7 +64,6 @@ func Append(slice []Line, data ...Line) []Line { l := len(slice) if l+len(data) > cap(slice) { // reallocate newSlice := make([]Line, (l+len(data))+10000) - // The copy function is predeclared and works for any slice type. copy(newSlice, slice) slice = newSlice } @@ -64,10 +75,11 @@ func Append(slice []Line, data ...Line) []Line { } // NewLineArray returns a new line array from an array of bytes -func NewLineArray(size int64, reader io.Reader) *LineArray { +func NewLineArray(size uint64, endings FileFormat, reader io.Reader) *LineArray { la := new(LineArray) la.lines = make([]Line, 0, 1000) + la.initsize = size br := bufio.NewReader(reader) var loaded int @@ -75,17 +87,27 @@ func NewLineArray(size int64, reader io.Reader) *LineArray { n := 0 for { data, err := br.ReadBytes('\n') - if len(data) > 1 && data[len(data)-2] == '\r' { - data = append(data[:len(data)-2], '\n') - if fileformat == 0 { - fileformat = 2 + // Detect the line ending by checking to see if there is a '\r' char + // before the '\n' + // Even if the file format is set to DOS, the '\r' is removed so + // that all lines end with '\n' + dlen := len(data) + if dlen > 1 && data[dlen-2] == '\r' { + data = append(data[:dlen-2], '\n') + if endings == FFAuto { + la.endings = FFDos } - } else if len(data) > 0 { - if fileformat == 0 { - fileformat = 1 + } else if dlen > 0 { + if endings == FFAuto { + la.endings = FFUnix } } + // If we are loading a large file (greater than 1000) we use the file + // size and the length of the first 1000 lines to try to estimate + // how many lines will need to be allocated for the rest of the file + // We add an extra 10000 to the original estimate to be safe and give + // plenty of room for expansion if n >= 1000 && loaded >= 0 { totalLinesNum := int(float64(size) * (float64(n) / float64(loaded))) newSlice := make([]Line, len(la.lines), totalLinesNum+10000) @@ -94,20 +116,19 @@ func NewLineArray(size int64, reader io.Reader) *LineArray { loaded = -1 } + // Counter for the number of bytes in the first 1000 lines if loaded >= 0 { - loaded += len(data) + loaded += dlen } if err != nil { if err == io.EOF { la.lines = Append(la.lines, Line{data[:], nil, nil, false}) - // la.lines = Append(la.lines, Line{data[:len(data)]}) } // Last line was read break } else { - // la.lines = Append(la.lines, Line{data[:len(data)-1]}) - la.lines = Append(la.lines, Line{data[:len(data)-1], nil, nil, false}) + la.lines = Append(la.lines, Line{data[:dlen-1], nil, nil, false}) } n++ } @@ -115,49 +136,35 @@ func NewLineArray(size int64, reader io.Reader) *LineArray { return la } -// Returns the String representation of the LineArray -func (la *LineArray) String() string { - str := "" - for i, l := range la.lines { - str += string(l.data) - if i != len(la.lines)-1 { - str += "\n" - } - } - return str -} - -// SaveString returns the string that should be written to disk when +// Bytes returns the string that should be written to disk when // the line array is saved -// It is the same as string but uses crlf or lf line endings depending -func (la *LineArray) SaveString(useCrlf bool) string { - str := "" +func (la *LineArray) Bytes() []byte { + str := make([]byte, 0, la.initsize+1000) // initsize should provide a good estimate for i, l := range la.lines { - str += string(l.data) + str = append(str, l.data...) if i != len(la.lines)-1 { - if useCrlf { - str += "\r" + if la.endings == FFDos { + str = append(str, '\r') } - str += "\n" + str = append(str, '\n') } } return str } -// NewlineBelow adds a newline below the given line number -func (la *LineArray) NewlineBelow(y int) { +// newlineBelow adds a newline below the given line number +func (la *LineArray) newlineBelow(y int) { la.lines = append(la.lines, Line{[]byte{' '}, nil, nil, false}) copy(la.lines[y+2:], la.lines[y+1:]) la.lines[y+1] = Line{[]byte{}, la.lines[y].state, nil, false} } -// inserts a byte array at a given location +// 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].data), pos.Y - // x, y := pos.x, pos.y for i := 0; i < len(value); i++ { if value[i] == '\n' { - la.Split(Loc{x, y}) + la.split(Loc{x, y}) x = 0 y++ continue @@ -167,33 +174,33 @@ func (la *LineArray) insert(pos Loc, value []byte) { } } -// inserts a byte at a given location +// InsertByte inserts a byte at a given location func (la *LineArray) insertByte(pos Loc, value byte) { la.lines[pos.Y].data = append(la.lines[pos.Y].data, 0) copy(la.lines[pos.Y].data[pos.X+1:], la.lines[pos.Y].data[pos.X:]) la.lines[pos.Y].data[pos.X] = value } -// JoinLines joins the two lines a and b -func (la *LineArray) JoinLines(a, b int) { +// joinLines joins the two lines a and b +func (la *LineArray) joinLines(a, b int) { la.insert(Loc{len(la.lines[a].data), a}, la.lines[b].data) - la.DeleteLine(b) + la.deleteLine(b) } -// Split splits a line at a given position -func (la *LineArray) Split(pos Loc) { - la.NewlineBelow(pos.Y) +// 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].data[pos.X:]) la.lines[pos.Y+1].state = la.lines[pos.Y].state la.lines[pos.Y].state = nil la.lines[pos.Y].match = nil la.lines[pos.Y+1].match = nil la.lines[pos.Y].rehighlight = true - la.DeleteToEnd(Loc{pos.X, pos.Y}) + la.deleteToEnd(Loc{pos.X, pos.Y}) } // removes from start to end -func (la *LineArray) remove(start, end Loc) string { +func (la *LineArray) remove(start, end Loc) []byte { sub := la.Substr(start, end) startX := runeToByteIndex(start.X, la.lines[start.Y].data) endX := runeToByteIndex(end.X, la.lines[end.Y].data) @@ -201,48 +208,50 @@ func (la *LineArray) remove(start, end Loc) string { la.lines[start.Y].data = append(la.lines[start.Y].data[:startX], la.lines[start.Y].data[endX:]...) } else { for i := start.Y + 1; i <= end.Y-1; i++ { - la.DeleteLine(start.Y + 1) + la.deleteLine(start.Y + 1) } - la.DeleteToEnd(Loc{startX, start.Y}) - la.DeleteFromStart(Loc{endX - 1, start.Y + 1}) - la.JoinLines(start.Y, start.Y+1) + la.deleteToEnd(Loc{startX, start.Y}) + la.deleteFromStart(Loc{endX - 1, start.Y + 1}) + la.joinLines(start.Y, start.Y+1) } return sub } -// DeleteToEnd deletes from the end of a line to the position -func (la *LineArray) DeleteToEnd(pos Loc) { +// deleteToEnd deletes from the end of a line to the position +func (la *LineArray) deleteToEnd(pos Loc) { la.lines[pos.Y].data = la.lines[pos.Y].data[:pos.X] } -// DeleteFromStart deletes from the start of a line to the position -func (la *LineArray) DeleteFromStart(pos Loc) { +// deleteFromStart deletes from the start of a line to the position +func (la *LineArray) deleteFromStart(pos Loc) { la.lines[pos.Y].data = la.lines[pos.Y].data[pos.X+1:] } -// DeleteLine deletes the line number -func (la *LineArray) DeleteLine(y int) { +// 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) { +func (la *LineArray) deleteByte(pos Loc) { la.lines[pos.Y].data = la.lines[pos.Y].data[:pos.X+copy(la.lines[pos.Y].data[pos.X:], la.lines[pos.Y].data[pos.X+1:])] } // Substr returns the string representation between two locations -func (la *LineArray) Substr(start, end Loc) string { +func (la *LineArray) Substr(start, end Loc) []byte { startX := runeToByteIndex(start.X, la.lines[start.Y].data) endX := runeToByteIndex(end.X, la.lines[end.Y].data) if start.Y == end.Y { - return string(la.lines[start.Y].data[startX:endX]) + return la.lines[start.Y].data[startX:endX] } - var str string - str += string(la.lines[start.Y].data[startX:]) + "\n" + str := make([]byte, 0, len(la.lines[start.Y+1].data)*(end.Y-start.Y)) + str = append(str, la.lines[start.Y].data[startX:]...) + str = append(str, '\n') for i := start.Y + 1; i <= end.Y-1; i++ { - str += string(la.lines[i].data) + "\n" + str = append(str, la.lines[i].data...) + str = append(str, '\n') } - str += string(la.lines[end.Y].data[:endX]) + str = append(str, la.lines[end.Y].data[:endX]...) return str } diff --git a/cmd/micro/line_array_test.go b/cmd/micro/line_array_test.go new file mode 100644 index 00000000..72e370ce --- /dev/null +++ b/cmd/micro/line_array_test.go @@ -0,0 +1,60 @@ +package main + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +var unicode_txt = `An preost wes on leoden, Laȝamon was ihoten +He wes Leovenaðes sone -- liðe him be Drihten. +He wonede at Ernleȝe at æðelen are chirechen, +Uppen Sevarne staþe, sel þar him þuhte, +Onfest Radestone, þer he bock radde.` + +var la *LineArray + +func init() { + reader := strings.NewReader(unicode_txt) + la = NewLineArray(uint64(len(unicode_txt)), FFAuto, reader) +} + +func TestSplit(t *testing.T) { + la.insert(Loc{17, 1}, []byte{'\n'}) + assert.Equal(t, len(la.lines), 6) + sub1 := la.Substr(Loc{0, 1}, Loc{17, 1}) + sub2 := la.Substr(Loc{0, 2}, Loc{30, 2}) + + assert.Equal(t, []byte("He wes Leovenaðes"), sub1) + assert.Equal(t, []byte(" sone -- liðe him be Drihten."), sub2) +} + +func TestJoin(t *testing.T) { + la.remove(Loc{47, 1}, Loc{0, 2}) + assert.Equal(t, len(la.lines), 5) + sub := la.Substr(Loc{0, 1}, Loc{47, 1}) + bytes := la.Bytes() + + assert.Equal(t, []byte("He wes Leovenaðes sone -- liðe him be Drihten."), sub) + assert.Equal(t, unicode_txt, string(bytes)) +} + +func TestInsert(t *testing.T) { + la.insert(Loc{20, 3}, []byte(" foobar")) + sub1 := la.Substr(Loc{0, 3}, Loc{50, 3}) + + assert.Equal(t, []byte("Uppen Sevarne staþe, foobar sel þar him þuhte,"), sub1) + + la.insert(Loc{25, 2}, []byte("ಮಣ್ಣಾಗಿ")) + sub2 := la.Substr(Loc{0, 2}, Loc{60, 2}) + assert.Equal(t, []byte("He wonede at Ernleȝe at æಮಣ್ಣಾಗಿðelen are chirechen,"), sub2) +} + +func TestRemove(t *testing.T) { + la.remove(Loc{20, 3}, Loc{27, 3}) + la.remove(Loc{25, 2}, Loc{32, 2}) + + bytes := la.Bytes() + assert.Equal(t, unicode_txt, string(bytes)) +} diff --git a/cmd/micro/loc.go b/cmd/micro/loc.go index a3806e54..d7ce8a1a 100644 --- a/cmd/micro/loc.go +++ b/cmd/micro/loc.go @@ -1,59 +1,52 @@ package main -// FromCharPos converts from a character position to an x, y position -func FromCharPos(loc int, buf *Buffer) Loc { - charNum := 0 - x, y := 0, 0 - - lineLen := Count(buf.Line(y)) + 1 - for charNum+lineLen <= loc { - charNum += lineLen - y++ - lineLen = Count(buf.Line(y)) + 1 - } - x = loc - charNum - - return Loc{x, y} -} - -// ToCharPos converts from an x, y position to a character position -func ToCharPos(start Loc, buf *Buffer) int { - x, y := start.X, start.Y - loc := 0 - for i := 0; i < y; i++ { - // + 1 for the newline - loc += Count(buf.Line(i)) + 1 - } - loc += x - return loc -} - -// InBounds returns whether the given location is a valid character position in the given buffer -func InBounds(pos Loc, buf *Buffer) bool { - if pos.Y < 0 || pos.Y >= buf.NumLines || pos.X < 0 || pos.X > Count(buf.Line(pos.Y)) { - return false - } - - return true -} - -// ByteOffset is just like ToCharPos except it counts bytes instead of runes -func ByteOffset(pos Loc, buf *Buffer) int { - x, y := pos.X, pos.Y - loc := 0 - for i := 0; i < y; i++ { - // + 1 for the newline - loc += len(buf.Line(i)) + 1 - } - loc += len(buf.Line(y)[:x]) - return loc -} +import "unicode/utf8" // Loc stores a location type Loc struct { X, Y int } +// LessThan returns true if b is smaller +func (l Loc) LessThan(b Loc) bool { + if l.Y < b.Y { + return true + } + return l.Y == b.Y && l.X < b.X +} + +// GreaterThan returns true if b is bigger +func (l Loc) GreaterThan(b Loc) bool { + if l.Y > b.Y { + return true + } + return l.Y == b.Y && l.X > b.X +} + +// GreaterEqual returns true if b is greater than or equal to b +func (l Loc) GreaterEqual(b Loc) bool { + if l.Y > b.Y { + return true + } + if l.Y == b.Y && l.X > b.X { + return true + } + return l == b +} + +// LessEqual returns true if b is less than or equal to b +func (l Loc) LessEqual(b Loc) bool { + if l.Y < b.Y { + return true + } + if l.Y == b.Y && l.X < b.X { + return true + } + return l == b +} + +// The following functions require a buffer to know where newlines are + // Diff returns the distance between two locations func Diff(a, b Loc, buf *Buffer) int { if a.Y == b.Y { @@ -71,69 +64,19 @@ func Diff(a, b Loc, buf *Buffer) int { loc := 0 for i := a.Y + 1; i < b.Y; i++ { // + 1 for the newline - loc += Count(buf.Line(i)) + 1 + loc += utf8.RuneCount(buf.LineBytes(i)) + 1 } - loc += Count(buf.Line(a.Y)) - a.X + b.X + 1 + loc += utf8.RuneCount(buf.LineBytes(a.Y)) - a.X + b.X + 1 return loc } -// LessThan returns true if b is smaller -func (l Loc) LessThan(b Loc) bool { - if l.Y < b.Y { - return true - } - if l.Y == b.Y && l.X < b.X { - return true - } - return false -} - -// GreaterThan returns true if b is bigger -func (l Loc) GreaterThan(b Loc) bool { - if l.Y > b.Y { - return true - } - if l.Y == b.Y && l.X > b.X { - return true - } - return false -} - -// GreaterEqual returns true if b is greater than or equal to b -func (l Loc) GreaterEqual(b Loc) bool { - if l.Y > b.Y { - return true - } - if l.Y == b.Y && l.X > b.X { - return true - } - if l == b { - return true - } - return false -} - -// LessEqual returns true if b is less than or equal to b -func (l Loc) LessEqual(b Loc) bool { - if l.Y < b.Y { - return true - } - if l.Y == b.Y && l.X < b.X { - return true - } - if l == b { - return true - } - 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} } var res Loc - if l.X < Count(buf.Line(l.Y)) { + if l.X < utf8.RuneCount(buf.LineBytes(l.Y)) { res = Loc{l.X + 1, l.Y} } else { res = Loc{0, l.Y + 1} @@ -150,7 +93,7 @@ func (l Loc) left(buf *Buffer) Loc { if l.X > 0 { res = Loc{l.X - 1, l.Y} } else { - res = Loc{Count(buf.Line(l.Y - 1)), l.Y - 1} + res = Loc{utf8.RuneCount(buf.LineBytes(l.Y - 1)), l.Y - 1} } return res } diff --git a/cmd/micro/lua.go b/cmd/micro/lua.go index d97a1191..0425efa2 100644 --- a/cmd/micro/lua.go +++ b/cmd/micro/lua.go @@ -16,9 +16,8 @@ import ( "strings" "time" - luar "layeh.com/gopher-luar" - lua "github.com/yuin/gopher-lua" + luar "layeh.com/gopher-luar" ) var L *lua.LState diff --git a/cmd/micro/message.go b/cmd/micro/message.go new file mode 100644 index 00000000..1c660fca --- /dev/null +++ b/cmd/micro/message.go @@ -0,0 +1,38 @@ +package main + +import ( + "bufio" + "fmt" + "os" + "strconv" +) + +// TermMessage sends a message to the user in the terminal. This usually occurs before +// micro has been fully initialized -- ie if there is an error in the syntax highlighting +// regular expressions +// The function must be called when the screen is not initialized +// This will write the message, and wait for the user +// to press and key to continue +func TermMessage(msg ...interface{}) { + screenWasNil := screen == nil + if !screenWasNil { + screen.Fini() + screen = nil + } + + fmt.Println(msg...) + fmt.Print("\nPress enter to continue") + + reader := bufio.NewReader(os.Stdin) + reader.ReadString('\n') + + if !screenWasNil { + InitScreen() + } +} + +// TermError sends an error to the user in the terminal. Like TermMessage except formatted +// as an error +func TermError(filename string, lineNum int, err string) { + TermMessage(filename + ", " + strconv.Itoa(lineNum) + ": " + err) +} diff --git a/cmd/micro/messenger.go b/cmd/micro/messenger.go deleted file mode 100644 index b55557af..00000000 --- a/cmd/micro/messenger.go +++ /dev/null @@ -1,669 +0,0 @@ -package main - -import ( - "bufio" - "bytes" - "encoding/gob" - "fmt" - "os" - "strconv" - - "github.com/mattn/go-runewidth" - "github.com/zyedidia/clipboard" - "github.com/zyedidia/micro/cmd/micro/shellwords" - "github.com/zyedidia/tcell" -) - -// TermMessage sends a message to the user in the terminal. This usually occurs before -// micro has been fully initialized -- ie if there is an error in the syntax highlighting -// regular expressions -// The function must be called when the screen is not initialized -// This will write the message, and wait for the user -// to press and key to continue -func TermMessage(msg ...interface{}) { - screenWasNil := screen == nil - if !screenWasNil { - screen.Fini() - screen = nil - } - - fmt.Println(msg...) - fmt.Print("\nPress enter to continue") - - reader := bufio.NewReader(os.Stdin) - reader.ReadString('\n') - - if !screenWasNil { - InitScreen() - } -} - -// TermError sends an error to the user in the terminal. Like TermMessage except formatted -// as an error -func TermError(filename string, lineNum int, err string) { - TermMessage(filename + ", " + strconv.Itoa(lineNum) + ": " + err) -} - -// Messenger is an object that makes it easy to send messages to the user -// and get input from the user -type Messenger struct { - log *Buffer - // Are we currently prompting the user? - hasPrompt bool - // Is there a message to print - hasMessage bool - - // Message to print - message string - // The user's response to a prompt - response string - // style to use when drawing the message - style tcell.Style - - // We have to keep track of the cursor for prompting - cursorx int - - // This map stores the history for all the different kinds of uses Prompt has - // It's a map of history type -> history array - history map[string][]string - historyNum int - - // Is the current message a message from the gutter - gutterMessage bool -} - -// AddLog sends a message to the log view -func (m *Messenger) AddLog(msg ...interface{}) { - logMessage := fmt.Sprint(msg...) - buffer := m.getBuffer() - buffer.insert(buffer.End(), []byte(logMessage+"\n")) - buffer.Cursor.Loc = buffer.End() - buffer.Cursor.Relocate() -} - -func (m *Messenger) getBuffer() *Buffer { - if m.log == nil { - m.log = NewBufferFromString("", "") - m.log.name = "Log" - } - return m.log -} - -// Message sends a message to the user -func (m *Messenger) Message(msg ...interface{}) { - displayMessage := fmt.Sprint(msg...) - // only display a new message if there isn't an active prompt - // this is to prevent overwriting an existing prompt to the user - if m.hasPrompt == false { - // if there is no active prompt then style and display the message as normal - m.message = displayMessage - - m.style = defStyle - - if _, ok := colorscheme["message"]; ok { - m.style = colorscheme["message"] - } - - m.hasMessage = true - } - // add the message to the log regardless of active prompts - m.AddLog(displayMessage) -} - -// Error sends an error message to the user -func (m *Messenger) Error(msg ...interface{}) { - buf := new(bytes.Buffer) - fmt.Fprint(buf, msg...) - - // only display a new message if there isn't an active prompt - // this is to prevent overwriting an existing prompt to the user - if m.hasPrompt == false { - // if there is no active prompt then style and display the message as normal - m.message = buf.String() - m.style = defStyle. - Foreground(tcell.ColorBlack). - Background(tcell.ColorMaroon) - - if _, ok := colorscheme["error-message"]; ok { - m.style = colorscheme["error-message"] - } - m.hasMessage = true - } - // add the message to the log regardless of active prompts - m.AddLog(buf.String()) -} - -func (m *Messenger) PromptText(msg ...interface{}) { - displayMessage := fmt.Sprint(msg...) - // if there is no active prompt then style and display the message as normal - m.message = displayMessage - - m.style = defStyle - - if _, ok := colorscheme["message"]; ok { - m.style = colorscheme["message"] - } - - m.hasMessage = true - // add the message to the log regardless of active prompts - m.AddLog(displayMessage) -} - -// YesNoPrompt asks the user a yes or no question (waits for y or n) and returns the result -func (m *Messenger) YesNoPrompt(prompt string) (bool, bool) { - m.hasPrompt = true - m.PromptText(prompt) - - _, h := screen.Size() - for { - m.Clear() - m.Display() - screen.ShowCursor(Count(m.message), h-1) - screen.Show() - event := <-events - - switch e := event.(type) { - case *tcell.EventKey: - switch e.Key() { - case tcell.KeyRune: - if e.Rune() == 'y' || e.Rune() == 'Y' { - m.AddLog("\t--> y") - m.hasPrompt = false - return true, false - } else if e.Rune() == 'n' || e.Rune() == 'N' { - m.AddLog("\t--> n") - m.hasPrompt = false - return false, false - } - case tcell.KeyCtrlC, tcell.KeyCtrlQ, tcell.KeyEscape: - m.AddLog("\t--> (cancel)") - m.Clear() - m.Reset() - m.hasPrompt = false - return false, true - } - } - } -} - -// LetterPrompt gives the user a prompt and waits for a one letter response -func (m *Messenger) LetterPrompt(prompt string, responses ...rune) (rune, bool) { - m.hasPrompt = true - m.PromptText(prompt) - - _, h := screen.Size() - for { - m.Clear() - m.Display() - screen.ShowCursor(Count(m.message), h-1) - screen.Show() - event := <-events - - switch e := event.(type) { - case *tcell.EventKey: - switch e.Key() { - case tcell.KeyRune: - for _, r := range responses { - if e.Rune() == r { - m.AddLog("\t--> " + string(r)) - m.Clear() - m.Reset() - m.hasPrompt = false - return r, false - } - } - case tcell.KeyCtrlC, tcell.KeyCtrlQ, tcell.KeyEscape: - m.AddLog("\t--> (cancel)") - m.Clear() - m.Reset() - m.hasPrompt = false - return ' ', true - } - } - } -} - -// Completion represents a type of completion -type Completion int - -const ( - NoCompletion Completion = iota - FileCompletion - CommandCompletion - HelpCompletion - OptionCompletion - PluginCmdCompletion - PluginNameCompletion - OptionValueCompletion -) - -// Prompt sends the user a message and waits for a response to be typed in -// This function blocks the main loop while waiting for input -func (m *Messenger) Prompt(prompt, placeholder, historyType string, completionTypes ...Completion) (string, bool) { - m.hasPrompt = true - m.PromptText(prompt) - if _, ok := m.history[historyType]; !ok { - m.history[historyType] = []string{""} - } else { - m.history[historyType] = append(m.history[historyType], "") - } - m.historyNum = len(m.history[historyType]) - 1 - - response, canceled := placeholder, true - m.response = response - m.cursorx = Count(placeholder) - - RedrawAll() - for m.hasPrompt { - var suggestions []string - m.Clear() - - event := <-events - - switch e := event.(type) { - case *tcell.EventResize: - for _, t := range tabs { - t.Resize() - } - RedrawAll() - case *tcell.EventKey: - switch e.Key() { - case tcell.KeyCtrlQ, tcell.KeyCtrlC, tcell.KeyEscape: - // Cancel - m.AddLog("\t--> (cancel)") - m.hasPrompt = false - case tcell.KeyEnter: - // User is done entering their response - m.AddLog("\t--> " + m.response) - m.hasPrompt = false - response, canceled = m.response, false - m.history[historyType][len(m.history[historyType])-1] = response - case tcell.KeyTab: - args, err := shellwords.Split(m.response) - if err != nil { - break - } - currentArg := "" - currentArgNum := 0 - if len(args) > 0 { - currentArgNum = len(args) - 1 - currentArg = args[currentArgNum] - } - var completionType Completion - - if completionTypes[0] == CommandCompletion && currentArgNum > 0 { - if command, ok := commands[args[0]]; ok { - completionTypes = append([]Completion{CommandCompletion}, command.completions...) - } - } - - if currentArgNum >= len(completionTypes) { - completionType = completionTypes[len(completionTypes)-1] - } else { - completionType = completionTypes[currentArgNum] - } - - var chosen string - if completionType == FileCompletion { - chosen, suggestions = FileComplete(currentArg) - } else if completionType == CommandCompletion { - chosen, suggestions = CommandComplete(currentArg) - } else if completionType == HelpCompletion { - chosen, suggestions = HelpComplete(currentArg) - } else if completionType == OptionCompletion { - chosen, suggestions = OptionComplete(currentArg) - } else if completionType == OptionValueCompletion { - if currentArgNum-1 > 0 { - chosen, suggestions = OptionValueComplete(args[currentArgNum-1], currentArg) - } - } else if completionType == PluginCmdCompletion { - chosen, suggestions = PluginCmdComplete(currentArg) - } else if completionType == PluginNameCompletion { - chosen, suggestions = PluginNameComplete(currentArg) - } else if completionType < NoCompletion { - chosen, suggestions = PluginComplete(completionType, currentArg) - } - - if len(suggestions) > 1 { - chosen = chosen + CommonSubstring(suggestions...) - } - - if len(suggestions) != 0 && chosen != "" { - m.response = shellwords.Join(append(args[:len(args)-1], chosen)...) - m.cursorx = Count(m.response) - } - } - } - - m.HandleEvent(event, m.history[historyType]) - - m.Clear() - for _, v := range tabs[curTab].Views { - v.Display() - } - DisplayTabs() - m.Display() - if len(suggestions) > 1 { - m.DisplaySuggestions(suggestions) - } - screen.Show() - } - - m.Clear() - m.Reset() - return response, canceled -} - -// UpHistory fetches the previous item in the history -func (m *Messenger) UpHistory(history []string) { - if m.historyNum > 0 { - m.historyNum-- - m.response = history[m.historyNum] - m.cursorx = Count(m.response) - } -} - -// DownHistory fetches the next item in the history -func (m *Messenger) DownHistory(history []string) { - if m.historyNum < len(history)-1 { - m.historyNum++ - m.response = history[m.historyNum] - m.cursorx = Count(m.response) - } -} - -// CursorLeft moves the cursor one character left -func (m *Messenger) CursorLeft() { - if m.cursorx > 0 { - m.cursorx-- - } -} - -// CursorRight moves the cursor one character right -func (m *Messenger) CursorRight() { - if m.cursorx < Count(m.response) { - m.cursorx++ - } -} - -// Start moves the cursor to the start of the line -func (m *Messenger) Start() { - m.cursorx = 0 -} - -// End moves the cursor to the end of the line -func (m *Messenger) End() { - m.cursorx = Count(m.response) -} - -// Backspace deletes one character -func (m *Messenger) Backspace() { - if m.cursorx > 0 { - m.response = string([]rune(m.response)[:m.cursorx-1]) + string([]rune(m.response)[m.cursorx:]) - m.cursorx-- - } -} - -// Paste pastes the clipboard -func (m *Messenger) Paste() { - clip, _ := clipboard.ReadAll("clipboard") - m.response = Insert(m.response, m.cursorx, clip) - m.cursorx += Count(clip) -} - -// WordLeft moves the cursor one word to the left -func (m *Messenger) WordLeft() { - response := []rune(m.response) - m.CursorLeft() - if m.cursorx <= 0 { - return - } - for IsWhitespace(response[m.cursorx]) { - if m.cursorx <= 0 { - return - } - m.CursorLeft() - } - m.CursorLeft() - for IsWordChar(string(response[m.cursorx])) { - if m.cursorx <= 0 { - return - } - m.CursorLeft() - } - m.CursorRight() -} - -// WordRight moves the cursor one word to the right -func (m *Messenger) WordRight() { - response := []rune(m.response) - if m.cursorx >= len(response) { - return - } - for IsWhitespace(response[m.cursorx]) { - m.CursorRight() - if m.cursorx >= len(response) { - m.CursorRight() - return - } - } - m.CursorRight() - if m.cursorx >= len(response) { - return - } - for IsWordChar(string(response[m.cursorx])) { - m.CursorRight() - if m.cursorx >= len(response) { - return - } - } -} - -// DeleteWordLeft deletes one word to the left -func (m *Messenger) DeleteWordLeft() { - m.WordLeft() - m.response = string([]rune(m.response)[:m.cursorx]) -} - -// HandleEvent handles an event for the prompter -func (m *Messenger) HandleEvent(event tcell.Event, history []string) { - switch e := event.(type) { - case *tcell.EventKey: - switch e.Key() { - case tcell.KeyCtrlA: - m.Start() - case tcell.KeyCtrlE: - m.End() - case tcell.KeyUp: - m.UpHistory(history) - case tcell.KeyDown: - m.DownHistory(history) - case tcell.KeyLeft: - if e.Modifiers() == tcell.ModCtrl { - m.Start() - } else if e.Modifiers() == tcell.ModAlt || e.Modifiers() == tcell.ModMeta { - m.WordLeft() - } else { - m.CursorLeft() - } - case tcell.KeyRight: - if e.Modifiers() == tcell.ModCtrl { - m.End() - } else if e.Modifiers() == tcell.ModAlt || e.Modifiers() == tcell.ModMeta { - m.WordRight() - } else { - m.CursorRight() - } - case tcell.KeyBackspace2, tcell.KeyBackspace: - if e.Modifiers() == tcell.ModCtrl || e.Modifiers() == tcell.ModAlt || e.Modifiers() == tcell.ModMeta { - m.DeleteWordLeft() - } else { - m.Backspace() - } - case tcell.KeyCtrlW: - m.DeleteWordLeft() - case tcell.KeyCtrlV: - m.Paste() - case tcell.KeyCtrlF: - m.WordRight() - case tcell.KeyCtrlB: - m.WordLeft() - case tcell.KeyRune: - m.response = Insert(m.response, m.cursorx, string(e.Rune())) - m.cursorx++ - } - history[m.historyNum] = m.response - - case *tcell.EventPaste: - clip := e.Text() - m.response = Insert(m.response, m.cursorx, clip) - m.cursorx += Count(clip) - case *tcell.EventMouse: - x, y := e.Position() - x -= Count(m.message) - button := e.Buttons() - _, screenH := screen.Size() - - if y == screenH-1 { - switch button { - case tcell.Button1: - m.cursorx = x - if m.cursorx < 0 { - m.cursorx = 0 - } else if m.cursorx > Count(m.response) { - m.cursorx = Count(m.response) - } - } - } - } -} - -// Reset resets the messenger's cursor, message and response -func (m *Messenger) Reset() { - m.cursorx = 0 - m.message = "" - m.response = "" -} - -// Clear clears the line at the bottom of the editor -func (m *Messenger) Clear() { - w, h := screen.Size() - for x := 0; x < w; x++ { - screen.SetContent(x, h-1, ' ', nil, defStyle) - } -} - -func (m *Messenger) DisplaySuggestions(suggestions []string) { - w, screenH := screen.Size() - - y := screenH - 2 - - statusLineStyle := defStyle.Reverse(true) - if style, ok := colorscheme["statusline"]; ok { - statusLineStyle = style - } - - for x := 0; x < w; x++ { - screen.SetContent(x, y, ' ', nil, statusLineStyle) - } - - x := 0 - for _, suggestion := range suggestions { - for _, c := range suggestion { - screen.SetContent(x, y, c, nil, statusLineStyle) - x++ - } - screen.SetContent(x, y, ' ', nil, statusLineStyle) - x++ - } -} - -// Display displays messages or prompts -func (m *Messenger) Display() { - _, h := screen.Size() - if m.hasMessage { - if m.hasPrompt || globalSettings["infobar"].(bool) { - runes := []rune(m.message + m.response) - posx := 0 - for x := 0; x < len(runes); x++ { - screen.SetContent(posx, h-1, runes[x], nil, m.style) - posx += runewidth.RuneWidth(runes[x]) - } - } - } - - if m.hasPrompt { - screen.ShowCursor(Count(m.message)+m.cursorx, h-1) - screen.Show() - } -} - -// LoadHistory attempts to load user history from configDir/buffers/history -// into the history map -// The savehistory option must be on -func (m *Messenger) LoadHistory() { - if GetGlobalOption("savehistory").(bool) { - file, err := os.Open(configDir + "/buffers/history") - defer file.Close() - var decodedMap map[string][]string - if err == nil { - decoder := gob.NewDecoder(file) - err = decoder.Decode(&decodedMap) - - if err != nil { - m.Error("Error loading history:", err) - return - } - } - - if decodedMap != nil { - m.history = decodedMap - } else { - m.history = make(map[string][]string) - } - } else { - m.history = make(map[string][]string) - } -} - -// SaveHistory saves the user's command history to configDir/buffers/history -// only if the savehistory option is on -func (m *Messenger) SaveHistory() { - if GetGlobalOption("savehistory").(bool) { - // Don't save history past 100 - for k, v := range m.history { - if len(v) > 100 { - m.history[k] = v[len(m.history[k])-100:] - } - } - - file, err := os.Create(configDir + "/buffers/history") - defer file.Close() - if err == nil { - encoder := gob.NewEncoder(file) - - err = encoder.Encode(m.history) - if err != nil { - m.Error("Error saving history:", err) - return - } - } - } -} - -// A GutterMessage is a message displayed on the side of the editor -type GutterMessage struct { - lineNum int - msg string - kind int -} - -// These are the different types of messages -const ( - // GutterInfo represents a simple info message - GutterInfo = iota - // GutterWarning represents a compiler warning - GutterWarning - // GutterError represents a compiler error - GutterError -) diff --git a/cmd/micro/micro.go b/cmd/micro/micro.go index e1787c77..bd01dda1 100644 --- a/cmd/micro/micro.go +++ b/cmd/micro/micro.go @@ -3,22 +3,13 @@ package main import ( "flag" "fmt" - "io/ioutil" "os" - "path/filepath" - "runtime" - "strings" "time" "github.com/go-errors/errors" - "github.com/mattn/go-isatty" - "github.com/mitchellh/go-homedir" - "github.com/yuin/gopher-lua" - "github.com/zyedidia/clipboard" + homedir "github.com/mitchellh/go-homedir" "github.com/zyedidia/micro/cmd/micro/terminfo" "github.com/zyedidia/tcell" - "github.com/zyedidia/tcell/encoding" - "layeh.com/gopher-luar" ) const ( @@ -31,13 +22,6 @@ var ( // The main screen screen tcell.Screen - // Object to send messages and prompts to the user - messenger *Messenger - - // The default highlighting style - // This simply defines the default foreground and background colors - defStyle tcell.Style - // Where the user's configuration is // This should be $XDG_CONFIG_HOME/micro // If $XDG_CONFIG_HOME is not set, it is ~/.config/micro @@ -50,88 +34,23 @@ var ( CommitHash = "Unknown" // CompileDate is the date this binary was compiled on CompileDate = "Unknown" - - // The list of views - tabs []*Tab - // This is the currently open tab - // It's just an index to the tab in the tabs array - curTab int - - // Channel of jobs running in the background - jobs chan JobFunction + // Debug logging + Debug = "ON" // Event channel events chan tcell.Event autosave chan bool - // Channels for the terminal emulator - updateterm chan bool - closeterm chan int - // How many redraws have happened numRedraw uint + + // Command line flags + flagVersion = flag.Bool("version", false, "Show the version number and information") + flagStartPos = flag.String("startpos", "", "LINE,COL to start the cursor at when opening a buffer.") + flagConfigDir = flag.String("config-dir", "", "Specify a custom location for the configuration directory") + flagOptions = flag.Bool("options", false, "Show all option help") ) -// LoadInput determines which files should be loaded into buffers -// based on the input stored in flag.Args() -func LoadInput() []*Buffer { - // There are a number of ways micro should start given its input - - // 1. If it is given a files in flag.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 - // empty buffer - - // 3. If there is no input file and the input is a terminal, an empty buffer - // should be opened - - var filename string - var input []byte - var err error - args := flag.Args() - buffers := make([]*Buffer, 0, len(args)) - - if len(args) > 0 { - // Option 1 - // We go through each file and load it - for i := 0; i < len(args); i++ { - if strings.HasPrefix(args[i], "+") { - if strings.Contains(args[i], ":") { - split := strings.Split(args[i], ":") - *flagStartPos = split[0][1:] + "," + split[1] - } else { - *flagStartPos = args[i][1:] + ",0" - } - continue - } - - buf, err := NewBufferFromFile(args[i]) - 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, buf) - } - } 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) - if err != nil { - TermMessage("Error reading from stdin: ", err) - input = []byte{} - } - buffers = append(buffers, NewBufferFromString(string(input), filename)) - } else { - // Option 3, just open an empty buffer - buffers = append(buffers, NewBufferFromString(string(input), filename)) - } - - return buffers -} - // InitConfigDir finds the configuration directory for micro according to the XDG spec. // If no directory is found, it creates one. func InitConfigDir() { @@ -230,63 +149,7 @@ func InitScreen() { // screen.SetStyle(defStyle) } -// RedrawAll redraws everything -- all the views and the messenger -func RedrawAll() { - messenger.Clear() - - w, h := screen.Size() - for x := 0; x < w; x++ { - for y := 0; y < h; y++ { - screen.SetContent(x, y, ' ', nil, defStyle) - } - } - - for _, v := range tabs[curTab].Views { - v.Display() - } - DisplayTabs() - messenger.Display() - if globalSettings["keymenu"].(bool) { - DisplayKeyMenu() - } - screen.Show() - - if numRedraw%50 == 0 { - runtime.GC() - } - numRedraw++ -} - -func LoadAll() { - // Find the user's configuration directory (probably $XDG_CONFIG_HOME/micro) - InitConfigDir() - - // Build a list of available Extensions (Syntax, Colorscheme etc.) - InitRuntimeFiles() - - // Load the user's settings - InitGlobalSettings() - - InitCommands() - InitBindings() - - InitColorscheme() - LoadPlugins() - - for _, tab := range tabs { - for _, v := range tab.Views { - v.Buf.UpdateRules() - } - } -} - -// Command line flags -var flagVersion = flag.Bool("version", false, "Show the version number and information") -var flagStartPos = flag.String("startpos", "", "LINE,COL to start the cursor at when opening a buffer.") -var flagConfigDir = flag.String("config-dir", "", "Specify a custom location for the configuration directory") -var flagOptions = flag.Bool("options", false, "Show all option help") - -func main() { +func InitFlags() { flag.Usage = func() { fmt.Println("Usage: micro [OPTIONS] [FILE]...") fmt.Println("-config-dir dir") @@ -327,35 +190,32 @@ func main() { // If -options was passed for k, v := range DefaultGlobalSettings() { fmt.Printf("-%s value\n", k) - fmt.Printf(" \tThe %s option. Default value: '%v'\n", k, v) + fmt.Printf(" \tDefault value: '%v'\n", v) } os.Exit(0) } +} - // Start the Lua VM for running plugins - L = lua.NewState() - defer L.Close() +func main() { + var err error - // Some encoding stuff in case the user isn't using UTF-8 - encoding.Register() - tcell.SetEncodingFallback(tcell.EncodingFallbackASCII) - - // Find the user's configuration directory (probably $XDG_CONFIG_HOME/micro) + InitLog() + InitFlags() InitConfigDir() - - // Build a list of available Extensions (Syntax, Colorscheme etc.) InitRuntimeFiles() - - // Load the user's settings + err = ReadSettings() + if err != nil { + TermMessage(err) + } InitGlobalSettings() + err = InitColorscheme() + if err != nil { + TermMessage(err) + } - InitCommands() - InitBindings() - - // Start the screen InitScreen() - // This is just so if we have an error, we can exit cleanly and not completely + // 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() { @@ -368,227 +228,26 @@ func main() { } }() - // Create a new messenger - // This is used for sending the user messages in the bottom of the editor - messenger = new(Messenger) - messenger.LoadHistory() + b, err := NewBufferFromFile(os.Args[1]) - // Now we load the input - buffers := LoadInput() - if len(buffers) == 0 { - screen.Fini() - os.Exit(1) + if err != nil { + TermMessage(err) } - 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) - for _, t := range tabs { - for _, v := range t.Views { - v.Center(false) - } + width, height := screen.Size() - t.Resize() - } + w := NewWindow(0, 0, width/2, height/2, b) + + for i := 0; i < 5; i++ { + screen.Clear() + w.DisplayBuffer() + w.DisplayStatusLine() + screen.Show() + time.Sleep(200 * time.Millisecond) + w.StartLine++ } - for k, v := range optionFlags { - if *v != "" { - SetOption(k, *v) - } - } + // time.Sleep(2 * time.Second) - // 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("GetTabs", luar.New(L, func() []*Tab { - return 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("SetOption", luar.New(L, SetOption)) - L.SetGlobal("SetLocalOption", luar.New(L, SetLocalOption)) - L.SetGlobal("BindKey", luar.New(L, BindKey)) - L.SetGlobal("MakeCommand", luar.New(L, MakeCommand)) - L.SetGlobal("CurView", luar.New(L, CurView)) - L.SetGlobal("IsWordChar", luar.New(L, IsWordChar)) - L.SetGlobal("HandleCommand", luar.New(L, HandleCommand)) - L.SetGlobal("HandleShellCommand", luar.New(L, HandleShellCommand)) - L.SetGlobal("ExecCommand", luar.New(L, ExecCommand)) - L.SetGlobal("RunShellCommand", luar.New(L, RunShellCommand)) - L.SetGlobal("RunBackgroundShell", luar.New(L, RunBackgroundShell)) - L.SetGlobal("RunInteractiveShell", luar.New(L, RunInteractiveShell)) - L.SetGlobal("TermEmuSupported", luar.New(L, TermEmuSupported)) - L.SetGlobal("RunTermEmulator", luar.New(L, RunTermEmulator)) - L.SetGlobal("GetLeadingWhitespace", luar.New(L, GetLeadingWhitespace)) - L.SetGlobal("MakeCompletion", luar.New(L, MakeCompletion)) - L.SetGlobal("NewBuffer", luar.New(L, NewBufferFromString)) - L.SetGlobal("NewBufferFromFile", luar.New(L, NewBufferFromFile)) - L.SetGlobal("RuneStr", luar.New(L, func(r rune) string { - return string(r) - })) - L.SetGlobal("Loc", luar.New(L, func(x, y int) Loc { - return Loc{x, y} - })) - L.SetGlobal("WorkingDirectory", luar.New(L, os.Getwd)) - L.SetGlobal("JoinPaths", luar.New(L, filepath.Join)) - L.SetGlobal("DirectoryName", luar.New(L, filepath.Dir)) - L.SetGlobal("configDir", luar.New(L, configDir)) - L.SetGlobal("Reload", luar.New(L, LoadAll)) - L.SetGlobal("ByteOffset", luar.New(L, ByteOffset)) - L.SetGlobal("ToCharPos", luar.New(L, ToCharPos)) - - // Used for asynchronous jobs - L.SetGlobal("JobStart", luar.New(L, JobStart)) - L.SetGlobal("JobSpawn", luar.New(L, JobSpawn)) - L.SetGlobal("JobSend", luar.New(L, JobSend)) - L.SetGlobal("JobStop", luar.New(L, JobStop)) - - // Extension Files - L.SetGlobal("ReadRuntimeFile", luar.New(L, PluginReadRuntimeFile)) - L.SetGlobal("ListRuntimeFiles", luar.New(L, PluginListRuntimeFiles)) - L.SetGlobal("AddRuntimeFile", luar.New(L, PluginAddRuntimeFile)) - L.SetGlobal("AddRuntimeFilesFromDirectory", luar.New(L, PluginAddRuntimeFilesFromDirectory)) - L.SetGlobal("AddRuntimeFileFromMemory", luar.New(L, PluginAddRuntimeFileFromMemory)) - - // Access to Go stdlib - L.SetGlobal("import", luar.New(L, Import)) - - jobs = make(chan JobFunction, 100) - events = make(chan tcell.Event, 100) - autosave = make(chan bool) - updateterm = make(chan bool) - closeterm = make(chan int) - - LoadPlugins() - - for _, t := range tabs { - for _, v := range t.Views { - GlobalPluginCall("onViewOpen", v) - GlobalPluginCall("onBufferOpen", v.Buf) - } - } - - InitColorscheme() - messenger.style = defStyle - - // Here is the event loop which runs in a separate thread - go func() { - for { - if screen != nil { - events <- screen.PollEvent() - } - } - }() - - go func() { - for { - time.Sleep(autosaveTime * time.Second) - if globalSettings["autosave"].(bool) { - autosave <- true - } - } - }() - - for { - // Display everything - 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 <-autosave: - if CurView().Buf.Path != "" { - CurView().Save(true) - } - case <-updateterm: - continue - case vnum := <-closeterm: - tabs[curTab].Views[vnum].CloseTerminal() - case event = <-events: - } - - for event != nil { - didAction := false - - switch e := event.(type) { - case *tcell.EventResize: - for _, t := range tabs { - t.Resize() - } - case *tcell.EventMouse: - if !searching { - 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 != "" && globalSettings["infobar"].(bool) { - // 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, "primary") - break - } - - if CurView().mouseReleased { - // We loop through each view in the current tab and make sure the current view - // is 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 - } - } - } - } else if e.Buttons() == tcell.WheelUp || e.Buttons() == tcell.WheelDown { - var view *View - x, y := e.Position() - for _, v := range tabs[curTab].Views { - if x >= v.x && x < v.x+v.Width && y >= v.y && y < v.y+v.Height { - view = tabs[curTab].Views[v.Num] - } - } - if view != nil { - view.HandleEvent(e) - didAction = true - } - } - } - } - - if !didAction { - // 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) { - break - } - - if searching { - // Since searching is done in real time, we need to redraw every time - // 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 - CurView().HandleEvent(event) - } - } - - select { - case event = <-events: - default: - event = nil - } - } - } + screen.Fini() } diff --git a/cmd/micro/plugin.go b/cmd/micro/plugin.go deleted file mode 100644 index b3350780..00000000 --- a/cmd/micro/plugin.go +++ /dev/null @@ -1,184 +0,0 @@ -package main - -import ( - "errors" - "io/ioutil" - "os" - "strings" - - "github.com/yuin/gopher-lua" - "github.com/zyedidia/tcell" - "layeh.com/gopher-luar" -) - -var loadedPlugins map[string]string - -// Call calls the lua function 'function' -// If it does not exist nothing happens, if there is an error, -// the error is returned -func Call(function string, args ...interface{}) (lua.LValue, error) { - var luaFunc lua.LValue - if strings.Contains(function, ".") { - plugin := L.GetGlobal(strings.Split(function, ".")[0]) - if plugin.String() == "nil" { - return nil, errors.New("function does not exist: " + function) - } - luaFunc = L.GetField(plugin, strings.Split(function, ".")[1]) - } else { - luaFunc = L.GetGlobal(function) - } - - if luaFunc.String() == "nil" { - return nil, errors.New("function does not exist: " + function) - } - var luaArgs []lua.LValue - for _, v := range args { - luaArgs = append(luaArgs, luar.New(L, v)) - } - err := L.CallByParam(lua.P{ - Fn: luaFunc, - NRet: 1, - Protect: true, - }, luaArgs...) - ret := L.Get(-1) // returned value - if ret.String() != "nil" { - L.Pop(1) // remove received value - } - return ret, err -} - -// LuaFunctionBinding is a function generator which takes the name of a lua function -// and creates a function that will call that lua function -// Specifically it creates a function that can be called as a binding because this is used -// to bind keys to lua functions -func LuaFunctionBinding(function string) func(*View, bool) bool { - return func(v *View, _ bool) bool { - _, err := Call(function, nil) - if err != nil { - TermMessage(err) - } - return false - } -} - -// LuaFunctionMouseBinding is a function generator which takes the name of a lua function -// and creates a function that will call that lua function -// Specifically it creates a function that can be called as a mouse binding because this is used -// to bind mouse actions to lua functions -func LuaFunctionMouseBinding(function string) func(*View, bool, *tcell.EventMouse) bool { - return func(v *View, _ bool, e *tcell.EventMouse) bool { - _, err := Call(function, e) - if err != nil { - TermMessage(err) - } - return false - } -} - -func unpack(old []string) []interface{} { - new := make([]interface{}, len(old)) - for i, v := range old { - new[i] = v - } - return new -} - -// LuaFunctionCommand is the same as LuaFunctionBinding except it returns a normal function -// so that a command can be bound to a lua function -func LuaFunctionCommand(function string) func([]string) { - return func(args []string) { - _, err := Call(function, unpack(args)...) - if err != nil { - TermMessage(err) - } - } -} - -// LuaFunctionComplete returns a function which can be used for autocomplete in plugins -func LuaFunctionComplete(function string) func(string) []string { - return func(input string) (result []string) { - - res, err := Call(function, input) - if err != nil { - TermMessage(err) - } - if tbl, ok := res.(*lua.LTable); !ok { - TermMessage(function, "should return a table of strings") - } else { - for i := 1; i <= tbl.Len(); i++ { - val := tbl.RawGetInt(i) - if v, ok := val.(lua.LString); !ok { - TermMessage(function, "should return a table of strings") - } else { - result = append(result, string(v)) - } - } - } - return result - } -} - -// LuaFunctionJob returns a function that will call the given lua function -// structured as a job call i.e. the job output and arguments are provided -// to the lua function -func LuaFunctionJob(function string) func(string, ...string) { - return func(output string, args ...string) { - _, err := Call(function, unpack(append([]string{output}, args...))...) - if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") { - TermMessage(err) - } - } -} - -// luaPluginName convert a human-friendly plugin name into a valid lua variable name. -func luaPluginName(name string) string { - return strings.Replace(name, "-", "_", -1) -} - -// LoadPlugins loads the pre-installed plugins and the plugins located in ~/.config/micro/plugins -func LoadPlugins() { - loadedPlugins = make(map[string]string) - - for _, plugin := range ListRuntimeFiles(RTPlugin) { - pluginName := plugin.Name() - if _, ok := loadedPlugins[pluginName]; ok { - continue - } - - data, err := plugin.Data() - if err != nil { - TermMessage("Error loading plugin: " + pluginName) - continue - } - - pluginLuaName := luaPluginName(pluginName) - - if err := LoadFile(pluginLuaName, pluginLuaName, string(data)); err != nil { - TermMessage(err) - continue - } - - loadedPlugins[pluginName] = pluginLuaName - - } - - if _, err := os.Stat(configDir + "/init.lua"); err == nil { - data, _ := ioutil.ReadFile(configDir + "/init.lua") - if err := LoadFile("init", configDir+"init.lua", string(data)); err != nil { - TermMessage(err) - } - loadedPlugins["init"] = "init" - } -} - -// GlobalCall makes a call to a function in every plugin that is currently -// loaded -func GlobalPluginCall(function string, args ...interface{}) { - for pl := range loadedPlugins { - _, err := Call(pl+"."+function, args...) - if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") { - TermMessage(err) - continue - } - } -} diff --git a/cmd/micro/pluginmanager.go b/cmd/micro/pluginmanager.go deleted file mode 100644 index ad7843e6..00000000 --- a/cmd/micro/pluginmanager.go +++ /dev/null @@ -1,622 +0,0 @@ -package main - -import ( - "archive/zip" - "bytes" - "fmt" - "io" - "io/ioutil" - "net/http" - "os" - "path/filepath" - "sort" - "strings" - "sync" - - "github.com/blang/semver" - "github.com/flynn/json5" - "github.com/yuin/gopher-lua" -) - -var ( - allPluginPackages PluginPackages -) - -// CorePluginName is a plugin dependency name for the micro core. -const CorePluginName = "micro" - -// PluginChannel contains an url to a json list of PluginRepository -type PluginChannel string - -// PluginChannels is a slice of PluginChannel -type PluginChannels []PluginChannel - -// PluginRepository contains an url to json file containing PluginPackages -type PluginRepository string - -// PluginPackage contains the meta-data of a plugin and all available versions -type PluginPackage struct { - Name string - Description string - Author string - Tags []string - Versions PluginVersions -} - -// PluginPackages is a list of PluginPackage instances. -type PluginPackages []*PluginPackage - -// PluginVersion descripes a version of a PluginPackage. Containing a version, download url and also dependencies. -type PluginVersion struct { - pack *PluginPackage - Version semver.Version - Url string - Require PluginDependencies -} - -// PluginVersions is a slice of PluginVersion -type PluginVersions []*PluginVersion - -// PluginDependency descripes a dependency to another plugin or micro itself. -type PluginDependency struct { - Name string - Range semver.Range -} - -// PluginDependencies is a slice of PluginDependency -type PluginDependencies []*PluginDependency - -func (pp *PluginPackage) String() string { - buf := new(bytes.Buffer) - buf.WriteString("Plugin: ") - buf.WriteString(pp.Name) - buf.WriteRune('\n') - if pp.Author != "" { - buf.WriteString("Author: ") - buf.WriteString(pp.Author) - buf.WriteRune('\n') - } - if pp.Description != "" { - buf.WriteRune('\n') - buf.WriteString(pp.Description) - } - return buf.String() -} - -func fetchAllSources(count int, fetcher func(i int) PluginPackages) PluginPackages { - wgQuery := new(sync.WaitGroup) - wgQuery.Add(count) - - results := make(chan PluginPackages) - - wgDone := new(sync.WaitGroup) - wgDone.Add(1) - var packages PluginPackages - for i := 0; i < count; i++ { - go func(i int) { - results <- fetcher(i) - wgQuery.Done() - }(i) - } - go func() { - packages = make(PluginPackages, 0) - for res := range results { - packages = append(packages, res...) - } - wgDone.Done() - }() - wgQuery.Wait() - close(results) - wgDone.Wait() - return packages -} - -// Fetch retrieves all available PluginPackages from the given channels -func (pc PluginChannels) Fetch() PluginPackages { - return fetchAllSources(len(pc), func(i int) PluginPackages { - return pc[i].Fetch() - }) -} - -// Fetch retrieves all available PluginPackages from the given channel -func (pc PluginChannel) Fetch() PluginPackages { - // messenger.AddLog(fmt.Sprintf("Fetching channel: %q", string(pc))) - resp, err := http.Get(string(pc)) - if err != nil { - TermMessage("Failed to query plugin channel:\n", err) - return PluginPackages{} - } - defer resp.Body.Close() - decoder := json5.NewDecoder(resp.Body) - - var repositories []PluginRepository - if err := decoder.Decode(&repositories); err != nil { - TermMessage("Failed to decode channel data:\n", err) - return PluginPackages{} - } - return fetchAllSources(len(repositories), func(i int) PluginPackages { - return repositories[i].Fetch() - }) -} - -// Fetch retrieves all available PluginPackages from the given repository -func (pr PluginRepository) Fetch() PluginPackages { - // messenger.AddLog(fmt.Sprintf("Fetching repository: %q", string(pr))) - resp, err := http.Get(string(pr)) - if err != nil { - TermMessage("Failed to query plugin repository:\n", err) - return PluginPackages{} - } - defer resp.Body.Close() - decoder := json5.NewDecoder(resp.Body) - - var plugins PluginPackages - if err := decoder.Decode(&plugins); err != nil { - TermMessage("Failed to decode repository data:\n", err) - return PluginPackages{} - } - if len(plugins) > 0 { - return PluginPackages{plugins[0]} - } - return nil - // return plugins -} - -// UnmarshalJSON unmarshals raw json to a PluginVersion -func (pv *PluginVersion) UnmarshalJSON(data []byte) error { - var values struct { - Version semver.Version - Url string - Require map[string]string - } - - if err := json5.Unmarshal(data, &values); err != nil { - return err - } - pv.Version = values.Version - pv.Url = values.Url - pv.Require = make(PluginDependencies, 0) - - for k, v := range values.Require { - // don't add the dependency if it's the core and - // we have a unknown version number. - // in that case just accept that dependency (which equals to not adding it.) - if k != CorePluginName || !isUnknownCoreVersion() { - if vRange, err := semver.ParseRange(v); err == nil { - pv.Require = append(pv.Require, &PluginDependency{k, vRange}) - } - } - } - return nil -} - -// UnmarshalJSON unmarshals raw json to a PluginPackage -func (pp *PluginPackage) UnmarshalJSON(data []byte) error { - var values struct { - Name string - Description string - Author string - Tags []string - Versions PluginVersions - } - if err := json5.Unmarshal(data, &values); err != nil { - return err - } - pp.Name = values.Name - pp.Description = values.Description - pp.Author = values.Author - pp.Tags = values.Tags - pp.Versions = values.Versions - for _, v := range pp.Versions { - v.pack = pp - } - return nil -} - -// GetAllPluginPackages gets all PluginPackages which may be available. -func GetAllPluginPackages() PluginPackages { - if allPluginPackages == nil { - getOption := func(name string) []string { - data := GetOption(name) - if strs, ok := data.([]string); ok { - return strs - } - if ifs, ok := data.([]interface{}); ok { - result := make([]string, len(ifs)) - for i, urlIf := range ifs { - if url, ok := urlIf.(string); ok { - result[i] = url - } else { - return nil - } - } - return result - } - return nil - } - - channels := PluginChannels{} - for _, url := range getOption("pluginchannels") { - channels = append(channels, PluginChannel(url)) - } - repos := []PluginRepository{} - for _, url := range getOption("pluginrepos") { - repos = append(repos, PluginRepository(url)) - } - allPluginPackages = fetchAllSources(len(repos)+1, func(i int) PluginPackages { - if i == 0 { - return channels.Fetch() - } - return repos[i-1].Fetch() - }) - } - return allPluginPackages -} - -func (pv PluginVersions) find(ppName string) *PluginVersion { - for _, v := range pv { - if v.pack.Name == ppName { - return v - } - } - return nil -} - -// Len returns the number of pluginversions in this slice -func (pv PluginVersions) Len() int { - return len(pv) -} - -// Swap two entries of the slice -func (pv PluginVersions) Swap(i, j int) { - pv[i], pv[j] = pv[j], pv[i] -} - -// Less returns true if the version at position i is greater then the version at position j (used for sorting) -func (pv PluginVersions) Less(i, j int) bool { - return pv[i].Version.GT(pv[j].Version) -} - -// Match returns true if the package matches a given search text -func (pp PluginPackage) Match(text string) bool { - text = strings.ToLower(text) - for _, t := range pp.Tags { - if strings.ToLower(t) == text { - return true - } - } - if strings.Contains(strings.ToLower(pp.Name), text) { - return true - } - - if strings.Contains(strings.ToLower(pp.Description), text) { - return true - } - - return false -} - -// IsInstallable returns true if the package can be installed. -func (pp PluginPackage) IsInstallable() error { - _, err := GetAllPluginPackages().Resolve(GetInstalledVersions(true), PluginDependencies{ - &PluginDependency{ - Name: pp.Name, - Range: semver.Range(func(v semver.Version) bool { return true }), - }}) - return err -} - -// SearchPlugin retrieves a list of all PluginPackages which match the given search text and -// could be or are already installed -func SearchPlugin(texts []string) (plugins PluginPackages) { - plugins = make(PluginPackages, 0) - -pluginLoop: - for _, pp := range GetAllPluginPackages() { - for _, text := range texts { - if !pp.Match(text) { - continue pluginLoop - } - } - - if err := pp.IsInstallable(); err == nil { - plugins = append(plugins, pp) - } - } - return -} - -func isUnknownCoreVersion() bool { - _, err := semver.ParseTolerant(Version) - return err != nil -} - -func newStaticPluginVersion(name, version string) *PluginVersion { - vers, err := semver.ParseTolerant(version) - - if err != nil { - if vers, err = semver.ParseTolerant("0.0.0-" + version); err != nil { - vers = semver.MustParse("0.0.0-unknown") - } - } - pl := &PluginPackage{ - Name: name, - } - pv := &PluginVersion{ - pack: pl, - Version: vers, - } - pl.Versions = PluginVersions{pv} - return pv -} - -// GetInstalledVersions returns a list of all currently installed plugins including an entry for -// micro itself. This can be used to resolve dependencies. -func GetInstalledVersions(withCore bool) PluginVersions { - result := PluginVersions{} - if withCore { - result = append(result, newStaticPluginVersion(CorePluginName, Version)) - } - - for name, lpname := range loadedPlugins { - version := GetInstalledPluginVersion(lpname) - if pv := newStaticPluginVersion(name, version); pv != nil { - result = append(result, pv) - } - } - - return result -} - -// GetInstalledPluginVersion returns the string of the exported VERSION variable of a loaded plugin -func GetInstalledPluginVersion(name string) string { - plugin := L.GetGlobal(name) - if plugin != lua.LNil { - version := L.GetField(plugin, "VERSION") - if str, ok := version.(lua.LString); ok { - return string(str) - - } - } - return "" -} - -// DownloadAndInstall downloads and installs the given plugin and version -func (pv *PluginVersion) DownloadAndInstall() error { - messenger.AddLog(fmt.Sprintf("Downloading %q (%s) from %q", pv.pack.Name, pv.Version, pv.Url)) - resp, err := http.Get(pv.Url) - if err != nil { - return err - } - defer resp.Body.Close() - data, err := ioutil.ReadAll(resp.Body) - if err != nil { - return err - } - zipbuf := bytes.NewReader(data) - z, err := zip.NewReader(zipbuf, zipbuf.Size()) - if err != nil { - return err - } - targetDir := filepath.Join(configDir, "plugins", pv.pack.Name) - dirPerm := os.FileMode(0755) - if err = os.MkdirAll(targetDir, dirPerm); err != nil { - return err - } - - // Check if all files in zip are in the same directory. - // this might be the case if the plugin zip contains the whole plugin dir - // instead of its content. - var prefix string - allPrefixed := false - for i, f := range z.File { - parts := strings.Split(f.Name, "/") - if i == 0 { - prefix = parts[0] - } else if parts[0] != prefix { - allPrefixed = false - break - } else { - // switch to true since we have at least a second file - allPrefixed = true - } - } - - // Install files and directory's - for _, f := range z.File { - parts := strings.Split(f.Name, "/") - if allPrefixed { - parts = parts[1:] - } - - targetName := filepath.Join(targetDir, filepath.Join(parts...)) - if f.FileInfo().IsDir() { - if err := os.MkdirAll(targetName, dirPerm); err != nil { - return err - } - } else { - basepath := filepath.Dir(targetName) - - if err := os.MkdirAll(basepath, dirPerm); err != nil { - return err - } - - content, err := f.Open() - if err != nil { - return err - } - defer content.Close() - target, err := os.Create(targetName) - if err != nil { - return err - } - defer target.Close() - if _, err = io.Copy(target, content); err != nil { - return err - } - } - } - return nil -} - -func (pl PluginPackages) Get(name string) *PluginPackage { - for _, p := range pl { - if p.Name == name { - return p - } - } - return nil -} - -func (pl PluginPackages) GetAllVersions(name string) PluginVersions { - result := make(PluginVersions, 0) - p := pl.Get(name) - if p != nil { - for _, v := range p.Versions { - result = append(result, v) - } - } - return result -} - -func (req PluginDependencies) Join(other PluginDependencies) PluginDependencies { - m := make(map[string]*PluginDependency) - for _, r := range req { - m[r.Name] = r - } - for _, o := range other { - cur, ok := m[o.Name] - if ok { - m[o.Name] = &PluginDependency{ - o.Name, - o.Range.AND(cur.Range), - } - } else { - m[o.Name] = o - } - } - result := make(PluginDependencies, 0, len(m)) - for _, v := range m { - result = append(result, v) - } - return result -} - -// Resolve resolves dependencies between different plugins -func (all PluginPackages) Resolve(selectedVersions PluginVersions, open PluginDependencies) (PluginVersions, error) { - if len(open) == 0 { - return selectedVersions, nil - } - currentRequirement, stillOpen := open[0], open[1:] - if currentRequirement != nil { - if selVersion := selectedVersions.find(currentRequirement.Name); selVersion != nil { - if currentRequirement.Range(selVersion.Version) { - return all.Resolve(selectedVersions, stillOpen) - } - return nil, fmt.Errorf("unable to find a matching version for \"%s\"", currentRequirement.Name) - } - availableVersions := all.GetAllVersions(currentRequirement.Name) - sort.Sort(availableVersions) - - for _, version := range availableVersions { - if currentRequirement.Range(version.Version) { - resolved, err := all.Resolve(append(selectedVersions, version), stillOpen.Join(version.Require)) - - if err == nil { - return resolved, nil - } - } - } - return nil, fmt.Errorf("unable to find a matching version for \"%s\"", currentRequirement.Name) - } - return selectedVersions, nil -} - -func (pv PluginVersions) install() { - anyInstalled := false - currentlyInstalled := GetInstalledVersions(true) - - for _, sel := range pv { - if sel.pack.Name != CorePluginName { - shouldInstall := true - if pv := currentlyInstalled.find(sel.pack.Name); pv != nil { - if pv.Version.NE(sel.Version) { - messenger.AddLog("Uninstalling", sel.pack.Name) - UninstallPlugin(sel.pack.Name) - } else { - shouldInstall = false - } - } - - if shouldInstall { - if err := sel.DownloadAndInstall(); err != nil { - messenger.Error(err) - return - } - anyInstalled = true - } - } - } - if anyInstalled { - messenger.Message("One or more plugins installed. Please restart micro.") - } else { - messenger.AddLog("Nothing to install / update") - } -} - -// UninstallPlugin deletes the plugin folder of the given plugin -func UninstallPlugin(name string) { - if err := os.RemoveAll(filepath.Join(configDir, "plugins", name)); err != nil { - messenger.Error(err) - return - } - delete(loadedPlugins, name) -} - -// Install installs the plugin -func (pl PluginPackage) Install() { - selected, err := GetAllPluginPackages().Resolve(GetInstalledVersions(true), PluginDependencies{ - &PluginDependency{ - Name: pl.Name, - Range: semver.Range(func(v semver.Version) bool { return true }), - }}) - if err != nil { - TermMessage(err) - return - } - selected.install() -} - -// UpdatePlugins updates the given plugins -func UpdatePlugins(plugins []string) { - // if no plugins are specified, update all installed plugins. - if len(plugins) == 0 { - for name := range loadedPlugins { - plugins = append(plugins, name) - } - } - - messenger.AddLog("Checking for plugin updates") - microVersion := PluginVersions{ - newStaticPluginVersion(CorePluginName, Version), - } - - var updates = make(PluginDependencies, 0) - for _, name := range plugins { - pv := GetInstalledPluginVersion(name) - r, err := semver.ParseRange(">=" + pv) // Try to get newer versions. - if err == nil { - updates = append(updates, &PluginDependency{ - Name: name, - Range: r, - }) - } - } - - selected, err := GetAllPluginPackages().Resolve(microVersion, updates) - if err != nil { - TermMessage(err) - return - } - selected.install() -} diff --git a/cmd/micro/pluginmanager_test.go b/cmd/micro/pluginmanager_test.go deleted file mode 100644 index e7ef5e97..00000000 --- a/cmd/micro/pluginmanager_test.go +++ /dev/null @@ -1,56 +0,0 @@ -package main - -import ( - "testing" - - "github.com/blang/semver" - - "github.com/flynn/json5" -) - -func TestDependencyResolving(t *testing.T) { - js := ` -[{ - "Name": "Foo", - "Versions": [{ "Version": "1.0.0" }, { "Version": "1.5.0" },{ "Version": "2.0.0" }] -}, { - "Name": "Bar", - "Versions": [{ "Version": "1.0.0", "Require": {"Foo": ">1.0.0 <2.0.0"} }] -}, { - "Name": "Unresolvable", - "Versions": [{ "Version": "1.0.0", "Require": {"Foo": "<=1.0.0", "Bar": ">0.0.0"} }] - }] -` - var all PluginPackages - err := json5.Unmarshal([]byte(js), &all) - if err != nil { - t.Error(err) - } - selected, err := all.Resolve(PluginVersions{}, PluginDependencies{ - &PluginDependency{"Bar", semver.MustParseRange(">=1.0.0")}, - }) - - check := func(name, version string) { - v := selected.find(name) - expected := semver.MustParse(version) - if v == nil { - t.Errorf("Failed to resolve %s", name) - } else if expected.NE(v.Version) { - t.Errorf("%s resolved in wrong version %v", name, v) - } - } - - if err != nil { - t.Error(err) - } else { - check("Foo", "1.5.0") - check("Bar", "1.0.0") - } - - selected, err = all.Resolve(PluginVersions{}, PluginDependencies{ - &PluginDependency{"Unresolvable", semver.MustParseRange(">0.0.0")}, - }) - if err == nil { - t.Error("Unresolvable package resolved:", selected) - } -} diff --git a/cmd/micro/profile.go b/cmd/micro/profile.go new file mode 100644 index 00000000..86ac558f --- /dev/null +++ b/cmd/micro/profile.go @@ -0,0 +1,14 @@ +package main + +import ( + "fmt" + "runtime" + + humanize "github.com/dustin/go-humanize" +) + +func GetMemStats() string { + var memstats runtime.MemStats + runtime.ReadMemStats(&memstats) + return fmt.Sprintf("Alloc: %s, Sys: %s, GC: %d, PauseTotalNs: %dns", humanize.Bytes(memstats.Alloc), humanize.Bytes(memstats.Sys), memstats.NumGC, memstats.PauseTotalNs) +} diff --git a/cmd/micro/rtfiles.go b/cmd/micro/rtfiles.go index a72b6ee3..947c4e13 100644 --- a/cmd/micro/rtfiles.go +++ b/cmd/micro/rtfiles.go @@ -8,12 +8,15 @@ import ( ) const ( - RTColorscheme = "colorscheme" - RTSyntax = "syntax" - RTHelp = "help" - RTPlugin = "plugin" + RTColorscheme = 0 + RTSyntax = 1 + RTHelp = 2 + RTPlugin = 3 + NumTypes = 4 // How many filetypes are there ) +type RTFiletype byte + // RuntimeFile allows the program to read runtime data like colorschemes or syntax files type RuntimeFile interface { // Name returns a name of the file without paths or extensions @@ -23,7 +26,7 @@ type RuntimeFile interface { } // allFiles contains all available files, mapped by filetype -var allFiles map[string][]RuntimeFile +var allFiles [NumTypes][]RuntimeFile // some file on filesystem type realFile string @@ -73,16 +76,13 @@ func (nf namedFile) Name() string { } // AddRuntimeFile registers a file for the given filetype -func AddRuntimeFile(fileType string, file RuntimeFile) { - if allFiles == nil { - allFiles = make(map[string][]RuntimeFile) - } +func AddRuntimeFile(fileType RTFiletype, file RuntimeFile) { allFiles[fileType] = append(allFiles[fileType], file) } // AddRuntimeFilesFromDirectory registers each file from the given directory for // the filetype which matches the file-pattern -func AddRuntimeFilesFromDirectory(fileType, directory, pattern string) { +func AddRuntimeFilesFromDirectory(fileType RTFiletype, directory, pattern string) { files, _ := ioutil.ReadDir(directory) for _, f := range files { if ok, _ := filepath.Match(pattern, f.Name()); !f.IsDir() && ok { @@ -94,7 +94,7 @@ func AddRuntimeFilesFromDirectory(fileType, directory, pattern string) { // AddRuntimeFilesFromAssets registers each file from the given asset-directory for // the filetype which matches the file-pattern -func AddRuntimeFilesFromAssets(fileType, directory, pattern string) { +func AddRuntimeFilesFromAssets(fileType RTFiletype, directory, pattern string) { files, err := AssetDir(directory) if err != nil { return @@ -108,7 +108,7 @@ func AddRuntimeFilesFromAssets(fileType, directory, pattern string) { // FindRuntimeFile finds a runtime file of the given filetype and name // will return nil if no file was found -func FindRuntimeFile(fileType, name string) RuntimeFile { +func FindRuntimeFile(fileType RTFiletype, name string) RuntimeFile { for _, f := range ListRuntimeFiles(fileType) { if f.Name() == name { return f @@ -118,16 +118,13 @@ func FindRuntimeFile(fileType, name string) RuntimeFile { } // ListRuntimeFiles lists all known runtime files for the given filetype -func ListRuntimeFiles(fileType string) []RuntimeFile { - if files, ok := allFiles[fileType]; ok { - return files - } - return []RuntimeFile{} +func ListRuntimeFiles(fileType RTFiletype) []RuntimeFile { + return allFiles[fileType] } // InitRuntimeFiles initializes all assets file and the config directory func InitRuntimeFiles() { - add := func(fileType, dir, pattern string) { + add := func(fileType RTFiletype, dir, pattern string) { AddRuntimeFilesFromDirectory(fileType, filepath.Join(configDir, dir), pattern) AddRuntimeFilesFromAssets(fileType, path.Join("runtime", dir), pattern) } @@ -160,7 +157,7 @@ func InitRuntimeFiles() { } // PluginReadRuntimeFile allows plugin scripts to read the content of a runtime file -func PluginReadRuntimeFile(fileType, name string) string { +func PluginReadRuntimeFile(fileType RTFiletype, name string) string { if file := FindRuntimeFile(fileType, name); file != nil { if data, err := file.Data(); err == nil { return string(data) @@ -170,7 +167,7 @@ func PluginReadRuntimeFile(fileType, name string) string { } // PluginListRuntimeFiles allows plugins to lists all runtime files of the given type -func PluginListRuntimeFiles(fileType string) []string { +func PluginListRuntimeFiles(fileType RTFiletype) []string { files := ListRuntimeFiles(fileType) result := make([]string, len(files)) for i, f := range files { @@ -180,7 +177,7 @@ func PluginListRuntimeFiles(fileType string) []string { } // PluginAddRuntimeFile adds a file to the runtime files for a plugin -func PluginAddRuntimeFile(plugin, filetype, filePath string) { +func PluginAddRuntimeFile(plugin string, filetype RTFiletype, filePath string) { fullpath := filepath.Join(configDir, "plugins", plugin, filePath) if _, err := os.Stat(fullpath); err == nil { AddRuntimeFile(filetype, realFile(fullpath)) @@ -191,7 +188,7 @@ func PluginAddRuntimeFile(plugin, filetype, filePath string) { } // PluginAddRuntimeFilesFromDirectory adds files from a directory to the runtime files for a plugin -func PluginAddRuntimeFilesFromDirectory(plugin, filetype, directory, pattern string) { +func PluginAddRuntimeFilesFromDirectory(plugin string, filetype RTFiletype, directory, pattern string) { fullpath := filepath.Join(configDir, "plugins", plugin, directory) if _, err := os.Stat(fullpath); err == nil { AddRuntimeFilesFromDirectory(filetype, fullpath, pattern) @@ -202,6 +199,6 @@ func PluginAddRuntimeFilesFromDirectory(plugin, filetype, directory, pattern str } // PluginAddRuntimeFileFromMemory adds a file to the runtime files for a plugin from a given string -func PluginAddRuntimeFileFromMemory(plugin, filetype, filename, data string) { +func PluginAddRuntimeFileFromMemory(plugin string, filetype RTFiletype, filename, data string) { AddRuntimeFile(filetype, memoryFile{filename, []byte(data)}) } diff --git a/cmd/micro/rtfiles_test.go b/cmd/micro/rtfiles_test.go new file mode 100644 index 00000000..f3a8b0df --- /dev/null +++ b/cmd/micro/rtfiles_test.go @@ -0,0 +1,42 @@ +package main + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func init() { + InitRuntimeFiles() +} + +func TestAddFile(t *testing.T) { + AddRuntimeFile(RTPlugin, memoryFile{"foo.lua", []byte("hello world\n")}) + AddRuntimeFile(RTSyntax, memoryFile{"bar", []byte("some syntax file\n")}) + + f1 := FindRuntimeFile(RTPlugin, "foo.lua") + assert.NotNil(t, f1) + assert.Equal(t, "foo.lua", f1.Name()) + data, err := f1.Data() + assert.Nil(t, err) + assert.Equal(t, []byte("hello world\n"), data) + + f2 := FindRuntimeFile(RTSyntax, "bar") + assert.NotNil(t, f2) + assert.Equal(t, "bar", f2.Name()) + data, err = f2.Data() + assert.Nil(t, err) + assert.Equal(t, []byte("some syntax file\n"), data) +} + +func TestFindFile(t *testing.T) { + f := FindRuntimeFile(RTSyntax, "go") + assert.NotNil(t, f) + assert.Equal(t, "go", f.Name()) + data, err := f.Data() + assert.Nil(t, err) + assert.Equal(t, []byte("filetype: go"), data[:12]) + + e := FindRuntimeFile(RTSyntax, "foobar") + assert.Nil(t, e) +} diff --git a/cmd/micro/runtime.go b/cmd/micro/runtime.go index a85f317d..94741c4e 100644 --- a/cmd/micro/runtime.go +++ b/cmd/micro/runtime.go @@ -3176,7 +3176,7 @@ func runtimeSyntaxSedYaml() (*asset, error) { return a, nil } -var _runtimeSyntaxShYaml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x94\x54\x7f\x73\x1b\x45\x0c\xfd\x3f\x9f\xe2\xea\x64\xa8\xdd\x62\xd3\x96\xb6\x03\xe6\x87\x29\x2d\x30\x9d\x02\xed\x0c\x30\x93\x21\x9b\x96\xf5\xae\xce\xb7\xdc\xfe\xb8\xee\xea\xe2\x84\xbe\x7c\x77\x46\x67\x3b\x69\x4d\xa7\x40\x26\xb7\x3a\x49\x96\xf4\xa4\x7d\xa7\xda\x79\xe2\x8b\x8e\xe6\x55\x69\xc8\xfb\x83\x03\x4b\x4c\x86\xe7\x07\x55\x55\x55\xe2\x8c\x3a\xd0\xbc\x1a\x8d\x95\x9a\x95\xe6\x08\x4a\xcd\x96\xba\x34\x22\xb7\x42\xd4\x6c\xb0\x15\x5b\xc3\x2b\xed\x9d\x2e\x54\xf0\x8e\xb2\x73\xd6\x7d\x34\xec\x52\xdc\xba\xaf\xd5\xdd\x0f\xba\x9c\xa4\x36\xde\x51\x94\x9a\xed\x5e\x77\xf2\x45\xbb\xda\xe8\xed\x2a\xb4\x33\x93\x62\x7d\xe5\xca\x66\xa3\xbf\x78\xf6\xc3\xb7\xbf\x3d\xfd\xf1\x09\x66\xb4\xec\x9d\xb7\x4a\x1d\xe1\xd1\x8b\x67\x83\x6d\x32\x1a\xda\x6c\x48\x5b\xca\xf3\x6a\xf4\xf2\xf0\xc6\xec\xd6\x27\x63\x8a\x67\xd5\xed\xc9\x62\xbc\xd4\x93\xc5\x58\x4f\x16\xa5\x19\x57\x38\x9a\x8c\x0e\x0e\x72\xef\xa9\x6c\x66\x73\x58\xfd\xdc\x87\x25\xe5\x32\x68\xd3\xca\xa4\x58\x58\x47\x9e\xc5\xc1\x3c\xaf\x46\x4a\x2d\x4f\xee\x4c\x3f\x3f\xbd\xad\xd4\x72\x53\x48\xa2\x1e\xa7\x68\x9d\x74\xab\x7d\xa9\x74\xb4\x12\xc8\x39\xf9\xaa\xf6\x69\xbd\xcd\x55\x58\x33\x05\x8a\xbc\xc9\x32\x36\xba\x10\x6c\x82\x4d\x91\x40\xde\xd5\x20\x5f\x08\x54\xb4\x01\x9d\x3b\x46\xed\x50\xa7\x8c\xdd\x20\xe1\x6a\xb8\x08\x9f\x8c\xf6\xc8\xa4\x2d\x32\x71\x9f\x23\x0a\x79\x32\x8c\xd2\xb8\x9a\xc1\x0d\x45\xb0\x0b\x84\x3e\xb2\xf3\x58\x37\xce\xd3\xe4\x0a\xec\xb4\x2a\x1d\x19\xa7\xfd\xe6\xf6\xdf\x40\xa9\x4b\x28\x35\x86\x52\x13\x28\xf5\x05\x94\x3a\x85\x52\x27\xf8\x03\x4a\x29\x05\x19\xec\x97\xf8\x1a\x37\xf0\x15\x3e\x82\x52\x98\x5c\x37\xfd\x8b\x70\xab\x32\x29\x04\x1d\xed\x6e\x62\x1b\xda\x6d\x1a\xb4\x20\xd3\x24\xd0\x79\x97\x32\xc3\x13\xa3\x10\xa3\x0f\xba\xb4\xe8\x63\x21\x9e\xec\x0d\x31\x84\x14\x2b\xef\x62\x7f\xfe\x81\xb4\xe3\x15\xdc\x6a\xb2\xd0\xeb\x76\x60\x12\xac\x1c\xb5\x8b\x16\x4a\xad\xdf\xdc\xf9\xf8\xfe\xe5\x2a\x53\x87\xd6\x79\x3f\x1c\xda\xfb\x2b\x8f\xa7\x52\x10\x74\x2b\xe4\x12\x7f\x21\x8b\xd2\x80\x75\xde\x87\x92\xa9\x67\xe7\xcb\x07\x70\x2c\x75\xa1\x87\xf7\x05\xc3\xf0\x39\xc1\x68\x86\x69\x4c\x8a\x30\xcd\x2a\x77\x30\x4d\x48\x16\xa6\x49\x6b\xb1\xe4\x94\x18\xa6\x2d\x7d\x80\xa4\x84\xe9\x60\x4a\xe7\x1d\xc3\xf4\x0c\xab\x99\x60\x2d\x6c\x0d\xeb\xb2\x3c\x26\xf9\x94\x8b\xbc\x0d\xc9\x6d\x0f\x8a\x67\x32\x4a\x1d\xad\x88\x8c\x5a\x1b\x16\x7a\x68\x61\x4d\x1d\x18\x75\xf2\x16\x42\x7b\x34\xa9\xb0\xb3\x90\x7f\xe1\xaf\xf7\xf8\x33\x09\x75\x5c\x6c\xe1\x85\x42\xab\x21\xa9\x2f\x08\xf6\x81\x40\x0a\xad\x94\x0d\x6d\xed\xea\x84\xd0\xc6\x64\x11\x5a\xa6\xd0\x21\x9c\x21\x3a\x43\x88\x1e\x31\x35\x7d\x87\xd8\xe5\x64\x10\xfb\x20\x25\x93\x45\xa7\x0b\x13\x3a\xcd\x8d\x69\x5a\x74\x2e\xb6\x17\xe8\x32\xba\xec\x22\x0b\xe4\xe1\xa5\x46\xc7\xe7\xe8\xd6\x76\x20\xee\x80\x23\x93\xf6\x12\x85\x1c\x90\x83\x94\xcf\x7d\x94\xe9\x15\x7a\x8d\x71\x69\xf4\x5d\x94\x46\xdf\xbb\x77\x7f\x10\x0f\x1e\x8a\xf8\xf4\xb3\x41\x7b\x70\xf7\xde\x44\x50\x97\x26\x0f\xf7\xd7\xd7\x28\x9e\xa8\x43\x11\x9e\x6d\xa6\x2a\x5f\x1b\x0a\xdb\xa5\x38\x99\x2f\x30\x04\x5c\x44\x03\xd6\xf2\x38\x0f\x26\x02\x53\xe1\xcd\xe7\x22\x47\xea\x19\x9c\x7a\xd3\x80\x33\x38\xf7\x24\x47\x34\x72\x39\x3c\xe4\x96\x44\xfd\x30\xbb\x3e\x6e\xef\xa2\x8f\xee\x35\xfa\x38\xf4\xd4\x17\xca\x05\x67\xd2\xcc\xda\x60\xdd\x24\x79\x74\x70\xb8\xa0\xb2\x4f\xb0\xab\x85\x51\xd5\x5e\xaf\xca\x7b\x96\xc4\x74\x7a\xa2\xa7\x7f\x4d\x4f\x6f\x8f\xde\xbb\x41\xaa\xc1\x2d\xde\xad\xdb\x59\x8a\xec\x6a\xb7\xdd\x53\x47\x4a\xbd\x59\xc8\xae\x7a\x34\xfd\xfd\xd5\x8d\x6f\x0e\x8f\x6e\x2d\xa6\xb2\xb6\x2e\x17\xa3\xff\x1f\xb0\xbf\x0f\x0b\x67\x17\x57\xf3\x6d\x3f\x95\x40\xcb\x03\xac\xd1\xe8\xca\x46\xd1\xee\x59\x4a\xeb\xba\xa1\x94\x52\xb3\x6b\xeb\x5b\xfb\x77\xf7\xf7\x76\xa5\xcd\xbe\x7a\xdc\xe8\x7c\x1d\xfa\x5f\xe1\xdc\xdc\x47\x73\x73\xbf\x6c\x75\x72\x7a\x9d\x2d\x0c\xc3\xfd\x47\x96\xf1\x4b\x28\x55\x26\x87\xfb\xc9\x8e\xfe\xa5\x07\x4e\x36\x49\xf8\xaf\xcf\x9f\x3c\xc7\xf1\xf1\x31\xbe\x7f\x7a\xfc\xd3\x77\x93\xb9\xcc\xf3\xef\x00\x00\x00\xff\xff\xef\x45\x76\x90\xa3\x07\x00\x00") +var _runtimeSyntaxShYaml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x94\x54\x7f\x73\x1b\x45\x0c\xfd\x3f\x9f\xe2\xea\x64\xa8\xdd\x62\xd3\x96\xb6\x03\xe6\x87\x29\x2d\x30\x9d\x02\xed\x0c\x30\x93\x21\x9b\x96\xf5\xae\xce\xb7\xdc\xfe\xb8\xee\xea\xe2\x84\xbe\x7c\x77\x46\x67\x3b\x69\x4d\xa7\x40\x26\xb7\x3a\x49\x96\xf4\xa4\x7d\xa7\xda\x79\xe2\x8b\x8e\xe6\x55\x69\xc8\xfb\x83\x03\x4b\x4c\x86\xe7\x07\x55\x55\x55\xe2\x8c\x3a\xd0\xbc\x1a\x8d\x95\x9a\x95\xe6\x08\x4a\xcd\x96\xba\x34\x3b\x99\x0d\xb6\x62\x6b\x78\xa5\xbd\xd3\x85\x0a\xde\x51\x76\xce\xba\x8f\x86\x5d\x8a\x5b\xf7\xb5\xba\xfb\x41\x97\x93\x14\xc5\x3b\xca\x8b\x76\x35\xc8\xae\x5d\x85\x76\x66\x52\xac\xb1\x73\x65\xb3\xd1\x5f\x3c\xfb\xe1\xdb\xdf\x9e\xfe\xf8\x04\x33\x5a\xf6\xce\x5b\xa5\x8e\xf0\xe8\xc5\xb3\xc1\x36\x19\x0d\xcd\x34\xa4\x2d\xe5\x79\x35\x7a\x79\x78\x63\x76\xeb\x93\x31\xc5\xb3\xea\xf6\x64\x31\x5e\xea\xc9\xa2\x34\xe3\x0a\x47\x93\xd1\xc1\x41\xee\x3d\x95\x4d\xf7\x87\xd5\xcf\x7d\x58\x52\x2e\x83\x36\xad\x4c\x8a\x85\x75\xe4\x59\x1c\xcc\xf3\x6a\xa4\xd4\xf2\xe4\xce\xf4\xf3\xd3\xdb\x4a\x2d\x37\x45\x24\xea\x71\x8a\xd6\x49\x5b\xda\x97\x4a\x47\x2b\x81\x9c\x93\xaf\x6a\x9f\xd6\xdb\x5c\x85\x35\x53\xa0\xc8\x9b\x2c\x63\xa3\x0b\xc1\x26\xd8\x14\x09\xe4\x5d\x0d\xf2\x85\x40\x45\x1b\xd0\xb9\x63\xd4\x0e\x75\xca\xd8\x4d\x0c\xae\x86\x8b\xf0\xc9\x68\x8f\x4c\xda\x22\x13\xf7\x39\xa2\x90\x27\xc3\x28\x8d\xab\x19\xdc\x50\x04\xbb\x40\xe8\x23\x3b\x8f\x75\xe3\x3c\x4d\xae\xc0\x4e\xab\xd2\x91\x71\xda\x6f\xee\xf7\x0d\x94\xba\x84\x52\x63\x28\x35\x81\x52\x5f\x40\xa9\x53\x28\x75\x82\x3f\xa0\x94\x52\x90\xa1\x7e\x89\xaf\x71\x03\x5f\xe1\x23\x28\x85\xc9\x75\xd3\xbf\x08\x7b\x2a\x93\x42\xd0\xd1\xee\x26\xb6\x21\xd6\xa6\x41\x0b\x32\x4d\x02\x9d\x77\x29\x33\x3c\x31\x0a\x31\xfa\xa0\x4b\x8b\x3e\x16\xe2\xc9\xde\x10\x43\x48\xb1\xf2\x2e\xf6\xe7\x1f\x48\x3b\x5e\xc1\xad\x26\x0b\xbd\x6e\x07\xca\xc0\xca\x51\xbb\x68\xa1\xd4\xfa\xcd\x9d\x8f\xef\x5f\xae\x32\x75\x68\x9d\xf7\xc3\xa1\xbd\xbf\xf2\x78\x2a\x05\x41\xb7\x42\x2c\xf1\x17\xb2\x28\x0d\x58\xe7\x7d\x28\x99\x7a\x76\xbe\x7c\x00\xc7\x52\x17\x7a\x78\x5f\x30\x0c\x1f\x0c\x8c\x66\x98\xc6\xa4\x08\xd3\xac\x72\x07\xd3\x84\x64\x61\x9a\xb4\x16\x4b\x4e\x89\x61\xda\xd2\x07\x48\x4a\x98\x0e\xa6\x74\xde\x31\x4c\xcf\xb0\x9a\x09\xd6\xc2\xd6\xb0\x2e\xcb\x63\x92\x4f\xb9\xc8\xdb\x90\xdc\xf6\xa0\x78\x26\xa3\xd4\xd1\x8a\xc8\xa8\xb5\x61\xa1\x87\x16\xd6\xd4\x81\x51\x27\x6f\x21\x94\x47\x93\x0a\x3b\x0b\xf9\x17\xfe\x7a\x8f\x3f\x93\x50\xc7\xc5\x16\x5e\x28\xb4\x1a\x92\xfa\x82\x60\x1f\x08\xa4\xd0\x4a\xd9\xd0\xd6\xae\x4e\x08\x6d\x4c\x16\xa1\x65\x0a\x1d\xc2\x19\xa2\x33\x84\xe8\x11\x53\xd3\x77\x88\x5d\x4e\x06\xb1\x0f\x52\x32\x59\x74\xba\x30\xa1\xd3\xdc\x98\xa6\x45\xe7\x62\x7b\x81\x2e\xa3\xcb\x2e\xb2\x40\x1e\x5e\x6a\x74\x7c\x8e\x6e\x6d\x07\xe2\x0e\x38\x32\x69\x2f\x51\xc8\x01\x39\x48\xf9\xdc\x47\x99\x5e\xa1\xd7\x18\x97\x46\xdf\x45\x69\xf4\xbd\x7b\xf7\x07\xf1\xe0\xa1\x88\x4f\x3f\x1b\xb4\x07\x77\xef\x4d\x04\x75\x69\xf2\x70\x7f\x7d\x8d\xe2\x89\x3a\x14\xe1\xd9\x66\xaa\xf2\xb5\xa1\xb0\x5d\x8a\x93\xf9\x02\x43\xc0\x45\x34\x60\x2d\x8f\xf3\x60\x22\x30\x15\xde\x7c\x2e\x72\xa4\x9e\xc1\xa9\x37\x0d\x38\x83\x73\x4f\x72\x44\x23\x97\xc3\x43\x6e\x49\xd4\x0f\xb3\xeb\xe3\xf6\x2e\xfa\xe8\x5e\xa3\x8f\x43\x4f\x7d\xa1\x5c\x70\x26\xcd\xac\x0d\xd6\x4d\x92\x47\x07\x87\x0b\x2a\xfb\x04\xbb\x5a\x18\x55\xed\xf5\xaa\xbc\x67\x49\x4c\xa7\x27\x7a\xfa\xd7\xf4\xf4\xf6\xe8\xbd\x1b\xa4\x1a\xdc\xe2\xdd\xba\x9d\xa5\xc8\xae\x76\xdb\x3d\x75\xa4\xd4\x9b\x85\xec\xaa\x47\xd3\xdf\x5f\xdd\xf8\xe6\xf0\xe8\xd6\x62\x2a\x6b\xeb\x72\x31\xfa\xff\x01\xfb\xfb\xb0\x70\x76\x71\x35\xdf\xf6\x53\x09\xb4\x3c\xc0\x1a\x8d\xae\x6c\x14\xed\x9e\xa5\xb4\xae\x1b\x4a\x29\x35\xbb\xb6\xbe\xb5\x7f\x77\x7f\x6f\x57\xda\xec\xab\xc7\x8d\xce\xd7\xa1\xff\x15\xce\xcd\x7d\x34\x37\xf7\xcb\x56\x27\xa7\xd7\xd9\xc2\x30\xdc\x7f\x64\x19\xbf\x84\x52\x65\x72\xb8\x9f\xec\xe8\x5f\x7a\xe0\x64\x93\x84\xff\xfa\xfc\xc9\x73\x1c\x1f\x1f\xe3\xfb\xa7\xc7\x3f\x7d\x37\x99\xcb\x3c\xff\x0e\x00\x00\xff\xff\x42\xc3\x8d\xb7\x85\x07\x00\x00") func runtimeSyntaxShYamlBytes() ([]byte, error) { return bindataRead( diff --git a/cmd/micro/scrollbar.go b/cmd/micro/scrollbar.go deleted file mode 100644 index f4a9a078..00000000 --- a/cmd/micro/scrollbar.go +++ /dev/null @@ -1,20 +0,0 @@ -package main - -// ScrollBar represents an optional scrollbar that can be used -type ScrollBar struct { - view *View -} - -// Display shows the scrollbar -func (sb *ScrollBar) Display() { - style := defStyle.Reverse(true) - screen.SetContent(sb.view.x+sb.view.Width-1, sb.view.y+sb.pos(), ' ', nil, style) -} - -func (sb *ScrollBar) pos() int { - numlines := sb.view.Buf.NumLines - h := sb.view.Height - filepercent := float32(sb.view.Topline) / float32(numlines) - - return int(filepercent * float32(h)) -} diff --git a/cmd/micro/search.go b/cmd/micro/search.go deleted file mode 100644 index 7139f844..00000000 --- a/cmd/micro/search.go +++ /dev/null @@ -1,214 +0,0 @@ -package main - -import ( - "regexp" - "strings" - - "github.com/zyedidia/tcell" -) - -var ( - // What was the last search - lastSearch string - - // Where should we start the search down from (or up from) - searchStart Loc - - // Is there currently a search in progress - searching bool - - // Stores the history for searching - searchHistory []string -) - -// BeginSearch starts a search -func BeginSearch(searchStr string) { - searchHistory = append(searchHistory, "") - messenger.historyNum = len(searchHistory) - 1 - searching = true - messenger.response = searchStr - messenger.cursorx = Count(searchStr) - messenger.Message("Find: ") - messenger.hasPrompt = true -} - -// EndSearch stops the current search -func EndSearch() { - searchHistory[len(searchHistory)-1] = messenger.response - searching = false - messenger.hasPrompt = false - messenger.Clear() - messenger.Reset() - if lastSearch != "" { - messenger.Message("^P Previous ^N Next") - } -} - -// ExitSearch exits the search mode, reset active search phrase, and clear status bar -func ExitSearch(v *View) { - lastSearch = "" - searching = false - messenger.hasPrompt = false - messenger.Clear() - messenger.Reset() - v.Cursor.ResetSelection() -} - -// HandleSearchEvent takes an event and a view and will do a real time match from the messenger's output -// to the current buffer. It searches down the buffer. -func HandleSearchEvent(event tcell.Event, v *View) { - switch e := event.(type) { - case *tcell.EventKey: - switch e.Key() { - case tcell.KeyEscape: - // Exit the search mode - ExitSearch(v) - return - case tcell.KeyEnter: - // If the user has pressed Enter, they want this to be the lastSearch - lastSearch = messenger.response - EndSearch() - return - case tcell.KeyCtrlQ, tcell.KeyCtrlC: - // Done - EndSearch() - return - } - } - - messenger.HandleEvent(event, searchHistory) - - if messenger.cursorx < 0 { - // Done - EndSearch() - return - } - - if messenger.response == "" { - v.Cursor.ResetSelection() - // We don't end the search though - return - } - - Search(messenger.response, v, true) - - v.Relocate() - - return -} - -func searchDown(r *regexp.Regexp, v *View, start, end Loc) bool { - if start.Y >= v.Buf.NumLines { - start.Y = v.Buf.NumLines - 1 - } - if start.Y < 0 { - start.Y = 0 - } - for i := start.Y; i <= end.Y; i++ { - var l []byte - var charPos int - if i == start.Y { - runes := []rune(string(v.Buf.lines[i].data)) - if start.X >= len(runes) { - start.X = len(runes) - 1 - } - if start.X < 0 { - start.X = 0 - } - l = []byte(string(runes[start.X:])) - charPos = start.X - - if strings.Contains(r.String(), "^") && start.X != 0 { - continue - } - } else { - l = v.Buf.lines[i].data - } - - match := r.FindIndex(l) - - if match != nil { - v.Cursor.SetSelectionStart(Loc{charPos + runePos(match[0], string(l)), i}) - v.Cursor.SetSelectionEnd(Loc{charPos + runePos(match[1], string(l)), i}) - v.Cursor.OrigSelection[0] = v.Cursor.CurSelection[0] - v.Cursor.OrigSelection[1] = v.Cursor.CurSelection[1] - v.Cursor.Loc = v.Cursor.CurSelection[1] - - return true - } - } - return false -} - -func searchUp(r *regexp.Regexp, v *View, start, end Loc) bool { - if start.Y >= v.Buf.NumLines { - start.Y = v.Buf.NumLines - 1 - } - if start.Y < 0 { - start.Y = 0 - } - for i := start.Y; i >= end.Y; i-- { - var l []byte - if i == start.Y { - runes := []rune(string(v.Buf.lines[i].data)) - if start.X >= len(runes) { - start.X = len(runes) - 1 - } - if start.X < 0 { - start.X = 0 - } - l = []byte(string(runes[:start.X])) - - if strings.Contains(r.String(), "$") && start.X != Count(string(l)) { - continue - } - } else { - l = v.Buf.lines[i].data - } - - match := r.FindIndex(l) - - if match != nil { - v.Cursor.SetSelectionStart(Loc{runePos(match[0], string(l)), i}) - v.Cursor.SetSelectionEnd(Loc{runePos(match[1], string(l)), i}) - v.Cursor.OrigSelection[0] = v.Cursor.CurSelection[0] - v.Cursor.OrigSelection[1] = v.Cursor.CurSelection[1] - v.Cursor.Loc = v.Cursor.CurSelection[1] - - return true - } - } - return false -} - -// Search searches in the view for the given regex. The down bool -// specifies whether it should search down from the searchStart position -// or up from there -func Search(searchStr string, v *View, down bool) { - if searchStr == "" { - return - } - r, err := regexp.Compile(searchStr) - if v.Buf.Settings["ignorecase"].(bool) { - r, err = regexp.Compile("(?i)" + searchStr) - } - if err != nil { - return - } - - var found bool - if down { - found = searchDown(r, v, searchStart, v.Buf.End()) - if !found { - found = searchDown(r, v, v.Buf.Start(), searchStart) - } - } else { - found = searchUp(r, v, searchStart, v.Buf.Start()) - if !found { - found = searchUp(r, v, v.Buf.End(), searchStart) - } - } - if !found { - v.Cursor.ResetSelection() - } -} diff --git a/cmd/micro/settings.go b/cmd/micro/settings.go index c8e50e12..e4ac6344 100644 --- a/cmd/micro/settings.go +++ b/cmd/micro/settings.go @@ -1,16 +1,12 @@ package main import ( - "crypto/md5" "encoding/json" "errors" - "io" "io/ioutil" "os" "reflect" - "strconv" "strings" - "sync" "github.com/flynn/json5" "github.com/zyedidia/glob" @@ -21,7 +17,8 @@ type optionValidator func(string, interface{}) error // The options that the user can set var globalSettings map[string]interface{} -var invalidSettings bool +// This is the raw parsed json +var parsedSettings map[string]interface{} // Options with validators var optionValidators = map[string]optionValidator{ @@ -33,74 +30,42 @@ var optionValidators = map[string]optionValidator{ "fileformat": validateLineEnding, } -// InitGlobalSettings initializes the options map and sets all options to their default values -func InitGlobalSettings() { - invalidSettings = false - defaults := DefaultGlobalSettings() - var parsed map[string]interface{} - +func ReadSettings() error { filename := configDir + "/settings.json" - writeSettings := false if _, e := os.Stat(filename); e == nil { input, err := ioutil.ReadFile(filename) + if err != nil { + return errors.New("Error reading settings.json file: " + err.Error()) + } if !strings.HasPrefix(string(input), "null") { + // Unmarshal the input into the parsed map + err = json5.Unmarshal(input, &parsedSettings) if err != nil { - TermMessage("Error reading settings.json file: " + err.Error()) - invalidSettings = true - return + return errors.New("Error reading settings.json: " + err.Error()) } - - err = json5.Unmarshal(input, &parsed) - if err != nil { - TermMessage("Error reading settings.json:", err.Error()) - invalidSettings = true - } - } else { - writeSettings = true } } + return nil +} - globalSettings = make(map[string]interface{}) - for k, v := range defaults { - globalSettings[k] = v - } - for k, v := range parsed { +// InitGlobalSettings initializes the options map and sets all options to their default values +// Must be called after ReadSettings +func InitGlobalSettings() { + globalSettings = DefaultGlobalSettings() + + for k, v := range parsedSettings { if !strings.HasPrefix(reflect.TypeOf(v).String(), "map") { globalSettings[k] = v } } - - if _, err := os.Stat(filename); os.IsNotExist(err) || writeSettings { - err := WriteSettings(filename) - if err != nil { - TermMessage("Error writing settings.json file: " + err.Error()) - } - } } // InitLocalSettings scans the json in settings.json and sets the options locally based // on whether the buffer matches the glob -func InitLocalSettings(buf *Buffer) { - invalidSettings = false - var parsed map[string]interface{} - - filename := configDir + "/settings.json" - if _, e := os.Stat(filename); e == nil { - input, err := ioutil.ReadFile(filename) - if err != nil { - TermMessage("Error reading settings.json file: " + err.Error()) - invalidSettings = true - return - } - - err = json5.Unmarshal(input, &parsed) - if err != nil { - TermMessage("Error reading settings.json:", err.Error()) - invalidSettings = true - } - } - - for k, v := range parsed { +// Must be called after ReadSettings +func InitLocalSettings(buf *Buffer) error { + var parseError error + for k, v := range parsedSettings { if strings.HasPrefix(reflect.TypeOf(v).String(), "map") { if strings.HasPrefix(k, "ft:") { if buf.Settings["filetype"].(string) == k[3:] { @@ -111,7 +76,7 @@ func InitLocalSettings(buf *Buffer) { } else { g, err := glob.Compile(k) if err != nil { - TermMessage("Error with glob setting ", k, ": ", err) + parseError = errors.New("Error with glob setting " + k + ": " + err.Error()) continue } @@ -123,59 +88,31 @@ func InitLocalSettings(buf *Buffer) { } } } + return parseError } // WriteSettings writes the settings to the specified filename as JSON func WriteSettings(filename string) error { - if invalidSettings { - // Do not write the settings if there was an error when reading them - return nil - } - var err error if _, e := os.Stat(configDir); e == nil { - parsed := make(map[string]interface{}) - - filename := configDir + "/settings.json" for k, v := range globalSettings { - parsed[k] = v - } - if _, e := os.Stat(filename); e == nil { - input, err := ioutil.ReadFile(filename) - if string(input) != "null" { - if err != nil { - return err - } - - err = json5.Unmarshal(input, &parsed) - if err != nil { - TermMessage("Error reading settings.json:", err.Error()) - invalidSettings = true - } - - for k, v := range parsed { - if !strings.HasPrefix(reflect.TypeOf(v).String(), "map") { - if _, ok := globalSettings[k]; ok { - parsed[k] = globalSettings[k] - } - } - } - } + parsedSettings[k] = v } - txt, _ := json.MarshalIndent(parsed, "", " ") + txt, _ := json.MarshalIndent(parsedSettings, "", " ") err = ioutil.WriteFile(filename, append(txt, '\n'), 0644) } return err } // AddOption creates a new option. This is meant to be called by plugins to add options. -func AddOption(name string, value interface{}) { +func AddOption(name string, value interface{}) error { globalSettings[name] = value err := WriteSettings(configDir + "/settings.json") if err != nil { - TermMessage("Error writing settings.json file: " + err.Error()) + return errors.New("Error writing settings.json file: " + err.Error()) } + return nil } // GetGlobalOption returns the global value of the given option @@ -188,291 +125,268 @@ func GetLocalOption(name string, buf *Buffer) interface{} { return buf.Settings[name] } +// TODO: get option for current buffer // GetOption returns the value of the given option // If there is a local version of the option, it returns that // otherwise it will return the global version -func GetOption(name string) interface{} { - if GetLocalOption(name, CurView().Buf) != nil { - return GetLocalOption(name, CurView().Buf) +// func GetOption(name string) interface{} { +// if GetLocalOption(name, CurView().Buf) != nil { +// return GetLocalOption(name, CurView().Buf) +// } +// return GetGlobalOption(name) +// } + +func DefaultCommonSettings() map[string]interface{} { + return map[string]interface{}{ + "autoindent": true, + "autosave": false, + "basename": false, + "colorcolumn": float64(0), + "cursorline": true, + "eofnewline": false, + "fastdirty": true, + "fileformat": "unix", + "hidehelp": false, + "ignorecase": false, + "indentchar": " ", + "keepautoindent": false, + "matchbrace": false, + "matchbraceleft": false, + "rmtrailingws": false, + "ruler": true, + "savecursor": false, + "saveundo": false, + "scrollbar": false, + "scrollmargin": float64(3), + "scrollspeed": float64(2), + "softwrap": false, + "smartpaste": true, + "splitbottom": true, + "splitright": true, + "statusline": true, + "syntax": true, + "tabmovement": false, + "tabsize": float64(4), + "tabstospaces": false, + "useprimary": true, } - return GetGlobalOption(name) } // DefaultGlobalSettings returns the default global settings for micro // Note that colorscheme is a global only option func DefaultGlobalSettings() map[string]interface{} { - return map[string]interface{}{ - "autoindent": true, - "autosave": false, - "basename": false, - "colorcolumn": float64(0), - "colorscheme": "default", - "cursorline": true, - "eofnewline": false, - "fastdirty": true, - "fileformat": "unix", - "hidehelp": false, - "ignorecase": false, - "indentchar": " ", - "infobar": true, - "keepautoindent": false, - "keymenu": false, - "matchbrace": false, - "matchbraceleft": false, - "mouse": true, - "pluginchannels": []string{"https://raw.githubusercontent.com/micro-editor/plugin-channel/master/channel.json"}, - "pluginrepos": []string{}, - "rmtrailingws": false, - "ruler": true, - "savecursor": false, - "savehistory": true, - "saveundo": false, - "scrollbar": false, - "scrollmargin": float64(3), - "scrollspeed": float64(2), - "softwrap": false, - "smartpaste": true, - "splitbottom": true, - "splitright": true, - "statusline": true, - "sucmd": "sudo", - "syntax": true, - "tabmovement": false, - "tabsize": float64(4), - "tabstospaces": false, - "termtitle": false, - "useprimary": true, - } + common := DefaultCommonSettings() + common["colorscheme"] = "default" + common["infobar"] = true + common["keymenu"] = false + common["mouse"] = true + common["pluginchannels"] = []string{"https://raw.githubusercontent.com/micro-editor/plugin-channel/master/channel.json"} + common["pluginrepos"] = []string{} + common["savehistory"] = true + common["sucmd"] = "sudo" + common["termtitle"] = false + return common } // DefaultLocalSettings returns the default local settings // Note that filetype is a local only option func DefaultLocalSettings() map[string]interface{} { - return map[string]interface{}{ - "autoindent": true, - "autosave": false, - "basename": false, - "colorcolumn": float64(0), - "cursorline": true, - "eofnewline": false, - "fastdirty": true, - "fileformat": "unix", - "filetype": "Unknown", - "hidehelp": false, - "ignorecase": false, - "indentchar": " ", - "keepautoindent": false, - "matchbrace": false, - "matchbraceleft": false, - "rmtrailingws": false, - "ruler": true, - "savecursor": false, - "saveundo": false, - "scrollbar": false, - "scrollmargin": float64(3), - "scrollspeed": float64(2), - "softwrap": false, - "smartpaste": true, - "splitbottom": true, - "splitright": true, - "statusline": true, - "syntax": true, - "tabmovement": false, - "tabsize": float64(4), - "tabstospaces": false, - "useprimary": true, - } + common := DefaultCommonSettings() + common["filetype"] = "Unknown" + return common } +// TODO: everything else + // SetOption attempts to set the given option to the value // By default it will set the option as global, but if the option // is local only it will set the local version // Use setlocal to force an option to be set locally -func SetOption(option, value string) error { - if _, ok := globalSettings[option]; !ok { - if _, ok := CurView().Buf.Settings[option]; !ok { - return errors.New("Invalid option") - } - SetLocalOption(option, value, CurView()) - return nil - } - - var nativeValue interface{} - - kind := reflect.TypeOf(globalSettings[option]).Kind() - if kind == reflect.Bool { - b, err := ParseBool(value) - if err != nil { - return errors.New("Invalid value") - } - nativeValue = b - } else if kind == reflect.String { - nativeValue = value - } else if kind == reflect.Float64 { - i, err := strconv.Atoi(value) - if err != nil { - return errors.New("Invalid value") - } - nativeValue = float64(i) - } else { - return errors.New("Option has unsupported value type") - } - - if err := optionIsValid(option, nativeValue); err != nil { - return err - } - - globalSettings[option] = nativeValue - - if option == "colorscheme" { - // LoadSyntaxFiles() - InitColorscheme() - for _, tab := range tabs { - for _, view := range tab.Views { - view.Buf.UpdateRules() - } - } - } - - if option == "infobar" || option == "keymenu" { - for _, tab := range tabs { - tab.Resize() - } - } - - if option == "mouse" { - if !nativeValue.(bool) { - screen.DisableMouse() - } else { - screen.EnableMouse() - } - } - - if len(tabs) != 0 { - if _, ok := CurView().Buf.Settings[option]; ok { - for _, tab := range tabs { - for _, view := range tab.Views { - SetLocalOption(option, value, view) - } - } - } - } - - return nil -} - -// SetLocalOption sets the local version of this option -func SetLocalOption(option, value string, view *View) error { - buf := view.Buf - if _, ok := buf.Settings[option]; !ok { - return errors.New("Invalid option") - } - - var nativeValue interface{} - - kind := reflect.TypeOf(buf.Settings[option]).Kind() - if kind == reflect.Bool { - b, err := ParseBool(value) - if err != nil { - return errors.New("Invalid value") - } - nativeValue = b - } else if kind == reflect.String { - nativeValue = value - } else if kind == reflect.Float64 { - i, err := strconv.Atoi(value) - if err != nil { - return errors.New("Invalid value") - } - nativeValue = float64(i) - } else { - return errors.New("Option has unsupported value type") - } - - if err := optionIsValid(option, nativeValue); err != nil { - return err - } - - if option == "fastdirty" { - // If it is being turned off, we have to hash every open buffer - var empty [md5.Size]byte - var wg sync.WaitGroup - - for _, tab := range tabs { - for _, v := range tab.Views { - if !nativeValue.(bool) { - if v.Buf.origHash == empty { - wg.Add(1) - - go func(b *Buffer) { // calculate md5 hash of the file - defer wg.Done() - - if file, e := os.Open(b.AbsPath); e == nil { - defer file.Close() - - h := md5.New() - - if _, e = io.Copy(h, file); e == nil { - h.Sum(b.origHash[:0]) - } - } - }(v.Buf) - } - } else { - v.Buf.IsModified = v.Buf.Modified() - } - } - } - - wg.Wait() - } - - buf.Settings[option] = nativeValue - - if option == "statusline" { - view.ToggleStatusLine() - } - - if option == "filetype" { - // LoadSyntaxFiles() - InitColorscheme() - buf.UpdateRules() - } - - if option == "fileformat" { - buf.IsModified = true - } - - if option == "syntax" { - if !nativeValue.(bool) { - buf.ClearMatches() - } else { - if buf.highlighter != nil { - buf.highlighter.HighlightStates(buf) - } - } - } - - return nil -} - -// SetOptionAndSettings sets the given option and saves the option setting to the settings config file -func SetOptionAndSettings(option, value string) { - filename := configDir + "/settings.json" - - err := SetOption(option, value) - - if err != nil { - messenger.Error(err.Error()) - return - } - - err = WriteSettings(filename) - if err != nil { - messenger.Error("Error writing to settings.json: " + err.Error()) - return - } -} +// func SetOption(option, value string) error { +// if _, ok := globalSettings[option]; !ok { +// if _, ok := CurView().Buf.Settings[option]; !ok { +// return errors.New("Invalid option") +// } +// SetLocalOption(option, value, CurView()) +// return nil +// } +// +// var nativeValue interface{} +// +// kind := reflect.TypeOf(globalSettings[option]).Kind() +// if kind == reflect.Bool { +// b, err := ParseBool(value) +// if err != nil { +// return errors.New("Invalid value") +// } +// nativeValue = b +// } else if kind == reflect.String { +// nativeValue = value +// } else if kind == reflect.Float64 { +// i, err := strconv.Atoi(value) +// if err != nil { +// return errors.New("Invalid value") +// } +// nativeValue = float64(i) +// } else { +// return errors.New("Option has unsupported value type") +// } +// +// if err := optionIsValid(option, nativeValue); err != nil { +// return err +// } +// +// globalSettings[option] = nativeValue +// +// if option == "colorscheme" { +// // LoadSyntaxFiles() +// InitColorscheme() +// for _, tab := range tabs { +// for _, view := range tab.Views { +// view.Buf.UpdateRules() +// } +// } +// } +// +// if option == "infobar" || option == "keymenu" { +// for _, tab := range tabs { +// tab.Resize() +// } +// } +// +// if option == "mouse" { +// if !nativeValue.(bool) { +// screen.DisableMouse() +// } else { +// screen.EnableMouse() +// } +// } +// +// if len(tabs) != 0 { +// if _, ok := CurView().Buf.Settings[option]; ok { +// for _, tab := range tabs { +// for _, view := range tab.Views { +// SetLocalOption(option, value, view) +// } +// } +// } +// } +// +// return nil +// } +// +// // SetLocalOption sets the local version of this option +// func SetLocalOption(option, value string, view *View) error { +// buf := view.Buf +// if _, ok := buf.Settings[option]; !ok { +// return errors.New("Invalid option") +// } +// +// var nativeValue interface{} +// +// kind := reflect.TypeOf(buf.Settings[option]).Kind() +// if kind == reflect.Bool { +// b, err := ParseBool(value) +// if err != nil { +// return errors.New("Invalid value") +// } +// nativeValue = b +// } else if kind == reflect.String { +// nativeValue = value +// } else if kind == reflect.Float64 { +// i, err := strconv.Atoi(value) +// if err != nil { +// return errors.New("Invalid value") +// } +// nativeValue = float64(i) +// } else { +// return errors.New("Option has unsupported value type") +// } +// +// if err := optionIsValid(option, nativeValue); err != nil { +// return err +// } +// +// if option == "fastdirty" { +// // If it is being turned off, we have to hash every open buffer +// var empty [md5.Size]byte +// var wg sync.WaitGroup +// +// for _, tab := range tabs { +// for _, v := range tab.Views { +// if !nativeValue.(bool) { +// if v.Buf.origHash == empty { +// wg.Add(1) +// +// go func(b *Buffer) { // calculate md5 hash of the file +// defer wg.Done() +// +// if file, e := os.Open(b.AbsPath); e == nil { +// defer file.Close() +// +// h := md5.New() +// +// if _, e = io.Copy(h, file); e == nil { +// h.Sum(b.origHash[:0]) +// } +// } +// }(v.Buf) +// } +// } else { +// v.Buf.IsModified = v.Buf.Modified() +// } +// } +// } +// +// wg.Wait() +// } +// +// buf.Settings[option] = nativeValue +// +// if option == "statusline" { +// view.ToggleStatusLine() +// } +// +// if option == "filetype" { +// // LoadSyntaxFiles() +// InitColorscheme() +// buf.UpdateRules() +// } +// +// if option == "fileformat" { +// buf.IsModified = true +// } +// +// if option == "syntax" { +// if !nativeValue.(bool) { +// buf.ClearMatches() +// } else { +// if buf.highlighter != nil { +// buf.highlighter.HighlightStates(buf) +// } +// } +// } +// +// return nil +// } +// +// // SetOptionAndSettings sets the given option and saves the option setting to the settings config file +// func SetOptionAndSettings(option, value string) { +// filename := configDir + "/settings.json" +// +// err := SetOption(option, value) +// +// if err != nil { +// messenger.Error(err.Error()) +// return +// } +// +// err = WriteSettings(filename) +// if err != nil { +// messenger.Error("Error writing to settings.json: " + err.Error()) +// return +// } +// } func optionIsValid(option string, value interface{}) error { if validator, ok := optionValidators[option]; ok { diff --git a/cmd/micro/shell.go b/cmd/micro/shell.go deleted file mode 100644 index 3e9e4aca..00000000 --- a/cmd/micro/shell.go +++ /dev/null @@ -1,129 +0,0 @@ -package main - -import ( - "bytes" - "io" - "os" - "os/exec" - "os/signal" - "strings" - - "github.com/zyedidia/micro/cmd/micro/shellwords" -) - -// ExecCommand executes a command using exec -// It returns any output/errors -func ExecCommand(name string, arg ...string) (string, error) { - var err error - cmd := exec.Command(name, arg...) - outputBytes := &bytes.Buffer{} - cmd.Stdout = outputBytes - cmd.Stderr = outputBytes - err = cmd.Start() - if err != nil { - return "", err - } - err = cmd.Wait() // wait for command to finish - outstring := outputBytes.String() - return outstring, err -} - -// RunShellCommand executes a shell command and returns the output/error -func RunShellCommand(input string) (string, error) { - args, err := shellwords.Split(input) - if err != nil { - return "", err - } - inputCmd := args[0] - - return ExecCommand(inputCmd, args[1:]...) -} - -func RunBackgroundShell(input string) { - args, err := shellwords.Split(input) - if err != nil { - messenger.Error(err) - return - } - inputCmd := args[0] - messenger.Message("Running...") - go func() { - output, err := RunShellCommand(input) - totalLines := strings.Split(output, "\n") - - if len(totalLines) < 3 { - if err == nil { - messenger.Message(inputCmd, " exited without error") - } else { - messenger.Message(inputCmd, " exited with error: ", err, ": ", output) - } - } else { - messenger.Message(output) - } - // We have to make sure to redraw - RedrawAll() - }() -} - -func RunInteractiveShell(input string, wait bool, getOutput bool) (string, error) { - args, err := shellwords.Split(input) - if err != nil { - return "", err - } - inputCmd := args[0] - - // Shut down the screen because we're going to interact directly with the shell - screen.Fini() - screen = nil - - args = args[1:] - - // Set up everything for the command - outputBytes := &bytes.Buffer{} - cmd := exec.Command(inputCmd, args...) - cmd.Stdin = os.Stdin - if getOutput { - cmd.Stdout = io.MultiWriter(os.Stdout, outputBytes) - } else { - 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() { - for range c { - cmd.Process.Kill() - } - }() - - cmd.Start() - err = cmd.Wait() - - output := outputBytes.String() - - if 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() - - return output, err -} - -// 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, waitToFinish bool) string { - if !openTerm { - RunBackgroundShell(input) - return "" - } else { - output, _ := RunInteractiveShell(input, waitToFinish, false) - return output - } -} diff --git a/cmd/micro/shell_supported.go b/cmd/micro/shell_supported.go deleted file mode 100644 index 215e1992..00000000 --- a/cmd/micro/shell_supported.go +++ /dev/null @@ -1,18 +0,0 @@ -// +build linux darwin dragonfly openbsd_amd64 freebsd - -package main - -import ( - "github.com/zyedidia/micro/cmd/micro/shellwords" -) - -const TermEmuSupported = true - -func RunTermEmulator(input string, wait bool, getOutput bool, callback string) error { - args, err := shellwords.Split(input) - if err != nil { - return err - } - err = CurView().StartTerminal(args, wait, getOutput, callback) - return err -} diff --git a/cmd/micro/shell_unsupported.go b/cmd/micro/shell_unsupported.go deleted file mode 100644 index 4d078457..00000000 --- a/cmd/micro/shell_unsupported.go +++ /dev/null @@ -1,11 +0,0 @@ -// +build !linux,!darwin,!freebsd,!dragonfly,!openbsd_amd64 - -package main - -import "errors" - -const TermEmuSupported = false - -func RunTermEmulator(input string, wait bool, getOutput bool) error { - return errors.New("Unsupported operating system") -} diff --git a/cmd/micro/split_tree.go b/cmd/micro/split_tree.go deleted file mode 100644 index e34c37f1..00000000 --- a/cmd/micro/split_tree.go +++ /dev/null @@ -1,317 +0,0 @@ -package main - -// SplitType specifies whether a split is horizontal or vertical -type SplitType bool - -const ( - // VerticalSplit type - VerticalSplit = false - // HorizontalSplit type - HorizontalSplit = true -) - -// A Node on the split tree -type Node interface { - VSplit(buf *Buffer, splitIndex int) - HSplit(buf *Buffer, splitIndex int) - String() string -} - -// A LeafNode is an actual split so it contains a view -type LeafNode struct { - view *View - - parent *SplitTree -} - -// NewLeafNode returns a new leaf node containing the given view -func NewLeafNode(v *View, parent *SplitTree) *LeafNode { - n := new(LeafNode) - n.view = v - n.view.splitNode = n - n.parent = parent - return n -} - -// A SplitTree is a Node itself and it contains other nodes -type SplitTree struct { - kind SplitType - - parent *SplitTree - children []Node - - x int - y int - - width int - height int - lockWidth bool - lockHeight bool - - tabNum int -} - -// VSplit creates a vertical split -func (l *LeafNode) VSplit(buf *Buffer, splitIndex int) { - if splitIndex < 0 { - splitIndex = 0 - } - - tab := tabs[l.parent.tabNum] - if l.parent.kind == VerticalSplit { - if splitIndex > len(l.parent.children) { - splitIndex = len(l.parent.children) - } - - newView := NewView(buf) - newView.TabNum = l.parent.tabNum - - l.parent.children = append(l.parent.children, nil) - copy(l.parent.children[splitIndex+1:], l.parent.children[splitIndex:]) - l.parent.children[splitIndex] = NewLeafNode(newView, l.parent) - - tab.Views = append(tab.Views, nil) - copy(tab.Views[splitIndex+1:], tab.Views[splitIndex:]) - tab.Views[splitIndex] = newView - - tab.CurView = splitIndex - } else { - if splitIndex > 1 { - splitIndex = 1 - } - - s := new(SplitTree) - s.kind = VerticalSplit - s.parent = l.parent - s.tabNum = l.parent.tabNum - newView := NewView(buf) - newView.TabNum = l.parent.tabNum - if splitIndex == 1 { - s.children = []Node{l, NewLeafNode(newView, s)} - } else { - s.children = []Node{NewLeafNode(newView, s), l} - } - l.parent.children[search(l.parent.children, l)] = s - l.parent = s - - tab.Views = append(tab.Views, nil) - copy(tab.Views[splitIndex+1:], tab.Views[splitIndex:]) - tab.Views[splitIndex] = newView - - tab.CurView = splitIndex - } - - tab.Resize() -} - -// HSplit creates a horizontal split -func (l *LeafNode) HSplit(buf *Buffer, splitIndex int) { - if splitIndex < 0 { - splitIndex = 0 - } - - tab := tabs[l.parent.tabNum] - if l.parent.kind == HorizontalSplit { - if splitIndex > len(l.parent.children) { - splitIndex = len(l.parent.children) - } - - newView := NewView(buf) - newView.TabNum = l.parent.tabNum - - l.parent.children = append(l.parent.children, nil) - copy(l.parent.children[splitIndex+1:], l.parent.children[splitIndex:]) - l.parent.children[splitIndex] = NewLeafNode(newView, l.parent) - - tab.Views = append(tab.Views, nil) - copy(tab.Views[splitIndex+1:], tab.Views[splitIndex:]) - tab.Views[splitIndex] = newView - - tab.CurView = splitIndex - } else { - if splitIndex > 1 { - splitIndex = 1 - } - - s := new(SplitTree) - s.kind = HorizontalSplit - s.tabNum = l.parent.tabNum - s.parent = l.parent - newView := NewView(buf) - newView.TabNum = l.parent.tabNum - newView.Num = len(tab.Views) - if splitIndex == 1 { - s.children = []Node{l, NewLeafNode(newView, s)} - } else { - s.children = []Node{NewLeafNode(newView, s), l} - } - l.parent.children[search(l.parent.children, l)] = s - l.parent = s - - tab.Views = append(tab.Views, nil) - copy(tab.Views[splitIndex+1:], tab.Views[splitIndex:]) - tab.Views[splitIndex] = newView - - tab.CurView = splitIndex - } - - tab.Resize() -} - -// Delete deletes a split -func (l *LeafNode) Delete() { - i := search(l.parent.children, l) - - copy(l.parent.children[i:], l.parent.children[i+1:]) - l.parent.children[len(l.parent.children)-1] = nil - l.parent.children = l.parent.children[:len(l.parent.children)-1] - - tab := tabs[l.parent.tabNum] - j := findView(tab.Views, l.view) - copy(tab.Views[j:], tab.Views[j+1:]) - tab.Views[len(tab.Views)-1] = nil // or the zero value of T - tab.Views = tab.Views[:len(tab.Views)-1] - - for i, v := range tab.Views { - v.Num = i - } - if tab.CurView > 0 { - tab.CurView-- - } -} - -// Cleanup rearranges all the parents after a split has been deleted -func (s *SplitTree) Cleanup() { - for i, node := range s.children { - if n, ok := node.(*SplitTree); ok { - if len(n.children) == 1 { - if child, ok := n.children[0].(*LeafNode); ok { - s.children[i] = child - child.parent = s - continue - } - } - n.Cleanup() - } - } -} - -// ResizeSplits resizes all the splits correctly -func (s *SplitTree) ResizeSplits() { - lockedWidth := 0 - lockedHeight := 0 - lockedChildren := 0 - for _, node := range s.children { - if n, ok := node.(*LeafNode); ok { - if s.kind == VerticalSplit { - if n.view.LockWidth { - lockedWidth += n.view.Width - lockedChildren++ - } - } else { - if n.view.LockHeight { - lockedHeight += n.view.Height - lockedChildren++ - } - } - } else if n, ok := node.(*SplitTree); ok { - if s.kind == VerticalSplit { - if n.lockWidth { - lockedWidth += n.width - lockedChildren++ - } - } else { - if n.lockHeight { - lockedHeight += n.height - lockedChildren++ - } - } - } - } - x, y := 0, 0 - for _, node := range s.children { - if n, ok := node.(*LeafNode); ok { - if s.kind == VerticalSplit { - if !n.view.LockWidth { - n.view.Width = (s.width - lockedWidth) / (len(s.children) - lockedChildren) - } - n.view.Height = s.height - - n.view.x = s.x + x - n.view.y = s.y - x += n.view.Width - } else { - if !n.view.LockHeight { - n.view.Height = (s.height - lockedHeight) / (len(s.children) - lockedChildren) - } - n.view.Width = s.width - - n.view.y = s.y + y - n.view.x = s.x - y += n.view.Height - } - if n.view.Buf.Settings["statusline"].(bool) { - n.view.Height-- - } - - n.view.ToggleTabbar() - } else if n, ok := node.(*SplitTree); ok { - if s.kind == VerticalSplit { - if !n.lockWidth { - n.width = (s.width - lockedWidth) / (len(s.children) - lockedChildren) - } - n.height = s.height - - n.x = s.x + x - n.y = s.y - x += n.width - } else { - if !n.lockHeight { - n.height = (s.height - lockedHeight) / (len(s.children) - lockedChildren) - } - n.width = s.width - - n.y = s.y + y - n.x = s.x - y += n.height - } - n.ResizeSplits() - } - } -} - -func (l *LeafNode) String() string { - return l.view.Buf.GetName() -} - -func search(haystack []Node, needle Node) int { - for i, x := range haystack { - if x == needle { - return i - } - } - return 0 -} - -func findView(haystack []*View, needle *View) int { - for i, x := range haystack { - if x == needle { - return i - } - } - return 0 -} - -// VSplit is here just to make SplitTree fit the Node interface -func (s *SplitTree) VSplit(buf *Buffer, splitIndex int) {} - -// HSplit is here just to make SplitTree fit the Node interface -func (s *SplitTree) HSplit(buf *Buffer, splitIndex int) {} - -func (s *SplitTree) String() string { - str := "[" - for _, child := range s.children { - str += child.String() + ", " - } - return str + "]" -} diff --git a/cmd/micro/stack.go b/cmd/micro/stack.go index 070bd769..bbb6b971 100644 --- a/cmd/micro/stack.go +++ b/cmd/micro/stack.go @@ -1,7 +1,7 @@ package main -// Stack is a simple implementation of a LIFO stack for text events -type Stack struct { +// TEStack is a simple implementation of a LIFO stack for text events +type TEStack struct { Top *Element Size int } @@ -13,19 +13,19 @@ type Element struct { } // Len returns the stack's length -func (s *Stack) Len() int { +func (s *TEStack) Len() int { return s.Size } // Push a new element onto the stack -func (s *Stack) Push(value *TextEvent) { +func (s *TEStack) Push(value *TextEvent) { s.Top = &Element{value, s.Top} s.Size++ } // Pop removes the top element from the stack and returns its value // If the stack is empty, return nil -func (s *Stack) Pop() (value *TextEvent) { +func (s *TEStack) Pop() (value *TextEvent) { if s.Size > 0 { value, s.Top = s.Top.Value, s.Top.Next s.Size-- @@ -35,7 +35,7 @@ func (s *Stack) Pop() (value *TextEvent) { } // Peek returns the top element of the stack without removing it -func (s *Stack) Peek() *TextEvent { +func (s *TEStack) Peek() *TextEvent { if s.Size > 0 { return s.Top.Value } diff --git a/cmd/micro/stack_test.go b/cmd/micro/stack_test.go new file mode 100644 index 00000000..51e4aa76 --- /dev/null +++ b/cmd/micro/stack_test.go @@ -0,0 +1,35 @@ +package main + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestStack(t *testing.T) { + s := new(TEStack) + e1 := &TextEvent{ + EventType: TextEventReplace, + Time: time.Now(), + } + e2 := &TextEvent{ + EventType: TextEventInsert, + Time: time.Now(), + } + s.Push(e1) + s.Push(e2) + + p := s.Peek() + assert.Equal(t, p.EventType, TextEventInsert) + p = s.Pop() + assert.Equal(t, p.EventType, TextEventInsert) + p = s.Peek() + assert.Equal(t, p.EventType, TextEventReplace) + p = s.Pop() + assert.Equal(t, p.EventType, TextEventReplace) + p = s.Pop() + assert.Nil(t, p) + p = s.Peek() + assert.Nil(t, p) +} diff --git a/cmd/micro/statusline.go b/cmd/micro/statusline.go index d346a033..e9062985 100644 --- a/cmd/micro/statusline.go +++ b/cmd/micro/statusline.go @@ -1,98 +1,116 @@ package main import ( + "bytes" + "fmt" "path" + "regexp" "strconv" + "unicode/utf8" ) -// Statusline represents the information line at the bottom -// of each view +// StatusLine represents the information line at the bottom +// of each window // It gives information such as filename, whether the file has been // modified, filetype, cursor location -type Statusline struct { - view *View +type StatusLine struct { + FormatLeft string + FormatRight string + Info map[string]func(*Buffer) string + + win *Window } +// TODO: plugin modify status line formatter + +// NewStatusLine returns a statusline bound to a window +func NewStatusLine(win *Window) *StatusLine { + s := new(StatusLine) + // s.FormatLeft = "$(filename) $(modified)($(line),$(col)) $(opt:filetype) $(opt:fileformat)" + s.FormatLeft = "$(filename) $(modified)(line,col) $(opt:filetype) $(opt:fileformat)" + s.FormatRight = "$(bind:ToggleKeyMenu): show bindings, $(bind:ToggleHelp): open help" + s.Info = map[string]func(*Buffer) string{ + "filename": func(b *Buffer) string { + if b.Settings["basename"].(bool) { + return path.Base(b.GetName()) + } + return b.GetName() + }, + "line": func(b *Buffer) string { + return strconv.Itoa(b.GetActiveCursor().Y) + }, + "col": func(b *Buffer) string { + return strconv.Itoa(b.GetActiveCursor().X) + }, + "modified": func(b *Buffer) string { + if b.Modified() { + return "+ " + } + return "" + }, + } + s.win = win + return s +} + +// FindOpt finds a given option in the current buffer's settings +func (s *StatusLine) FindOpt(opt string) interface{} { + if val, ok := s.win.Buf.Settings[opt]; ok { + return val + } + return "null" +} + +var formatParser = regexp.MustCompile(`\$\(.+?\)`) + // Display draws the statusline to the screen -func (sline *Statusline) Display() { - if messenger.hasPrompt && !GetGlobalOption("infobar").(bool) { - return - } +func (s *StatusLine) Display() { + // TODO: don't display if infobar off and has message + // if !GetGlobalOption("infobar").(bool) { + // return + // } - // We'll draw the line at the lowest line in the view - y := sline.view.Height + sline.view.y + // We'll draw the line at the lowest line in the window + y := s.win.Height + s.win.Y - file := sline.view.Buf.GetName() - if sline.view.Buf.Settings["basename"].(bool) { - file = path.Base(file) - } - - // If the buffer is dirty (has been modified) write a little '+' - if sline.view.Buf.Modified() { - file += " +" - } - - // Add one to cursor.x and cursor.y because (0,0) is the top left, - // but users will be used to (1,1) (first line,first column) - // We use GetVisualX() here because otherwise we get the column number in runes - // so a '\t' is only 1, when it should be tabSize - columnNum := strconv.Itoa(sline.view.Cursor.GetVisualX() + 1) - lineNum := strconv.Itoa(sline.view.Cursor.Y + 1) - - file += " (" + lineNum + "," + columnNum + ")" - - // Add the filetype - file += " " + sline.view.Buf.FileType() - - file += " " + sline.view.Buf.Settings["fileformat"].(string) - - rightText := "" - if !sline.view.Buf.Settings["hidehelp"].(bool) { - if len(kmenuBinding) > 0 { - if globalSettings["keymenu"].(bool) { - rightText += kmenuBinding + ": hide bindings" - } else { - rightText += kmenuBinding + ": show bindings" - } + formatter := func(match []byte) []byte { + name := match[2 : len(match)-1] + if bytes.HasPrefix(name, []byte("opt")) { + option := name[4:] + return []byte(fmt.Sprint(s.FindOpt(string(option)))) + } else if bytes.HasPrefix(name, []byte("bind")) { + // TODO: status line bindings + return []byte("TODO") + } else { + return []byte(s.Info[string(name)](s.win.Buf)) } - if len(helpBinding) > 0 { - if len(kmenuBinding) > 0 { - rightText += ", " - } - if sline.view.Type == vtHelp { - rightText += helpBinding + ": close help" - } else { - rightText += helpBinding + ": open help" - } - } - rightText += " " } + leftText := []byte(s.FormatLeft) + leftText = formatParser.ReplaceAllFunc([]byte(s.FormatLeft), formatter) + rightText := []byte(s.FormatRight) + rightText = formatParser.ReplaceAllFunc([]byte(s.FormatRight), formatter) + statusLineStyle := defStyle.Reverse(true) if style, ok := colorscheme["statusline"]; ok { statusLineStyle = style } - // Maybe there is a unicode filename? - fileRunes := []rune(file) + leftLen := utf8.RuneCount(leftText) + rightLen := utf8.RuneCount(rightText) - if sline.view.Type == vtTerm { - fileRunes = []rune(sline.view.term.title) - rightText = "" - } - - 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(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(viewX+x, y, []rune(rightText)[x-sline.view.Width+len(rightText)], nil, statusLineStyle) + winX := s.win.X + for x := 0; x < s.win.Width; x++ { + if x < leftLen { + r, size := utf8.DecodeRune(leftText) + leftText = leftText[size:] + screen.SetContent(winX+x, y, r, nil, statusLineStyle) + } else if x >= s.win.Width-rightLen && x < rightLen+s.win.Width-rightLen { + r, size := utf8.DecodeRune(rightText) + rightText = rightText[size:] + screen.SetContent(winX+x, y, r, nil, statusLineStyle) } else { - screen.SetContent(viewX+x, y, ' ', nil, statusLineStyle) + screen.SetContent(winX+x, y, ' ', nil, statusLineStyle) } } } diff --git a/cmd/micro/tab.go b/cmd/micro/tab.go deleted file mode 100644 index 88c260a6..00000000 --- a/cmd/micro/tab.go +++ /dev/null @@ -1,293 +0,0 @@ -package main - -import ( - "path/filepath" - "sort" - - "github.com/zyedidia/tcell" -) - -var tabBarOffset int - -// A Tab holds an array of views and a splitTree to determine how the -// views should be arranged -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 - - tree *SplitTree -} - -// 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) - t.Views[0].Num = 0 - - t.tree = new(SplitTree) - t.tree.kind = VerticalSplit - t.tree.children = []Node{NewLeafNode(t.Views[0], t.tree)} - - w, h := screen.Size() - t.tree.width = w - t.tree.height = h - - if globalSettings["infobar"].(bool) { - t.tree.height-- - } - if globalSettings["keymenu"].(bool) { - t.tree.height -= 2 - } - - t.Resize() - - return t -} - -// SetNum sets all this tab's views to have the correct tab number -func (t *Tab) SetNum(num int) { - t.tree.tabNum = num - for _, v := range t.Views { - v.TabNum = num - } -} - -// Cleanup cleans up the tree (for example if views have closed) -func (t *Tab) Cleanup() { - t.tree.Cleanup() -} - -// Resize handles a resize event from the terminal and resizes -// all child views correctly -func (t *Tab) Resize() { - w, h := screen.Size() - t.tree.width = w - t.tree.height = h - - if globalSettings["infobar"].(bool) { - t.tree.height-- - } - if globalSettings["keymenu"].(bool) { - t.tree.height -= 2 - } - - t.tree.ResizeSplits() - - for i, v := range t.Views { - v.Num = i - if v.Type == vtTerm { - v.term.Resize(v.Width, v.Height) - } - } -} - -// CurView returns the current view -func CurView() *View { - curTab := tabs[curTab] - 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 occurred 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) - unique := make(map[string]int) - - for _, t := range tabs { - unique[filepath.Base(t.Views[t.CurView].Buf.GetName())]++ - } - - for i, t := range tabs { - buf := t.Views[t.CurView].Buf - name := filepath.Base(buf.GetName()) - - if i == curTab { - str += "[" - } else { - str += " " - } - if unique[name] == 1 { - str += name - } else { - str += buf.GetName() - } - if buf.Modified() { - str += " +" - } - if i == curTab { - str += "]" - } else { - str += " " - } - str += " " - - indicies[Count(str)-2] = i + 1 - } - 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 - } - - 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 { - return false - } - str, indicies := TabbarString() - if x+tabBarOffset >= 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+tabBarOffset <= k { - tabnum = indicies[k] - 1 - break - } - } - curTab = tabnum - return true - } - } - - return false -} - -// DisplayTabs displays the tabbar at the top of the editor if there are multiple tabs -func DisplayTabs() { - if len(tabs) <= 1 { - return - } - - str, indicies := 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() - tooWide := (w < len(fileRunes)) - - // if the entire tab-bar is longer than the screen is wide, - // then it should be truncated appropriately to keep the - // active tab visible on the UI. - if tooWide == true { - // first we have to work out where the selected tab is - // out of the total length of the tab bar. this is done - // by extracting the hit-areas from the indicies map - // that was constructed by `TabbarString()` - var keys []int - for offset := range indicies { - keys = append(keys, offset) - } - // sort them to be in ascending order so that values will - // correctly reflect the displayed ordering of the tabs - sort.Ints(keys) - // record the offset of each tab and the previous tab so - // we can find the position of the tab's hit-box. - previousTabOffset := 0 - currentTabOffset := 0 - for _, k := range keys { - tabIndex := indicies[k] - 1 - if tabIndex == curTab { - currentTabOffset = k - break - } - // this is +2 because there are two padding spaces that aren't accounted - // for in the display. please note that this is for cosmetic purposes only. - previousTabOffset = k + 2 - } - // get the width of the hitbox of the active tab, from there calculate the offsets - // to the left and right of it to approximately center it on the tab bar display. - centeringOffset := (w - (currentTabOffset - previousTabOffset)) - leftBuffer := previousTabOffset - (centeringOffset / 2) - rightBuffer := currentTabOffset + (centeringOffset / 2) - - // check to make sure we haven't overshot the bounds of the string, - // if we have, then take that remainder and put it on the left side - overshotRight := rightBuffer - len(fileRunes) - if overshotRight > 0 { - leftBuffer = leftBuffer + overshotRight - } - - overshotLeft := leftBuffer - 0 - if overshotLeft < 0 { - leftBuffer = 0 - rightBuffer = leftBuffer + (w - 1) - } else { - rightBuffer = leftBuffer + (w - 2) - } - - if rightBuffer > len(fileRunes)-1 { - rightBuffer = len(fileRunes) - 1 - } - - // construct a new buffer of text to put the - // newly formatted tab bar text into. - var displayText []rune - - // if the left-side of the tab bar isn't at the start - // of the constructed tab bar text, then show that are - // more tabs to the left by displaying a "+" - if leftBuffer != 0 { - displayText = append(displayText, '+') - } - // copy the runes in from the original tab bar text string - // into the new display buffer - for x := leftBuffer; x < rightBuffer; x++ { - displayText = append(displayText, fileRunes[x]) - } - // if there is more text to the right of the right-most - // column in the tab bar text, then indicate there are more - // tabs to the right by displaying a "+" - if rightBuffer < len(fileRunes)-1 { - displayText = append(displayText, '+') - } - - // now store the offset from zero of the left-most text - // that is being displayed. This is to ensure that when - // clicking on the tab bar, the correct tab gets selected. - tabBarOffset = leftBuffer - - // use the constructed buffer as the display buffer to print - // onscreen. - fileRunes = displayText - } else { - tabBarOffset = 0 - } - - // iterate over the width of the terminal display and for each column, - // write a character into the tab display area with the appropriate style. - 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) - } - } -} diff --git a/cmd/micro/terminal.go b/cmd/micro/terminal.go deleted file mode 100644 index 04ca1242..00000000 --- a/cmd/micro/terminal.go +++ /dev/null @@ -1,228 +0,0 @@ -package main - -import ( - "bytes" - "fmt" - "os" - "os/exec" - "strconv" - "strings" - - "github.com/zyedidia/clipboard" - "github.com/zyedidia/tcell" - "github.com/zyedidia/terminal" -) - -const ( - VTIdle = iota // Waiting for a new command - VTRunning // Currently running a command - VTDone // Finished running a command -) - -// A Terminal holds information for the terminal emulator -type Terminal struct { - state terminal.State - view *View - vtOld ViewType - term *terminal.VT - title string - status int - selection [2]Loc - wait bool - getOutput bool - output *bytes.Buffer - callback string -} - -// HasSelection returns whether this terminal has a valid selection -func (t *Terminal) HasSelection() bool { - return t.selection[0] != t.selection[1] -} - -// GetSelection returns the selected text -func (t *Terminal) GetSelection(width int) string { - start := t.selection[0] - end := t.selection[1] - if start.GreaterThan(end) { - start, end = end, start - } - var ret string - var l Loc - for y := start.Y; y <= end.Y; y++ { - for x := 0; x < width; x++ { - l.X, l.Y = x, y - if l.GreaterEqual(start) && l.LessThan(end) { - c, _, _ := t.state.Cell(x, y) - ret += string(c) - } - } - } - return ret -} - -// Start begins a new command in this terminal with a given view -func (t *Terminal) Start(execCmd []string, view *View, getOutput bool) error { - if len(execCmd) <= 0 { - return nil - } - - cmd := exec.Command(execCmd[0], execCmd[1:]...) - t.output = nil - if getOutput { - t.output = bytes.NewBuffer([]byte{}) - } - term, _, err := terminal.Start(&t.state, cmd, t.output) - if err != nil { - return err - } - t.term = term - t.view = view - t.getOutput = getOutput - t.vtOld = view.Type - t.status = VTRunning - t.title = execCmd[0] + ":" + strconv.Itoa(cmd.Process.Pid) - - go func() { - for { - err := term.Parse() - if err != nil { - fmt.Fprintln(os.Stderr, "[Press enter to close]") - break - } - updateterm <- true - } - closeterm <- view.Num - }() - - return nil -} - -// Resize informs the terminal of a resize event -func (t *Terminal) Resize(width, height int) { - t.term.Resize(width, height) -} - -// HandleEvent handles a tcell event by forwarding it to the terminal emulator -// If the event is a mouse event and the program running in the emulator -// does not have mouse support, the emulator will support selections and -// copy-paste -func (t *Terminal) HandleEvent(event tcell.Event) { - if e, ok := event.(*tcell.EventKey); ok { - if t.status == VTDone { - switch e.Key() { - case tcell.KeyEscape, tcell.KeyCtrlQ, tcell.KeyEnter: - t.Close() - t.view.Type = vtDefault - default: - } - } - if e.Key() == tcell.KeyCtrlC && t.HasSelection() { - clipboard.WriteAll(t.GetSelection(t.view.Width), "clipboard") - messenger.Message("Copied selection to clipboard") - } else if t.status != VTDone { - t.WriteString(event.EscSeq()) - } - } else if e, ok := event.(*tcell.EventMouse); !ok || t.state.Mode(terminal.ModeMouseMask) { - t.WriteString(event.EscSeq()) - } else { - x, y := e.Position() - x -= t.view.x - y += t.view.y - - if e.Buttons() == tcell.Button1 { - if !t.view.mouseReleased { - // drag - t.selection[1].X = x - t.selection[1].Y = y - } else { - t.selection[0].X = x - t.selection[0].Y = y - t.selection[1].X = x - t.selection[1].Y = y - } - - t.view.mouseReleased = false - } else if e.Buttons() == tcell.ButtonNone { - if !t.view.mouseReleased { - t.selection[1].X = x - t.selection[1].Y = y - } - t.view.mouseReleased = true - } - } -} - -// Stop stops execution of the terminal and sets the status -// to VTDone -func (t *Terminal) Stop() { - t.term.File().Close() - t.term.Close() - if t.wait { - t.status = VTDone - } else { - t.Close() - t.view.Type = t.vtOld - } -} - -// Close sets the status to VTIdle indicating that the terminal -// is ready for a new command to execute -func (t *Terminal) Close() { - t.status = VTIdle - // call the lua function that the user has given as a callback - if t.getOutput { - _, err := Call(t.callback, t.output.String()) - if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") { - TermMessage(err) - } - } -} - -// WriteString writes a given string to this terminal's pty -func (t *Terminal) WriteString(str string) { - t.term.File().WriteString(str) -} - -// Display displays this terminal in a view -func (t *Terminal) Display() { - divider := 0 - if t.view.x != 0 { - divider = 1 - dividerStyle := defStyle - if style, ok := colorscheme["divider"]; ok { - dividerStyle = style - } - for i := 0; i < t.view.Height; i++ { - screen.SetContent(t.view.x, t.view.y+i, '|', nil, dividerStyle.Reverse(true)) - } - } - t.state.Lock() - defer t.state.Unlock() - - var l Loc - for y := 0; y < t.view.Height; y++ { - for x := 0; x < t.view.Width; x++ { - l.X, l.Y = x, y - c, f, b := t.state.Cell(x, y) - - fg, bg := int(f), int(b) - if f == terminal.DefaultFG { - fg = int(tcell.ColorDefault) - } - if b == terminal.DefaultBG { - bg = int(tcell.ColorDefault) - } - st := tcell.StyleDefault.Foreground(GetColor256(int(fg))).Background(GetColor256(int(bg))) - - if l.LessThan(t.selection[1]) && l.GreaterEqual(t.selection[0]) || l.LessThan(t.selection[0]) && l.GreaterEqual(t.selection[1]) { - st = st.Reverse(true) - } - - screen.SetContent(t.view.x+x+divider, t.view.y+y, c, nil, st) - } - } - if t.state.CursorVisible() && tabs[curTab].CurView == t.view.Num { - curx, cury := t.state.Cursor() - screen.ShowCursor(curx+t.view.x+divider, cury+t.view.y) - } -} diff --git a/cmd/micro/util.go b/cmd/micro/util.go index 7d457d6e..b4315220 100644 --- a/cmd/micro/util.go +++ b/cmd/micro/util.go @@ -1,45 +1,20 @@ package main import ( + "errors" "os" "os/user" - "path/filepath" - "reflect" - "runtime" - "strconv" + "regexp" "strings" "time" "unicode/utf8" - "github.com/go-errors/errors" - "github.com/mattn/go-runewidth" - "regexp" + runewidth "github.com/mattn/go-runewidth" ) -// Util.go is a collection of utility functions that are used throughout -// the program - -// Count returns the length of a string in runes -// This is exactly equivalent to utf8.RuneCountInString(), just less characters -func Count(s string) int { - return utf8.RuneCountInString(s) -} - -// Convert byte array to rune array -func toRunes(b []byte) []rune { - runes := make([]rune, 0, utf8.RuneCount(b)) - - for len(b) > 0 { - r, size := utf8.DecodeRune(b) - runes = append(runes, r) - - b = b[size:] - } - - return runes -} - -func sliceStart(slc []byte, index int) []byte { +// SliceEnd returns a byte slice where the index is a rune index +// Slices off the start of the slice +func SliceEnd(slc []byte, index int) []byte { len := len(slc) i := 0 totalSize := 0 @@ -56,7 +31,9 @@ func sliceStart(slc []byte, index int) []byte { return slc[totalSize:] } -func sliceEnd(slc []byte, index int) []byte { +// SliceStart returns a byte slice where the index is a rune index +// Slices off the end of the slice +func SliceStart(slc []byte, index int) []byte { len := len(slc) i := 0 totalSize := 0 @@ -73,20 +50,63 @@ func sliceEnd(slc []byte, index int) []byte { return slc[:totalSize] } -// NumOccurrences counts the number of occurrences of a byte in a string -func NumOccurrences(s string, c byte) int { - var n int - for i := 0; i < len(s); i++ { - if s[i] == c { - n++ +// SliceVisualEnd will take a byte slice and slice off the start +// up to a given visual index. If the index is in the middle of a +// rune the number of visual columns into the rune will be returned +func SliceVisualEnd(b []byte, n, tabsize int) ([]byte, int) { + width := 0 + for len(b) > 0 { + r, size := utf8.DecodeRune(b) + + w := 0 + switch r { + case '\t': + ts := tabsize - (width % tabsize) + w = ts + default: + w = runewidth.RuneWidth(r) } + if width+w > n { + return b, n - width + } + width += w + b = b[size:] + } + return b, width +} + +// Abs is a simple absolute value function for ints +func Abs(n int) int { + if n < 0 { + return -n } return n } -// Spaces returns a string with n spaces -func Spaces(n int) string { - return strings.Repeat(" ", n) +// StringWidth returns the visual width of a byte array indexed from 0 to n (rune index) +// with a given tabsize +func StringWidth(b []byte, n, tabsize int) int { + i := 0 + width := 0 + for len(b) > 0 { + r, size := utf8.DecodeRune(b) + b = b[size:] + + switch r { + case '\t': + ts := tabsize - (width % tabsize) + width += ts + default: + width += runewidth.RuneWidth(r) + } + + i++ + + if i == n { + return width + } + } + return width } // Min takes the min of two ints @@ -108,7 +128,6 @@ func Max(a, b int) int { // FSize gets the size of a file func FSize(f *os.File) int64 { fi, _ := f.Stat() - // get the size return fi.Size() } @@ -131,6 +150,7 @@ func IsWhitespace(c rune) bool { // IsStrWhitespace returns true if the given string is all whitespace func IsStrWhitespace(str string) bool { + // Range loop for unicode correctness for _, c := range str { if !IsWhitespace(c) { return false @@ -139,197 +159,12 @@ func IsStrWhitespace(str string) bool { return true } -// Contains returns whether or not a string array contains a given string -func Contains(list []string, a string) bool { - for _, b := range list { - if b == a { - return true - } - } - return false -} - -// Insert makes a simple insert into a string at the given position -func Insert(str string, pos int, value string) string { - return string([]rune(str)[:pos]) + value + string([]rune(str)[pos:]) -} - -// MakeRelative will attempt to make a relative path between path and base -func MakeRelative(path, base string) (string, error) { - if len(path) > 0 { - rel, err := filepath.Rel(base, path) - if err != nil { - return path, err - } - return rel, nil - } - return path, nil -} - -// GetLeadingWhitespace returns the leading whitespace of the given string -func GetLeadingWhitespace(str string) string { - ws := "" - for _, c := range str { - if c == ' ' || c == '\t' { - ws += string(c) - } else { - break - } - } - return ws -} - -// IsSpaces checks if a given string is only spaces -func IsSpaces(str []byte) bool { - for _, c := range str { - if c != ' ' { - return false - } - } - - return true -} - -// IsSpacesOrTabs checks if a given string contains only spaces and tabs -func IsSpacesOrTabs(str string) bool { - for _, c := range str { - if c != ' ' && c != '\t' { - return false - } - } - - return true -} - -// ParseBool is almost exactly like strconv.ParseBool, except it also accepts 'on' and 'off' -// as 'true' and 'false' respectively -func ParseBool(str string) (bool, error) { - if str == "on" { - return true, nil - } - if str == "off" { - return false, nil - } - return strconv.ParseBool(str) -} - -// EscapePath replaces every path separator in a given path with a % -func EscapePath(path string) string { - path = filepath.ToSlash(path) - return strings.Replace(path, "/", "%", -1) -} - -// GetModTime returns the last modification time for a given file -// It also returns a boolean if there was a problem accessing the file -func GetModTime(path string) (time.Time, bool) { - info, err := os.Stat(path) - if err != nil { - return time.Now(), false - } - return info.ModTime(), true -} - -// StringWidth returns the width of a string where tabs count as `tabsize` width -func StringWidth(str string, tabsize int) int { - sw := runewidth.StringWidth(str) - lineIdx := 0 - for _, ch := range str { - switch ch { - case '\t': - ts := tabsize - (lineIdx % tabsize) - sw += ts - lineIdx += ts - case '\n': - lineIdx = 0 - default: - lineIdx++ - } - } - 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, tabsize int) int { - count := 0 - lineIdx := 0 - for _, ch := range str { - var w int - if ch == '\t' { - w = tabsize - (lineIdx % tabsize) - } else { - w = runewidth.RuneWidth(ch) - } - if w > 1 { - count += (w - 1) - } - if ch == '\n' { - lineIdx = 0 - } else { - lineIdx += w - } - } - 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]) -} - -func lcs(a, b string) string { - arunes := []rune(a) - brunes := []rune(b) - - lcs := "" - for i, r := range arunes { - if i >= len(brunes) { - break - } - if r == brunes[i] { - lcs += string(r) - } else { - break - } - } - return lcs -} - -// CommonSubstring gets a common substring among the inputs -func CommonSubstring(arr ...string) string { - commonStr := arr[0] - - for _, str := range arr[1:] { - commonStr = lcs(commonStr, str) - } - - return commonStr -} - -// Abs is a simple absolute value function for ints -func Abs(n int) int { - if n < 0 { - return -n - } - return n -} - -// FuncName returns the full name of a given function object -func FuncName(i interface{}) string { - return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name() -} - -// ShortFuncName returns the name only of a given function object -func ShortFuncName(i interface{}) string { - return strings.TrimPrefix(runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name(), "main.(*View).") -} - +// TODO: consider changing because of snap segfault // ReplaceHome takes a path as input and replaces ~ at the start of the path with the user's // home directory. Does nothing if the path does not start with '~'. -func ReplaceHome(path string) string { +func ReplaceHome(path string) (string, error) { if !strings.HasPrefix(path, "~") { - return path + return path, nil } var userData *user.User @@ -339,23 +174,18 @@ func ReplaceHome(path string) string { if homeString == "~" { userData, err = user.Current() if err != nil { - messenger.Error("Could not find user: ", err) + return "", errors.New("Could not find user: " + err.Error()) } } else { userData, err = user.Lookup(homeString[1:]) if err != nil { - if messenger != nil { - messenger.Error("Could not find user: ", err) - } else { - TermMessage("Could not find user: ", err) - } - return "" + return "", errors.New("Could not find user: " + err.Error()) } } home := userData.HomeDir - return strings.Replace(path, homeString, home, 1) + return strings.Replace(path, homeString, home, 1), nil } // GetPathAndCursorPosition returns a filename without everything following a `:` @@ -375,26 +205,11 @@ func GetPathAndCursorPosition(path string) (string, []string) { return match[1], []string{match[2], "0"} } -func ParseCursorLocation(cursorPositions []string) (Loc, error) { - startpos := Loc{0, 0} - var err error - - // if no positions are available exit early - if cursorPositions == nil { - return startpos, errors.New("No cursor positions were provided.") - } - - startpos.Y, err = strconv.Atoi(cursorPositions[0]) +// GetModTime returns the last modification time for a given file +func GetModTime(path string) (time.Time, error) { + info, err := os.Stat(path) if err != nil { - messenger.Error("Error parsing cursor position: ", err) - } else { - if len(cursorPositions) > 1 { - startpos.X, err = strconv.Atoi(cursorPositions[1]) - if err != nil { - messenger.Error("Error parsing cursor position: ", err) - } - } + return time.Now(), err } - - return startpos, err + return info.ModTime(), nil } diff --git a/cmd/micro/util_test.go b/cmd/micro/util_test.go index 849f52cb..0f900db9 100644 --- a/cmd/micro/util_test.go +++ b/cmd/micro/util_test.go @@ -2,330 +2,32 @@ package main import ( "testing" + + "github.com/stretchr/testify/assert" ) -func TestNumOccurences(t *testing.T) { - var tests = []struct { - inputStr string - inputChar byte - want int - }{ - {"aaaa", 'a', 4}, - {"\trfd\ta", '\t', 2}, - {"∆ƒ\tø ® \t\t", '\t', 3}, - } - for _, test := range tests { - if got := NumOccurrences(test.inputStr, test.inputChar); got != test.want { - t.Errorf("NumOccurences(%s, %c) = %d", test.inputStr, test.inputChar, got) - } - } -} - -func TestSpaces(t *testing.T) { - var tests = []struct { - input int - want string - }{ - {4, " "}, - {0, ""}, - } - for _, test := range tests { - if got := Spaces(test.input); got != test.want { - t.Errorf("Spaces(%d) = \"%s\"", test.input, got) - } - } -} - -func TestIsWordChar(t *testing.T) { - if IsWordChar("t") == false { - t.Errorf("IsWordChar(t) = false") - } - if IsWordChar("T") == false { - t.Errorf("IsWordChar(T) = false") - } - if IsWordChar("5") == false { - t.Errorf("IsWordChar(5) = false") - } - if IsWordChar("_") == false { - t.Errorf("IsWordChar(_) = false") - } - if IsWordChar("ß") == false { - t.Errorf("IsWordChar(ß) = false") - } - if IsWordChar("~") == true { - t.Errorf("IsWordChar(~) = true") - } - if IsWordChar(" ") == true { - t.Errorf("IsWordChar( ) = true") - } - if IsWordChar(")") == true { - t.Errorf("IsWordChar()) = true") - } - if IsWordChar("\n") == true { - t.Errorf("IsWordChar(\n)) = true") - } -} - func TestStringWidth(t *testing.T) { - tabsize := 4 - if w := StringWidth("1\t2", tabsize); w != 5 { - t.Error("StringWidth 1 Failed. Got", w) - } - if w := StringWidth("\t", tabsize); w != 4 { - t.Error("StringWidth 2 Failed. Got", w) - } - if w := StringWidth("1\t", tabsize); w != 4 { - t.Error("StringWidth 3 Failed. Got", w) - } - if w := StringWidth("\t\t", tabsize); w != 8 { - t.Error("StringWidth 4 Failed. Got", w) - } - if w := StringWidth("12\t2\t", tabsize); w != 8 { - t.Error("StringWidth 5 Failed. Got", w) - } + bytes := []byte("\tPot să \tmănânc sticlă și ea nu mă rănește.") + + n := StringWidth(bytes, 23, 4) + assert.Equal(t, 26, n) } -func TestWidthOfLargeRunes(t *testing.T) { - tabsize := 4 - if w := WidthOfLargeRunes("1\t2", tabsize); w != 2 { - t.Error("WidthOfLargeRunes 1 Failed. Got", w) - } - if w := WidthOfLargeRunes("\t", tabsize); w != 3 { - t.Error("WidthOfLargeRunes 2 Failed. Got", w) - } - if w := WidthOfLargeRunes("1\t", tabsize); w != 2 { - t.Error("WidthOfLargeRunes 3 Failed. Got", w) - } - if w := WidthOfLargeRunes("\t\t", tabsize); w != 6 { - t.Error("WidthOfLargeRunes 4 Failed. Got", w) - } - if w := WidthOfLargeRunes("12\t2\t", tabsize); w != 3 { - t.Error("WidthOfLargeRunes 5 Failed. Got", w) - } -} - -func assertEqual(t *testing.T, expected interface{}, result interface{}) { - if expected != result { - t.Fatalf("Expected: %d != Got: %d", expected, result) - } -} - -func assertTrue(t *testing.T, condition bool) { - if !condition { - t.Fatalf("Condition was not true. Got false") - } -} - -func TestGetPathRelativeWithDot(t *testing.T) { - path, cursorPosition := GetPathAndCursorPosition("./myfile:10:5") - - assertEqual(t, path, "./myfile") - assertEqual(t, "10", cursorPosition[0]) - assertEqual(t, "5", cursorPosition[1]) -} -func TestGetPathRelativeWithDotWindows(t *testing.T) { - path, cursorPosition := GetPathAndCursorPosition(".\\myfile:10:5") - - assertEqual(t, path, ".\\myfile") - assertEqual(t, "10", cursorPosition[0]) - assertEqual(t, cursorPosition[1], "5") -} -func TestGetPathRelativeNoDot(t *testing.T) { - path, cursorPosition := GetPathAndCursorPosition("myfile:10:5") - - assertEqual(t, path, "myfile") - assertEqual(t, "10", cursorPosition[0]) - - assertEqual(t, cursorPosition[1], "5") -} -func TestGetPathAbsoluteWindows(t *testing.T) { - path, cursorPosition := GetPathAndCursorPosition("C:\\myfile:10:5") - - assertEqual(t, path, "C:\\myfile") - assertEqual(t, "10", cursorPosition[0]) - - assertEqual(t, cursorPosition[1], "5") - - path, cursorPosition = GetPathAndCursorPosition("C:/myfile:10:5") - - assertEqual(t, path, "C:/myfile") - assertEqual(t, "10", cursorPosition[0]) - assertEqual(t, "5", cursorPosition[1]) -} - -func TestGetPathAbsoluteUnix(t *testing.T) { - path, cursorPosition := GetPathAndCursorPosition("/home/user/myfile:10:5") - - assertEqual(t, path, "/home/user/myfile") - assertEqual(t, "10", cursorPosition[0]) - assertEqual(t, "5", cursorPosition[1]) -} - -func TestGetPathRelativeWithDotWithoutLineAndColumn(t *testing.T) { - path, cursorPosition := GetPathAndCursorPosition("./myfile") - - assertEqual(t, path, "./myfile") - // no cursor position in filename, nil should be returned - assertTrue(t, cursorPosition == nil) -} -func TestGetPathRelativeWithDotWindowsWithoutLineAndColumn(t *testing.T) { - path, cursorPosition := GetPathAndCursorPosition(".\\myfile") - - assertEqual(t, path, ".\\myfile") - assertTrue(t, cursorPosition == nil) - -} -func TestGetPathRelativeNoDotWithoutLineAndColumn(t *testing.T) { - path, cursorPosition := GetPathAndCursorPosition("myfile") - - assertEqual(t, path, "myfile") - assertTrue(t, cursorPosition == nil) - -} -func TestGetPathAbsoluteWindowsWithoutLineAndColumn(t *testing.T) { - path, cursorPosition := GetPathAndCursorPosition("C:\\myfile") - - assertEqual(t, path, "C:\\myfile") - assertTrue(t, cursorPosition == nil) - - path, cursorPosition = GetPathAndCursorPosition("C:/myfile") - - assertEqual(t, path, "C:/myfile") - assertTrue(t, cursorPosition == nil) - -} -func TestGetPathAbsoluteUnixWithoutLineAndColumn(t *testing.T) { - path, cursorPosition := GetPathAndCursorPosition("/home/user/myfile") - - assertEqual(t, path, "/home/user/myfile") - assertTrue(t, cursorPosition == nil) - -} -func TestGetPathSingleLetterFileRelativePath(t *testing.T) { - path, cursorPosition := GetPathAndCursorPosition("a:5:6") - - assertEqual(t, path, "a") - assertEqual(t, "5", cursorPosition[0]) - assertEqual(t, "6", cursorPosition[1]) -} -func TestGetPathSingleLetterFileAbsolutePathWindows(t *testing.T) { - path, cursorPosition := GetPathAndCursorPosition("C:\\a:5:6") - - assertEqual(t, path, "C:\\a") - assertEqual(t, "5", cursorPosition[0]) - assertEqual(t, "6", cursorPosition[1]) - - path, cursorPosition = GetPathAndCursorPosition("C:/a:5:6") - - assertEqual(t, path, "C:/a") - assertEqual(t, "5", cursorPosition[0]) - assertEqual(t, "6", cursorPosition[1]) -} -func TestGetPathSingleLetterFileAbsolutePathUnix(t *testing.T) { - path, cursorPosition := GetPathAndCursorPosition("/home/user/a:5:6") - - assertEqual(t, path, "/home/user/a") - assertEqual(t, "5", cursorPosition[0]) - assertEqual(t, "6", cursorPosition[1]) -} -func TestGetPathSingleLetterFileAbsolutePathWindowsWithoutLineAndColumn(t *testing.T) { - path, cursorPosition := GetPathAndCursorPosition("C:\\a") - - assertEqual(t, path, "C:\\a") - assertTrue(t, cursorPosition == nil) - - path, cursorPosition = GetPathAndCursorPosition("C:/a") - - assertEqual(t, path, "C:/a") - assertTrue(t, cursorPosition == nil) - -} -func TestGetPathSingleLetterFileAbsolutePathUnixWithoutLineAndColumn(t *testing.T) { - path, cursorPosition := GetPathAndCursorPosition("/home/user/a") - - assertEqual(t, path, "/home/user/a") - assertTrue(t, cursorPosition == nil) - -} - -func TestGetPathRelativeWithDotOnlyLine(t *testing.T) { - path, cursorPosition := GetPathAndCursorPosition("./myfile:10") - - assertEqual(t, path, "./myfile") - assertEqual(t, "10", cursorPosition[0]) - assertEqual(t, "0", cursorPosition[1]) -} -func TestGetPathRelativeWithDotWindowsOnlyLine(t *testing.T) { - path, cursorPosition := GetPathAndCursorPosition(".\\myfile:10") - - assertEqual(t, path, ".\\myfile") - assertEqual(t, "10", cursorPosition[0]) - assertEqual(t, "0", cursorPosition[1]) -} -func TestGetPathRelativeNoDotOnlyLine(t *testing.T) { - path, cursorPosition := GetPathAndCursorPosition("myfile:10") - - assertEqual(t, path, "myfile") - assertEqual(t, "10", cursorPosition[0]) - assertEqual(t, "0", cursorPosition[1]) -} -func TestGetPathAbsoluteWindowsOnlyLine(t *testing.T) { - path, cursorPosition := GetPathAndCursorPosition("C:\\myfile:10") - - assertEqual(t, path, "C:\\myfile") - assertEqual(t, "10", cursorPosition[0]) - assertEqual(t, "0", cursorPosition[1]) - - path, cursorPosition = GetPathAndCursorPosition("C:/myfile:10") - - assertEqual(t, path, "C:/myfile") - assertEqual(t, "10", cursorPosition[0]) - assertEqual(t, "0", cursorPosition[1]) -} -func TestGetPathAbsoluteUnixOnlyLine(t *testing.T) { - path, cursorPosition := GetPathAndCursorPosition("/home/user/myfile:10") - - assertEqual(t, path, "/home/user/myfile") - assertEqual(t, "10", cursorPosition[0]) - assertEqual(t, "0", cursorPosition[1]) -} -func TestParseCursorLocationOneArg(t *testing.T) { - location, err := ParseCursorLocation([]string{"3"}) - - assertEqual(t, 3, location.Y) - assertEqual(t, 0, location.X) - assertEqual(t, nil, err) -} -func TestParseCursorLocationTwoArgs(t *testing.T) { - location, err := ParseCursorLocation([]string{"3", "15"}) - - assertEqual(t, 3, location.Y) - assertEqual(t, 15, location.X) - assertEqual(t, nil, err) -} -func TestParseCursorLocationNoArgs(t *testing.T) { - location, err := ParseCursorLocation(nil) - // the expected result is the start position - 0, 0 - assertEqual(t, 0, location.Y) - assertEqual(t, 0, location.X) - // an error will be present here as the positions we're parsing are a nil - assertTrue(t, err != nil) -} -func TestParseCursorLocationFirstArgNotValidNumber(t *testing.T) { - // the messenger is necessary as ParseCursorLocation - // puts a message in it on error - messenger = new(Messenger) - _, err := ParseCursorLocation([]string{"apples", "1"}) - // the expected result is the start position - 0, 0 - assertTrue(t, messenger.hasMessage) - assertTrue(t, err != nil) -} -func TestParseCursorLocationSecondArgNotValidNumber(t *testing.T) { - // the messenger is necessary as ParseCursorLocation - // puts a message in it on error - messenger = new(Messenger) - _, err := ParseCursorLocation([]string{"1", "apples"}) - // the expected result is the start position - 0, 0 - assertTrue(t, messenger.hasMessage) - assertTrue(t, err != nil) +func TestSliceVisualEnd(t *testing.T) { + s := []byte("\thello") + slc, n := SliceVisualEnd(s, 2, 4) + assert.Equal(t, []byte("\thello"), slc) + assert.Equal(t, 2, n) + + slc, n = SliceVisualEnd(s, 1, 4) + assert.Equal(t, []byte("\thello"), slc) + assert.Equal(t, 1, n) + + slc, n = SliceVisualEnd(s, 4, 4) + assert.Equal(t, []byte("hello"), slc) + assert.Equal(t, 0, n) + + slc, n = SliceVisualEnd(s, 5, 4) + assert.Equal(t, []byte("ello"), slc) + assert.Equal(t, 0, n) } diff --git a/cmd/micro/view.go b/cmd/micro/view.go deleted file mode 100644 index 7c9a0f12..00000000 --- a/cmd/micro/view.go +++ /dev/null @@ -1,1117 +0,0 @@ -package main - -import ( - "fmt" - "reflect" - "strconv" - "strings" - "time" - - "github.com/zyedidia/tcell" -) - -// The ViewType defines what kind of view this is -type ViewType struct { - Kind int - Readonly bool // The file cannot be edited - Scratch bool // The file cannot be saved -} - -var ( - vtDefault = ViewType{0, false, false} - vtHelp = ViewType{1, true, true} - vtLog = ViewType{2, true, true} - vtScratch = ViewType{3, false, true} - vtRaw = ViewType{4, true, true} - vtTerm = ViewType{5, true, true} -) - -// 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. -type View struct { - // A pointer to the buffer's cursor for ease of access - Cursor *Cursor - - // The topmost line, used for vertical scrolling - Topline int - // The leftmost column, used for horizontal scrolling - leftCol int - - // Specifies whether or not this view holds a help buffer - Type ViewType - - // Actual width and height - Width int - Height int - - LockWidth bool - LockHeight bool - - // Where this view is located - x, y int - - // How much to offset because of line numbers - lineNumOffset int - - // Holds the list of gutter messages - messages map[string][]GutterMessage - - // This is the index of this view in the views array - Num int - // What tab is this view stored in - TabNum int - - // The buffer - Buf *Buffer - // The statusline - sline *Statusline - - // Since tcell doesn't differentiate between a mouse release event - // and a mouse move event with no keys pressed, we need to keep - // track of whether or not the mouse was pressed (or not released) last event to determine - // mouse release events - mouseReleased bool - - // We need to keep track of insert key press toggle - isOverwriteMode bool - // This stores when the last click was - // This is useful for detecting double and triple clicks - lastClickTime time.Time - lastLoc Loc - - // lastCutTime stores when the last ctrl+k was issued. - // It is used for clearing the clipboard to replace it with fresh cut lines. - lastCutTime time.Time - - // freshClip returns true if the clipboard has never been pasted. - freshClip bool - - // Was the last mouse event actually a double click? - // Useful for detecting triple clicks -- if a double click is detected - // but the last mouse event was actually a double click, it's a triple click - doubleClick bool - // Same here, just to keep track for mouse move events - tripleClick bool - - // The cellview used for displaying and syntax highlighting - cellview *CellView - - splitNode *LeafNode - - // The scrollbar - scrollbar *ScrollBar - - // Virtual terminal - term *Terminal -} - -// NewView returns a new fullscreen view -func NewView(buf *Buffer) *View { - screenW, screenH := screen.Size() - return NewViewWidthHeight(buf, screenW, screenH) -} - -// 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) - - v.x, v.y = 0, 0 - - v.Width = w - v.Height = h - v.cellview = new(CellView) - - v.ToggleTabbar() - - v.OpenBuffer(buf) - - v.messages = make(map[string][]GutterMessage) - - v.sline = &Statusline{ - view: v, - } - - v.scrollbar = &ScrollBar{ - view: v, - } - - if v.Buf.Settings["statusline"].(bool) { - v.Height-- - } - - v.term = new(Terminal) - - 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 v.Buf.Settings["statusline"].(bool) { - v.Height-- - } else { - v.Height++ - } -} - -// StartTerminal execs a command in this view -func (v *View) StartTerminal(execCmd []string, wait bool, getOutput bool, luaCallback string) error { - err := v.term.Start(execCmd, v, getOutput) - v.term.wait = wait - v.term.callback = luaCallback - if err == nil { - v.term.Resize(v.Width, v.Height) - v.Type = vtTerm - } - return err -} - -// CloseTerminal shuts down the tty running in this view -// and returns it to the default view type -func (v *View) CloseTerminal() { - v.term.Stop() -} - -// ToggleTabbar creates an extra row for the tabbar if necessary -func (v *View) ToggleTabbar() { - if len(tabs) > 1 { - if v.y == 0 { - // Include one line for the tab bar at the top - v.Height-- - v.y = 1 - } - } else { - if v.y == 1 { - v.y = 0 - v.Height++ - } - } -} - -func (v *View) paste(clip string) { - if v.Buf.Settings["smartpaste"].(bool) { - if v.Cursor.X > 0 && GetLeadingWhitespace(strings.TrimLeft(clip, "\r\n")) == "" { - leadingWS := GetLeadingWhitespace(v.Buf.Line(v.Cursor.Y)) - clip = strings.Replace(clip, "\n", "\n"+leadingWS, -1) - } - } - - if v.Cursor.HasSelection() { - v.Cursor.DeleteSelection() - v.Cursor.ResetSelection() - } - - 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 - if v.Topline-n >= 0 { - v.Topline -= n - } else if v.Topline > 0 { - v.Topline-- - } -} - -// ScrollDown scrolls the view down n lines (if possible) -func (v *View) ScrollDown(n int) { - // Try to scroll by n but if it would overflow, scroll by 1 - if v.Topline+n <= v.Buf.NumLines { - v.Topline += n - } else if v.Topline < v.Buf.NumLines-1 { - v.Topline++ - } -} - -// 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 -func (v *View) CanClose() bool { - if v.Type == vtDefault && v.Buf.Modified() { - var choice bool - var canceled bool - if v.Buf.Settings["autosave"].(bool) { - choice = true - } else { - choice, canceled = messenger.YesNoPrompt("Save changes to " + v.Buf.GetName() + " before closing? (y,n,esc) ") - } - if !canceled { - //if char == 'y' { - if choice { - v.Save(true) - } - } else { - return false - } - } - return true -} - -// OpenBuffer opens a new buffer in this view. -// This resets the topline, event handler and cursor. -func (v *View) OpenBuffer(buf *Buffer) { - screen.Clear() - v.CloseBuffer() - v.Buf = buf - v.Cursor = &buf.Cursor - v.Topline = 0 - v.leftCol = 0 - v.Cursor.ResetSelection() - v.Relocate() - v.Center(false) - v.messages = make(map[string][]GutterMessage) - - // Set mouseReleased to true because we assume the mouse is not being pressed when - // the editor is opened - v.mouseReleased = true - // Set isOverwriteMode to false, because we assume we are in the default mode when editor - // is opened - v.isOverwriteMode = false - v.lastClickTime = time.Time{} - - GlobalPluginCall("onBufferOpen", v.Buf) - GlobalPluginCall("onViewOpen", v) -} - -// Open opens the given file in the view -func (v *View) Open(path string) { - buf, err := NewBufferFromFile(path) - if err != nil { - messenger.Error(err) - return - } - v.OpenBuffer(buf) -} - -// CloseBuffer performs any closing functions on the buffer -func (v *View) CloseBuffer() { - if v.Buf != nil { - v.Buf.Serialize() - } -} - -// ReOpen reloads the current buffer -func (v *View) ReOpen() { - if v.CanClose() { - screen.Clear() - v.Buf.ReOpen() - v.Relocate() - } -} - -// HSplit opens a horizontal split with the given buffer -func (v *View) HSplit(buf *Buffer) { - i := 0 - if v.Buf.Settings["splitbottom"].(bool) { - i = 1 - } - v.splitNode.HSplit(buf, v.Num+i) -} - -// VSplit opens a vertical split with the given buffer -func (v *View) VSplit(buf *Buffer) { - i := 0 - if v.Buf.Settings["splitright"].(bool) { - i = 1 - } - v.splitNode.VSplit(buf, v.Num+i) -} - -// HSplitIndex opens a horizontal split with the given buffer at the given index -func (v *View) HSplitIndex(buf *Buffer, splitIndex int) { - v.splitNode.HSplit(buf, splitIndex) -} - -// VSplitIndex opens a vertical split with the given buffer at the given index -func (v *View) VSplitIndex(buf *Buffer, splitIndex int) { - v.splitNode.VSplit(buf, splitIndex) -} - -// 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) { - if vy >= v.Buf.NumLines { - vy = v.Buf.NumLines - 1 - } - 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) - if lineN >= v.Buf.NumLines { - return 0, v.Buf.NumLines - 1 - } - - 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 -} - -// Bottomline returns the line number of the lowest line in the view -// You might think that this is obviously just v.Topline + v.Height -// but if softwrap is enabled things get complicated since one buffer -// line can take up multiple lines in the view -func (v *View) Bottomline() int { - if !v.Buf.Settings["softwrap"].(bool) { - return v.Topline + v.Height - } - - 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(v.Buf.Settings["scrollmargin"].(float64)) - if cy < v.Topline+scrollmargin && cy > scrollmargin-1 { - v.Topline = cy - scrollmargin - ret = true - } else if cy < v.Topline { - v.Topline = cy - ret = true - } - 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 >= height { - v.Topline = v.Buf.NumLines - height - 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 -} - -// GetMouseClickLocation gets the location in the buffer from a mouse click -// on the screen -func (v *View) GetMouseClickLocation(x, y int) (int, int) { - x -= v.lineNumOffset - v.leftCol + v.x - y += v.Topline - v.y - - if y-v.Topline > v.Height-1 { - v.ScrollDown(1) - y = v.Height + v.Topline - 1 - } - if y < 0 { - y = 0 - } - if x < 0 { - x = 0 - } - - newX, newY := v.GetSoftWrapLocation(x, y) - if newX > Count(v.Buf.Line(newY)) { - newX = Count(v.Buf.Line(newY)) - } - - return newX, newY -} - -// MoveToMouseClick moves the cursor to location x, y assuming x, y were given -// by a mouse click -func (v *View) MoveToMouseClick(x, y int) { - if y-v.Topline > v.Height-1 { - v.ScrollDown(1) - y = v.Height + v.Topline - 1 - } - if y < 0 { - y = 0 - } - if x < 0 { - x = 0 - } - - x, y = v.GetSoftWrapLocation(x, y) - if x > Count(v.Buf.Line(y)) { - x = Count(v.Buf.Line(y)) - } - v.Cursor.X = x - v.Cursor.Y = y - v.Cursor.LastVisualX = v.Cursor.GetVisualX() -} - -// Execute actions executes the supplied actions -func (v *View) ExecuteActions(actions []func(*View, bool) bool) bool { - relocate := false - readonlyBindingsList := []string{"Delete", "Insert", "Backspace", "Cut", "Play", "Paste", "Move", "Add", "DuplicateLine", "Macro"} - for _, action := range actions { - readonlyBindingsResult := false - funcName := ShortFuncName(action) - curv := CurView() - if curv.Type.Readonly == true { - // check for readonly and if true only let key bindings get called if they do not change the contents. - for _, readonlyBindings := range readonlyBindingsList { - if strings.Contains(funcName, readonlyBindings) { - readonlyBindingsResult = true - } - } - } - if !readonlyBindingsResult { - // call the key binding - relocate = action(curv, true) || relocate - // Macro - if funcName != "ToggleMacro" && funcName != "PlayMacro" { - if recordingMacro { - curMacro = append(curMacro, action) - } - } - } - } - - return relocate -} - -// SetCursor sets the view's and buffer's cursor -func (v *View) SetCursor(c *Cursor) bool { - if c == nil { - return false - } - v.Cursor = c - v.Buf.curCursor = c.Num - - return true -} - -// HandleEvent handles an event passed by the main loop -func (v *View) HandleEvent(event tcell.Event) { - if v.Type == vtTerm { - v.term.HandleEvent(event) - return - } - - if v.Type == vtRaw { - v.Buf.Insert(v.Cursor.Loc, reflect.TypeOf(event).String()[7:]) - v.Buf.Insert(v.Cursor.Loc, fmt.Sprintf(": %q\n", event.EscSeq())) - - switch e := event.(type) { - case *tcell.EventKey: - if e.Key() == tcell.KeyCtrlQ { - v.Quit(true) - } - } - - return - } - - // This bool determines whether the view is relocated at the end of the function - // By default it's true because most events should cause a relocate - relocate := true - - v.Buf.CheckModTime() - - switch e := event.(type) { - case *tcell.EventRaw: - for key, actions := range bindings { - if key.keyCode == -1 { - if e.EscSeq() == key.escape { - for _, c := range v.Buf.cursors { - ok := v.SetCursor(c) - if !ok { - break - } - relocate = false - relocate = v.ExecuteActions(actions) || relocate - } - v.SetCursor(&v.Buf.Cursor) - v.Buf.MergeCursors() - break - } - } - } - case *tcell.EventKey: - // Check first if input is a key binding, if it is we 'eat' the input and don't insert a rune - isBinding := false - for key, actions := range bindings { - if e.Key() == key.keyCode { - if e.Key() == tcell.KeyRune { - if e.Rune() != key.r { - continue - } - } - if e.Modifiers() == key.modifiers { - for _, c := range v.Buf.cursors { - ok := v.SetCursor(c) - if !ok { - break - } - relocate = false - isBinding = true - relocate = v.ExecuteActions(actions) || relocate - } - v.SetCursor(&v.Buf.Cursor) - v.Buf.MergeCursors() - break - } - } - } - - if !isBinding && e.Key() == tcell.KeyRune { - // Check viewtype if readonly don't insert a rune (readonly help and log view etc.) - if v.Type.Readonly == false { - for _, c := range v.Buf.cursors { - v.SetCursor(c) - - // Insert a character - if v.Cursor.HasSelection() { - v.Cursor.DeleteSelection() - v.Cursor.ResetSelection() - } - - if v.isOverwriteMode { - next := v.Cursor.Loc - next.X++ - v.Buf.Replace(v.Cursor.Loc, next, string(e.Rune())) - } else { - v.Buf.Insert(v.Cursor.Loc, string(e.Rune())) - } - - 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()) - } - } - v.SetCursor(&v.Buf.Cursor) - } - } - case *tcell.EventPaste: - // Check viewtype if readonly don't paste (readonly help and log view etc.) - if v.Type.Readonly == false { - if !PreActionCall("Paste", v) { - break - } - - for _, c := range v.Buf.cursors { - v.SetCursor(c) - v.paste(e.Text()) - } - v.SetCursor(&v.Buf.Cursor) - - PostActionCall("Paste", v) - } - case *tcell.EventMouse: - // Don't relocate for mouse events - relocate = false - - button := e.Buttons() - - for key, actions := range bindings { - if button == key.buttons && e.Modifiers() == key.modifiers { - for _, c := range v.Buf.cursors { - ok := v.SetCursor(c) - if !ok { - break - } - relocate = v.ExecuteActions(actions) || relocate - } - v.SetCursor(&v.Buf.Cursor) - v.Buf.MergeCursors() - } - } - - for key, actions := range mouseBindings { - if button == key.buttons && e.Modifiers() == key.modifiers { - for _, action := range actions { - action(v, true, e) - } - } - } - - switch button { - case tcell.ButtonNone: - // Mouse event with no click - if !v.mouseReleased { - // Mouse was just released - - x, y := e.Position() - x -= v.lineNumOffset - v.leftCol + v.x - y += v.Topline - v.y - - // Relocating here isn't really necessary because the cursor will - // be in the right place from the last mouse event - // However, if we are running in a terminal that doesn't support mouse motion - // events, this still allows the user to make selections, except only after they - // release the mouse - - if !v.doubleClick && !v.tripleClick { - v.MoveToMouseClick(x, y) - v.Cursor.SetSelectionEnd(v.Cursor.Loc) - v.Cursor.CopySelection("primary") - } - v.mouseReleased = true - } - } - } - - if relocate { - v.Relocate() - // We run relocate again because there's a bug with relocating with softwrap - // when for example you jump to the bottom of the buffer and it tries to - // calculate where to put the topline so that the bottom line is at the bottom - // of the terminal and it runs into problems with visual lines vs real lines. - // This is (hopefully) a temporary solution - v.Relocate() - } -} - -func (v *View) mainCursor() bool { - return v.Buf.curCursor == len(v.Buf.cursors)-1 -} - -// GutterMessage creates a message in this view's gutter -func (v *View) GutterMessage(section string, lineN int, msg string, kind int) { - lineN-- - gutterMsg := GutterMessage{ - lineNum: lineN, - msg: msg, - kind: kind, - } - for _, v := range v.messages { - for _, gmsg := range v { - if gmsg.lineNum == lineN { - return - } - } - } - messages := v.messages[section] - v.messages[section] = append(messages, gutterMsg) -} - -// ClearGutterMessages clears all gutter messages from a given section -func (v *View) ClearGutterMessages(section string) { - v.messages[section] = []GutterMessage{} -} - -// ClearAllGutterMessages clears all the gutter messages -func (v *View) ClearAllGutterMessages() { - for k := range v.messages { - v.messages[k] = []GutterMessage{} - } -} - -// 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 := NewBufferFromString(string(data), helpPage+".md") - helpBuffer.name = "Help" - - if v.Type == vtHelp { - v.OpenBuffer(helpBuffer) - } else { - v.HSplit(helpBuffer) - CurView().Type = vtHelp - } - } -} - -// DisplayView draws the view to the screen -func (v *View) DisplayView() { - if v.Type == vtTerm { - v.term.Display() - return - } - - if v.Buf.Settings["softwrap"].(bool) && v.leftCol != 0 { - v.leftCol = 0 - } - - if v.Type == vtLog || v.Type == vtRaw { - // Log or raw views should always follow the cursor... - v.Relocate() - } - - // We need to know the string length of the largest line number - // so we can pad appropriately when displaying line numbers - maxLineNumLength := len(strconv.Itoa(v.Buf.NumLines)) - - if v.Buf.Settings["ruler"] == true { - // + 1 for the little space after the line number - v.lineNumOffset = maxLineNumLength + 1 - } else { - v.lineNumOffset = 0 - } - - // We need to add to the line offset if there are gutter messages - var hasGutterMessages bool - for _, v := range v.messages { - if len(v) > 0 { - hasGutterMessages = true - } - } - if hasGutterMessages { - v.lineNumOffset += 2 - } - - divider := 0 - if v.x != 0 { - // One space for the extra split divider - v.lineNumOffset++ - divider = 1 - } - - xOffset := v.x + v.lineNumOffset - yOffset := v.y - - height := v.Height - width := v.Width - left := v.leftCol - top := v.Topline - - v.cellview.Draw(v.Buf, top, height, left, width-v.lineNumOffset) - - screenX := v.x - realLineN := top - 1 - visualLineN := 0 - var line []*Char - for visualLineN, line = range v.cellview.lines { - var firstChar *Char - if len(line) > 0 { - firstChar = line[0] - } - - var softwrapped bool - if firstChar != nil { - if firstChar.realLoc.Y == realLineN { - softwrapped = true - } - realLineN = firstChar.realLoc.Y - } else { - realLineN++ - } - - colorcolumn := int(v.Buf.Settings["colorcolumn"].(float64)) - if colorcolumn != 0 && xOffset+colorcolumn-v.leftCol < v.Width { - style := GetColor("color-column") - fg, _, _ := style.Decompose() - st := defStyle.Background(fg) - screen.SetContent(xOffset+colorcolumn-v.leftCol, yOffset+visualLineN, ' ', nil, st) - } - - screenX = v.x - - // If there are gutter messages we need to display the '>>' symbol here - if hasGutterMessages { - // msgOnLine stores whether or not there is a gutter message on this line in particular - msgOnLine := false - for k := range v.messages { - for _, msg := range v.messages[k] { - if msg.lineNum == realLineN { - msgOnLine = true - gutterStyle := defStyle - switch msg.kind { - case GutterInfo: - if style, ok := colorscheme["gutter-info"]; ok { - gutterStyle = style - } - case GutterWarning: - if style, ok := colorscheme["gutter-warning"]; ok { - gutterStyle = style - } - case GutterError: - if style, ok := colorscheme["gutter-error"]; ok { - gutterStyle = style - } - } - screen.SetContent(screenX, yOffset+visualLineN, '>', nil, gutterStyle) - screenX++ - screen.SetContent(screenX, yOffset+visualLineN, '>', nil, gutterStyle) - screenX++ - if v.Cursor.Y == realLineN && !messenger.hasPrompt { - messenger.Message(msg.msg) - messenger.gutterMessage = true - } - } - } - } - // If there is no message on this line we just display an empty offset - if !msgOnLine { - screen.SetContent(screenX, yOffset+visualLineN, ' ', nil, defStyle) - screenX++ - screen.SetContent(screenX, yOffset+visualLineN, ' ', nil, defStyle) - screenX++ - if v.Cursor.Y == realLineN && messenger.gutterMessage { - messenger.Reset() - messenger.gutterMessage = false - } - } - } - - lineNumStyle := defStyle - if v.Buf.Settings["ruler"] == true { - // Write the line number - if style, ok := colorscheme["line-number"]; ok { - lineNumStyle = style - } - if style, ok := colorscheme["current-line-number"]; ok { - if realLineN == v.Cursor.Y && tabs[curTab].CurView == v.Num && !v.Cursor.HasSelection() { - lineNumStyle = style - } - } - - lineNum := strconv.Itoa(realLineN + 1) - - // Write the spaces before the line number if necessary - for i := 0; i < maxLineNumLength-len(lineNum); i++ { - screen.SetContent(screenX+divider, yOffset+visualLineN, ' ', nil, lineNumStyle) - screenX++ - } - if softwrapped && visualLineN != 0 { - // Pad without the line number because it was written on the visual line before - for range lineNum { - screen.SetContent(screenX+divider, yOffset+visualLineN, ' ', nil, lineNumStyle) - screenX++ - } - } else { - // Write the actual line number - for _, ch := range lineNum { - screen.SetContent(screenX+divider, yOffset+visualLineN, ch, nil, lineNumStyle) - screenX++ - } - } - - // Write the extra space - screen.SetContent(screenX+divider, yOffset+visualLineN, ' ', nil, lineNumStyle) - screenX++ - } - - var lastChar *Char - cursorSet := false - for _, char := range line { - if char != nil { - lineStyle := char.style - - colorcolumn := int(v.Buf.Settings["colorcolumn"].(float64)) - if colorcolumn != 0 && char.visualLoc.X == colorcolumn { - style := GetColor("color-column") - fg, _, _ := style.Decompose() - lineStyle = lineStyle.Background(fg) - } - - charLoc := char.realLoc - for _, c := range v.Buf.cursors { - v.SetCursor(c) - if v.Cursor.HasSelection() && - (charLoc.GreaterEqual(v.Cursor.CurSelection[0]) && charLoc.LessThan(v.Cursor.CurSelection[1]) || - charLoc.LessThan(v.Cursor.CurSelection[0]) && charLoc.GreaterEqual(v.Cursor.CurSelection[1])) { - // The current character is selected - lineStyle = defStyle.Reverse(true) - - if style, ok := colorscheme["selection"]; ok { - lineStyle = style - } - } - } - v.SetCursor(&v.Buf.Cursor) - - if v.Buf.Settings["cursorline"].(bool) && tabs[curTab].CurView == v.Num && - !v.Cursor.HasSelection() && v.Cursor.Y == realLineN { - style := GetColor("cursor-line") - fg, _, _ := style.Decompose() - lineStyle = lineStyle.Background(fg) - } - - screen.SetContent(xOffset+char.visualLoc.X, yOffset+char.visualLoc.Y, char.drawChar, nil, lineStyle) - - for i, c := range v.Buf.cursors { - v.SetCursor(c) - if tabs[curTab].CurView == v.Num && !v.Cursor.HasSelection() && - v.Cursor.Y == char.realLoc.Y && v.Cursor.X == char.realLoc.X && (!cursorSet || i != 0) { - ShowMultiCursor(xOffset+char.visualLoc.X, yOffset+char.visualLoc.Y, i) - cursorSet = true - } - } - v.SetCursor(&v.Buf.Cursor) - - lastChar = char - } - } - - lastX := 0 - var realLoc Loc - var visualLoc Loc - var cx, cy int - if lastChar != nil { - lastX = xOffset + lastChar.visualLoc.X + lastChar.width - for i, c := range v.Buf.cursors { - v.SetCursor(c) - if tabs[curTab].CurView == v.Num && !v.Cursor.HasSelection() && - v.Cursor.Y == lastChar.realLoc.Y && v.Cursor.X == lastChar.realLoc.X+1 { - ShowMultiCursor(lastX, yOffset+lastChar.visualLoc.Y, i) - cx, cy = lastX, yOffset+lastChar.visualLoc.Y - } - } - v.SetCursor(&v.Buf.Cursor) - realLoc = Loc{lastChar.realLoc.X + 1, realLineN} - visualLoc = Loc{lastX - xOffset, lastChar.visualLoc.Y} - } else if len(line) == 0 { - for i, c := range v.Buf.cursors { - v.SetCursor(c) - if tabs[curTab].CurView == v.Num && !v.Cursor.HasSelection() && - v.Cursor.Y == realLineN { - ShowMultiCursor(xOffset, yOffset+visualLineN, i) - cx, cy = xOffset, yOffset+visualLineN - } - } - v.SetCursor(&v.Buf.Cursor) - lastX = xOffset - realLoc = Loc{0, realLineN} - visualLoc = Loc{0, visualLineN} - } - - if v.Cursor.HasSelection() && - (realLoc.GreaterEqual(v.Cursor.CurSelection[0]) && realLoc.LessThan(v.Cursor.CurSelection[1]) || - realLoc.LessThan(v.Cursor.CurSelection[0]) && realLoc.GreaterEqual(v.Cursor.CurSelection[1])) { - // The current character is selected - selectStyle := defStyle.Reverse(true) - - if style, ok := colorscheme["selection"]; ok { - selectStyle = style - } - screen.SetContent(xOffset+visualLoc.X, yOffset+visualLoc.Y, ' ', nil, selectStyle) - } - - if v.Buf.Settings["cursorline"].(bool) && tabs[curTab].CurView == v.Num && - !v.Cursor.HasSelection() && v.Cursor.Y == realLineN { - for i := lastX; i < xOffset+v.Width-v.lineNumOffset; i++ { - style := GetColor("cursor-line") - fg, _, _ := style.Decompose() - style = style.Background(fg) - if !(tabs[curTab].CurView == v.Num && !v.Cursor.HasSelection() && i == cx && yOffset+visualLineN == cy) { - screen.SetContent(i, yOffset+visualLineN, ' ', nil, style) - } - } - } - } - - if divider != 0 { - dividerStyle := defStyle - if style, ok := colorscheme["divider"]; ok { - dividerStyle = style - } - for i := 0; i < v.Height; i++ { - screen.SetContent(v.x, yOffset+i, '|', nil, dividerStyle.Reverse(true)) - } - } -} - -// ShowMultiCursor will display a cursor at a location -// If i == 0 then the terminal cursor will be used -// Otherwise a fake cursor will be drawn at the position -func ShowMultiCursor(x, y, i int) { - if i == 0 { - screen.ShowCursor(x, y) - } else { - r, _, _, _ := screen.GetContent(x, y) - screen.SetContent(x, y, r, nil, defStyle.Reverse(true)) - } -} - -// Display renders the view, the cursor, and statusline -func (v *View) Display() { - if globalSettings["termtitle"].(bool) { - screen.SetTitle("micro: " + v.Buf.GetName()) - } - v.DisplayView() - // Don't draw the cursor if it is out of the viewport or if it has a selection - if v.Num == tabs[curTab].CurView && (v.Cursor.Y-v.Topline < 0 || v.Cursor.Y-v.Topline > v.Height-1 || v.Cursor.HasSelection()) { - screen.HideCursor() - } - _, screenH := screen.Size() - - if v.Buf.Settings["scrollbar"].(bool) { - v.scrollbar.Display() - } - - if v.Buf.Settings["statusline"].(bool) { - v.sline.Display() - } else if (v.y + v.Height) != screenH-1 { - for x := 0; x < v.Width; x++ { - screen.SetContent(v.x+x, v.y+v.Height, '-', nil, defStyle.Reverse(true)) - } - } -} diff --git a/cmd/micro/window.go b/cmd/micro/window.go new file mode 100644 index 00000000..4a14aed9 --- /dev/null +++ b/cmd/micro/window.go @@ -0,0 +1,187 @@ +package main + +import ( + "strconv" + "unicode/utf8" + + runewidth "github.com/mattn/go-runewidth" + "github.com/zyedidia/tcell" +) + +type Window struct { + // X and Y coordinates for the top left of the window + X int + Y int + + // Width and Height for the window + Width int + Height int + + // Which line in the buffer to start displaying at (vertical scroll) + StartLine int + // Which visual column in the to start displaying at (horizontal scroll) + StartCol int + + // Buffer being shown in this window + Buf *Buffer + + sline *StatusLine +} + +func NewWindow(x, y, width, height int, buf *Buffer) *Window { + w := new(Window) + w.X, w.Y, w.Width, w.Height, w.Buf = x, y, width, height, buf + + w.sline = NewStatusLine(w) + + return w +} + +func (w *Window) DrawLineNum(lineNumStyle tcell.Style, softwrapped bool, maxLineNumLength int, vloc *Loc, bloc *Loc) { + lineNum := strconv.Itoa(bloc.Y + 1) + + // Write the spaces before the line number if necessary + for i := 0; i < maxLineNumLength-len(lineNum); i++ { + screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ' ', nil, lineNumStyle) + vloc.X++ + } + // Write the actual line number + for _, ch := range lineNum { + if softwrapped { + screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ' ', nil, lineNumStyle) + } else { + screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ch, nil, lineNumStyle) + } + vloc.X++ + } + + // Write the extra space + screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ' ', nil, lineNumStyle) + vloc.X++ +} + +// GetStyle returns the highlight style for the given character position +// If there is no change to the current highlight style it just returns that +func (w *Window) GetStyle(style tcell.Style, bloc Loc, r rune) tcell.Style { + if group, ok := w.Buf.Match(bloc.Y)[bloc.X]; ok { + s := GetColor(group.String()) + return s + } + return style +} + +// DisplayBuffer draws the buffer being shown in this window on the screen +func (w *Window) DisplayBuffer() { + b := w.Buf + + bufHeight := w.Height + if b.Settings["statusline"].(bool) { + bufHeight-- + } + + // TODO: Rehighlighting + // start := w.StartLine + if b.Settings["syntax"].(bool) && b.syntaxDef != nil { + // if start > 0 && b.lines[start-1].rehighlight { + // b.highlighter.ReHighlightLine(b, start-1) + // b.lines[start-1].rehighlight = false + // } + // + // b.highlighter.ReHighlightStates(b, start) + // + b.highlighter.HighlightMatches(b, w.StartLine, w.StartLine+bufHeight) + } + + lineNumStyle := defStyle + if style, ok := colorscheme["line-number"]; ok { + lineNumStyle = style + } + + // We need to know the string length of the largest line number + // so we can pad appropriately when displaying line numbers + maxLineNumLength := len(strconv.Itoa(len(b.lines))) + + tabsize := int(b.Settings["tabsize"].(float64)) + softwrap := b.Settings["softwrap"].(bool) + + // this represents the current draw position + // within the current window + vloc := Loc{0, 0} + + // this represents the current draw position in the buffer (char positions) + bloc := Loc{w.StartCol, w.StartLine} + + curStyle := defStyle + for vloc.Y = 0; vloc.Y < bufHeight; vloc.Y++ { + vloc.X = 0 + if b.Settings["ruler"].(bool) { + w.DrawLineNum(lineNumStyle, false, maxLineNumLength, &vloc, &bloc) + } + + line := b.LineBytes(bloc.Y) + line, nColsBeforeStart := SliceVisualEnd(line, bloc.X, tabsize) + totalwidth := bloc.X - nColsBeforeStart + for len(line) > 0 { + r, size := utf8.DecodeRune(line) + + curStyle = w.GetStyle(curStyle, bloc, r) + + if nColsBeforeStart <= 0 { + screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, r, nil, curStyle) + vloc.X++ + } + nColsBeforeStart-- + + width := 0 + + char := ' ' + switch r { + case '\t': + ts := tabsize - (totalwidth % tabsize) + width = ts + default: + width = runewidth.RuneWidth(r) + char = '@' + } + + bloc.X++ + line = line[size:] + + // Draw any extra characters either spaces for tabs or @ for incomplete wide runes + if width > 1 { + for i := 1; i < width; i++ { + if nColsBeforeStart <= 0 { + screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, char, nil, curStyle) + vloc.X++ + } + nColsBeforeStart-- + } + } + totalwidth += width + + // If we reach the end of the window then we either stop or we wrap for softwrap + if vloc.X >= w.Width { + if !softwrap { + break + } else { + vloc.Y++ + if vloc.Y >= bufHeight { + break + } + vloc.X = 0 + // This will draw an empty line number because the current line is wrapped + w.DrawLineNum(lineNumStyle, true, maxLineNumLength, &vloc, &bloc) + } + } + } + bloc.X = w.StartCol + bloc.Y++ + if bloc.Y >= len(b.lines) { + break + } + } +} + +func (w *Window) DisplayStatusLine() { + w.sline.Display() +}