test/
.idea/
packages/
+todo.txt
+test.txt
+log.txt
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
--- /dev/null
+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
+}
+++ /dev/null
-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
-}
+++ /dev/null
-// +build plan9 nacl windows
-
-package main
-
-func (v *View) Suspend(usePlugin bool) bool {
- messenger.Error("Suspend is only supported on Posix")
-
- return false
-}
+++ /dev/null
-// +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
-}
+++ /dev/null
-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
-}
+++ /dev/null
-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",
- }
-}
"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
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
// 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)
}
// 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 {
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)
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 {
return b.name
}
-// UpdateRules updates the syntax rules and filetype for this buffer
-// This is called when the colorscheme changes
-func (b *Buffer) UpdateRules() {
- rehighlight := false
- var files []*highlight.File
- for _, f := range ListRuntimeFiles(RTSyntax) {
- data, err := f.Data()
- if err != nil {
- TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
- } else {
- file, err := highlight.ParseFile(data)
- if err != nil {
- TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
- continue
- }
- ftdetect, err := highlight.ParseFtDetect(file)
- if err != nil {
- TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
- continue
- }
-
- ft := b.Settings["filetype"].(string)
- if (ft == "Unknown" || ft == "") && !rehighlight {
- if highlight.MatchFiletype(ftdetect, b.Path, b.lines[0].data) {
- header := new(highlight.Header)
- header.FileType = file.FileType
- header.FtDetect = ftdetect
- b.syntaxDef, err = highlight.ParseDef(file, header)
- if err != nil {
- TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
- continue
- }
- rehighlight = true
- }
- } else {
- if file.FileType == ft && !rehighlight {
- header := new(highlight.Header)
- header.FileType = file.FileType
- header.FtDetect = ftdetect
- b.syntaxDef, err = highlight.ParseDef(file, header)
- if err != nil {
- TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
- continue
- }
- rehighlight = true
- }
- }
- files = append(files, file)
- }
- }
-
- if b.syntaxDef != nil {
- highlight.ResolveIncludes(b.syntaxDef, files)
- }
-
- if b.highlighter == nil || rehighlight {
- if b.syntaxDef != nil {
- b.Settings["filetype"] = b.syntaxDef.FileType
- b.highlighter = highlight.NewHighlighter(b.syntaxDef)
- if b.Settings["syntax"].(bool) {
- b.highlighter.HighlightStates(b)
- }
- }
- }
-}
-
// 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() {
+func (b *Buffer) ReOpen() error {
data, err := ioutil.ReadFile(b.Path)
txt := string(data)
if err != nil {
- messenger.Error(err.Error())
- return
+ return err
}
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)
+ b.ModTime, err = GetModTime(b.Path)
+ b.isModified = false
+ return err
+ // TODO: buffer cursor
+ // b.Cursor.Relocate()
}
-// 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
- }
-}
+// Saving
// 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 {
+ // 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["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()
}
}
+ // 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)
-
- // 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")
- }
- }
- }
+ 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
// end of line
var eol []byte
-
if b.Settings["fileformat"] == "dos" {
eol = []byte{'\r', '\n'}
} else {
if _, e = file.Write(eol); e != nil {
return
}
-
if _, e = file.Write(l.data); e != nil {
return
}
-
fileSize += len(eol) + len(l.data)
}
-
return
})
}
b.Path = filename
- b.IsModified = false
- return b.Serialize()
+ 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
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])
+// 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
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
+ absPath, _ := filepath.Abs(filename)
+ b.AbsPath = absPath
// Set up everything for the command
cmd := exec.Command(globalSettings["sucmd"].(string), "tee", filename)
- cmd.Stdin = bytes.NewBufferString(b.SaveString(b.Settings["fileformat"] == "dos"))
+ 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
cmd.Start()
err := cmd.Wait()
- // Start the screen back up
- InitScreen()
if err == nil {
- b.IsModified = false
+ b.isModified = false
b.ModTime, _ = GetModTime(filename)
- b.Serialize()
+ // TODO: 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()
+func (b *Buffer) GetActiveCursor() *Cursor {
+ return nil
}
-// Start returns the location of the first character in the buffer
-func (b *Buffer) Start() Loc {
- return Loc{0, 0}
+func (b *Buffer) GetCursor(n int) *Cursor {
+ return nil
}
-// 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}
+func (b *Buffer) GetCursors() []*Cursor {
+ return nil
}
-// 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'
+func (b *Buffer) NumCursors() int {
+ return 0
}
-// LineBytes returns a single line as an array of runes
func (b *Buffer) LineBytes(n int) []byte {
- if n >= len(b.lines) {
+ if n >= len(b.lines) || n < 0 {
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)
+func (b *Buffer) LinesNum() int {
+ return len(b.lines)
}
-// Line returns a single line
-func (b *Buffer) Line(n int) string {
- if n >= len(b.lines) {
- return ""
- }
- return string(b.lines[n].data)
+func (b *Buffer) Start() Loc {
+ return Loc{0, 0}
}
-// LinesNum returns the number of lines in the buffer
-func (b *Buffer) LinesNum() int {
- return len(b.lines)
+// 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}
}
-// 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))
+// 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 slice
+ return '\n'
}
-// 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
+// Modified returns if this buffer has been modified since
+// being opened
+func (b *Buffer) Modified() bool {
+ if b.Settings["fastdirty"].(bool) {
+ return b.isModified
}
- return
-}
+ var buff [md5.Size]byte
-// 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},
- )
+ calcHash(b, &buff)
+ return buff != b.origHash
}
-// 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},
- )
-}
+// calcHash calculates md5 hash of all lines in the buffer
+func calcHash(b *Buffer, out *[md5.Size]byte) {
+ h := md5.New()
-// 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)
- }
-}
+ if len(b.lines) > 0 {
+ h.Write(b.lines[0].data)
-func (b *Buffer) clearCursors() {
- for i := 1; i < len(b.cursors); i++ {
- b.cursors[i] = nil
+ for _, l := range b.lines[1:] {
+ h.Write([]byte{'\n'})
+ h.Write(l.data)
+ }
}
- b.cursors = b.cursors[:1]
- b.UpdateCursors()
- b.Cursor.ResetSelection()
-}
-var bracePairs = [][2]rune{
- {'(', ')'},
- {'{', '}'},
- {'[', ']'},
+ h.Sum((*out)[:0])
}
-// 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
+// UpdateRules updates the syntax rules and filetype for this buffer
+// This is called when the colorscheme changes
+func (b *Buffer) UpdateRules() {
+ rehighlight := false
+ var files []*highlight.File
+ for _, f := range ListRuntimeFiles(RTSyntax) {
+ data, err := f.Data()
+ if err != nil {
+ TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
+ } else {
+ file, err := highlight.ParseFile(data)
+ if err != nil {
+ TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
+ continue
}
- 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}
+ ftdetect, err := highlight.ParseFtDetect(file)
+ if err != nil {
+ TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
+ continue
+ }
+
+ ft := b.Settings["filetype"].(string)
+ if (ft == "Unknown" || ft == "") && !rehighlight {
+ if highlight.MatchFiletype(ftdetect, b.Path, b.lines[0].data) {
+ header := new(highlight.Header)
+ header.FileType = file.FileType
+ header.FtDetect = ftdetect
+ b.syntaxDef, err = highlight.ParseDef(file, header)
+ if err != nil {
+ TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
+ continue
}
+ rehighlight = true
}
- }
- }
- } 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 file.FileType == ft && !rehighlight {
+ header := new(highlight.Header)
+ header.FileType = file.FileType
+ header.FtDetect = ftdetect
+ b.syntaxDef, err = highlight.ParseDef(file, header)
+ if err != nil {
+ TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
+ continue
}
- } else if r == braceType[1] {
- i++
+ rehighlight = true
}
}
+ files = append(files, file)
+ }
+ }
+
+ if b.syntaxDef != nil {
+ highlight.ResolveIncludes(b.syntaxDef, files)
+ }
+
+ if b.highlighter == nil || rehighlight {
+ if b.syntaxDef != nil {
+ b.Settings["filetype"] = b.syntaxDef.FileType
+ b.highlighter = highlight.NewHighlighter(b.syntaxDef)
+ if b.Settings["syntax"].(bool) {
+ b.highlighter.HighlightStates(b)
+ }
}
}
- return start
}
+++ /dev/null
-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)
-
-//}
+++ /dev/null
-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)
- }
-}
package main
import (
- "fmt"
+ "errors"
"regexp"
"strconv"
"strings"
"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
}
// 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")
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
--- /dev/null
+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)
+}
+++ /dev/null
-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:])
- }
-}
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
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)
}
}
}
}
// 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
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)
}
}
-// 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
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()
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))
}
}
--- /dev/null
+package main
--- /dev/null
+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")
+ }
+}
package main
import (
- "strings"
"time"
+ "unicode/utf8"
dmp "github.com/sergi/go-diff/diffmatchpatch"
- "github.com/yuin/gopher-lua"
)
const (
// A Delta is a change to the buffer
type Delta struct {
- Text string
+ Text []byte
Start Loc
End Loc
}
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 {
} 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]
// 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
}
// 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
}
// 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
// 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(),
// 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)
}
// 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
}
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
}
+++ /dev/null
-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)
-}
+++ /dev/null
-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))
-}
+++ /dev/null
-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)
- }
- }
- }
-}
+++ /dev/null
-package main
-
-import (
- "bufio"
- "io"
- "unicode/utf8"
-
- "github.com/zyedidia/micro/cmd/micro/highlight"
-)
-
-func runeToByteIndex(n int, txt []byte) int {
- if n == 0 {
- return 0
- }
-
- count := 0
- i := 0
- for len(txt) > 0 {
- _, size := utf8.DecodeRune(txt)
-
- txt = txt[size:]
- count += size
- i++
-
- if i == n {
- break
- }
- }
- return count
-}
-
-// A Line contains the data in bytes as well as a highlight state, match
-// and a flag for whether the highlighting needs to be updated
-type Line struct {
- data []byte
-
- state highlight.State
- match highlight.LineMatch
- rehighlight bool
-}
-
-// A LineArray simply stores and array of lines and makes it easy to insert
-// and delete in it
-type LineArray struct {
- lines []Line
-}
-
-// Append efficiently appends lines together
-// It allocates an additional 10000 lines if the original estimate
-// is incorrect
-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
- }
- slice = slice[0 : l+len(data)]
- for i, c := range data {
- slice[l+i] = c
- }
- return slice
-}
-
-// NewLineArray returns a new line array from an array of bytes
-func NewLineArray(size int64, reader io.Reader) *LineArray {
- la := new(LineArray)
-
- la.lines = make([]Line, 0, 1000)
-
- br := bufio.NewReader(reader)
- var loaded int
-
- 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
- }
- } else if len(data) > 0 {
- if fileformat == 0 {
- fileformat = 1
- }
- }
-
- if n >= 1000 && loaded >= 0 {
- totalLinesNum := int(float64(size) * (float64(n) / float64(loaded)))
- newSlice := make([]Line, len(la.lines), totalLinesNum+10000)
- copy(newSlice, la.lines)
- la.lines = newSlice
- loaded = -1
- }
-
- if loaded >= 0 {
- loaded += len(data)
- }
-
- 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})
- }
- n++
- }
-
- 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
-// 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 := ""
- for i, l := range la.lines {
- str += string(l.data)
- if i != len(la.lines)-1 {
- if useCrlf {
- str += "\r"
- }
- str += "\n"
- }
- }
- return str
-}
-
-// 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
-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})
- x = 0
- y++
- continue
- }
- la.insertByte(Loc{x, y}, value[i])
- x++
- }
-}
-
-// 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) {
- la.insert(Loc{len(la.lines[a].data), a}, la.lines[b].data)
- la.DeleteLine(b)
-}
-
-// Split splits a line at a given position
-func (la *LineArray) Split(pos Loc) {
- la.NewlineBelow(pos.Y)
- la.insert(Loc{0, pos.Y + 1}, la.lines[pos.Y].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})
-}
-
-// removes from start to end
-func (la *LineArray) remove(start, end Loc) string {
- sub := la.Substr(start, end)
- startX := runeToByteIndex(start.X, la.lines[start.Y].data)
- endX := runeToByteIndex(end.X, la.lines[end.Y].data)
- if start.Y == end.Y {
- 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.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) {
- 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) {
- la.lines[pos.Y].data = la.lines[pos.Y].data[pos.X+1:]
-}
-
-// DeleteLine deletes the line number
-func (la *LineArray) DeleteLine(y int) {
- la.lines = la.lines[:y+copy(la.lines[y:], la.lines[y+1:])]
-}
-
-// DeleteByte deletes the byte at a position
-func (la *LineArray) DeleteByte(pos Loc) {
- la.lines[pos.Y].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 {
- 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])
- }
- var str string
- str += string(la.lines[start.Y].data[startX:]) + "\n"
- for i := start.Y + 1; i <= end.Y-1; i++ {
- str += string(la.lines[i].data) + "\n"
- }
- str += string(la.lines[end.Y].data[:endX])
- return str
-}
-
-// State gets the highlight state for the given line number
-func (la *LineArray) State(lineN int) highlight.State {
- return la.lines[lineN].state
-}
-
-// SetState sets the highlight state at the given line number
-func (la *LineArray) SetState(lineN int, s highlight.State) {
- la.lines[lineN].state = s
-}
-
-// SetMatch sets the match at the given line number
-func (la *LineArray) SetMatch(lineN int, m highlight.LineMatch) {
- la.lines[lineN].match = m
-}
-
-// Match retrieves the match for the given line number
-func (la *LineArray) Match(lineN int) highlight.LineMatch {
- return la.lines[lineN].match
-}
--- /dev/null
+package main
+
+import (
+ "bufio"
+ "io"
+ "unicode/utf8"
+
+ "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
+ }
+
+ count := 0
+ i := 0
+ for len(txt) > 0 {
+ _, size := utf8.DecodeRune(txt)
+
+ txt = txt[size:]
+ count += size
+ i++
+
+ if i == n {
+ break
+ }
+ }
+ return count
+}
+
+// A Line contains the data in bytes as well as a highlight state, match
+// and a flag for whether the highlighting needs to be updated
+type Line struct {
+ data []byte
+
+ state highlight.State
+ match highlight.LineMatch
+ 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
+ endings FileFormat
+ initsize uint64
+}
+
+// Append efficiently appends lines together
+// It allocates an additional 10000 lines if the original estimate
+// is incorrect
+func Append(slice []Line, data ...Line) []Line {
+ l := len(slice)
+ if l+len(data) > cap(slice) { // reallocate
+ newSlice := make([]Line, (l+len(data))+10000)
+ copy(newSlice, slice)
+ slice = newSlice
+ }
+ slice = slice[0 : l+len(data)]
+ for i, c := range data {
+ slice[l+i] = c
+ }
+ return slice
+}
+
+// NewLineArray returns a new line array from an array of bytes
+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
+
+ n := 0
+ for {
+ data, err := br.ReadBytes('\n')
+ // 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 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)
+ copy(newSlice, la.lines)
+ la.lines = newSlice
+ loaded = -1
+ }
+
+ // Counter for the number of bytes in the first 1000 lines
+ if loaded >= 0 {
+ loaded += dlen
+ }
+
+ if err != nil {
+ if err == io.EOF {
+ la.lines = Append(la.lines, Line{data[:], nil, nil, false})
+ }
+ // Last line was read
+ break
+ } else {
+ la.lines = Append(la.lines, Line{data[:dlen-1], nil, nil, false})
+ }
+ n++
+ }
+
+ return la
+}
+
+// Bytes returns the string that should be written to disk when
+// the line array is saved
+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 = append(str, l.data...)
+ if i != len(la.lines)-1 {
+ if la.endings == FFDos {
+ str = append(str, '\r')
+ }
+ str = append(str, '\n')
+ }
+ }
+ return str
+}
+
+// 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
+func (la *LineArray) insert(pos Loc, value []byte) {
+ x, y := runeToByteIndex(pos.X, la.lines[pos.Y].data), pos.Y
+ for i := 0; i < len(value); i++ {
+ if value[i] == '\n' {
+ la.split(Loc{x, y})
+ x = 0
+ y++
+ continue
+ }
+ la.insertByte(Loc{x, y}, value[i])
+ x++
+ }
+}
+
+// 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) {
+ la.insert(Loc{len(la.lines[a].data), a}, la.lines[b].data)
+ la.deleteLine(b)
+}
+
+// split splits a line at a given position
+func (la *LineArray) split(pos Loc) {
+ la.newlineBelow(pos.Y)
+ la.insert(Loc{0, pos.Y + 1}, la.lines[pos.Y].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})
+}
+
+// removes from start to end
+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)
+ if start.Y == end.Y {
+ 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.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) {
+ 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) {
+ la.lines[pos.Y].data = la.lines[pos.Y].data[pos.X+1:]
+}
+
+// deleteLine deletes the line number
+func (la *LineArray) deleteLine(y int) {
+ la.lines = la.lines[:y+copy(la.lines[y:], la.lines[y+1:])]
+}
+
+// DeleteByte deletes the byte at a position
+func (la *LineArray) deleteByte(pos Loc) {
+ la.lines[pos.Y].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) []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 la.lines[start.Y].data[startX:endX]
+ }
+ 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 = append(str, la.lines[i].data...)
+ str = append(str, '\n')
+ }
+ str = append(str, la.lines[end.Y].data[:endX]...)
+ return str
+}
+
+// State gets the highlight state for the given line number
+func (la *LineArray) State(lineN int) highlight.State {
+ return la.lines[lineN].state
+}
+
+// SetState sets the highlight state at the given line number
+func (la *LineArray) SetState(lineN int, s highlight.State) {
+ la.lines[lineN].state = s
+}
+
+// SetMatch sets the match at the given line number
+func (la *LineArray) SetMatch(lineN int, m highlight.LineMatch) {
+ la.lines[lineN].match = m
+}
+
+// Match retrieves the match for the given line number
+func (la *LineArray) Match(lineN int) highlight.LineMatch {
+ return la.lines[lineN].match
+}
--- /dev/null
+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))
+}
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
}
-// Diff returns the distance between two locations
-func Diff(a, b Loc, buf *Buffer) int {
- if a.Y == b.Y {
- if a.X > b.X {
- return a.X - b.X
- }
- return b.X - a.X
- }
-
- // Make sure a is guaranteed to be less than b
- if b.LessThan(a) {
- a, b = b, a
- }
-
- loc := 0
- for i := a.Y + 1; i < b.Y; i++ {
- // + 1 for the newline
- loc += Count(buf.Line(i)) + 1
- }
- loc += Count(buf.Line(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
+ return l.Y == b.Y && l.X < b.X
}
// GreaterThan returns true if b is bigger
if l.Y > b.Y {
return true
}
- if l.Y == b.Y && l.X > b.X {
- return true
- }
- return false
+ return l.Y == b.Y && l.X > b.X
}
// GreaterEqual returns true if b is greater than or equal to b
if l.Y == b.Y && l.X > b.X {
return true
}
- if l == b {
- return true
- }
- return false
+ return l == b
}
// LessEqual returns true if b is less than or equal to b
if l.Y == b.Y && l.X < b.X {
return true
}
- if l == b {
- 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 {
+ if a.X > b.X {
+ return a.X - b.X
+ }
+ return b.X - a.X
}
- return false
+
+ // Make sure a is guaranteed to be less than b
+ if b.LessThan(a) {
+ a, b = b, a
+ }
+
+ loc := 0
+ for i := a.Y + 1; i < b.Y; i++ {
+ // + 1 for the newline
+ loc += utf8.RuneCount(buf.LineBytes(i)) + 1
+ }
+ loc += utf8.RuneCount(buf.LineBytes(a.Y)) - a.X + b.X + 1
+ return loc
}
// This moves the location one character to the right
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}
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
}
"strings"
"time"
- luar "layeh.com/gopher-luar"
-
lua "github.com/yuin/gopher-lua"
+ luar "layeh.com/gopher-luar"
)
var L *lua.LState
--- /dev/null
+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)
+}
+++ /dev/null
-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
-)
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 (
// 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
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
-)
-// 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
-}
+ // 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")
+)
// InitConfigDir finds the configuration directory for micro according to the XDG spec.
// If no directory is found, it creates one.
// 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")
// 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()
-
- // Some encoding stuff in case the user isn't using UTF-8
- encoding.Register()
- tcell.SetEncodingFallback(tcell.EncodingFallbackASCII)
+func main() {
+ var err error
- // 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() {
}
}()
- // 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)
- }
-
- t.Resize()
- }
- }
+ width, height := screen.Size()
- for k, v := range optionFlags {
- if *v != "" {
- SetOption(k, *v)
- }
- }
+ w := NewWindow(0, 0, width/2, height/2, b)
- // 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)
- }
+ for i := 0; i < 5; i++ {
+ screen.Clear()
+ w.DisplayBuffer()
+ w.DisplayStatusLine()
+ screen.Show()
+ time.Sleep(200 * time.Millisecond)
+ w.StartLine++
}
- 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
- }
- }
- }()
+ // time.Sleep(2 * time.Second)
- 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()
}
+++ /dev/null
-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
- }
- }
-}
+++ /dev/null
-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()
-}
+++ /dev/null
-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)
- }
-}
--- /dev/null
+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)
+}
)
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
}
// allFiles contains all available files, mapped by filetype
-var allFiles map[string][]RuntimeFile
+var allFiles [NumTypes][]RuntimeFile
// some file on filesystem
type realFile 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 {
// 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
// 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
}
// 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)
}
}
// 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)
}
// 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 {
}
// 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))
}
// 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)
}
// 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)})
}
--- /dev/null
+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)
+}
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(
+++ /dev/null
-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))
-}
+++ /dev/null
-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()
- }
-}
package main
import (
- "crypto/md5"
"encoding/json"
"errors"
- "io"
"io/ioutil"
"os"
"reflect"
- "strconv"
"strings"
- "sync"
"github.com/flynn/json5"
"github.com/zyedidia/glob"
// 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{
"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:] {
} 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
}
}
}
}
+ 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
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)
- }
- return GetGlobalOption(name)
-}
-
-// DefaultGlobalSettings returns the default global settings for micro
-// Note that colorscheme is a global only option
-func DefaultGlobalSettings() map[string]interface{} {
+// 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),
- "colorscheme": "default",
"cursorline": true,
"eofnewline": false,
"fastdirty": true,
"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),
"splitbottom": true,
"splitright": true,
"statusline": true,
- "sucmd": "sudo",
"syntax": true,
"tabmovement": false,
"tabsize": float64(4),
"tabstospaces": false,
- "termtitle": false,
"useprimary": true,
}
}
+// DefaultGlobalSettings returns the default global settings for micro
+// Note that colorscheme is a global only option
+func DefaultGlobalSettings() map[string]interface{} {
+ 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 {
+++ /dev/null
-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
- }
-}
+++ /dev/null
-// +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
-}
+++ /dev/null
-// +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")
-}
+++ /dev/null
-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 + "]"
-}
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
}
}
// 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--
}
// 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
}
--- /dev/null
+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)
+}
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
-// Display draws the statusline to the screen
-func (sline *Statusline) Display() {
- if messenger.hasPrompt && !GetGlobalOption("infobar").(bool) {
- return
- }
+ win *Window
+}
- // We'll draw the line at the lowest line in the view
- y := sline.view.Height + sline.view.y
+// TODO: plugin modify status line formatter
- file := sline.view.Buf.GetName()
- if sline.view.Buf.Settings["basename"].(bool) {
- file = path.Base(file)
+// 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
+}
- // If the buffer is dirty (has been modified) write a little '+'
- if sline.view.Buf.Modified() {
- file += " +"
+// 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"
+}
- // 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 + ")"
+var formatParser = regexp.MustCompile(`\$\(.+?\)`)
- // Add the filetype
- file += " " + sline.view.Buf.FileType()
+// Display draws the statusline to the screen
+func (s *StatusLine) Display() {
+ // TODO: don't display if infobar off and has message
+ // if !GetGlobalOption("infobar").(bool) {
+ // return
+ // }
- file += " " + sline.view.Buf.Settings["fileformat"].(string)
+ // We'll draw the line at the lowest line in the window
+ y := s.win.Height + s.win.Y
- 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"
- }
- }
- if len(helpBinding) > 0 {
- if len(kmenuBinding) > 0 {
- rightText += ", "
- }
- if sline.view.Type == vtHelp {
- rightText += helpBinding + ": close help"
- } else {
- rightText += helpBinding + ": open help"
- }
+ 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))
}
- 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)
-
- if sline.view.Type == vtTerm {
- fileRunes = []rune(sline.view.term.title)
- rightText = ""
- }
+ leftLen := utf8.RuneCount(leftText)
+ rightLen := utf8.RuneCount(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)
}
}
}
+++ /dev/null
-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)
- }
- }
-}
+++ /dev/null
-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)
- }
-}
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
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
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
// FSize gets the size of a file
func FSize(f *os.File) int64 {
fi, _ := f.Stat()
- // get the size
return fi.Size()
}
// 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
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
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 `:`
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
}
import (
"testing"
-)
-
-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")
- }
-}
+ "github.com/stretchr/testify/assert"
+)
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)
- }
-}
-
-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")
+ bytes := []byte("\tPot să \tmănânc sticlă și ea nu mă rănește.")
- 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])
+ n := StringWidth(bytes, 23, 4)
+ assert.Equal(t, 26, n)
}
-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")
+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)
- assertEqual(t, path, "C:\\myfile")
- assertEqual(t, "10", cursorPosition[0])
- assertEqual(t, "0", cursorPosition[1])
+ slc, n = SliceVisualEnd(s, 1, 4)
+ assert.Equal(t, []byte("\thello"), slc)
+ assert.Equal(t, 1, n)
- path, cursorPosition = GetPathAndCursorPosition("C:/myfile:10")
+ slc, n = SliceVisualEnd(s, 4, 4)
+ assert.Equal(t, []byte("hello"), slc)
+ assert.Equal(t, 0, n)
- 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)
+ slc, n = SliceVisualEnd(s, 5, 4)
+ assert.Equal(t, []byte("ello"), slc)
+ assert.Equal(t, 0, n)
}
+++ /dev/null
-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))
- }
- }
-}
--- /dev/null
+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()
+}