Start refactor

This commit is contained in:
Zachary Yedidia 2018-08-25 23:06:44 -04:00
parent d9735e5c3b
commit dc68183fc1
51 changed files with 1621 additions and 10822 deletions

3
.gitignore vendored
View File

@ -7,3 +7,6 @@ tmp.sh
test/
.idea/
packages/
todo.txt
test.txt
log.txt

View File

@ -9,28 +9,29 @@ ADDITIONAL_GO_LINKER_FLAGS := $(shell GOOS=$(shell go env GOHOSTOS) \
GOARCH=$(shell go env GOHOSTARCH) \
go run tools/info-plist.go "$(VERSION)")
GOBIN ?= $(shell go env GOPATH)/bin
GOVARS := -X main.Version=$(VERSION) -X main.CommitHash=$(HASH) -X 'main.CompileDate=$(DATE)' -X main.Debug=OFF
# Builds micro after checking dependencies but without updating the runtime
build: update
go build -ldflags "-s -w -X main.Version=$(VERSION) -X main.CommitHash=$(HASH) -X 'main.CompileDate=$(DATE)' $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro
go build -ldflags "-s -w $(GOVARS) $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro
# Builds micro after building the runtime and checking dependencies
build-all: runtime build
# Builds micro without checking for dependencies
build-quick:
go build -ldflags "-s -w -X main.Version=$(VERSION) -X main.CommitHash=$(HASH) -X 'main.CompileDate=$(DATE)' $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro
go build -ldflags "-s -w $(GOVARS) $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro
# Same as 'build' but installs to $GOBIN afterward
install: update
go install -ldflags "-s -w -X main.Version=$(VERSION) -X main.CommitHash=$(HASH) -X 'main.CompileDate=$(DATE)' $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro
go install -ldflags "-s -w $(GOVARS) $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro
# Same as 'build-all' but installs to $GOBIN afterward
install-all: runtime install
# Same as 'build-quick' but installs to $GOBIN afterward
install-quick:
go install -ldflags "-s -w -X main.Version=$(VERSION) -X main.CommitHash=$(HASH) -X 'main.CompileDate=$(DATE)' $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro
go install -ldflags "-s -w $(GOVARS) $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro
update:
git pull

View File

@ -0,0 +1,40 @@
package main
import "time"
// The ActionHandler connects the buffer and the window
// It provides a cursor (or multiple) and defines a set of actions
// that can be taken on the buffer
// The ActionHandler can access the window for necessary info about
// visual positions for mouse clicks and scrolling
type ActionHandler struct {
Buf *Buffer
Win *Window
// Since tcell doesn't differentiate between a mouse release event
// and a mouse move event with no keys pressed, we need to keep
// track of whether or not the mouse was pressed (or not released) last event to determine
// mouse release events
mouseReleased bool
// We need to keep track of insert key press toggle
isOverwriteMode bool
// This stores when the last click was
// This is useful for detecting double and triple clicks
lastClickTime time.Time
lastLoc Loc
// lastCutTime stores when the last ctrl+k was issued.
// It is used for clearing the clipboard to replace it with fresh cut lines.
lastCutTime time.Time
// freshClip returns true if the clipboard has never been pasted.
freshClip bool
// Was the last mouse event actually a double click?
// Useful for detecting triple clicks -- if a double click is detected
// but the last mouse event was actually a double click, it's a triple click
doubleClick bool
// Same here, just to keep track for mouse move events
tripleClick bool
}

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +0,0 @@
// +build plan9 nacl windows
package main
func (v *View) Suspend(usePlugin bool) bool {
messenger.Error("Suspend is only supported on Posix")
return false
}

View File

@ -1,37 +0,0 @@
// +build linux darwin dragonfly solaris openbsd netbsd freebsd
package main
import "syscall"
// Suspend sends micro to the background. This is the same as pressing CtrlZ in most unix programs.
// This only works on linux and has no default binding.
// This code was adapted from the suspend code in nsf/godit
func (v *View) Suspend(usePlugin bool) bool {
if usePlugin && !PreActionCall("Suspend", v) {
return false
}
screenWasNil := screen == nil
if !screenWasNil {
screen.Fini()
screen = nil
}
// suspend the process
pid := syscall.Getpid()
err := syscall.Kill(pid, syscall.SIGSTOP)
if err != nil {
TermMessage(err)
}
if !screenWasNil {
InitScreen()
}
if usePlugin {
return PostActionCall("Suspend", v)
}
return true
}

View File

@ -1,249 +0,0 @@
package main
import (
"io/ioutil"
"os"
"strings"
)
var pluginCompletions []func(string) []string
// This file is meant (for now) for autocompletion in command mode, not
// while coding. This helps micro autocomplete commands and then filenames
// for example with `vsplit filename`.
// FileComplete autocompletes filenames
func FileComplete(input string) (string, []string) {
var sep string = string(os.PathSeparator)
dirs := strings.Split(input, sep)
var files []os.FileInfo
var err error
if len(dirs) > 1 {
directories := strings.Join(dirs[:len(dirs)-1], sep) + sep
directories = ReplaceHome(directories)
files, err = ioutil.ReadDir(directories)
} else {
files, err = ioutil.ReadDir(".")
}
var suggestions []string
if err != nil {
return "", suggestions
}
for _, f := range files {
name := f.Name()
if f.IsDir() {
name += sep
}
if strings.HasPrefix(name, dirs[len(dirs)-1]) {
suggestions = append(suggestions, name)
}
}
var chosen string
if len(suggestions) == 1 {
if len(dirs) > 1 {
chosen = strings.Join(dirs[:len(dirs)-1], sep) + sep + suggestions[0]
} else {
chosen = suggestions[0]
}
} else {
if len(dirs) > 1 {
chosen = strings.Join(dirs[:len(dirs)-1], sep) + sep
}
}
return chosen, suggestions
}
// CommandComplete autocompletes commands
func CommandComplete(input string) (string, []string) {
var suggestions []string
for cmd := range commands {
if strings.HasPrefix(cmd, input) {
suggestions = append(suggestions, cmd)
}
}
var chosen string
if len(suggestions) == 1 {
chosen = suggestions[0]
}
return chosen, suggestions
}
// HelpComplete autocompletes help topics
func HelpComplete(input string) (string, []string) {
var suggestions []string
for _, file := range ListRuntimeFiles(RTHelp) {
topic := file.Name()
if strings.HasPrefix(topic, input) {
suggestions = append(suggestions, topic)
}
}
var chosen string
if len(suggestions) == 1 {
chosen = suggestions[0]
}
return chosen, suggestions
}
// ColorschemeComplete tab-completes names of colorschemes.
func ColorschemeComplete(input string) (string, []string) {
var suggestions []string
files := ListRuntimeFiles(RTColorscheme)
for _, f := range files {
if strings.HasPrefix(f.Name(), input) {
suggestions = append(suggestions, f.Name())
}
}
var chosen string
if len(suggestions) == 1 {
chosen = suggestions[0]
}
return chosen, suggestions
}
func contains(s []string, e string) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
}
// OptionComplete autocompletes options
func OptionComplete(input string) (string, []string) {
var suggestions []string
localSettings := DefaultLocalSettings()
for option := range globalSettings {
if strings.HasPrefix(option, input) {
suggestions = append(suggestions, option)
}
}
for option := range localSettings {
if strings.HasPrefix(option, input) && !contains(suggestions, option) {
suggestions = append(suggestions, option)
}
}
var chosen string
if len(suggestions) == 1 {
chosen = suggestions[0]
}
return chosen, suggestions
}
// OptionValueComplete completes values for various options
func OptionValueComplete(inputOpt, input string) (string, []string) {
inputOpt = strings.TrimSpace(inputOpt)
var suggestions []string
localSettings := DefaultLocalSettings()
var optionVal interface{}
for k, option := range globalSettings {
if k == inputOpt {
optionVal = option
}
}
for k, option := range localSettings {
if k == inputOpt {
optionVal = option
}
}
switch optionVal.(type) {
case bool:
if strings.HasPrefix("on", input) {
suggestions = append(suggestions, "on")
} else if strings.HasPrefix("true", input) {
suggestions = append(suggestions, "true")
}
if strings.HasPrefix("off", input) {
suggestions = append(suggestions, "off")
} else if strings.HasPrefix("false", input) {
suggestions = append(suggestions, "false")
}
case string:
switch inputOpt {
case "colorscheme":
_, suggestions = ColorschemeComplete(input)
case "fileformat":
if strings.HasPrefix("unix", input) {
suggestions = append(suggestions, "unix")
}
if strings.HasPrefix("dos", input) {
suggestions = append(suggestions, "dos")
}
case "sucmd":
if strings.HasPrefix("sudo", input) {
suggestions = append(suggestions, "sudo")
}
if strings.HasPrefix("doas", input) {
suggestions = append(suggestions, "doas")
}
}
}
var chosen string
if len(suggestions) == 1 {
chosen = suggestions[0]
}
return chosen, suggestions
}
// MakeCompletion registers a function from a plugin for autocomplete commands
func MakeCompletion(function string) Completion {
pluginCompletions = append(pluginCompletions, LuaFunctionComplete(function))
return Completion(-len(pluginCompletions))
}
// PluginComplete autocompletes from plugin function
func PluginComplete(complete Completion, input string) (chosen string, suggestions []string) {
idx := int(-complete) - 1
if len(pluginCompletions) <= idx {
return "", nil
}
suggestions = pluginCompletions[idx](input)
if len(suggestions) == 1 {
chosen = suggestions[0]
}
return
}
// PluginCmdComplete completes with possible choices for the `> plugin` command
func PluginCmdComplete(input string) (chosen string, suggestions []string) {
for _, cmd := range []string{"install", "remove", "search", "update", "list"} {
if strings.HasPrefix(cmd, input) {
suggestions = append(suggestions, cmd)
}
}
if len(suggestions) == 1 {
chosen = suggestions[0]
}
return chosen, suggestions
}
// PluginnameComplete completes with the names of loaded plugins
func PluginNameComplete(input string) (chosen string, suggestions []string) {
for _, pp := range GetAllPluginPackages() {
if strings.HasPrefix(pp.Name, input) {
suggestions = append(suggestions, pp.Name)
}
}
if len(suggestions) == 1 {
chosen = suggestions[0]
}
return chosen, suggestions
}

View File

@ -1,616 +0,0 @@
package main
import (
"fmt"
"io/ioutil"
"os"
"strings"
"unicode"
"github.com/flynn/json5"
"github.com/zyedidia/tcell"
)
var bindingsStr map[string]string
var bindings map[Key][]func(*View, bool) bool
var mouseBindings map[Key][]func(*View, bool, *tcell.EventMouse) bool
var helpBinding string
var kmenuBinding string
var mouseBindingActions = map[string]func(*View, bool, *tcell.EventMouse) bool{
"MousePress": (*View).MousePress,
"MouseMultiCursor": (*View).MouseMultiCursor,
}
var bindingActions = map[string]func(*View, bool) bool{
"CursorUp": (*View).CursorUp,
"CursorDown": (*View).CursorDown,
"CursorPageUp": (*View).CursorPageUp,
"CursorPageDown": (*View).CursorPageDown,
"CursorLeft": (*View).CursorLeft,
"CursorRight": (*View).CursorRight,
"CursorStart": (*View).CursorStart,
"CursorEnd": (*View).CursorEnd,
"SelectToStart": (*View).SelectToStart,
"SelectToEnd": (*View).SelectToEnd,
"SelectUp": (*View).SelectUp,
"SelectDown": (*View).SelectDown,
"SelectLeft": (*View).SelectLeft,
"SelectRight": (*View).SelectRight,
"WordRight": (*View).WordRight,
"WordLeft": (*View).WordLeft,
"SelectWordRight": (*View).SelectWordRight,
"SelectWordLeft": (*View).SelectWordLeft,
"DeleteWordRight": (*View).DeleteWordRight,
"DeleteWordLeft": (*View).DeleteWordLeft,
"SelectLine": (*View).SelectLine,
"SelectToStartOfLine": (*View).SelectToStartOfLine,
"SelectToEndOfLine": (*View).SelectToEndOfLine,
"ParagraphPrevious": (*View).ParagraphPrevious,
"ParagraphNext": (*View).ParagraphNext,
"InsertNewline": (*View).InsertNewline,
"InsertSpace": (*View).InsertSpace,
"Backspace": (*View).Backspace,
"Delete": (*View).Delete,
"InsertTab": (*View).InsertTab,
"Save": (*View).Save,
"SaveAll": (*View).SaveAll,
"SaveAs": (*View).SaveAs,
"Find": (*View).Find,
"FindNext": (*View).FindNext,
"FindPrevious": (*View).FindPrevious,
"Center": (*View).Center,
"Undo": (*View).Undo,
"Redo": (*View).Redo,
"Copy": (*View).Copy,
"Cut": (*View).Cut,
"CutLine": (*View).CutLine,
"DuplicateLine": (*View).DuplicateLine,
"DeleteLine": (*View).DeleteLine,
"MoveLinesUp": (*View).MoveLinesUp,
"MoveLinesDown": (*View).MoveLinesDown,
"IndentSelection": (*View).IndentSelection,
"OutdentSelection": (*View).OutdentSelection,
"OutdentLine": (*View).OutdentLine,
"Paste": (*View).Paste,
"PastePrimary": (*View).PastePrimary,
"SelectAll": (*View).SelectAll,
"OpenFile": (*View).OpenFile,
"Start": (*View).Start,
"End": (*View).End,
"PageUp": (*View).PageUp,
"PageDown": (*View).PageDown,
"SelectPageUp": (*View).SelectPageUp,
"SelectPageDown": (*View).SelectPageDown,
"HalfPageUp": (*View).HalfPageUp,
"HalfPageDown": (*View).HalfPageDown,
"StartOfLine": (*View).StartOfLine,
"EndOfLine": (*View).EndOfLine,
"ToggleHelp": (*View).ToggleHelp,
"ToggleKeyMenu": (*View).ToggleKeyMenu,
"ToggleRuler": (*View).ToggleRuler,
"JumpLine": (*View).JumpLine,
"ClearStatus": (*View).ClearStatus,
"ShellMode": (*View).ShellMode,
"CommandMode": (*View).CommandMode,
"ToggleOverwriteMode": (*View).ToggleOverwriteMode,
"Escape": (*View).Escape,
"Quit": (*View).Quit,
"QuitAll": (*View).QuitAll,
"AddTab": (*View).AddTab,
"PreviousTab": (*View).PreviousTab,
"NextTab": (*View).NextTab,
"NextSplit": (*View).NextSplit,
"PreviousSplit": (*View).PreviousSplit,
"Unsplit": (*View).Unsplit,
"VSplit": (*View).VSplitBinding,
"HSplit": (*View).HSplitBinding,
"ToggleMacro": (*View).ToggleMacro,
"PlayMacro": (*View).PlayMacro,
"Suspend": (*View).Suspend,
"ScrollUp": (*View).ScrollUpAction,
"ScrollDown": (*View).ScrollDownAction,
"SpawnMultiCursor": (*View).SpawnMultiCursor,
"SpawnMultiCursorSelect": (*View).SpawnMultiCursorSelect,
"RemoveMultiCursor": (*View).RemoveMultiCursor,
"RemoveAllMultiCursors": (*View).RemoveAllMultiCursors,
"SkipMultiCursor": (*View).SkipMultiCursor,
"JumpToMatchingBrace": (*View).JumpToMatchingBrace,
// This was changed to InsertNewline but I don't want to break backwards compatibility
"InsertEnter": (*View).InsertNewline,
}
var bindingMouse = map[string]tcell.ButtonMask{
"MouseLeft": tcell.Button1,
"MouseMiddle": tcell.Button2,
"MouseRight": tcell.Button3,
"MouseWheelUp": tcell.WheelUp,
"MouseWheelDown": tcell.WheelDown,
"MouseWheelLeft": tcell.WheelLeft,
"MouseWheelRight": tcell.WheelRight,
}
var bindingKeys = map[string]tcell.Key{
"Up": tcell.KeyUp,
"Down": tcell.KeyDown,
"Right": tcell.KeyRight,
"Left": tcell.KeyLeft,
"UpLeft": tcell.KeyUpLeft,
"UpRight": tcell.KeyUpRight,
"DownLeft": tcell.KeyDownLeft,
"DownRight": tcell.KeyDownRight,
"Center": tcell.KeyCenter,
"PageUp": tcell.KeyPgUp,
"PageDown": tcell.KeyPgDn,
"Home": tcell.KeyHome,
"End": tcell.KeyEnd,
"Insert": tcell.KeyInsert,
"Delete": tcell.KeyDelete,
"Help": tcell.KeyHelp,
"Exit": tcell.KeyExit,
"Clear": tcell.KeyClear,
"Cancel": tcell.KeyCancel,
"Print": tcell.KeyPrint,
"Pause": tcell.KeyPause,
"Backtab": tcell.KeyBacktab,
"F1": tcell.KeyF1,
"F2": tcell.KeyF2,
"F3": tcell.KeyF3,
"F4": tcell.KeyF4,
"F5": tcell.KeyF5,
"F6": tcell.KeyF6,
"F7": tcell.KeyF7,
"F8": tcell.KeyF8,
"F9": tcell.KeyF9,
"F10": tcell.KeyF10,
"F11": tcell.KeyF11,
"F12": tcell.KeyF12,
"F13": tcell.KeyF13,
"F14": tcell.KeyF14,
"F15": tcell.KeyF15,
"F16": tcell.KeyF16,
"F17": tcell.KeyF17,
"F18": tcell.KeyF18,
"F19": tcell.KeyF19,
"F20": tcell.KeyF20,
"F21": tcell.KeyF21,
"F22": tcell.KeyF22,
"F23": tcell.KeyF23,
"F24": tcell.KeyF24,
"F25": tcell.KeyF25,
"F26": tcell.KeyF26,
"F27": tcell.KeyF27,
"F28": tcell.KeyF28,
"F29": tcell.KeyF29,
"F30": tcell.KeyF30,
"F31": tcell.KeyF31,
"F32": tcell.KeyF32,
"F33": tcell.KeyF33,
"F34": tcell.KeyF34,
"F35": tcell.KeyF35,
"F36": tcell.KeyF36,
"F37": tcell.KeyF37,
"F38": tcell.KeyF38,
"F39": tcell.KeyF39,
"F40": tcell.KeyF40,
"F41": tcell.KeyF41,
"F42": tcell.KeyF42,
"F43": tcell.KeyF43,
"F44": tcell.KeyF44,
"F45": tcell.KeyF45,
"F46": tcell.KeyF46,
"F47": tcell.KeyF47,
"F48": tcell.KeyF48,
"F49": tcell.KeyF49,
"F50": tcell.KeyF50,
"F51": tcell.KeyF51,
"F52": tcell.KeyF52,
"F53": tcell.KeyF53,
"F54": tcell.KeyF54,
"F55": tcell.KeyF55,
"F56": tcell.KeyF56,
"F57": tcell.KeyF57,
"F58": tcell.KeyF58,
"F59": tcell.KeyF59,
"F60": tcell.KeyF60,
"F61": tcell.KeyF61,
"F62": tcell.KeyF62,
"F63": tcell.KeyF63,
"F64": tcell.KeyF64,
"CtrlSpace": tcell.KeyCtrlSpace,
"CtrlA": tcell.KeyCtrlA,
"CtrlB": tcell.KeyCtrlB,
"CtrlC": tcell.KeyCtrlC,
"CtrlD": tcell.KeyCtrlD,
"CtrlE": tcell.KeyCtrlE,
"CtrlF": tcell.KeyCtrlF,
"CtrlG": tcell.KeyCtrlG,
"CtrlH": tcell.KeyCtrlH,
"CtrlI": tcell.KeyCtrlI,
"CtrlJ": tcell.KeyCtrlJ,
"CtrlK": tcell.KeyCtrlK,
"CtrlL": tcell.KeyCtrlL,
"CtrlM": tcell.KeyCtrlM,
"CtrlN": tcell.KeyCtrlN,
"CtrlO": tcell.KeyCtrlO,
"CtrlP": tcell.KeyCtrlP,
"CtrlQ": tcell.KeyCtrlQ,
"CtrlR": tcell.KeyCtrlR,
"CtrlS": tcell.KeyCtrlS,
"CtrlT": tcell.KeyCtrlT,
"CtrlU": tcell.KeyCtrlU,
"CtrlV": tcell.KeyCtrlV,
"CtrlW": tcell.KeyCtrlW,
"CtrlX": tcell.KeyCtrlX,
"CtrlY": tcell.KeyCtrlY,
"CtrlZ": tcell.KeyCtrlZ,
"CtrlLeftSq": tcell.KeyCtrlLeftSq,
"CtrlBackslash": tcell.KeyCtrlBackslash,
"CtrlRightSq": tcell.KeyCtrlRightSq,
"CtrlCarat": tcell.KeyCtrlCarat,
"CtrlUnderscore": tcell.KeyCtrlUnderscore,
"CtrlPageUp": tcell.KeyCtrlPgUp,
"CtrlPageDown": tcell.KeyCtrlPgDn,
"Tab": tcell.KeyTab,
"Esc": tcell.KeyEsc,
"Escape": tcell.KeyEscape,
"Enter": tcell.KeyEnter,
"Backspace": tcell.KeyBackspace2,
"OldBackspace": tcell.KeyBackspace,
// I renamed these keys to PageUp and PageDown but I don't want to break someone's keybindings
"PgUp": tcell.KeyPgUp,
"PgDown": tcell.KeyPgDn,
}
// The Key struct holds the data for a keypress (keycode + modifiers)
type Key struct {
keyCode tcell.Key
modifiers tcell.ModMask
buttons tcell.ButtonMask
r rune
escape string
}
// InitBindings initializes the keybindings for micro
func InitBindings() {
bindings = make(map[Key][]func(*View, bool) bool)
bindingsStr = make(map[string]string)
mouseBindings = make(map[Key][]func(*View, bool, *tcell.EventMouse) bool)
var parsed map[string]string
defaults := DefaultBindings()
filename := configDir + "/bindings.json"
if _, e := os.Stat(filename); e == nil {
input, err := ioutil.ReadFile(filename)
if err != nil {
TermMessage("Error reading bindings.json file: " + err.Error())
return
}
err = json5.Unmarshal(input, &parsed)
if err != nil {
TermMessage("Error reading bindings.json:", err.Error())
}
}
parseBindings(defaults)
parseBindings(parsed)
}
func parseBindings(userBindings map[string]string) {
for k, v := range userBindings {
BindKey(k, v)
}
}
// findKey will find binding Key 'b' using string 'k'
func findKey(k string) (b Key, ok bool) {
modifiers := tcell.ModNone
// First, we'll strip off all the modifiers in the name and add them to the
// ModMask
modSearch:
for {
switch {
case strings.HasPrefix(k, "-"):
// We optionally support dashes between modifiers
k = k[1:]
case strings.HasPrefix(k, "Ctrl") && k != "CtrlH":
// CtrlH technically does not have a 'Ctrl' modifier because it is really backspace
k = k[4:]
modifiers |= tcell.ModCtrl
case strings.HasPrefix(k, "Alt"):
k = k[3:]
modifiers |= tcell.ModAlt
case strings.HasPrefix(k, "Shift"):
k = k[5:]
modifiers |= tcell.ModShift
case strings.HasPrefix(k, "\x1b"):
return Key{
keyCode: -1,
modifiers: modifiers,
buttons: -1,
r: 0,
escape: k,
}, true
default:
break modSearch
}
}
if len(k) == 0 {
return Key{buttons: -1}, false
}
// Control is handled specially, since some character codes in bindingKeys
// are different when Control is depressed. We should check for Control keys
// first.
if modifiers&tcell.ModCtrl != 0 {
// see if the key is in bindingKeys with the Ctrl prefix.
k = string(unicode.ToUpper(rune(k[0]))) + k[1:]
if code, ok := bindingKeys["Ctrl"+k]; ok {
// It is, we're done.
return Key{
keyCode: code,
modifiers: modifiers,
buttons: -1,
r: 0,
}, true
}
}
// See if we can find the key in bindingKeys
if code, ok := bindingKeys[k]; ok {
return Key{
keyCode: code,
modifiers: modifiers,
buttons: -1,
r: 0,
}, true
}
// See if we can find the key in bindingMouse
if code, ok := bindingMouse[k]; ok {
return Key{
modifiers: modifiers,
buttons: code,
r: 0,
}, true
}
// If we were given one character, then we've got a rune.
if len(k) == 1 {
return Key{
keyCode: tcell.KeyRune,
modifiers: modifiers,
buttons: -1,
r: rune(k[0]),
}, true
}
// We don't know what happened.
return Key{buttons: -1}, false
}
// findAction will find 'action' using string 'v'
func findAction(v string) (action func(*View, bool) bool) {
action, ok := bindingActions[v]
if !ok {
// If the user seems to be binding a function that doesn't exist
// We hope that it's a lua function that exists and bind it to that
action = LuaFunctionBinding(v)
}
return action
}
func findMouseAction(v string) func(*View, bool, *tcell.EventMouse) bool {
action, ok := mouseBindingActions[v]
if !ok {
// If the user seems to be binding a function that doesn't exist
// We hope that it's a lua function that exists and bind it to that
action = LuaFunctionMouseBinding(v)
}
return action
}
// TryBindKey tries to bind a key by writing to configDir/bindings.json
// This function is unused for now
func TryBindKey(k, v string) {
filename := configDir + "/bindings.json"
if _, e := os.Stat(filename); e == nil {
input, err := ioutil.ReadFile(filename)
if err != nil {
TermMessage("Error reading bindings.json file: " + err.Error())
return
}
conflict := -1
lines := strings.Split(string(input), "\n")
for i, l := range lines {
parts := strings.Split(l, ":")
if len(parts) >= 2 {
if strings.Contains(parts[0], k) {
conflict = i
TermMessage("Warning: Keybinding conflict:", k, " has been overwritten")
}
}
}
binding := fmt.Sprintf(" \"%s\": \"%s\",", k, v)
if conflict == -1 {
lines = append([]string{lines[0], binding}, lines[conflict:]...)
} else {
lines = append(append(lines[:conflict], binding), lines[conflict+1:]...)
}
txt := strings.Join(lines, "\n")
err = ioutil.WriteFile(filename, []byte(txt), 0644)
if err != nil {
TermMessage("Error")
}
}
}
// BindKey takes a key and an action and binds the two together
func BindKey(k, v string) {
key, ok := findKey(k)
if !ok {
TermMessage("Unknown keybinding: " + k)
return
}
if v == "ToggleHelp" {
helpBinding = k
}
if v == "ToggleKeyMenu" {
kmenuBinding = k
}
if helpBinding == k && v != "ToggleHelp" {
helpBinding = ""
}
if kmenuBinding == k && v != "ToggleKeyMenu" {
kmenuBinding = ""
}
actionNames := strings.Split(v, ",")
if actionNames[0] == "UnbindKey" {
delete(bindings, key)
delete(mouseBindings, key)
delete(bindingsStr, k)
if len(actionNames) == 1 {
return
}
actionNames = append(actionNames[:0], actionNames[1:]...)
}
actions := make([]func(*View, bool) bool, 0, len(actionNames))
mouseActions := make([]func(*View, bool, *tcell.EventMouse) bool, 0, len(actionNames))
for _, actionName := range actionNames {
if strings.HasPrefix(actionName, "Mouse") {
mouseActions = append(mouseActions, findMouseAction(actionName))
} else if strings.HasPrefix(actionName, "command:") {
cmd := strings.SplitN(actionName, ":", 2)[1]
actions = append(actions, CommandAction(cmd))
} else if strings.HasPrefix(actionName, "command-edit:") {
cmd := strings.SplitN(actionName, ":", 2)[1]
actions = append(actions, CommandEditAction(cmd))
} else {
actions = append(actions, findAction(actionName))
}
}
if len(actions) > 0 {
// Can't have a binding be both mouse and normal
delete(mouseBindings, key)
bindings[key] = actions
bindingsStr[k] = v
} else if len(mouseActions) > 0 {
// Can't have a binding be both mouse and normal
delete(bindings, key)
mouseBindings[key] = mouseActions
}
}
// DefaultBindings returns a map containing micro's default keybindings
func DefaultBindings() map[string]string {
return map[string]string{
"Up": "CursorUp",
"Down": "CursorDown",
"Right": "CursorRight",
"Left": "CursorLeft",
"ShiftUp": "SelectUp",
"ShiftDown": "SelectDown",
"ShiftLeft": "SelectLeft",
"ShiftRight": "SelectRight",
"AltLeft": "WordLeft",
"AltRight": "WordRight",
"AltUp": "MoveLinesUp",
"AltDown": "MoveLinesDown",
"AltShiftRight": "SelectWordRight",
"AltShiftLeft": "SelectWordLeft",
"CtrlLeft": "StartOfLine",
"CtrlRight": "EndOfLine",
"CtrlShiftLeft": "SelectToStartOfLine",
"ShiftHome": "SelectToStartOfLine",
"CtrlShiftRight": "SelectToEndOfLine",
"ShiftEnd": "SelectToEndOfLine",
"CtrlUp": "CursorStart",
"CtrlDown": "CursorEnd",
"CtrlShiftUp": "SelectToStart",
"CtrlShiftDown": "SelectToEnd",
"Alt-{": "ParagraphPrevious",
"Alt-}": "ParagraphNext",
"Enter": "InsertNewline",
"CtrlH": "Backspace",
"Backspace": "Backspace",
"Alt-CtrlH": "DeleteWordLeft",
"Alt-Backspace": "DeleteWordLeft",
"Tab": "IndentSelection,InsertTab",
"Backtab": "OutdentSelection,OutdentLine",
"CtrlO": "OpenFile",
"CtrlS": "Save",
"CtrlF": "Find",
"CtrlN": "FindNext",
"CtrlP": "FindPrevious",
"CtrlZ": "Undo",
"CtrlY": "Redo",
"CtrlC": "Copy",
"CtrlX": "Cut",
"CtrlK": "CutLine",
"CtrlD": "DuplicateLine",
"CtrlV": "Paste",
"CtrlA": "SelectAll",
"CtrlT": "AddTab",
"Alt,": "PreviousTab",
"Alt.": "NextTab",
"Home": "StartOfLine",
"End": "EndOfLine",
"CtrlHome": "CursorStart",
"CtrlEnd": "CursorEnd",
"PageUp": "CursorPageUp",
"PageDown": "CursorPageDown",
"CtrlPageUp": "PreviousTab",
"CtrlPageDown": "NextTab",
"CtrlG": "ToggleHelp",
"Alt-g": "ToggleKeyMenu",
"CtrlR": "ToggleRuler",
"CtrlL": "JumpLine",
"Delete": "Delete",
"CtrlB": "ShellMode",
"CtrlQ": "Quit",
"CtrlE": "CommandMode",
"CtrlW": "NextSplit",
"CtrlU": "ToggleMacro",
"CtrlJ": "PlayMacro",
"Insert": "ToggleOverwriteMode",
// Emacs-style keybindings
"Alt-f": "WordRight",
"Alt-b": "WordLeft",
"Alt-a": "StartOfLine",
"Alt-e": "EndOfLine",
// "Alt-p": "CursorUp",
// "Alt-n": "CursorDown",
// Integration with file managers
"F2": "Save",
"F3": "Find",
"F4": "Quit",
"F7": "Find",
"F10": "Quit",
"Esc": "Escape",
// Mouse bindings
"MouseWheelUp": "ScrollUp",
"MouseWheelDown": "ScrollDown",
"MouseLeft": "MousePress",
"MouseMiddle": "PastePrimary",
"Ctrl-MouseLeft": "MouseMultiCursor",
"Alt-n": "SpawnMultiCursor",
"Alt-m": "SpawnMultiCursorSelect",
"Alt-p": "RemoveMultiCursor",
"Alt-c": "RemoveAllMultiCursors",
"Alt-x": "SkipMultiCursor",
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,117 +0,0 @@
package main
import (
"testing"
)
func TestGetBufferCursorLocationEmptyArgs(t *testing.T) {
buf := NewBufferFromString("this is my\nbuffer\nfile\nhello", "")
location, err := GetBufferCursorLocation(nil, buf)
assertEqual(t, 0, location.Y)
assertEqual(t, 0, location.X)
// an error is present due to the cursorLocation being nil
assertTrue(t, err != nil)
}
func TestGetBufferCursorLocationStartposFlag(t *testing.T) {
buf := NewBufferFromString("this is my\nbuffer\nfile\nhello", "")
*flagStartPos = "1,2"
location, err := GetBufferCursorLocation(nil, buf)
// note: 1 is subtracted from the line to get the correct index in the buffer
assertTrue(t, 0 == location.Y)
assertTrue(t, 2 == location.X)
// an error is present due to the cursorLocation being nil
assertTrue(t, err != nil)
}
func TestGetBufferCursorLocationInvalidStartposFlag(t *testing.T) {
buf := NewBufferFromString("this is my\nbuffer\nfile\nhello", "")
*flagStartPos = "apples,2"
location, err := GetBufferCursorLocation(nil, buf)
// expect to default to the start of the file, which is 0,0
assertEqual(t, 0, location.Y)
assertEqual(t, 0, location.X)
// an error is present due to the cursorLocation being nil
assertTrue(t, err != nil)
}
func TestGetBufferCursorLocationStartposFlagAndCursorPosition(t *testing.T) {
text := "this is my\nbuffer\nfile\nhello"
cursorPosition := []string{"3", "1"}
buf := NewBufferFromString(text, "")
*flagStartPos = "1,2"
location, err := GetBufferCursorLocation(cursorPosition, buf)
// expect to have the flag positions, not the cursor position
// note: 1 is subtracted from the line to get the correct index in the buffer
assertEqual(t, 0, location.Y)
assertEqual(t, 2, location.X)
assertTrue(t, err == nil)
}
func TestGetBufferCursorLocationCursorPositionAndInvalidStartposFlag(t *testing.T) {
text := "this is my\nbuffer\nfile\nhello"
cursorPosition := []string{"3", "1"}
buf := NewBufferFromString(text, "")
*flagStartPos = "apples,2"
location, err := GetBufferCursorLocation(cursorPosition, buf)
// expect to have the flag positions, not the cursor position
// note: 1 is subtracted from the line to get the correct index in the buffer
assertEqual(t, 2, location.Y)
assertEqual(t, 1, location.X)
// no errors this time as cursorPosition is not nil
assertTrue(t, err == nil)
}
func TestGetBufferCursorLocationNoErrorWhenOverflowWithStartpos(t *testing.T) {
text := "this is my\nbuffer\nfile\nhello"
buf := NewBufferFromString(text, "")
*flagStartPos = "50,50"
location, err := GetBufferCursorLocation(nil, buf)
// expect to have the flag positions, not the cursor position
assertEqual(t, buf.NumLines-1, location.Y)
assertEqual(t, 5, location.X)
// error is expected as cursorPosition is nil
assertTrue(t, err != nil)
}
func TestGetBufferCursorLocationNoErrorWhenOverflowWithCursorPosition(t *testing.T) {
text := "this is my\nbuffer\nfile\nhello"
cursorPosition := []string{"50", "2"}
*flagStartPos = ""
buf := NewBufferFromString(text, "")
location, err := GetBufferCursorLocation(cursorPosition, buf)
// expect to have the flag positions, not the cursor position
assertEqual(t, buf.NumLines-1, location.Y)
assertEqual(t, 2, location.X)
// error is expected as cursorPosition is nil
assertTrue(t, err == nil)
}
//func TestGetBufferCursorLocationColonArgs(t *testing.T) {
// buf := new(Buffer)
//}

View File

@ -1,238 +0,0 @@
package main
import (
"github.com/mattn/go-runewidth"
"github.com/zyedidia/tcell"
)
func min(a, b int) int {
if a <= b {
return a
}
return b
}
func visualToCharPos(visualIndex int, lineN int, str string, buf *Buffer, tabsize int) (int, int, *tcell.Style) {
charPos := 0
var lineIdx int
var lastWidth int
var style *tcell.Style
var width int
var rw int
for i, c := range str {
// width := StringWidth(str[:i], tabsize)
if group, ok := buf.Match(lineN)[charPos]; ok {
s := GetColor(group.String())
style = &s
}
if width >= visualIndex {
return charPos, visualIndex - lastWidth, style
}
if i != 0 {
charPos++
lineIdx += rw
}
lastWidth = width
rw = 0
if c == '\t' {
rw = tabsize - (lineIdx % tabsize)
width += rw
} else {
rw = runewidth.RuneWidth(c)
width += rw
}
}
return -1, -1, style
}
type Char struct {
visualLoc Loc
realLoc Loc
char rune
// The actual character that is drawn
// This is only different from char if it's for example hidden character
drawChar rune
style tcell.Style
width int
}
type CellView struct {
lines [][]*Char
}
func (c *CellView) Draw(buf *Buffer, top, height, left, width int) {
if width <= 0 {
return
}
matchingBrace := Loc{-1, -1}
// bracePairs is defined in buffer.go
if buf.Settings["matchbrace"].(bool) {
for _, bp := range bracePairs {
curX := buf.Cursor.X
curLoc := buf.Cursor.Loc
if buf.Settings["matchbraceleft"].(bool) {
if curX > 0 {
curX--
curLoc = curLoc.Move(-1, buf)
}
}
r := buf.Cursor.RuneUnder(curX)
if r == bp[0] || r == bp[1] {
matchingBrace = buf.FindMatchingBrace(bp, curLoc)
}
}
}
tabsize := int(buf.Settings["tabsize"].(float64))
softwrap := buf.Settings["softwrap"].(bool)
indentrunes := []rune(buf.Settings["indentchar"].(string))
// if empty indentchar settings, use space
if indentrunes == nil || len(indentrunes) == 0 {
indentrunes = []rune{' '}
}
indentchar := indentrunes[0]
start := buf.Cursor.Y
if buf.Settings["syntax"].(bool) && buf.syntaxDef != nil {
if start > 0 && buf.lines[start-1].rehighlight {
buf.highlighter.ReHighlightLine(buf, start-1)
buf.lines[start-1].rehighlight = false
}
buf.highlighter.ReHighlightStates(buf, start)
buf.highlighter.HighlightMatches(buf, top, top+height)
}
c.lines = make([][]*Char, 0)
viewLine := 0
lineN := top
curStyle := defStyle
for viewLine < height {
if lineN >= len(buf.lines) {
break
}
lineStr := buf.Line(lineN)
line := []rune(lineStr)
colN, startOffset, startStyle := visualToCharPos(left, lineN, lineStr, buf, tabsize)
if colN < 0 {
colN = len(line)
}
viewCol := -startOffset
if startStyle != nil {
curStyle = *startStyle
}
// We'll either draw the length of the line, or the width of the screen
// whichever is smaller
lineLength := min(StringWidth(lineStr, tabsize), width)
c.lines = append(c.lines, make([]*Char, lineLength))
wrap := false
// We only need to wrap if the length of the line is greater than the width of the terminal screen
if softwrap && StringWidth(lineStr, tabsize) > width {
wrap = true
// We're going to draw the entire line now
lineLength = StringWidth(lineStr, tabsize)
}
for viewCol < lineLength {
if colN >= len(line) {
break
}
if group, ok := buf.Match(lineN)[colN]; ok {
curStyle = GetColor(group.String())
}
char := line[colN]
if viewCol >= 0 {
st := curStyle
if colN == matchingBrace.X && lineN == matchingBrace.Y && !buf.Cursor.HasSelection() {
st = curStyle.Reverse(true)
}
if viewCol < len(c.lines[viewLine]) {
c.lines[viewLine][viewCol] = &Char{Loc{viewCol, viewLine}, Loc{colN, lineN}, char, char, st, 1}
}
}
if char == '\t' {
charWidth := tabsize - (viewCol+left)%tabsize
if viewCol >= 0 {
c.lines[viewLine][viewCol].drawChar = indentchar
c.lines[viewLine][viewCol].width = charWidth
indentStyle := curStyle
ch := buf.Settings["indentchar"].(string)
if group, ok := colorscheme["indent-char"]; ok && !IsStrWhitespace(ch) && ch != "" {
indentStyle = group
}
c.lines[viewLine][viewCol].style = indentStyle
}
for i := 1; i < charWidth; i++ {
viewCol++
if viewCol >= 0 && viewCol < lineLength && viewCol < len(c.lines[viewLine]) {
c.lines[viewLine][viewCol] = &Char{Loc{viewCol, viewLine}, Loc{colN, lineN}, char, ' ', curStyle, 1}
}
}
viewCol++
} else if runewidth.RuneWidth(char) > 1 {
charWidth := runewidth.RuneWidth(char)
if viewCol >= 0 {
c.lines[viewLine][viewCol].width = charWidth
}
for i := 1; i < charWidth; i++ {
viewCol++
if viewCol >= 0 && viewCol < lineLength && viewCol < len(c.lines[viewLine]) {
c.lines[viewLine][viewCol] = &Char{Loc{viewCol, viewLine}, Loc{colN, lineN}, char, ' ', curStyle, 1}
}
}
viewCol++
} else {
viewCol++
}
colN++
if wrap && viewCol >= width {
viewLine++
// If we go too far soft wrapping we have to cut off
if viewLine >= height {
break
}
nextLine := line[colN:]
lineLength := min(StringWidth(string(nextLine), tabsize), width)
c.lines = append(c.lines, make([]*Char, lineLength))
viewCol = 0
}
}
if group, ok := buf.Match(lineN)[len(line)]; ok {
curStyle = GetColor(group.String())
}
// newline
viewLine++
lineN++
}
for i := top; i < top+height; i++ {
if i >= buf.NumLines {
break
}
buf.SetMatch(i, nil)
}
}

View File

@ -1,7 +1,7 @@
package main
import (
"fmt"
"errors"
"regexp"
"strconv"
"strings"
@ -9,6 +9,9 @@ import (
"github.com/zyedidia/tcell"
)
// Micro's default style
var defStyle tcell.Style = tcell.StyleDefault
// Colorscheme is a map from string to style -- it represents a colorscheme
type Colorscheme map[string]tcell.Style
@ -48,42 +51,41 @@ func ColorschemeExists(colorschemeName string) bool {
}
// InitColorscheme picks and initializes the colorscheme when micro starts
func InitColorscheme() {
func InitColorscheme() error {
colorscheme = make(Colorscheme)
defStyle = tcell.StyleDefault.
Foreground(tcell.ColorDefault).
Background(tcell.ColorDefault)
if screen != nil {
// screen.SetStyle(defStyle)
}
defStyle = tcell.StyleDefault
LoadDefaultColorscheme()
return LoadDefaultColorscheme()
}
// LoadDefaultColorscheme loads the default colorscheme from $(configDir)/colorschemes
func LoadDefaultColorscheme() {
LoadColorscheme(globalSettings["colorscheme"].(string))
func LoadDefaultColorscheme() error {
return LoadColorscheme(globalSettings["colorscheme"].(string))
}
// LoadColorscheme loads the given colorscheme from a directory
func LoadColorscheme(colorschemeName string) {
func LoadColorscheme(colorschemeName string) error {
file := FindRuntimeFile(RTColorscheme, colorschemeName)
if file == nil {
TermMessage(colorschemeName, "is not a valid colorscheme")
return errors.New(colorschemeName + " is not a valid colorscheme")
}
if data, err := file.Data(); err != nil {
return errors.New("Error loading colorscheme: " + err.Error())
} else {
if data, err := file.Data(); err != nil {
TermMessage("Error loading colorscheme:", err)
} else {
colorscheme = ParseColorscheme(string(data))
colorscheme, err = ParseColorscheme(string(data))
if err != nil {
return err
}
}
return nil
}
// ParseColorscheme parses the text definition for a colorscheme and returns the corresponding object
// Colorschemes are made up of color-link statements linking a color group to a list of colors
// For example, color-link keyword (blue,red) makes all keywords have a blue foreground and
// red background
func ParseColorscheme(text string) Colorscheme {
func ParseColorscheme(text string) (Colorscheme, error) {
var err error
parser := regexp.MustCompile(`color-link\s+(\S*)\s+"(.*)"`)
lines := strings.Split(text, "\n")
@ -108,15 +110,12 @@ func ParseColorscheme(text string) Colorscheme {
if link == "default" {
defStyle = style
}
if screen != nil {
// screen.SetStyle(defStyle)
}
} else {
fmt.Println("Color-link statement is not valid:", line)
err = errors.New("Color-link statement is not valid: " + line)
}
}
return c
return c, err
}
// StringToStyle returns a style from a string

View File

@ -0,0 +1,62 @@
package main
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/zyedidia/tcell"
)
func TestSimpleStringToStyle(t *testing.T) {
s := StringToStyle("lightblue,magenta")
fg, bg, _ := s.Decompose()
assert.Equal(t, tcell.ColorBlue, fg)
assert.Equal(t, tcell.ColorPurple, bg)
}
func TestAttributeStringToStyle(t *testing.T) {
s := StringToStyle("bold cyan,brightcyan")
fg, bg, attr := s.Decompose()
assert.Equal(t, tcell.ColorTeal, fg)
assert.Equal(t, tcell.ColorAqua, bg)
assert.NotEqual(t, 0, attr&tcell.AttrBold)
}
func TestColor256StringToStyle(t *testing.T) {
s := StringToStyle("128,60")
fg, bg, _ := s.Decompose()
assert.Equal(t, tcell.Color128, fg)
assert.Equal(t, tcell.Color60, bg)
}
func TestColorHexStringToStyle(t *testing.T) {
s := StringToStyle("#deadbe,#ef1234")
fg, bg, _ := s.Decompose()
assert.Equal(t, tcell.NewRGBColor(222, 173, 190), fg)
assert.Equal(t, tcell.NewRGBColor(239, 18, 52), bg)
}
func TestColorschemeParser(t *testing.T) {
testColorscheme := `color-link default "#F8F8F2,#282828"
color-link comment "#75715E,#282828"
# comment
color-link identifier "#66D9EF,#282828" #comment
color-link constant "#AE81FF,#282828"
color-link constant.string "#E6DB74,#282828"
color-link constant.string.char "#BDE6AD,#282828"`
c, err := ParseColorscheme(testColorscheme)
assert.Nil(t, err)
fg, bg, _ := c["comment"].Decompose()
assert.Equal(t, tcell.NewRGBColor(117, 113, 94), fg)
assert.Equal(t, tcell.NewRGBColor(40, 40, 40), bg)
}

View File

@ -1,694 +0,0 @@
package main
import (
"fmt"
"os"
"path/filepath"
"regexp"
"runtime"
"strconv"
"strings"
"unicode/utf8"
humanize "github.com/dustin/go-humanize"
"github.com/zyedidia/micro/cmd/micro/shellwords"
)
// A Command contains a action (a function to call) as well as information about how to autocomplete the command
type Command struct {
action func([]string)
completions []Completion
}
// A StrCommand is similar to a command but keeps the name of the action
type StrCommand struct {
action string
completions []Completion
}
var commands map[string]Command
var commandActions map[string]func([]string)
func init() {
commandActions = map[string]func([]string){
"Set": Set,
"SetLocal": SetLocal,
"Show": Show,
"ShowKey": ShowKey,
"Run": Run,
"Bind": Bind,
"Quit": Quit,
"Save": Save,
"Replace": Replace,
"ReplaceAll": ReplaceAll,
"VSplit": VSplit,
"HSplit": HSplit,
"Tab": NewTab,
"Help": Help,
"Eval": Eval,
"ToggleLog": ToggleLog,
"Plugin": PluginCmd,
"Reload": Reload,
"Cd": Cd,
"Pwd": Pwd,
"Open": Open,
"TabSwitch": TabSwitch,
"Term": Term,
"MemUsage": MemUsage,
"Retab": Retab,
"Raw": Raw,
"TextFilter": TextFilter,
}
}
// InitCommands initializes the default commands
func InitCommands() {
commands = make(map[string]Command)
defaults := DefaultCommands()
parseCommands(defaults)
}
func parseCommands(userCommands map[string]StrCommand) {
for k, v := range userCommands {
MakeCommand(k, v.action, v.completions...)
}
}
// MakeCommand is a function to easily create new commands
// This can be called by plugins in Lua so that plugins can define their own commands
func MakeCommand(name, function string, completions ...Completion) {
action := commandActions[function]
if _, ok := commandActions[function]; !ok {
// If the user seems to be binding a function that doesn't exist
// We hope that it's a lua function that exists and bind it to that
action = LuaFunctionCommand(function)
}
commands[name] = Command{action, completions}
}
// DefaultCommands returns a map containing micro's default commands
func DefaultCommands() map[string]StrCommand {
return map[string]StrCommand{
"set": {"Set", []Completion{OptionCompletion, OptionValueCompletion}},
"setlocal": {"SetLocal", []Completion{OptionCompletion, OptionValueCompletion}},
"show": {"Show", []Completion{OptionCompletion, NoCompletion}},
"showkey": {"ShowKey", []Completion{NoCompletion}},
"bind": {"Bind", []Completion{NoCompletion}},
"run": {"Run", []Completion{NoCompletion}},
"quit": {"Quit", []Completion{NoCompletion}},
"save": {"Save", []Completion{NoCompletion}},
"replace": {"Replace", []Completion{NoCompletion}},
"replaceall": {"ReplaceAll", []Completion{NoCompletion}},
"vsplit": {"VSplit", []Completion{FileCompletion, NoCompletion}},
"hsplit": {"HSplit", []Completion{FileCompletion, NoCompletion}},
"tab": {"Tab", []Completion{FileCompletion, NoCompletion}},
"help": {"Help", []Completion{HelpCompletion, NoCompletion}},
"eval": {"Eval", []Completion{NoCompletion}},
"log": {"ToggleLog", []Completion{NoCompletion}},
"plugin": {"Plugin", []Completion{PluginCmdCompletion, PluginNameCompletion}},
"reload": {"Reload", []Completion{NoCompletion}},
"cd": {"Cd", []Completion{FileCompletion}},
"pwd": {"Pwd", []Completion{NoCompletion}},
"open": {"Open", []Completion{FileCompletion}},
"tabswitch": {"TabSwitch", []Completion{NoCompletion}},
"term": {"Term", []Completion{NoCompletion}},
"memusage": {"MemUsage", []Completion{NoCompletion}},
"retab": {"Retab", []Completion{NoCompletion}},
"raw": {"Raw", []Completion{NoCompletion}},
"textfilter": {"TextFilter", []Completion{NoCompletion}},
}
}
// CommandEditAction returns a bindable function that opens a prompt with
// the given string and executes the command when the user presses
// enter
func CommandEditAction(prompt string) func(*View, bool) bool {
return func(v *View, usePlugin bool) bool {
input, canceled := messenger.Prompt("> ", prompt, "Command", CommandCompletion)
if !canceled {
HandleCommand(input)
}
return false
}
}
// CommandAction returns a bindable function which executes the
// given command
func CommandAction(cmd string) func(*View, bool) bool {
return func(v *View, usePlugin bool) bool {
HandleCommand(cmd)
return false
}
}
// PluginCmd installs, removes, updates, lists, or searches for given plugins
func PluginCmd(args []string) {
if len(args) >= 1 {
switch args[0] {
case "install":
installedVersions := GetInstalledVersions(false)
for _, plugin := range args[1:] {
pp := GetAllPluginPackages().Get(plugin)
if pp == nil {
messenger.Error("Unknown plugin \"" + plugin + "\"")
} else if err := pp.IsInstallable(); err != nil {
messenger.Error("Error installing ", plugin, ": ", err)
} else {
for _, installed := range installedVersions {
if pp.Name == installed.pack.Name {
if pp.Versions[0].Version.Compare(installed.Version) == 1 {
messenger.Error(pp.Name, " is already installed but out-of-date: use 'plugin update ", pp.Name, "' to update")
} else {
messenger.Error(pp.Name, " is already installed")
}
}
}
pp.Install()
}
}
case "remove":
removed := ""
for _, plugin := range args[1:] {
// check if the plugin exists.
if _, ok := loadedPlugins[plugin]; ok {
UninstallPlugin(plugin)
removed += plugin + " "
continue
}
}
if !IsSpaces([]byte(removed)) {
messenger.Message("Removed ", removed)
} else {
messenger.Error("The requested plugins do not exist")
}
case "update":
UpdatePlugins(args[1:])
case "list":
plugins := GetInstalledVersions(false)
messenger.AddLog("----------------")
messenger.AddLog("The following plugins are currently installed:\n")
for _, p := range plugins {
messenger.AddLog(fmt.Sprintf("%s (%s)", p.pack.Name, p.Version))
}
messenger.AddLog("----------------")
if len(plugins) > 0 {
if CurView().Type != vtLog {
ToggleLog([]string{})
}
}
case "search":
plugins := SearchPlugin(args[1:])
messenger.Message(len(plugins), " plugins found")
for _, p := range plugins {
messenger.AddLog("----------------")
messenger.AddLog(p.String())
}
messenger.AddLog("----------------")
if len(plugins) > 0 {
if CurView().Type != vtLog {
ToggleLog([]string{})
}
}
case "available":
packages := GetAllPluginPackages()
messenger.AddLog("Available Plugins:")
for _, pkg := range packages {
messenger.AddLog(pkg.Name)
}
if CurView().Type != vtLog {
ToggleLog([]string{})
}
}
} else {
messenger.Error("Not enough arguments")
}
}
// Retab changes all spaces to tabs or all tabs to spaces
// depending on the user's settings
func Retab(args []string) {
CurView().Retab(true)
}
// Raw opens a new raw view which displays the escape sequences micro
// is receiving in real-time
func Raw(args []string) {
buf := NewBufferFromString("", "Raw events")
view := NewView(buf)
view.Buf.Insert(view.Cursor.Loc, "Warning: Showing raw event escape codes\n")
view.Buf.Insert(view.Cursor.Loc, "Use CtrlQ to exit\n")
view.Type = vtRaw
tab := NewTabFromView(view)
tab.SetNum(len(tabs))
tabs = append(tabs, tab)
curTab = len(tabs) - 1
if len(tabs) == 2 {
for _, t := range tabs {
for _, v := range t.Views {
v.ToggleTabbar()
}
}
}
}
// TabSwitch switches to a given tab either by name or by number
func TabSwitch(args []string) {
if len(args) > 0 {
num, err := strconv.Atoi(args[0])
if err != nil {
// Check for tab with this name
found := false
for _, t := range tabs {
v := t.Views[t.CurView]
if v.Buf.GetName() == args[0] {
curTab = v.TabNum
found = true
}
}
if !found {
messenger.Error("Could not find tab: ", err)
}
} else {
num--
if num >= 0 && num < len(tabs) {
curTab = num
} else {
messenger.Error("Invalid tab index")
}
}
}
}
// Cd changes the current working directory
func Cd(args []string) {
if len(args) > 0 {
path := ReplaceHome(args[0])
err := os.Chdir(path)
if err != nil {
messenger.Error("Error with cd: ", err)
return
}
wd, _ := os.Getwd()
for _, tab := range tabs {
for _, view := range tab.Views {
if len(view.Buf.name) == 0 {
continue
}
view.Buf.Path, _ = MakeRelative(view.Buf.AbsPath, wd)
if p, _ := filepath.Abs(view.Buf.Path); !strings.Contains(p, wd) {
view.Buf.Path = view.Buf.AbsPath
}
}
}
}
}
// MemUsage prints micro's memory usage
// Alloc shows how many bytes are currently in use
// Sys shows how many bytes have been requested from the operating system
// NumGC shows how many times the GC has been run
// Note that Go commonly reserves more memory from the OS than is currently in-use/required
// Additionally, even if Go returns memory to the OS, the OS does not always claim it because
// there may be plenty of memory to spare
func MemUsage(args []string) {
var mem runtime.MemStats
runtime.ReadMemStats(&mem)
messenger.Message(fmt.Sprintf("Alloc: %v, Sys: %v, NumGC: %v", humanize.Bytes(mem.Alloc), humanize.Bytes(mem.Sys), mem.NumGC))
}
// Pwd prints the current working directory
func Pwd(args []string) {
wd, err := os.Getwd()
if err != nil {
messenger.Message(err.Error())
} else {
messenger.Message(wd)
}
}
// Open opens a new buffer with a given filename
func Open(args []string) {
if len(args) > 0 {
filename := args[0]
// the filename might or might not be quoted, so unquote first then join the strings.
args, err := shellwords.Split(filename)
if err != nil {
messenger.Error("Error parsing args ", err)
return
}
filename = strings.Join(args, " ")
CurView().Open(filename)
} else {
messenger.Error("No filename")
}
}
// ToggleLog toggles the log view
func ToggleLog(args []string) {
buffer := messenger.getBuffer()
if CurView().Type != vtLog {
CurView().HSplit(buffer)
CurView().Type = vtLog
RedrawAll()
buffer.Cursor.Loc = buffer.Start()
CurView().Relocate()
buffer.Cursor.Loc = buffer.End()
CurView().Relocate()
} else {
CurView().Quit(true)
}
}
// Reload reloads all files (syntax files, colorschemes...)
func Reload(args []string) {
LoadAll()
}
// Help tries to open the given help page in a horizontal split
func Help(args []string) {
if len(args) < 1 {
// Open the default help if the user just typed "> help"
CurView().openHelp("help")
} else {
helpPage := args[0]
if FindRuntimeFile(RTHelp, helpPage) != nil {
CurView().openHelp(helpPage)
} else {
messenger.Error("Sorry, no help for ", helpPage)
}
}
}
// VSplit opens a vertical split with file given in the first argument
// If no file is given, it opens an empty buffer in a new split
func VSplit(args []string) {
if len(args) == 0 {
CurView().VSplit(NewBufferFromString("", ""))
} else {
buf, err := NewBufferFromFile(args[0])
if err != nil {
messenger.Error(err)
return
}
CurView().VSplit(buf)
}
}
// HSplit opens a horizontal split with file given in the first argument
// If no file is given, it opens an empty buffer in a new split
func HSplit(args []string) {
if len(args) == 0 {
CurView().HSplit(NewBufferFromString("", ""))
} else {
buf, err := NewBufferFromFile(args[0])
if err != nil {
messenger.Error(err)
return
}
CurView().HSplit(buf)
}
}
// Eval evaluates a lua expression
func Eval(args []string) {
if len(args) >= 1 {
err := L.DoString(args[0])
if err != nil {
messenger.Error(err)
}
} else {
messenger.Error("Not enough arguments")
}
}
// NewTab opens the given file in a new tab
func NewTab(args []string) {
if len(args) == 0 {
CurView().AddTab(true)
} else {
buf, err := NewBufferFromFile(args[0])
if err != nil {
messenger.Error(err)
return
}
tab := NewTabFromView(NewView(buf))
tab.SetNum(len(tabs))
tabs = append(tabs, tab)
curTab = len(tabs) - 1
if len(tabs) == 2 {
for _, t := range tabs {
for _, v := range t.Views {
v.ToggleTabbar()
}
}
}
}
}
// Set sets an option
func Set(args []string) {
if len(args) < 2 {
messenger.Error("Not enough arguments")
return
}
option := args[0]
value := args[1]
SetOptionAndSettings(option, value)
}
// SetLocal sets an option local to the buffer
func SetLocal(args []string) {
if len(args) < 2 {
messenger.Error("Not enough arguments")
return
}
option := args[0]
value := args[1]
err := SetLocalOption(option, value, CurView())
if err != nil {
messenger.Error(err.Error())
}
}
// Show shows the value of the given option
func Show(args []string) {
if len(args) < 1 {
messenger.Error("Please provide an option to show")
return
}
option := GetOption(args[0])
if option == nil {
messenger.Error(args[0], " is not a valid option")
return
}
messenger.Message(option)
}
// ShowKey displays the action that a key is bound to
func ShowKey(args []string) {
if len(args) < 1 {
messenger.Error("Please provide a key to show")
return
}
if action, ok := bindingsStr[args[0]]; ok {
messenger.Message(action)
} else {
messenger.Message(args[0], " has no binding")
}
}
// Bind creates a new keybinding
func Bind(args []string) {
if len(args) < 2 {
messenger.Error("Not enough arguments")
return
}
BindKey(args[0], args[1])
}
// Run runs a shell command in the background
func Run(args []string) {
// Run a shell command in the background (openTerm is false)
HandleShellCommand(shellwords.Join(args...), false, true)
}
// Quit closes the main view
func Quit(args []string) {
// Close the main view
CurView().Quit(true)
}
// Save saves the buffer in the main view
func Save(args []string) {
if len(args) == 0 {
// Save the main view
CurView().Save(true)
} else {
CurView().Buf.SaveAs(args[0])
}
}
// Replace runs search and replace
func Replace(args []string) {
if len(args) < 2 || len(args) > 4 {
// We need to find both a search and replace expression
messenger.Error("Invalid replace statement: " + strings.Join(args, " "))
return
}
all := false
noRegex := false
if len(args) > 2 {
for _, arg := range args[2:] {
switch arg {
case "-a":
all = true
case "-l":
noRegex = true
default:
messenger.Error("Invalid flag: " + arg)
return
}
}
}
search := string(args[0])
if noRegex {
search = regexp.QuoteMeta(search)
}
replace := string(args[1])
replaceBytes := []byte(replace)
regex, err := regexp.Compile("(?m)" + search)
if err != nil {
// There was an error with the user's regex
messenger.Error(err.Error())
return
}
view := CurView()
found := 0
replaceAll := func() {
var deltas []Delta
for i := 0; i < view.Buf.LinesNum(); i++ {
newText := regex.ReplaceAllFunc(view.Buf.lines[i].data, func(in []byte) []byte {
found++
return replaceBytes
})
from := Loc{0, i}
to := Loc{utf8.RuneCount(view.Buf.lines[i].data), i}
deltas = append(deltas, Delta{string(newText), from, to})
}
view.Buf.MultipleReplace(deltas)
}
if all {
replaceAll()
} else {
for {
// The 'check' flag was used
Search(search, view, true)
if !view.Cursor.HasSelection() {
break
}
view.Relocate()
RedrawAll()
choice, canceled := messenger.LetterPrompt("Perform replacement? (y,n,a)", 'y', 'n', 'a')
if canceled {
if view.Cursor.HasSelection() {
view.Cursor.Loc = view.Cursor.CurSelection[0]
view.Cursor.ResetSelection()
}
messenger.Reset()
break
} else if choice == 'a' {
if view.Cursor.HasSelection() {
view.Cursor.Loc = view.Cursor.CurSelection[0]
view.Cursor.ResetSelection()
}
messenger.Reset()
replaceAll()
break
} else if choice == 'y' {
view.Cursor.DeleteSelection()
view.Buf.Insert(view.Cursor.Loc, replace)
view.Cursor.ResetSelection()
messenger.Reset()
found++
}
if view.Cursor.HasSelection() {
searchStart = view.Cursor.CurSelection[1]
} else {
searchStart = view.Cursor.Loc
}
}
}
view.Cursor.Relocate()
if found > 1 {
messenger.Message("Replaced ", found, " occurrences of ", search)
} else if found == 1 {
messenger.Message("Replaced ", found, " occurrence of ", search)
} else {
messenger.Message("Nothing matched ", search)
}
}
// ReplaceAll replaces search term all at once
func ReplaceAll(args []string) {
// aliased to Replace command
Replace(append(args, "-a"))
}
// Term opens a terminal in the current view
func Term(args []string) {
var err error
if len(args) == 0 {
err = CurView().StartTerminal([]string{os.Getenv("SHELL"), "-i"}, true, false, "")
} else {
err = CurView().StartTerminal(args, true, false, "")
}
if err != nil {
messenger.Error(err)
}
}
// HandleCommand handles input from the user
func HandleCommand(input string) {
args, err := shellwords.Split(input)
if err != nil {
messenger.Error("Error parsing args ", err)
return
}
inputCmd := args[0]
if _, ok := commands[inputCmd]; !ok {
messenger.Error("Unknown command ", inputCmd)
} else {
commands[inputCmd].action(args[1:])
}
}

View File

@ -1,15 +1,23 @@
package main
import (
"unicode/utf8"
runewidth "github.com/mattn/go-runewidth"
"github.com/zyedidia/clipboard"
)
// The Cursor struct stores the location of the cursor in the view
// The complicated part about the cursor is storing its location.
// The cursor must be displayed at an x, y location, but since the buffer
// uses a rope to store text, to insert text we must have an index. It
// is also simpler to use character indicies for other tasks such as
// selection.
// InBounds returns whether the given location is a valid character position in the given buffer
func InBounds(pos Loc, buf *Buffer) bool {
if pos.Y < 0 || pos.Y >= len(buf.lines) || pos.X < 0 || pos.X > utf8.RuneCount(buf.LineBytes(pos.Y)) {
return false
}
return true
}
// The Cursor struct stores the location of the cursor in the buffer
// as well as the selection
type Cursor struct {
buf *Buffer
Loc
@ -42,12 +50,72 @@ func (c *Cursor) GotoLoc(l Loc) {
c.LastVisualX = c.GetVisualX()
}
// GetVisualX returns the x value of the cursor in visual spaces
func (c *Cursor) GetVisualX() int {
if c.X <= 0 {
c.X = 0
return 0
}
bytes := c.buf.LineBytes(c.Y)
tabsize := int(c.buf.Settings["tabsize"].(float64))
if c.X > utf8.RuneCount(bytes) {
c.X = utf8.RuneCount(bytes) - 1
}
return StringWidth(bytes, c.X, tabsize)
}
// GetCharPosInLine gets the char position of a visual x y
// coordinate (this is necessary because tabs are 1 char but
// 4 visual spaces)
func (c *Cursor) GetCharPosInLine(b []byte, visualPos int) int {
tabsize := int(c.buf.Settings["tabsize"].(float64))
// Scan rune by rune until we exceed the visual width that we are
// looking for. Then we can return the character position we have found
i := 0 // char pos
width := 0 // string visual width
for len(b) > 0 {
r, size := utf8.DecodeRune(b)
b = b[size:]
switch r {
case '\t':
ts := tabsize - (width % tabsize)
width += ts
default:
width += runewidth.RuneWidth(r)
}
i++
if width >= visualPos {
break
}
}
return i
}
// Start moves the cursor to the start of the line it is on
func (c *Cursor) Start() {
c.X = 0
c.LastVisualX = c.GetVisualX()
}
// End moves the cursor to the end of the line it is on
func (c *Cursor) End() {
c.X = utf8.RuneCount(c.buf.LineBytes(c.Y))
c.LastVisualX = c.GetVisualX()
}
// CopySelection copies the user's selection to either "primary"
// or "clipboard"
func (c *Cursor) CopySelection(target string) {
if c.HasSelection() {
if target != "primary" || c.buf.Settings["useprimary"].(bool) {
clipboard.WriteAll(c.GetSelection(), target)
clipboard.WriteAll(string(c.GetSelection()), target)
}
}
}
@ -87,14 +155,14 @@ func (c *Cursor) DeleteSelection() {
}
// GetSelection returns the cursor's selection
func (c *Cursor) GetSelection() string {
func (c *Cursor) GetSelection() []byte {
if InBounds(c.CurSelection[0], c.buf) && InBounds(c.CurSelection[1], c.buf) {
if c.CurSelection[0].GreaterThan(c.CurSelection[1]) {
return c.buf.Substr(c.CurSelection[1], c.CurSelection[0])
}
return c.buf.Substr(c.CurSelection[0], c.CurSelection[1])
}
return ""
return []byte{}
}
// SelectLine selects the current line
@ -102,7 +170,7 @@ func (c *Cursor) SelectLine() {
c.Start()
c.SetSelectionStart(c.Loc)
c.End()
if c.buf.NumLines-1 > c.Y {
if len(c.buf.lines)-1 > c.Y {
c.SetSelectionEnd(c.Loc.Move(1, c.buf))
} else {
c.SetSelectionEnd(c.Loc)
@ -129,146 +197,20 @@ func (c *Cursor) AddLineToSelection() {
}
}
// SelectWord selects the word the cursor is currently on
func (c *Cursor) SelectWord() {
if len(c.buf.Line(c.Y)) == 0 {
return
}
if !IsWordChar(string(c.RuneUnder(c.X))) {
c.SetSelectionStart(c.Loc)
c.SetSelectionEnd(c.Loc.Move(1, c.buf))
c.OrigSelection = c.CurSelection
return
}
forward, backward := c.X, c.X
for backward > 0 && IsWordChar(string(c.RuneUnder(backward-1))) {
backward--
}
c.SetSelectionStart(Loc{backward, c.Y})
c.OrigSelection[0] = c.CurSelection[0]
for forward < Count(c.buf.Line(c.Y))-1 && IsWordChar(string(c.RuneUnder(forward+1))) {
forward++
}
c.SetSelectionEnd(Loc{forward, c.Y}.Move(1, c.buf))
c.OrigSelection[1] = c.CurSelection[1]
c.Loc = c.CurSelection[1]
}
// AddWordToSelection adds the word the cursor is currently on
// to the selection
func (c *Cursor) AddWordToSelection() {
if c.Loc.GreaterThan(c.OrigSelection[0]) && c.Loc.LessThan(c.OrigSelection[1]) {
c.CurSelection = c.OrigSelection
return
}
if c.Loc.LessThan(c.OrigSelection[0]) {
backward := c.X
for backward > 0 && IsWordChar(string(c.RuneUnder(backward-1))) {
backward--
}
c.SetSelectionStart(Loc{backward, c.Y})
c.SetSelectionEnd(c.OrigSelection[1])
}
if c.Loc.GreaterThan(c.OrigSelection[1]) {
forward := c.X
for forward < Count(c.buf.Line(c.Y))-1 && IsWordChar(string(c.RuneUnder(forward+1))) {
forward++
}
c.SetSelectionEnd(Loc{forward, c.Y}.Move(1, c.buf))
c.SetSelectionStart(c.OrigSelection[0])
}
c.Loc = c.CurSelection[1]
}
// SelectTo selects from the current cursor location to the given
// location
func (c *Cursor) SelectTo(loc Loc) {
if loc.GreaterThan(c.OrigSelection[0]) {
c.SetSelectionStart(c.OrigSelection[0])
c.SetSelectionEnd(loc)
} else {
c.SetSelectionStart(loc)
c.SetSelectionEnd(c.OrigSelection[0])
}
}
// WordRight moves the cursor one word to the right
func (c *Cursor) WordRight() {
for IsWhitespace(c.RuneUnder(c.X)) {
if c.X == Count(c.buf.Line(c.Y)) {
c.Right()
return
}
c.Right()
}
c.Right()
for IsWordChar(string(c.RuneUnder(c.X))) {
if c.X == Count(c.buf.Line(c.Y)) {
return
}
c.Right()
}
}
// WordLeft moves the cursor one word to the left
func (c *Cursor) WordLeft() {
c.Left()
for IsWhitespace(c.RuneUnder(c.X)) {
if c.X == 0 {
return
}
c.Left()
}
c.Left()
for IsWordChar(string(c.RuneUnder(c.X))) {
if c.X == 0 {
return
}
c.Left()
}
c.Right()
}
// RuneUnder returns the rune under the given x position
func (c *Cursor) RuneUnder(x int) rune {
line := []rune(c.buf.Line(c.Y))
if len(line) == 0 {
return '\n'
}
if x >= len(line) {
return '\n'
} else if x < 0 {
x = 0
}
return line[x]
}
// UpN moves the cursor up N lines (if possible)
func (c *Cursor) UpN(amount int) {
proposedY := c.Y - amount
if proposedY < 0 {
proposedY = 0
c.LastVisualX = 0
} else if proposedY >= c.buf.NumLines {
proposedY = c.buf.NumLines - 1
} else if proposedY >= len(c.buf.lines) {
proposedY = len(c.buf.lines) - 1
}
runes := []rune(c.buf.Line(proposedY))
c.X = c.GetCharPosInLine(proposedY, c.LastVisualX)
if c.X > len(runes) || (amount < 0 && proposedY == c.Y) {
c.X = len(runes)
bytes := c.buf.LineBytes(proposedY)
c.X = c.GetCharPosInLine(bytes, c.LastVisualX)
if c.X > utf8.RuneCount(bytes) || (amount < 0 && proposedY == c.Y) {
c.X = utf8.RuneCount(bytes)
}
c.Y = proposedY
@ -310,7 +252,7 @@ func (c *Cursor) Right() {
if c.Loc == c.buf.End() {
return
}
if c.X < Count(c.buf.Line(c.Y)) {
if c.X < utf8.RuneCount(c.buf.LineBytes(c.Y)) {
c.X++
} else {
c.Down()
@ -319,80 +261,19 @@ func (c *Cursor) Right() {
c.LastVisualX = c.GetVisualX()
}
// End moves the cursor to the end of the line it is on
func (c *Cursor) End() {
c.X = Count(c.buf.Line(c.Y))
c.LastVisualX = c.GetVisualX()
}
// Start moves the cursor to the start of the line it is on
func (c *Cursor) Start() {
c.X = 0
c.LastVisualX = c.GetVisualX()
}
// StartOfText moves the cursor to the first non-whitespace rune of
// the line it is on
func (c *Cursor) StartOfText() {
c.Start()
for IsWhitespace(c.RuneUnder(c.X)) {
if c.X == Count(c.buf.Line(c.Y)) {
break
}
c.Right()
}
}
// GetCharPosInLine gets the char position of a visual x y
// coordinate (this is necessary because tabs are 1 char but
// 4 visual spaces)
func (c *Cursor) GetCharPosInLine(lineNum, visualPos int) int {
// Get the tab size
tabSize := int(c.buf.Settings["tabsize"].(float64))
visualLineLen := StringWidth(c.buf.Line(lineNum), tabSize)
if visualPos > visualLineLen {
visualPos = visualLineLen
}
width := WidthOfLargeRunes(c.buf.Line(lineNum), tabSize)
if visualPos >= width {
return visualPos - width
}
return visualPos / tabSize
}
// GetVisualX returns the x value of the cursor in visual spaces
func (c *Cursor) GetVisualX() int {
runes := []rune(c.buf.Line(c.Y))
tabSize := int(c.buf.Settings["tabsize"].(float64))
if c.X > len(runes) {
c.X = len(runes) - 1
}
if c.X < 0 {
c.X = 0
}
return StringWidth(string(runes[:c.X]), tabSize)
}
// StoreVisualX stores the current visual x value in the cursor
func (c *Cursor) StoreVisualX() {
c.LastVisualX = c.GetVisualX()
}
// Relocate makes sure that the cursor is inside the bounds
// of the buffer If it isn't, it moves it to be within the
// buffer's lines
func (c *Cursor) Relocate() {
if c.Y < 0 {
c.Y = 0
} else if c.Y >= c.buf.NumLines {
c.Y = c.buf.NumLines - 1
} else if c.Y >= len(c.buf.lines) {
c.Y = len(c.buf.lines) - 1
}
if c.X < 0 {
c.X = 0
} else if c.X > Count(c.buf.Line(c.Y)) {
c.X = Count(c.buf.Line(c.Y))
} else if c.X > utf8.RuneCount(c.buf.LineBytes(c.Y)) {
c.X = utf8.RuneCount(c.buf.LineBytes(c.Y))
}
}

1
cmd/micro/cursor_test.go Normal file
View File

@ -0,0 +1 @@
package main

27
cmd/micro/debug.go Normal file
View File

@ -0,0 +1,27 @@
package main
import (
"log"
"os"
)
type NullWriter struct{}
func (NullWriter) Write(data []byte) (n int, err error) {
return 0, nil
}
func InitLog() {
if Debug == "ON" {
f, err := os.OpenFile("log.txt", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
if err != nil {
log.Fatalf("error opening file: %v", err)
}
log.SetOutput(f)
log.Println("Micro started")
} else {
log.SetOutput(NullWriter{})
log.Println("Micro started")
}
}

View File

@ -1,11 +1,10 @@
package main
import (
"strings"
"time"
"unicode/utf8"
dmp "github.com/sergi/go-diff/diffmatchpatch"
"github.com/yuin/gopher-lua"
)
const (
@ -30,7 +29,7 @@ type TextEvent struct {
// A Delta is a change to the buffer
type Delta struct {
Text string
Text []byte
Start Loc
End Loc
}
@ -39,7 +38,7 @@ type Delta struct {
func ExecuteTextEvent(t *TextEvent, buf *Buffer) {
if t.EventType == TextEventInsert {
for _, d := range t.Deltas {
buf.insert(d.Start, []byte(d.Text))
buf.insert(d.Start, d.Text)
}
} else if t.EventType == TextEventRemove {
for i, d := range t.Deltas {
@ -48,9 +47,9 @@ func ExecuteTextEvent(t *TextEvent, buf *Buffer) {
} else if t.EventType == TextEventReplace {
for i, d := range t.Deltas {
t.Deltas[i].Text = buf.remove(d.Start, d.End)
buf.insert(d.Start, []byte(d.Text))
buf.insert(d.Start, d.Text)
t.Deltas[i].Start = d.Start
t.Deltas[i].End = Loc{d.Start.X + Count(d.Text), d.Start.Y}
t.Deltas[i].End = Loc{d.Start.X + utf8.RuneCount(d.Text), d.Start.Y}
}
for i, j := 0, len(t.Deltas)-1; i < j; i, j = i+1, j-1 {
t.Deltas[i], t.Deltas[j] = t.Deltas[j], t.Deltas[i]
@ -67,15 +66,15 @@ func UndoTextEvent(t *TextEvent, buf *Buffer) {
// EventHandler executes text manipulations and allows undoing and redoing
type EventHandler struct {
buf *Buffer
UndoStack *Stack
RedoStack *Stack
UndoStack *TEStack
RedoStack *TEStack
}
// NewEventHandler returns a new EventHandler
func NewEventHandler(buf *Buffer) *EventHandler {
eh := new(EventHandler)
eh.UndoStack = new(Stack)
eh.RedoStack = new(Stack)
eh.UndoStack = new(TEStack)
eh.RedoStack = new(TEStack)
eh.buf = buf
return eh
}
@ -86,38 +85,39 @@ func NewEventHandler(buf *Buffer) *EventHandler {
// through insert and delete events
func (eh *EventHandler) ApplyDiff(new string) {
differ := dmp.New()
diff := differ.DiffMain(eh.buf.String(), new, false)
diff := differ.DiffMain(string(eh.buf.Bytes()), new, false)
loc := eh.buf.Start()
for _, d := range diff {
if d.Type == dmp.DiffDelete {
eh.Remove(loc, loc.Move(Count(d.Text), eh.buf))
eh.Remove(loc, loc.Move(utf8.RuneCountInString(d.Text), eh.buf))
} else {
if d.Type == dmp.DiffInsert {
eh.Insert(loc, d.Text)
}
loc = loc.Move(Count(d.Text), eh.buf)
loc = loc.Move(utf8.RuneCountInString(d.Text), eh.buf)
}
}
}
// Insert creates an insert text event and executes it
func (eh *EventHandler) Insert(start Loc, text string) {
func (eh *EventHandler) Insert(start Loc, textStr string) {
text := []byte(textStr)
e := &TextEvent{
C: *eh.buf.cursors[eh.buf.curCursor],
C: *eh.buf.GetActiveCursor(),
EventType: TextEventInsert,
Deltas: []Delta{{text, start, Loc{0, 0}}},
Time: time.Now(),
}
eh.Execute(e)
e.Deltas[0].End = start.Move(Count(text), eh.buf)
e.Deltas[0].End = start.Move(utf8.RuneCount(text), eh.buf)
end := e.Deltas[0].End
for _, c := range eh.buf.cursors {
for _, c := range eh.buf.GetCursors() {
move := func(loc Loc) Loc {
if start.Y != end.Y && loc.GreaterThan(start) {
loc.Y += end.Y - start.Y
} else if loc.Y == start.Y && loc.GreaterEqual(start) {
loc = loc.Move(Count(text), eh.buf)
loc = loc.Move(utf8.RuneCount(text), eh.buf)
}
return loc
}
@ -133,14 +133,14 @@ func (eh *EventHandler) Insert(start Loc, text string) {
// Remove creates a remove text event and executes it
func (eh *EventHandler) Remove(start, end Loc) {
e := &TextEvent{
C: *eh.buf.cursors[eh.buf.curCursor],
C: *eh.buf.GetActiveCursor(),
EventType: TextEventRemove,
Deltas: []Delta{{"", start, end}},
Deltas: []Delta{{[]byte{}, start, end}},
Time: time.Now(),
}
eh.Execute(e)
for _, c := range eh.buf.cursors {
for _, c := range eh.buf.GetCursors() {
move := func(loc Loc) Loc {
if start.Y != end.Y && loc.GreaterThan(end) {
loc.Y -= end.Y - start.Y
@ -161,7 +161,7 @@ func (eh *EventHandler) Remove(start, end Loc) {
// MultipleReplace creates an multiple insertions executes them
func (eh *EventHandler) MultipleReplace(deltas []Delta) {
e := &TextEvent{
C: *eh.buf.cursors[eh.buf.curCursor],
C: *eh.buf.GetActiveCursor(),
EventType: TextEventReplace,
Deltas: deltas,
Time: time.Now(),
@ -178,19 +178,20 @@ func (eh *EventHandler) Replace(start, end Loc, replace string) {
// Execute a textevent and add it to the undo stack
func (eh *EventHandler) Execute(t *TextEvent) {
if eh.RedoStack.Len() > 0 {
eh.RedoStack = new(Stack)
eh.RedoStack = new(TEStack)
}
eh.UndoStack.Push(t)
for pl := range loadedPlugins {
ret, err := Call(pl+".onBeforeTextEvent", t)
if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") {
TermMessage(err)
}
if val, ok := ret.(lua.LBool); ok && val == lua.LFalse {
return
}
}
// TODO: Call plugins on text events
// for pl := range loadedPlugins {
// ret, err := Call(pl+".onBeforeTextEvent", t)
// if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") {
// TermMessage(err)
// }
// if val, ok := ret.(lua.LBool); ok && val == lua.LFalse {
// return
// }
// }
ExecuteTextEvent(t, eh.buf)
}
@ -236,9 +237,9 @@ func (eh *EventHandler) UndoOneEvent() {
// Set the cursor in the right place
teCursor := t.C
if teCursor.Num >= 0 && teCursor.Num < len(eh.buf.cursors) {
t.C = *eh.buf.cursors[teCursor.Num]
eh.buf.cursors[teCursor.Num].Goto(teCursor)
if teCursor.Num >= 0 && teCursor.Num < eh.buf.NumCursors() {
t.C = *eh.buf.GetCursor(teCursor.Num)
eh.buf.GetCursor(teCursor.Num).Goto(teCursor)
} else {
teCursor.Num = -1
}
@ -283,9 +284,9 @@ func (eh *EventHandler) RedoOneEvent() {
UndoTextEvent(t, eh.buf)
teCursor := t.C
if teCursor.Num >= 0 && teCursor.Num < len(eh.buf.cursors) {
t.C = *eh.buf.cursors[teCursor.Num]
eh.buf.cursors[teCursor.Num].Goto(teCursor)
if teCursor.Num >= 0 && teCursor.Num < eh.buf.NumCursors() {
t.C = *eh.buf.GetCursor(teCursor.Num)
eh.buf.GetCursor(teCursor.Num).Goto(teCursor)
} else {
teCursor.Num = -1
}

View File

@ -1,28 +0,0 @@
package main
import "github.com/zyedidia/micro/cmd/micro/highlight"
var syntaxFiles []*highlight.File
func LoadSyntaxFiles() {
InitColorscheme()
for _, f := range ListRuntimeFiles(RTSyntax) {
data, err := f.Data()
if err != nil {
TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
} else {
LoadSyntaxFile(data, f.Name())
}
}
}
func LoadSyntaxFile(text []byte, filename string) {
f, err := highlight.ParseFile(text)
if err != nil {
TermMessage("Syntax file error: " + filename + ": " + err.Error())
return
}
syntaxFiles = append(syntaxFiles, f)
}

View File

@ -1,88 +0,0 @@
package main
import (
"bytes"
"io"
"os/exec"
)
// Jobs are the way plugins can run processes in the background
// A job is simply a process that gets executed asynchronously
// There are callbacks for when the job exits, when the job creates stdout
// and when the job creates stderr
// These jobs run in a separate goroutine but the lua callbacks need to be
// executed in the main thread (where the Lua VM is running) so they are
// put into the jobs channel which gets read by the main loop
// JobFunction is a representation of a job (this data structure is what is loaded
// into the jobs channel)
type JobFunction struct {
function func(string, ...string)
output string
args []string
}
// A CallbackFile is the data structure that makes it possible to catch stderr and stdout write events
type CallbackFile struct {
io.Writer
callback func(string, ...string)
args []string
}
func (f *CallbackFile) Write(data []byte) (int, error) {
// This is either stderr or stdout
// In either case we create a new job function callback and put it in the jobs channel
jobFunc := JobFunction{f.callback, string(data), f.args}
jobs <- jobFunc
return f.Writer.Write(data)
}
// JobStart starts a shell command in the background with the given callbacks
// It returns an *exec.Cmd as the job id
func JobStart(cmd string, onStdout, onStderr, onExit string, userargs ...string) *exec.Cmd {
return JobSpawn("sh", []string{"-c", cmd}, onStdout, onStderr, onExit, userargs...)
}
// JobSpawn starts a process with args in the background with the given callbacks
// It returns an *exec.Cmd as the job id
func JobSpawn(cmdName string, cmdArgs []string, onStdout, onStderr, onExit string, userargs ...string) *exec.Cmd {
// Set up everything correctly if the functions have been provided
proc := exec.Command(cmdName, cmdArgs...)
var outbuf bytes.Buffer
if onStdout != "" {
proc.Stdout = &CallbackFile{&outbuf, LuaFunctionJob(onStdout), userargs}
} else {
proc.Stdout = &outbuf
}
if onStderr != "" {
proc.Stderr = &CallbackFile{&outbuf, LuaFunctionJob(onStderr), userargs}
} else {
proc.Stderr = &outbuf
}
go func() {
// Run the process in the background and create the onExit callback
proc.Run()
jobFunc := JobFunction{LuaFunctionJob(onExit), string(outbuf.Bytes()), userargs}
jobs <- jobFunc
}()
return proc
}
// JobStop kills a job
func JobStop(cmd *exec.Cmd) {
cmd.Process.Kill()
}
// JobSend sends the given data into the job's stdin stream
func JobSend(cmd *exec.Cmd, data string) {
stdin, err := cmd.StdinPipe()
if err != nil {
return
}
stdin.Write([]byte(data))
}

View File

@ -1,20 +0,0 @@
package main
// DisplayKeyMenu displays the nano-style key menu at the bottom of the screen
func DisplayKeyMenu() {
w, h := screen.Size()
bot := h - 3
display := []string{"^Q Quit, ^S Save, ^O Open, ^G Help, ^E Command Bar, ^K Cut Line", "^F Find, ^Z Undo, ^Y Redo, ^A Select All, ^D Duplicate Line, ^T New Tab"}
for y := 0; y < len(display); y++ {
for x := 0; x < w; x++ {
if x < len(display[y]) {
screen.SetContent(x, bot+y, rune(display[y][x]), nil, defStyle)
} else {
screen.SetContent(x, bot+y, ' ', nil, defStyle)
}
}
}
}

View File

@ -8,6 +8,7 @@ import (
"github.com/zyedidia/micro/cmd/micro/highlight"
)
// Finds the byte index of the nth rune in a byte slice
func runeToByteIndex(n int, txt []byte) int {
if n == 0 {
return 0
@ -39,10 +40,21 @@ type Line struct {
rehighlight bool
}
const (
// Line ending file formats
FFAuto = 0 // Autodetect format
FFUnix = 1 // LF line endings (unix style '\n')
FFDos = 2 // CRLF line endings (dos style '\r\n')
)
type FileFormat byte
// A LineArray simply stores and array of lines and makes it easy to insert
// and delete in it
type LineArray struct {
lines []Line
lines []Line
endings FileFormat
initsize uint64
}
// Append efficiently appends lines together
@ -52,7 +64,6 @@ func Append(slice []Line, data ...Line) []Line {
l := len(slice)
if l+len(data) > cap(slice) { // reallocate
newSlice := make([]Line, (l+len(data))+10000)
// The copy function is predeclared and works for any slice type.
copy(newSlice, slice)
slice = newSlice
}
@ -64,10 +75,11 @@ func Append(slice []Line, data ...Line) []Line {
}
// NewLineArray returns a new line array from an array of bytes
func NewLineArray(size int64, reader io.Reader) *LineArray {
func NewLineArray(size uint64, endings FileFormat, reader io.Reader) *LineArray {
la := new(LineArray)
la.lines = make([]Line, 0, 1000)
la.initsize = size
br := bufio.NewReader(reader)
var loaded int
@ -75,17 +87,27 @@ func NewLineArray(size int64, reader io.Reader) *LineArray {
n := 0
for {
data, err := br.ReadBytes('\n')
if len(data) > 1 && data[len(data)-2] == '\r' {
data = append(data[:len(data)-2], '\n')
if fileformat == 0 {
fileformat = 2
// Detect the line ending by checking to see if there is a '\r' char
// before the '\n'
// Even if the file format is set to DOS, the '\r' is removed so
// that all lines end with '\n'
dlen := len(data)
if dlen > 1 && data[dlen-2] == '\r' {
data = append(data[:dlen-2], '\n')
if endings == FFAuto {
la.endings = FFDos
}
} else if len(data) > 0 {
if fileformat == 0 {
fileformat = 1
} else if dlen > 0 {
if endings == FFAuto {
la.endings = FFUnix
}
}
// If we are loading a large file (greater than 1000) we use the file
// size and the length of the first 1000 lines to try to estimate
// how many lines will need to be allocated for the rest of the file
// We add an extra 10000 to the original estimate to be safe and give
// plenty of room for expansion
if n >= 1000 && loaded >= 0 {
totalLinesNum := int(float64(size) * (float64(n) / float64(loaded)))
newSlice := make([]Line, len(la.lines), totalLinesNum+10000)
@ -94,20 +116,19 @@ func NewLineArray(size int64, reader io.Reader) *LineArray {
loaded = -1
}
// Counter for the number of bytes in the first 1000 lines
if loaded >= 0 {
loaded += len(data)
loaded += dlen
}
if err != nil {
if err == io.EOF {
la.lines = Append(la.lines, Line{data[:], nil, nil, false})
// la.lines = Append(la.lines, Line{data[:len(data)]})
}
// Last line was read
break
} else {
// la.lines = Append(la.lines, Line{data[:len(data)-1]})
la.lines = Append(la.lines, Line{data[:len(data)-1], nil, nil, false})
la.lines = Append(la.lines, Line{data[:dlen-1], nil, nil, false})
}
n++
}
@ -115,49 +136,35 @@ func NewLineArray(size int64, reader io.Reader) *LineArray {
return la
}
// Returns the String representation of the LineArray
func (la *LineArray) String() string {
str := ""
for i, l := range la.lines {
str += string(l.data)
if i != len(la.lines)-1 {
str += "\n"
}
}
return str
}
// SaveString returns the string that should be written to disk when
// Bytes returns the string that should be written to disk when
// the line array is saved
// It is the same as string but uses crlf or lf line endings depending
func (la *LineArray) SaveString(useCrlf bool) string {
str := ""
func (la *LineArray) Bytes() []byte {
str := make([]byte, 0, la.initsize+1000) // initsize should provide a good estimate
for i, l := range la.lines {
str += string(l.data)
str = append(str, l.data...)
if i != len(la.lines)-1 {
if useCrlf {
str += "\r"
if la.endings == FFDos {
str = append(str, '\r')
}
str += "\n"
str = append(str, '\n')
}
}
return str
}
// NewlineBelow adds a newline below the given line number
func (la *LineArray) NewlineBelow(y int) {
// newlineBelow adds a newline below the given line number
func (la *LineArray) newlineBelow(y int) {
la.lines = append(la.lines, Line{[]byte{' '}, nil, nil, false})
copy(la.lines[y+2:], la.lines[y+1:])
la.lines[y+1] = Line{[]byte{}, la.lines[y].state, nil, false}
}
// inserts a byte array at a given location
// Inserts a byte array at a given location
func (la *LineArray) insert(pos Loc, value []byte) {
x, y := runeToByteIndex(pos.X, la.lines[pos.Y].data), pos.Y
// x, y := pos.x, pos.y
for i := 0; i < len(value); i++ {
if value[i] == '\n' {
la.Split(Loc{x, y})
la.split(Loc{x, y})
x = 0
y++
continue
@ -167,33 +174,33 @@ func (la *LineArray) insert(pos Loc, value []byte) {
}
}
// inserts a byte at a given location
// InsertByte inserts a byte at a given location
func (la *LineArray) insertByte(pos Loc, value byte) {
la.lines[pos.Y].data = append(la.lines[pos.Y].data, 0)
copy(la.lines[pos.Y].data[pos.X+1:], la.lines[pos.Y].data[pos.X:])
la.lines[pos.Y].data[pos.X] = value
}
// JoinLines joins the two lines a and b
func (la *LineArray) JoinLines(a, b int) {
// joinLines joins the two lines a and b
func (la *LineArray) joinLines(a, b int) {
la.insert(Loc{len(la.lines[a].data), a}, la.lines[b].data)
la.DeleteLine(b)
la.deleteLine(b)
}
// Split splits a line at a given position
func (la *LineArray) Split(pos Loc) {
la.NewlineBelow(pos.Y)
// split splits a line at a given position
func (la *LineArray) split(pos Loc) {
la.newlineBelow(pos.Y)
la.insert(Loc{0, pos.Y + 1}, la.lines[pos.Y].data[pos.X:])
la.lines[pos.Y+1].state = la.lines[pos.Y].state
la.lines[pos.Y].state = nil
la.lines[pos.Y].match = nil
la.lines[pos.Y+1].match = nil
la.lines[pos.Y].rehighlight = true
la.DeleteToEnd(Loc{pos.X, pos.Y})
la.deleteToEnd(Loc{pos.X, pos.Y})
}
// removes from start to end
func (la *LineArray) remove(start, end Loc) string {
func (la *LineArray) remove(start, end Loc) []byte {
sub := la.Substr(start, end)
startX := runeToByteIndex(start.X, la.lines[start.Y].data)
endX := runeToByteIndex(end.X, la.lines[end.Y].data)
@ -201,48 +208,50 @@ func (la *LineArray) remove(start, end Loc) string {
la.lines[start.Y].data = append(la.lines[start.Y].data[:startX], la.lines[start.Y].data[endX:]...)
} else {
for i := start.Y + 1; i <= end.Y-1; i++ {
la.DeleteLine(start.Y + 1)
la.deleteLine(start.Y + 1)
}
la.DeleteToEnd(Loc{startX, start.Y})
la.DeleteFromStart(Loc{endX - 1, start.Y + 1})
la.JoinLines(start.Y, start.Y+1)
la.deleteToEnd(Loc{startX, start.Y})
la.deleteFromStart(Loc{endX - 1, start.Y + 1})
la.joinLines(start.Y, start.Y+1)
}
return sub
}
// DeleteToEnd deletes from the end of a line to the position
func (la *LineArray) DeleteToEnd(pos Loc) {
// deleteToEnd deletes from the end of a line to the position
func (la *LineArray) deleteToEnd(pos Loc) {
la.lines[pos.Y].data = la.lines[pos.Y].data[:pos.X]
}
// DeleteFromStart deletes from the start of a line to the position
func (la *LineArray) DeleteFromStart(pos Loc) {
// deleteFromStart deletes from the start of a line to the position
func (la *LineArray) deleteFromStart(pos Loc) {
la.lines[pos.Y].data = la.lines[pos.Y].data[pos.X+1:]
}
// DeleteLine deletes the line number
func (la *LineArray) DeleteLine(y int) {
// deleteLine deletes the line number
func (la *LineArray) deleteLine(y int) {
la.lines = la.lines[:y+copy(la.lines[y:], la.lines[y+1:])]
}
// DeleteByte deletes the byte at a position
func (la *LineArray) DeleteByte(pos Loc) {
func (la *LineArray) deleteByte(pos Loc) {
la.lines[pos.Y].data = la.lines[pos.Y].data[:pos.X+copy(la.lines[pos.Y].data[pos.X:], la.lines[pos.Y].data[pos.X+1:])]
}
// Substr returns the string representation between two locations
func (la *LineArray) Substr(start, end Loc) string {
func (la *LineArray) Substr(start, end Loc) []byte {
startX := runeToByteIndex(start.X, la.lines[start.Y].data)
endX := runeToByteIndex(end.X, la.lines[end.Y].data)
if start.Y == end.Y {
return string(la.lines[start.Y].data[startX:endX])
return la.lines[start.Y].data[startX:endX]
}
var str string
str += string(la.lines[start.Y].data[startX:]) + "\n"
str := make([]byte, 0, len(la.lines[start.Y+1].data)*(end.Y-start.Y))
str = append(str, la.lines[start.Y].data[startX:]...)
str = append(str, '\n')
for i := start.Y + 1; i <= end.Y-1; i++ {
str += string(la.lines[i].data) + "\n"
str = append(str, la.lines[i].data...)
str = append(str, '\n')
}
str += string(la.lines[end.Y].data[:endX])
str = append(str, la.lines[end.Y].data[:endX]...)
return str
}

View File

@ -0,0 +1,60 @@
package main
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
var unicode_txt = `An preost wes on leoden, Laȝamon was ihoten
He wes Leovenaðes sone -- liðe him be Drihten.
He wonede at Ernleȝe at æðelen are chirechen,
Uppen Sevarne staþe, sel þar him þuhte,
Onfest Radestone, þer he bock radde.`
var la *LineArray
func init() {
reader := strings.NewReader(unicode_txt)
la = NewLineArray(uint64(len(unicode_txt)), FFAuto, reader)
}
func TestSplit(t *testing.T) {
la.insert(Loc{17, 1}, []byte{'\n'})
assert.Equal(t, len(la.lines), 6)
sub1 := la.Substr(Loc{0, 1}, Loc{17, 1})
sub2 := la.Substr(Loc{0, 2}, Loc{30, 2})
assert.Equal(t, []byte("He wes Leovenaðes"), sub1)
assert.Equal(t, []byte(" sone -- liðe him be Drihten."), sub2)
}
func TestJoin(t *testing.T) {
la.remove(Loc{47, 1}, Loc{0, 2})
assert.Equal(t, len(la.lines), 5)
sub := la.Substr(Loc{0, 1}, Loc{47, 1})
bytes := la.Bytes()
assert.Equal(t, []byte("He wes Leovenaðes sone -- liðe him be Drihten."), sub)
assert.Equal(t, unicode_txt, string(bytes))
}
func TestInsert(t *testing.T) {
la.insert(Loc{20, 3}, []byte(" foobar"))
sub1 := la.Substr(Loc{0, 3}, Loc{50, 3})
assert.Equal(t, []byte("Uppen Sevarne staþe, foobar sel þar him þuhte,"), sub1)
la.insert(Loc{25, 2}, []byte("ಮಣ್ಣಾಗಿ"))
sub2 := la.Substr(Loc{0, 2}, Loc{60, 2})
assert.Equal(t, []byte("He wonede at Ernleȝe at æಮಣ್ಣಾಗಿðelen are chirechen,"), sub2)
}
func TestRemove(t *testing.T) {
la.remove(Loc{20, 3}, Loc{27, 3})
la.remove(Loc{25, 2}, Loc{32, 2})
bytes := la.Bytes()
assert.Equal(t, unicode_txt, string(bytes))
}

View File

@ -1,59 +1,52 @@
package main
// FromCharPos converts from a character position to an x, y position
func FromCharPos(loc int, buf *Buffer) Loc {
charNum := 0
x, y := 0, 0
lineLen := Count(buf.Line(y)) + 1
for charNum+lineLen <= loc {
charNum += lineLen
y++
lineLen = Count(buf.Line(y)) + 1
}
x = loc - charNum
return Loc{x, y}
}
// ToCharPos converts from an x, y position to a character position
func ToCharPos(start Loc, buf *Buffer) int {
x, y := start.X, start.Y
loc := 0
for i := 0; i < y; i++ {
// + 1 for the newline
loc += Count(buf.Line(i)) + 1
}
loc += x
return loc
}
// InBounds returns whether the given location is a valid character position in the given buffer
func InBounds(pos Loc, buf *Buffer) bool {
if pos.Y < 0 || pos.Y >= buf.NumLines || pos.X < 0 || pos.X > Count(buf.Line(pos.Y)) {
return false
}
return true
}
// ByteOffset is just like ToCharPos except it counts bytes instead of runes
func ByteOffset(pos Loc, buf *Buffer) int {
x, y := pos.X, pos.Y
loc := 0
for i := 0; i < y; i++ {
// + 1 for the newline
loc += len(buf.Line(i)) + 1
}
loc += len(buf.Line(y)[:x])
return loc
}
import "unicode/utf8"
// Loc stores a location
type Loc struct {
X, Y int
}
// LessThan returns true if b is smaller
func (l Loc) LessThan(b Loc) bool {
if l.Y < b.Y {
return true
}
return l.Y == b.Y && l.X < b.X
}
// GreaterThan returns true if b is bigger
func (l Loc) GreaterThan(b Loc) bool {
if l.Y > b.Y {
return true
}
return l.Y == b.Y && l.X > b.X
}
// GreaterEqual returns true if b is greater than or equal to b
func (l Loc) GreaterEqual(b Loc) bool {
if l.Y > b.Y {
return true
}
if l.Y == b.Y && l.X > b.X {
return true
}
return l == b
}
// LessEqual returns true if b is less than or equal to b
func (l Loc) LessEqual(b Loc) bool {
if l.Y < b.Y {
return true
}
if l.Y == b.Y && l.X < b.X {
return true
}
return l == b
}
// The following functions require a buffer to know where newlines are
// Diff returns the distance between two locations
func Diff(a, b Loc, buf *Buffer) int {
if a.Y == b.Y {
@ -71,69 +64,19 @@ func Diff(a, b Loc, buf *Buffer) int {
loc := 0
for i := a.Y + 1; i < b.Y; i++ {
// + 1 for the newline
loc += Count(buf.Line(i)) + 1
loc += utf8.RuneCount(buf.LineBytes(i)) + 1
}
loc += Count(buf.Line(a.Y)) - a.X + b.X + 1
loc += utf8.RuneCount(buf.LineBytes(a.Y)) - a.X + b.X + 1
return loc
}
// LessThan returns true if b is smaller
func (l Loc) LessThan(b Loc) bool {
if l.Y < b.Y {
return true
}
if l.Y == b.Y && l.X < b.X {
return true
}
return false
}
// GreaterThan returns true if b is bigger
func (l Loc) GreaterThan(b Loc) bool {
if l.Y > b.Y {
return true
}
if l.Y == b.Y && l.X > b.X {
return true
}
return false
}
// GreaterEqual returns true if b is greater than or equal to b
func (l Loc) GreaterEqual(b Loc) bool {
if l.Y > b.Y {
return true
}
if l.Y == b.Y && l.X > b.X {
return true
}
if l == b {
return true
}
return false
}
// LessEqual returns true if b is less than or equal to b
func (l Loc) LessEqual(b Loc) bool {
if l.Y < b.Y {
return true
}
if l.Y == b.Y && l.X < b.X {
return true
}
if l == b {
return true
}
return false
}
// This moves the location one character to the right
func (l Loc) right(buf *Buffer) Loc {
if l == buf.End() {
return Loc{l.X + 1, l.Y}
}
var res Loc
if l.X < Count(buf.Line(l.Y)) {
if l.X < utf8.RuneCount(buf.LineBytes(l.Y)) {
res = Loc{l.X + 1, l.Y}
} else {
res = Loc{0, l.Y + 1}
@ -150,7 +93,7 @@ func (l Loc) left(buf *Buffer) Loc {
if l.X > 0 {
res = Loc{l.X - 1, l.Y}
} else {
res = Loc{Count(buf.Line(l.Y - 1)), l.Y - 1}
res = Loc{utf8.RuneCount(buf.LineBytes(l.Y - 1)), l.Y - 1}
}
return res
}

View File

@ -16,9 +16,8 @@ import (
"strings"
"time"
luar "layeh.com/gopher-luar"
lua "github.com/yuin/gopher-lua"
luar "layeh.com/gopher-luar"
)
var L *lua.LState

38
cmd/micro/message.go Normal file
View File

@ -0,0 +1,38 @@
package main
import (
"bufio"
"fmt"
"os"
"strconv"
)
// TermMessage sends a message to the user in the terminal. This usually occurs before
// micro has been fully initialized -- ie if there is an error in the syntax highlighting
// regular expressions
// The function must be called when the screen is not initialized
// This will write the message, and wait for the user
// to press and key to continue
func TermMessage(msg ...interface{}) {
screenWasNil := screen == nil
if !screenWasNil {
screen.Fini()
screen = nil
}
fmt.Println(msg...)
fmt.Print("\nPress enter to continue")
reader := bufio.NewReader(os.Stdin)
reader.ReadString('\n')
if !screenWasNil {
InitScreen()
}
}
// TermError sends an error to the user in the terminal. Like TermMessage except formatted
// as an error
func TermError(filename string, lineNum int, err string) {
TermMessage(filename + ", " + strconv.Itoa(lineNum) + ": " + err)
}

View File

@ -1,669 +0,0 @@
package main
import (
"bufio"
"bytes"
"encoding/gob"
"fmt"
"os"
"strconv"
"github.com/mattn/go-runewidth"
"github.com/zyedidia/clipboard"
"github.com/zyedidia/micro/cmd/micro/shellwords"
"github.com/zyedidia/tcell"
)
// TermMessage sends a message to the user in the terminal. This usually occurs before
// micro has been fully initialized -- ie if there is an error in the syntax highlighting
// regular expressions
// The function must be called when the screen is not initialized
// This will write the message, and wait for the user
// to press and key to continue
func TermMessage(msg ...interface{}) {
screenWasNil := screen == nil
if !screenWasNil {
screen.Fini()
screen = nil
}
fmt.Println(msg...)
fmt.Print("\nPress enter to continue")
reader := bufio.NewReader(os.Stdin)
reader.ReadString('\n')
if !screenWasNil {
InitScreen()
}
}
// TermError sends an error to the user in the terminal. Like TermMessage except formatted
// as an error
func TermError(filename string, lineNum int, err string) {
TermMessage(filename + ", " + strconv.Itoa(lineNum) + ": " + err)
}
// Messenger is an object that makes it easy to send messages to the user
// and get input from the user
type Messenger struct {
log *Buffer
// Are we currently prompting the user?
hasPrompt bool
// Is there a message to print
hasMessage bool
// Message to print
message string
// The user's response to a prompt
response string
// style to use when drawing the message
style tcell.Style
// We have to keep track of the cursor for prompting
cursorx int
// This map stores the history for all the different kinds of uses Prompt has
// It's a map of history type -> history array
history map[string][]string
historyNum int
// Is the current message a message from the gutter
gutterMessage bool
}
// AddLog sends a message to the log view
func (m *Messenger) AddLog(msg ...interface{}) {
logMessage := fmt.Sprint(msg...)
buffer := m.getBuffer()
buffer.insert(buffer.End(), []byte(logMessage+"\n"))
buffer.Cursor.Loc = buffer.End()
buffer.Cursor.Relocate()
}
func (m *Messenger) getBuffer() *Buffer {
if m.log == nil {
m.log = NewBufferFromString("", "")
m.log.name = "Log"
}
return m.log
}
// Message sends a message to the user
func (m *Messenger) Message(msg ...interface{}) {
displayMessage := fmt.Sprint(msg...)
// only display a new message if there isn't an active prompt
// this is to prevent overwriting an existing prompt to the user
if m.hasPrompt == false {
// if there is no active prompt then style and display the message as normal
m.message = displayMessage
m.style = defStyle
if _, ok := colorscheme["message"]; ok {
m.style = colorscheme["message"]
}
m.hasMessage = true
}
// add the message to the log regardless of active prompts
m.AddLog(displayMessage)
}
// Error sends an error message to the user
func (m *Messenger) Error(msg ...interface{}) {
buf := new(bytes.Buffer)
fmt.Fprint(buf, msg...)
// only display a new message if there isn't an active prompt
// this is to prevent overwriting an existing prompt to the user
if m.hasPrompt == false {
// if there is no active prompt then style and display the message as normal
m.message = buf.String()
m.style = defStyle.
Foreground(tcell.ColorBlack).
Background(tcell.ColorMaroon)
if _, ok := colorscheme["error-message"]; ok {
m.style = colorscheme["error-message"]
}
m.hasMessage = true
}
// add the message to the log regardless of active prompts
m.AddLog(buf.String())
}
func (m *Messenger) PromptText(msg ...interface{}) {
displayMessage := fmt.Sprint(msg...)
// if there is no active prompt then style and display the message as normal
m.message = displayMessage
m.style = defStyle
if _, ok := colorscheme["message"]; ok {
m.style = colorscheme["message"]
}
m.hasMessage = true
// add the message to the log regardless of active prompts
m.AddLog(displayMessage)
}
// YesNoPrompt asks the user a yes or no question (waits for y or n) and returns the result
func (m *Messenger) YesNoPrompt(prompt string) (bool, bool) {
m.hasPrompt = true
m.PromptText(prompt)
_, h := screen.Size()
for {
m.Clear()
m.Display()
screen.ShowCursor(Count(m.message), h-1)
screen.Show()
event := <-events
switch e := event.(type) {
case *tcell.EventKey:
switch e.Key() {
case tcell.KeyRune:
if e.Rune() == 'y' || e.Rune() == 'Y' {
m.AddLog("\t--> y")
m.hasPrompt = false
return true, false
} else if e.Rune() == 'n' || e.Rune() == 'N' {
m.AddLog("\t--> n")
m.hasPrompt = false
return false, false
}
case tcell.KeyCtrlC, tcell.KeyCtrlQ, tcell.KeyEscape:
m.AddLog("\t--> (cancel)")
m.Clear()
m.Reset()
m.hasPrompt = false
return false, true
}
}
}
}
// LetterPrompt gives the user a prompt and waits for a one letter response
func (m *Messenger) LetterPrompt(prompt string, responses ...rune) (rune, bool) {
m.hasPrompt = true
m.PromptText(prompt)
_, h := screen.Size()
for {
m.Clear()
m.Display()
screen.ShowCursor(Count(m.message), h-1)
screen.Show()
event := <-events
switch e := event.(type) {
case *tcell.EventKey:
switch e.Key() {
case tcell.KeyRune:
for _, r := range responses {
if e.Rune() == r {
m.AddLog("\t--> " + string(r))
m.Clear()
m.Reset()
m.hasPrompt = false
return r, false
}
}
case tcell.KeyCtrlC, tcell.KeyCtrlQ, tcell.KeyEscape:
m.AddLog("\t--> (cancel)")
m.Clear()
m.Reset()
m.hasPrompt = false
return ' ', true
}
}
}
}
// Completion represents a type of completion
type Completion int
const (
NoCompletion Completion = iota
FileCompletion
CommandCompletion
HelpCompletion
OptionCompletion
PluginCmdCompletion
PluginNameCompletion
OptionValueCompletion
)
// Prompt sends the user a message and waits for a response to be typed in
// This function blocks the main loop while waiting for input
func (m *Messenger) Prompt(prompt, placeholder, historyType string, completionTypes ...Completion) (string, bool) {
m.hasPrompt = true
m.PromptText(prompt)
if _, ok := m.history[historyType]; !ok {
m.history[historyType] = []string{""}
} else {
m.history[historyType] = append(m.history[historyType], "")
}
m.historyNum = len(m.history[historyType]) - 1
response, canceled := placeholder, true
m.response = response
m.cursorx = Count(placeholder)
RedrawAll()
for m.hasPrompt {
var suggestions []string
m.Clear()
event := <-events
switch e := event.(type) {
case *tcell.EventResize:
for _, t := range tabs {
t.Resize()
}
RedrawAll()
case *tcell.EventKey:
switch e.Key() {
case tcell.KeyCtrlQ, tcell.KeyCtrlC, tcell.KeyEscape:
// Cancel
m.AddLog("\t--> (cancel)")
m.hasPrompt = false
case tcell.KeyEnter:
// User is done entering their response
m.AddLog("\t--> " + m.response)
m.hasPrompt = false
response, canceled = m.response, false
m.history[historyType][len(m.history[historyType])-1] = response
case tcell.KeyTab:
args, err := shellwords.Split(m.response)
if err != nil {
break
}
currentArg := ""
currentArgNum := 0
if len(args) > 0 {
currentArgNum = len(args) - 1
currentArg = args[currentArgNum]
}
var completionType Completion
if completionTypes[0] == CommandCompletion && currentArgNum > 0 {
if command, ok := commands[args[0]]; ok {
completionTypes = append([]Completion{CommandCompletion}, command.completions...)
}
}
if currentArgNum >= len(completionTypes) {
completionType = completionTypes[len(completionTypes)-1]
} else {
completionType = completionTypes[currentArgNum]
}
var chosen string
if completionType == FileCompletion {
chosen, suggestions = FileComplete(currentArg)
} else if completionType == CommandCompletion {
chosen, suggestions = CommandComplete(currentArg)
} else if completionType == HelpCompletion {
chosen, suggestions = HelpComplete(currentArg)
} else if completionType == OptionCompletion {
chosen, suggestions = OptionComplete(currentArg)
} else if completionType == OptionValueCompletion {
if currentArgNum-1 > 0 {
chosen, suggestions = OptionValueComplete(args[currentArgNum-1], currentArg)
}
} else if completionType == PluginCmdCompletion {
chosen, suggestions = PluginCmdComplete(currentArg)
} else if completionType == PluginNameCompletion {
chosen, suggestions = PluginNameComplete(currentArg)
} else if completionType < NoCompletion {
chosen, suggestions = PluginComplete(completionType, currentArg)
}
if len(suggestions) > 1 {
chosen = chosen + CommonSubstring(suggestions...)
}
if len(suggestions) != 0 && chosen != "" {
m.response = shellwords.Join(append(args[:len(args)-1], chosen)...)
m.cursorx = Count(m.response)
}
}
}
m.HandleEvent(event, m.history[historyType])
m.Clear()
for _, v := range tabs[curTab].Views {
v.Display()
}
DisplayTabs()
m.Display()
if len(suggestions) > 1 {
m.DisplaySuggestions(suggestions)
}
screen.Show()
}
m.Clear()
m.Reset()
return response, canceled
}
// UpHistory fetches the previous item in the history
func (m *Messenger) UpHistory(history []string) {
if m.historyNum > 0 {
m.historyNum--
m.response = history[m.historyNum]
m.cursorx = Count(m.response)
}
}
// DownHistory fetches the next item in the history
func (m *Messenger) DownHistory(history []string) {
if m.historyNum < len(history)-1 {
m.historyNum++
m.response = history[m.historyNum]
m.cursorx = Count(m.response)
}
}
// CursorLeft moves the cursor one character left
func (m *Messenger) CursorLeft() {
if m.cursorx > 0 {
m.cursorx--
}
}
// CursorRight moves the cursor one character right
func (m *Messenger) CursorRight() {
if m.cursorx < Count(m.response) {
m.cursorx++
}
}
// Start moves the cursor to the start of the line
func (m *Messenger) Start() {
m.cursorx = 0
}
// End moves the cursor to the end of the line
func (m *Messenger) End() {
m.cursorx = Count(m.response)
}
// Backspace deletes one character
func (m *Messenger) Backspace() {
if m.cursorx > 0 {
m.response = string([]rune(m.response)[:m.cursorx-1]) + string([]rune(m.response)[m.cursorx:])
m.cursorx--
}
}
// Paste pastes the clipboard
func (m *Messenger) Paste() {
clip, _ := clipboard.ReadAll("clipboard")
m.response = Insert(m.response, m.cursorx, clip)
m.cursorx += Count(clip)
}
// WordLeft moves the cursor one word to the left
func (m *Messenger) WordLeft() {
response := []rune(m.response)
m.CursorLeft()
if m.cursorx <= 0 {
return
}
for IsWhitespace(response[m.cursorx]) {
if m.cursorx <= 0 {
return
}
m.CursorLeft()
}
m.CursorLeft()
for IsWordChar(string(response[m.cursorx])) {
if m.cursorx <= 0 {
return
}
m.CursorLeft()
}
m.CursorRight()
}
// WordRight moves the cursor one word to the right
func (m *Messenger) WordRight() {
response := []rune(m.response)
if m.cursorx >= len(response) {
return
}
for IsWhitespace(response[m.cursorx]) {
m.CursorRight()
if m.cursorx >= len(response) {
m.CursorRight()
return
}
}
m.CursorRight()
if m.cursorx >= len(response) {
return
}
for IsWordChar(string(response[m.cursorx])) {
m.CursorRight()
if m.cursorx >= len(response) {
return
}
}
}
// DeleteWordLeft deletes one word to the left
func (m *Messenger) DeleteWordLeft() {
m.WordLeft()
m.response = string([]rune(m.response)[:m.cursorx])
}
// HandleEvent handles an event for the prompter
func (m *Messenger) HandleEvent(event tcell.Event, history []string) {
switch e := event.(type) {
case *tcell.EventKey:
switch e.Key() {
case tcell.KeyCtrlA:
m.Start()
case tcell.KeyCtrlE:
m.End()
case tcell.KeyUp:
m.UpHistory(history)
case tcell.KeyDown:
m.DownHistory(history)
case tcell.KeyLeft:
if e.Modifiers() == tcell.ModCtrl {
m.Start()
} else if e.Modifiers() == tcell.ModAlt || e.Modifiers() == tcell.ModMeta {
m.WordLeft()
} else {
m.CursorLeft()
}
case tcell.KeyRight:
if e.Modifiers() == tcell.ModCtrl {
m.End()
} else if e.Modifiers() == tcell.ModAlt || e.Modifiers() == tcell.ModMeta {
m.WordRight()
} else {
m.CursorRight()
}
case tcell.KeyBackspace2, tcell.KeyBackspace:
if e.Modifiers() == tcell.ModCtrl || e.Modifiers() == tcell.ModAlt || e.Modifiers() == tcell.ModMeta {
m.DeleteWordLeft()
} else {
m.Backspace()
}
case tcell.KeyCtrlW:
m.DeleteWordLeft()
case tcell.KeyCtrlV:
m.Paste()
case tcell.KeyCtrlF:
m.WordRight()
case tcell.KeyCtrlB:
m.WordLeft()
case tcell.KeyRune:
m.response = Insert(m.response, m.cursorx, string(e.Rune()))
m.cursorx++
}
history[m.historyNum] = m.response
case *tcell.EventPaste:
clip := e.Text()
m.response = Insert(m.response, m.cursorx, clip)
m.cursorx += Count(clip)
case *tcell.EventMouse:
x, y := e.Position()
x -= Count(m.message)
button := e.Buttons()
_, screenH := screen.Size()
if y == screenH-1 {
switch button {
case tcell.Button1:
m.cursorx = x
if m.cursorx < 0 {
m.cursorx = 0
} else if m.cursorx > Count(m.response) {
m.cursorx = Count(m.response)
}
}
}
}
}
// Reset resets the messenger's cursor, message and response
func (m *Messenger) Reset() {
m.cursorx = 0
m.message = ""
m.response = ""
}
// Clear clears the line at the bottom of the editor
func (m *Messenger) Clear() {
w, h := screen.Size()
for x := 0; x < w; x++ {
screen.SetContent(x, h-1, ' ', nil, defStyle)
}
}
func (m *Messenger) DisplaySuggestions(suggestions []string) {
w, screenH := screen.Size()
y := screenH - 2
statusLineStyle := defStyle.Reverse(true)
if style, ok := colorscheme["statusline"]; ok {
statusLineStyle = style
}
for x := 0; x < w; x++ {
screen.SetContent(x, y, ' ', nil, statusLineStyle)
}
x := 0
for _, suggestion := range suggestions {
for _, c := range suggestion {
screen.SetContent(x, y, c, nil, statusLineStyle)
x++
}
screen.SetContent(x, y, ' ', nil, statusLineStyle)
x++
}
}
// Display displays messages or prompts
func (m *Messenger) Display() {
_, h := screen.Size()
if m.hasMessage {
if m.hasPrompt || globalSettings["infobar"].(bool) {
runes := []rune(m.message + m.response)
posx := 0
for x := 0; x < len(runes); x++ {
screen.SetContent(posx, h-1, runes[x], nil, m.style)
posx += runewidth.RuneWidth(runes[x])
}
}
}
if m.hasPrompt {
screen.ShowCursor(Count(m.message)+m.cursorx, h-1)
screen.Show()
}
}
// LoadHistory attempts to load user history from configDir/buffers/history
// into the history map
// The savehistory option must be on
func (m *Messenger) LoadHistory() {
if GetGlobalOption("savehistory").(bool) {
file, err := os.Open(configDir + "/buffers/history")
defer file.Close()
var decodedMap map[string][]string
if err == nil {
decoder := gob.NewDecoder(file)
err = decoder.Decode(&decodedMap)
if err != nil {
m.Error("Error loading history:", err)
return
}
}
if decodedMap != nil {
m.history = decodedMap
} else {
m.history = make(map[string][]string)
}
} else {
m.history = make(map[string][]string)
}
}
// SaveHistory saves the user's command history to configDir/buffers/history
// only if the savehistory option is on
func (m *Messenger) SaveHistory() {
if GetGlobalOption("savehistory").(bool) {
// Don't save history past 100
for k, v := range m.history {
if len(v) > 100 {
m.history[k] = v[len(m.history[k])-100:]
}
}
file, err := os.Create(configDir + "/buffers/history")
defer file.Close()
if err == nil {
encoder := gob.NewEncoder(file)
err = encoder.Encode(m.history)
if err != nil {
m.Error("Error saving history:", err)
return
}
}
}
}
// A GutterMessage is a message displayed on the side of the editor
type GutterMessage struct {
lineNum int
msg string
kind int
}
// These are the different types of messages
const (
// GutterInfo represents a simple info message
GutterInfo = iota
// GutterWarning represents a compiler warning
GutterWarning
// GutterError represents a compiler error
GutterError
)

View File

@ -3,22 +3,13 @@ package main
import (
"flag"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/go-errors/errors"
"github.com/mattn/go-isatty"
"github.com/mitchellh/go-homedir"
"github.com/yuin/gopher-lua"
"github.com/zyedidia/clipboard"
homedir "github.com/mitchellh/go-homedir"
"github.com/zyedidia/micro/cmd/micro/terminfo"
"github.com/zyedidia/tcell"
"github.com/zyedidia/tcell/encoding"
"layeh.com/gopher-luar"
)
const (
@ -31,13 +22,6 @@ var (
// The main screen
screen tcell.Screen
// Object to send messages and prompts to the user
messenger *Messenger
// The default highlighting style
// This simply defines the default foreground and background colors
defStyle tcell.Style
// Where the user's configuration is
// This should be $XDG_CONFIG_HOME/micro
// If $XDG_CONFIG_HOME is not set, it is ~/.config/micro
@ -50,88 +34,23 @@ var (
CommitHash = "Unknown"
// CompileDate is the date this binary was compiled on
CompileDate = "Unknown"
// The list of views
tabs []*Tab
// This is the currently open tab
// It's just an index to the tab in the tabs array
curTab int
// Channel of jobs running in the background
jobs chan JobFunction
// Debug logging
Debug = "ON"
// Event channel
events chan tcell.Event
autosave chan bool
// Channels for the terminal emulator
updateterm chan bool
closeterm chan int
// How many redraws have happened
numRedraw uint
// Command line flags
flagVersion = flag.Bool("version", false, "Show the version number and information")
flagStartPos = flag.String("startpos", "", "LINE,COL to start the cursor at when opening a buffer.")
flagConfigDir = flag.String("config-dir", "", "Specify a custom location for the configuration directory")
flagOptions = flag.Bool("options", false, "Show all option help")
)
// LoadInput determines which files should be loaded into buffers
// based on the input stored in flag.Args()
func LoadInput() []*Buffer {
// There are a number of ways micro should start given its input
// 1. If it is given a files in flag.Args(), it should open those
// 2. If there is no input file and the input is not a terminal, that means
// something is being piped in and the stdin should be opened in an
// empty buffer
// 3. If there is no input file and the input is a terminal, an empty buffer
// should be opened
var filename string
var input []byte
var err error
args := flag.Args()
buffers := make([]*Buffer, 0, len(args))
if len(args) > 0 {
// Option 1
// We go through each file and load it
for i := 0; i < len(args); i++ {
if strings.HasPrefix(args[i], "+") {
if strings.Contains(args[i], ":") {
split := strings.Split(args[i], ":")
*flagStartPos = split[0][1:] + "," + split[1]
} else {
*flagStartPos = args[i][1:] + ",0"
}
continue
}
buf, err := NewBufferFromFile(args[i])
if err != nil {
TermMessage(err)
continue
}
// If the file didn't exist, input will be empty, and we'll open an empty buffer
buffers = append(buffers, buf)
}
} else if !isatty.IsTerminal(os.Stdin.Fd()) {
// Option 2
// The input is not a terminal, so something is being piped in
// and we should read from stdin
input, err = ioutil.ReadAll(os.Stdin)
if err != nil {
TermMessage("Error reading from stdin: ", err)
input = []byte{}
}
buffers = append(buffers, NewBufferFromString(string(input), filename))
} else {
// Option 3, just open an empty buffer
buffers = append(buffers, NewBufferFromString(string(input), filename))
}
return buffers
}
// InitConfigDir finds the configuration directory for micro according to the XDG spec.
// If no directory is found, it creates one.
func InitConfigDir() {
@ -230,63 +149,7 @@ func InitScreen() {
// screen.SetStyle(defStyle)
}
// RedrawAll redraws everything -- all the views and the messenger
func RedrawAll() {
messenger.Clear()
w, h := screen.Size()
for x := 0; x < w; x++ {
for y := 0; y < h; y++ {
screen.SetContent(x, y, ' ', nil, defStyle)
}
}
for _, v := range tabs[curTab].Views {
v.Display()
}
DisplayTabs()
messenger.Display()
if globalSettings["keymenu"].(bool) {
DisplayKeyMenu()
}
screen.Show()
if numRedraw%50 == 0 {
runtime.GC()
}
numRedraw++
}
func LoadAll() {
// Find the user's configuration directory (probably $XDG_CONFIG_HOME/micro)
InitConfigDir()
// Build a list of available Extensions (Syntax, Colorscheme etc.)
InitRuntimeFiles()
// Load the user's settings
InitGlobalSettings()
InitCommands()
InitBindings()
InitColorscheme()
LoadPlugins()
for _, tab := range tabs {
for _, v := range tab.Views {
v.Buf.UpdateRules()
}
}
}
// Command line flags
var flagVersion = flag.Bool("version", false, "Show the version number and information")
var flagStartPos = flag.String("startpos", "", "LINE,COL to start the cursor at when opening a buffer.")
var flagConfigDir = flag.String("config-dir", "", "Specify a custom location for the configuration directory")
var flagOptions = flag.Bool("options", false, "Show all option help")
func main() {
func InitFlags() {
flag.Usage = func() {
fmt.Println("Usage: micro [OPTIONS] [FILE]...")
fmt.Println("-config-dir dir")
@ -327,35 +190,32 @@ func main() {
// If -options was passed
for k, v := range DefaultGlobalSettings() {
fmt.Printf("-%s value\n", k)
fmt.Printf(" \tThe %s option. Default value: '%v'\n", k, v)
fmt.Printf(" \tDefault value: '%v'\n", v)
}
os.Exit(0)
}
}
// Start the Lua VM for running plugins
L = lua.NewState()
defer L.Close()
func main() {
var err error
// Some encoding stuff in case the user isn't using UTF-8
encoding.Register()
tcell.SetEncodingFallback(tcell.EncodingFallbackASCII)
// Find the user's configuration directory (probably $XDG_CONFIG_HOME/micro)
InitLog()
InitFlags()
InitConfigDir()
// Build a list of available Extensions (Syntax, Colorscheme etc.)
InitRuntimeFiles()
// Load the user's settings
err = ReadSettings()
if err != nil {
TermMessage(err)
}
InitGlobalSettings()
err = InitColorscheme()
if err != nil {
TermMessage(err)
}
InitCommands()
InitBindings()
// Start the screen
InitScreen()
// This is just so if we have an error, we can exit cleanly and not completely
// If we have an error, we can exit cleanly and not completely
// mess up the terminal being worked in
// In other words we need to shut down tcell before the program crashes
defer func() {
@ -368,227 +228,26 @@ func main() {
}
}()
// Create a new messenger
// This is used for sending the user messages in the bottom of the editor
messenger = new(Messenger)
messenger.LoadHistory()
b, err := NewBufferFromFile(os.Args[1])
// Now we load the input
buffers := LoadInput()
if len(buffers) == 0 {
screen.Fini()
os.Exit(1)
if err != nil {
TermMessage(err)
}
for _, buf := range buffers {
// For each buffer we create a new tab and place the view in that tab
tab := NewTabFromView(NewView(buf))
tab.SetNum(len(tabs))
tabs = append(tabs, tab)
for _, t := range tabs {
for _, v := range t.Views {
v.Center(false)
}
width, height := screen.Size()
t.Resize()
}
w := NewWindow(0, 0, width/2, height/2, b)
for i := 0; i < 5; i++ {
screen.Clear()
w.DisplayBuffer()
w.DisplayStatusLine()
screen.Show()
time.Sleep(200 * time.Millisecond)
w.StartLine++
}
for k, v := range optionFlags {
if *v != "" {
SetOption(k, *v)
}
}
// time.Sleep(2 * time.Second)
// Load all the plugin stuff
// We give plugins access to a bunch of variables here which could be useful to them
L.SetGlobal("OS", luar.New(L, runtime.GOOS))
L.SetGlobal("tabs", luar.New(L, tabs))
L.SetGlobal("GetTabs", luar.New(L, func() []*Tab {
return tabs
}))
L.SetGlobal("curTab", luar.New(L, curTab))
L.SetGlobal("messenger", luar.New(L, messenger))
L.SetGlobal("GetOption", luar.New(L, GetOption))
L.SetGlobal("AddOption", luar.New(L, AddOption))
L.SetGlobal("SetOption", luar.New(L, SetOption))
L.SetGlobal("SetLocalOption", luar.New(L, SetLocalOption))
L.SetGlobal("BindKey", luar.New(L, BindKey))
L.SetGlobal("MakeCommand", luar.New(L, MakeCommand))
L.SetGlobal("CurView", luar.New(L, CurView))
L.SetGlobal("IsWordChar", luar.New(L, IsWordChar))
L.SetGlobal("HandleCommand", luar.New(L, HandleCommand))
L.SetGlobal("HandleShellCommand", luar.New(L, HandleShellCommand))
L.SetGlobal("ExecCommand", luar.New(L, ExecCommand))
L.SetGlobal("RunShellCommand", luar.New(L, RunShellCommand))
L.SetGlobal("RunBackgroundShell", luar.New(L, RunBackgroundShell))
L.SetGlobal("RunInteractiveShell", luar.New(L, RunInteractiveShell))
L.SetGlobal("TermEmuSupported", luar.New(L, TermEmuSupported))
L.SetGlobal("RunTermEmulator", luar.New(L, RunTermEmulator))
L.SetGlobal("GetLeadingWhitespace", luar.New(L, GetLeadingWhitespace))
L.SetGlobal("MakeCompletion", luar.New(L, MakeCompletion))
L.SetGlobal("NewBuffer", luar.New(L, NewBufferFromString))
L.SetGlobal("NewBufferFromFile", luar.New(L, NewBufferFromFile))
L.SetGlobal("RuneStr", luar.New(L, func(r rune) string {
return string(r)
}))
L.SetGlobal("Loc", luar.New(L, func(x, y int) Loc {
return Loc{x, y}
}))
L.SetGlobal("WorkingDirectory", luar.New(L, os.Getwd))
L.SetGlobal("JoinPaths", luar.New(L, filepath.Join))
L.SetGlobal("DirectoryName", luar.New(L, filepath.Dir))
L.SetGlobal("configDir", luar.New(L, configDir))
L.SetGlobal("Reload", luar.New(L, LoadAll))
L.SetGlobal("ByteOffset", luar.New(L, ByteOffset))
L.SetGlobal("ToCharPos", luar.New(L, ToCharPos))
// Used for asynchronous jobs
L.SetGlobal("JobStart", luar.New(L, JobStart))
L.SetGlobal("JobSpawn", luar.New(L, JobSpawn))
L.SetGlobal("JobSend", luar.New(L, JobSend))
L.SetGlobal("JobStop", luar.New(L, JobStop))
// Extension Files
L.SetGlobal("ReadRuntimeFile", luar.New(L, PluginReadRuntimeFile))
L.SetGlobal("ListRuntimeFiles", luar.New(L, PluginListRuntimeFiles))
L.SetGlobal("AddRuntimeFile", luar.New(L, PluginAddRuntimeFile))
L.SetGlobal("AddRuntimeFilesFromDirectory", luar.New(L, PluginAddRuntimeFilesFromDirectory))
L.SetGlobal("AddRuntimeFileFromMemory", luar.New(L, PluginAddRuntimeFileFromMemory))
// Access to Go stdlib
L.SetGlobal("import", luar.New(L, Import))
jobs = make(chan JobFunction, 100)
events = make(chan tcell.Event, 100)
autosave = make(chan bool)
updateterm = make(chan bool)
closeterm = make(chan int)
LoadPlugins()
for _, t := range tabs {
for _, v := range t.Views {
GlobalPluginCall("onViewOpen", v)
GlobalPluginCall("onBufferOpen", v.Buf)
}
}
InitColorscheme()
messenger.style = defStyle
// Here is the event loop which runs in a separate thread
go func() {
for {
if screen != nil {
events <- screen.PollEvent()
}
}
}()
go func() {
for {
time.Sleep(autosaveTime * time.Second)
if globalSettings["autosave"].(bool) {
autosave <- true
}
}
}()
for {
// Display everything
RedrawAll()
var event tcell.Event
// Check for new events
select {
case f := <-jobs:
// If a new job has finished while running in the background we should execute the callback
f.function(f.output, f.args...)
continue
case <-autosave:
if CurView().Buf.Path != "" {
CurView().Save(true)
}
case <-updateterm:
continue
case vnum := <-closeterm:
tabs[curTab].Views[vnum].CloseTerminal()
case event = <-events:
}
for event != nil {
didAction := false
switch e := event.(type) {
case *tcell.EventResize:
for _, t := range tabs {
t.Resize()
}
case *tcell.EventMouse:
if !searching {
if e.Buttons() == tcell.Button1 {
// If the user left clicked we check a couple things
_, h := screen.Size()
x, y := e.Position()
if y == h-1 && messenger.message != "" && globalSettings["infobar"].(bool) {
// If the user clicked in the bottom bar, and there is a message down there
// we copy it to the clipboard.
// Often error messages are displayed down there so it can be useful to easily
// copy the message
clipboard.WriteAll(messenger.message, "primary")
break
}
if CurView().mouseReleased {
// We loop through each view in the current tab and make sure the current view
// is the one being clicked in
for _, v := range tabs[curTab].Views {
if x >= v.x && x < v.x+v.Width && y >= v.y && y < v.y+v.Height {
tabs[curTab].CurView = v.Num
}
}
}
} else if e.Buttons() == tcell.WheelUp || e.Buttons() == tcell.WheelDown {
var view *View
x, y := e.Position()
for _, v := range tabs[curTab].Views {
if x >= v.x && x < v.x+v.Width && y >= v.y && y < v.y+v.Height {
view = tabs[curTab].Views[v.Num]
}
}
if view != nil {
view.HandleEvent(e)
didAction = true
}
}
}
}
if !didAction {
// This function checks the mouse event for the possibility of changing the current tab
// If the tab was changed it returns true
if TabbarHandleMouseEvent(event) {
break
}
if searching {
// Since searching is done in real time, we need to redraw every time
// there is a new event in the search bar so we need a special function
// to run instead of the standard HandleEvent.
HandleSearchEvent(event, CurView())
} else {
// Send it to the view
CurView().HandleEvent(event)
}
}
select {
case event = <-events:
default:
event = nil
}
}
}
screen.Fini()
}

View File

@ -1,184 +0,0 @@
package main
import (
"errors"
"io/ioutil"
"os"
"strings"
"github.com/yuin/gopher-lua"
"github.com/zyedidia/tcell"
"layeh.com/gopher-luar"
)
var loadedPlugins map[string]string
// Call calls the lua function 'function'
// If it does not exist nothing happens, if there is an error,
// the error is returned
func Call(function string, args ...interface{}) (lua.LValue, error) {
var luaFunc lua.LValue
if strings.Contains(function, ".") {
plugin := L.GetGlobal(strings.Split(function, ".")[0])
if plugin.String() == "nil" {
return nil, errors.New("function does not exist: " + function)
}
luaFunc = L.GetField(plugin, strings.Split(function, ".")[1])
} else {
luaFunc = L.GetGlobal(function)
}
if luaFunc.String() == "nil" {
return nil, errors.New("function does not exist: " + function)
}
var luaArgs []lua.LValue
for _, v := range args {
luaArgs = append(luaArgs, luar.New(L, v))
}
err := L.CallByParam(lua.P{
Fn: luaFunc,
NRet: 1,
Protect: true,
}, luaArgs...)
ret := L.Get(-1) // returned value
if ret.String() != "nil" {
L.Pop(1) // remove received value
}
return ret, err
}
// LuaFunctionBinding is a function generator which takes the name of a lua function
// and creates a function that will call that lua function
// Specifically it creates a function that can be called as a binding because this is used
// to bind keys to lua functions
func LuaFunctionBinding(function string) func(*View, bool) bool {
return func(v *View, _ bool) bool {
_, err := Call(function, nil)
if err != nil {
TermMessage(err)
}
return false
}
}
// LuaFunctionMouseBinding is a function generator which takes the name of a lua function
// and creates a function that will call that lua function
// Specifically it creates a function that can be called as a mouse binding because this is used
// to bind mouse actions to lua functions
func LuaFunctionMouseBinding(function string) func(*View, bool, *tcell.EventMouse) bool {
return func(v *View, _ bool, e *tcell.EventMouse) bool {
_, err := Call(function, e)
if err != nil {
TermMessage(err)
}
return false
}
}
func unpack(old []string) []interface{} {
new := make([]interface{}, len(old))
for i, v := range old {
new[i] = v
}
return new
}
// LuaFunctionCommand is the same as LuaFunctionBinding except it returns a normal function
// so that a command can be bound to a lua function
func LuaFunctionCommand(function string) func([]string) {
return func(args []string) {
_, err := Call(function, unpack(args)...)
if err != nil {
TermMessage(err)
}
}
}
// LuaFunctionComplete returns a function which can be used for autocomplete in plugins
func LuaFunctionComplete(function string) func(string) []string {
return func(input string) (result []string) {
res, err := Call(function, input)
if err != nil {
TermMessage(err)
}
if tbl, ok := res.(*lua.LTable); !ok {
TermMessage(function, "should return a table of strings")
} else {
for i := 1; i <= tbl.Len(); i++ {
val := tbl.RawGetInt(i)
if v, ok := val.(lua.LString); !ok {
TermMessage(function, "should return a table of strings")
} else {
result = append(result, string(v))
}
}
}
return result
}
}
// LuaFunctionJob returns a function that will call the given lua function
// structured as a job call i.e. the job output and arguments are provided
// to the lua function
func LuaFunctionJob(function string) func(string, ...string) {
return func(output string, args ...string) {
_, err := Call(function, unpack(append([]string{output}, args...))...)
if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") {
TermMessage(err)
}
}
}
// luaPluginName convert a human-friendly plugin name into a valid lua variable name.
func luaPluginName(name string) string {
return strings.Replace(name, "-", "_", -1)
}
// LoadPlugins loads the pre-installed plugins and the plugins located in ~/.config/micro/plugins
func LoadPlugins() {
loadedPlugins = make(map[string]string)
for _, plugin := range ListRuntimeFiles(RTPlugin) {
pluginName := plugin.Name()
if _, ok := loadedPlugins[pluginName]; ok {
continue
}
data, err := plugin.Data()
if err != nil {
TermMessage("Error loading plugin: " + pluginName)
continue
}
pluginLuaName := luaPluginName(pluginName)
if err := LoadFile(pluginLuaName, pluginLuaName, string(data)); err != nil {
TermMessage(err)
continue
}
loadedPlugins[pluginName] = pluginLuaName
}
if _, err := os.Stat(configDir + "/init.lua"); err == nil {
data, _ := ioutil.ReadFile(configDir + "/init.lua")
if err := LoadFile("init", configDir+"init.lua", string(data)); err != nil {
TermMessage(err)
}
loadedPlugins["init"] = "init"
}
}
// GlobalCall makes a call to a function in every plugin that is currently
// loaded
func GlobalPluginCall(function string, args ...interface{}) {
for pl := range loadedPlugins {
_, err := Call(pl+"."+function, args...)
if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") {
TermMessage(err)
continue
}
}
}

View File

@ -1,622 +0,0 @@
package main
import (
"archive/zip"
"bytes"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"github.com/blang/semver"
"github.com/flynn/json5"
"github.com/yuin/gopher-lua"
)
var (
allPluginPackages PluginPackages
)
// CorePluginName is a plugin dependency name for the micro core.
const CorePluginName = "micro"
// PluginChannel contains an url to a json list of PluginRepository
type PluginChannel string
// PluginChannels is a slice of PluginChannel
type PluginChannels []PluginChannel
// PluginRepository contains an url to json file containing PluginPackages
type PluginRepository string
// PluginPackage contains the meta-data of a plugin and all available versions
type PluginPackage struct {
Name string
Description string
Author string
Tags []string
Versions PluginVersions
}
// PluginPackages is a list of PluginPackage instances.
type PluginPackages []*PluginPackage
// PluginVersion descripes a version of a PluginPackage. Containing a version, download url and also dependencies.
type PluginVersion struct {
pack *PluginPackage
Version semver.Version
Url string
Require PluginDependencies
}
// PluginVersions is a slice of PluginVersion
type PluginVersions []*PluginVersion
// PluginDependency descripes a dependency to another plugin or micro itself.
type PluginDependency struct {
Name string
Range semver.Range
}
// PluginDependencies is a slice of PluginDependency
type PluginDependencies []*PluginDependency
func (pp *PluginPackage) String() string {
buf := new(bytes.Buffer)
buf.WriteString("Plugin: ")
buf.WriteString(pp.Name)
buf.WriteRune('\n')
if pp.Author != "" {
buf.WriteString("Author: ")
buf.WriteString(pp.Author)
buf.WriteRune('\n')
}
if pp.Description != "" {
buf.WriteRune('\n')
buf.WriteString(pp.Description)
}
return buf.String()
}
func fetchAllSources(count int, fetcher func(i int) PluginPackages) PluginPackages {
wgQuery := new(sync.WaitGroup)
wgQuery.Add(count)
results := make(chan PluginPackages)
wgDone := new(sync.WaitGroup)
wgDone.Add(1)
var packages PluginPackages
for i := 0; i < count; i++ {
go func(i int) {
results <- fetcher(i)
wgQuery.Done()
}(i)
}
go func() {
packages = make(PluginPackages, 0)
for res := range results {
packages = append(packages, res...)
}
wgDone.Done()
}()
wgQuery.Wait()
close(results)
wgDone.Wait()
return packages
}
// Fetch retrieves all available PluginPackages from the given channels
func (pc PluginChannels) Fetch() PluginPackages {
return fetchAllSources(len(pc), func(i int) PluginPackages {
return pc[i].Fetch()
})
}
// Fetch retrieves all available PluginPackages from the given channel
func (pc PluginChannel) Fetch() PluginPackages {
// messenger.AddLog(fmt.Sprintf("Fetching channel: %q", string(pc)))
resp, err := http.Get(string(pc))
if err != nil {
TermMessage("Failed to query plugin channel:\n", err)
return PluginPackages{}
}
defer resp.Body.Close()
decoder := json5.NewDecoder(resp.Body)
var repositories []PluginRepository
if err := decoder.Decode(&repositories); err != nil {
TermMessage("Failed to decode channel data:\n", err)
return PluginPackages{}
}
return fetchAllSources(len(repositories), func(i int) PluginPackages {
return repositories[i].Fetch()
})
}
// Fetch retrieves all available PluginPackages from the given repository
func (pr PluginRepository) Fetch() PluginPackages {
// messenger.AddLog(fmt.Sprintf("Fetching repository: %q", string(pr)))
resp, err := http.Get(string(pr))
if err != nil {
TermMessage("Failed to query plugin repository:\n", err)
return PluginPackages{}
}
defer resp.Body.Close()
decoder := json5.NewDecoder(resp.Body)
var plugins PluginPackages
if err := decoder.Decode(&plugins); err != nil {
TermMessage("Failed to decode repository data:\n", err)
return PluginPackages{}
}
if len(plugins) > 0 {
return PluginPackages{plugins[0]}
}
return nil
// return plugins
}
// UnmarshalJSON unmarshals raw json to a PluginVersion
func (pv *PluginVersion) UnmarshalJSON(data []byte) error {
var values struct {
Version semver.Version
Url string
Require map[string]string
}
if err := json5.Unmarshal(data, &values); err != nil {
return err
}
pv.Version = values.Version
pv.Url = values.Url
pv.Require = make(PluginDependencies, 0)
for k, v := range values.Require {
// don't add the dependency if it's the core and
// we have a unknown version number.
// in that case just accept that dependency (which equals to not adding it.)
if k != CorePluginName || !isUnknownCoreVersion() {
if vRange, err := semver.ParseRange(v); err == nil {
pv.Require = append(pv.Require, &PluginDependency{k, vRange})
}
}
}
return nil
}
// UnmarshalJSON unmarshals raw json to a PluginPackage
func (pp *PluginPackage) UnmarshalJSON(data []byte) error {
var values struct {
Name string
Description string
Author string
Tags []string
Versions PluginVersions
}
if err := json5.Unmarshal(data, &values); err != nil {
return err
}
pp.Name = values.Name
pp.Description = values.Description
pp.Author = values.Author
pp.Tags = values.Tags
pp.Versions = values.Versions
for _, v := range pp.Versions {
v.pack = pp
}
return nil
}
// GetAllPluginPackages gets all PluginPackages which may be available.
func GetAllPluginPackages() PluginPackages {
if allPluginPackages == nil {
getOption := func(name string) []string {
data := GetOption(name)
if strs, ok := data.([]string); ok {
return strs
}
if ifs, ok := data.([]interface{}); ok {
result := make([]string, len(ifs))
for i, urlIf := range ifs {
if url, ok := urlIf.(string); ok {
result[i] = url
} else {
return nil
}
}
return result
}
return nil
}
channels := PluginChannels{}
for _, url := range getOption("pluginchannels") {
channels = append(channels, PluginChannel(url))
}
repos := []PluginRepository{}
for _, url := range getOption("pluginrepos") {
repos = append(repos, PluginRepository(url))
}
allPluginPackages = fetchAllSources(len(repos)+1, func(i int) PluginPackages {
if i == 0 {
return channels.Fetch()
}
return repos[i-1].Fetch()
})
}
return allPluginPackages
}
func (pv PluginVersions) find(ppName string) *PluginVersion {
for _, v := range pv {
if v.pack.Name == ppName {
return v
}
}
return nil
}
// Len returns the number of pluginversions in this slice
func (pv PluginVersions) Len() int {
return len(pv)
}
// Swap two entries of the slice
func (pv PluginVersions) Swap(i, j int) {
pv[i], pv[j] = pv[j], pv[i]
}
// Less returns true if the version at position i is greater then the version at position j (used for sorting)
func (pv PluginVersions) Less(i, j int) bool {
return pv[i].Version.GT(pv[j].Version)
}
// Match returns true if the package matches a given search text
func (pp PluginPackage) Match(text string) bool {
text = strings.ToLower(text)
for _, t := range pp.Tags {
if strings.ToLower(t) == text {
return true
}
}
if strings.Contains(strings.ToLower(pp.Name), text) {
return true
}
if strings.Contains(strings.ToLower(pp.Description), text) {
return true
}
return false
}
// IsInstallable returns true if the package can be installed.
func (pp PluginPackage) IsInstallable() error {
_, err := GetAllPluginPackages().Resolve(GetInstalledVersions(true), PluginDependencies{
&PluginDependency{
Name: pp.Name,
Range: semver.Range(func(v semver.Version) bool { return true }),
}})
return err
}
// SearchPlugin retrieves a list of all PluginPackages which match the given search text and
// could be or are already installed
func SearchPlugin(texts []string) (plugins PluginPackages) {
plugins = make(PluginPackages, 0)
pluginLoop:
for _, pp := range GetAllPluginPackages() {
for _, text := range texts {
if !pp.Match(text) {
continue pluginLoop
}
}
if err := pp.IsInstallable(); err == nil {
plugins = append(plugins, pp)
}
}
return
}
func isUnknownCoreVersion() bool {
_, err := semver.ParseTolerant(Version)
return err != nil
}
func newStaticPluginVersion(name, version string) *PluginVersion {
vers, err := semver.ParseTolerant(version)
if err != nil {
if vers, err = semver.ParseTolerant("0.0.0-" + version); err != nil {
vers = semver.MustParse("0.0.0-unknown")
}
}
pl := &PluginPackage{
Name: name,
}
pv := &PluginVersion{
pack: pl,
Version: vers,
}
pl.Versions = PluginVersions{pv}
return pv
}
// GetInstalledVersions returns a list of all currently installed plugins including an entry for
// micro itself. This can be used to resolve dependencies.
func GetInstalledVersions(withCore bool) PluginVersions {
result := PluginVersions{}
if withCore {
result = append(result, newStaticPluginVersion(CorePluginName, Version))
}
for name, lpname := range loadedPlugins {
version := GetInstalledPluginVersion(lpname)
if pv := newStaticPluginVersion(name, version); pv != nil {
result = append(result, pv)
}
}
return result
}
// GetInstalledPluginVersion returns the string of the exported VERSION variable of a loaded plugin
func GetInstalledPluginVersion(name string) string {
plugin := L.GetGlobal(name)
if plugin != lua.LNil {
version := L.GetField(plugin, "VERSION")
if str, ok := version.(lua.LString); ok {
return string(str)
}
}
return ""
}
// DownloadAndInstall downloads and installs the given plugin and version
func (pv *PluginVersion) DownloadAndInstall() error {
messenger.AddLog(fmt.Sprintf("Downloading %q (%s) from %q", pv.pack.Name, pv.Version, pv.Url))
resp, err := http.Get(pv.Url)
if err != nil {
return err
}
defer resp.Body.Close()
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
zipbuf := bytes.NewReader(data)
z, err := zip.NewReader(zipbuf, zipbuf.Size())
if err != nil {
return err
}
targetDir := filepath.Join(configDir, "plugins", pv.pack.Name)
dirPerm := os.FileMode(0755)
if err = os.MkdirAll(targetDir, dirPerm); err != nil {
return err
}
// Check if all files in zip are in the same directory.
// this might be the case if the plugin zip contains the whole plugin dir
// instead of its content.
var prefix string
allPrefixed := false
for i, f := range z.File {
parts := strings.Split(f.Name, "/")
if i == 0 {
prefix = parts[0]
} else if parts[0] != prefix {
allPrefixed = false
break
} else {
// switch to true since we have at least a second file
allPrefixed = true
}
}
// Install files and directory's
for _, f := range z.File {
parts := strings.Split(f.Name, "/")
if allPrefixed {
parts = parts[1:]
}
targetName := filepath.Join(targetDir, filepath.Join(parts...))
if f.FileInfo().IsDir() {
if err := os.MkdirAll(targetName, dirPerm); err != nil {
return err
}
} else {
basepath := filepath.Dir(targetName)
if err := os.MkdirAll(basepath, dirPerm); err != nil {
return err
}
content, err := f.Open()
if err != nil {
return err
}
defer content.Close()
target, err := os.Create(targetName)
if err != nil {
return err
}
defer target.Close()
if _, err = io.Copy(target, content); err != nil {
return err
}
}
}
return nil
}
func (pl PluginPackages) Get(name string) *PluginPackage {
for _, p := range pl {
if p.Name == name {
return p
}
}
return nil
}
func (pl PluginPackages) GetAllVersions(name string) PluginVersions {
result := make(PluginVersions, 0)
p := pl.Get(name)
if p != nil {
for _, v := range p.Versions {
result = append(result, v)
}
}
return result
}
func (req PluginDependencies) Join(other PluginDependencies) PluginDependencies {
m := make(map[string]*PluginDependency)
for _, r := range req {
m[r.Name] = r
}
for _, o := range other {
cur, ok := m[o.Name]
if ok {
m[o.Name] = &PluginDependency{
o.Name,
o.Range.AND(cur.Range),
}
} else {
m[o.Name] = o
}
}
result := make(PluginDependencies, 0, len(m))
for _, v := range m {
result = append(result, v)
}
return result
}
// Resolve resolves dependencies between different plugins
func (all PluginPackages) Resolve(selectedVersions PluginVersions, open PluginDependencies) (PluginVersions, error) {
if len(open) == 0 {
return selectedVersions, nil
}
currentRequirement, stillOpen := open[0], open[1:]
if currentRequirement != nil {
if selVersion := selectedVersions.find(currentRequirement.Name); selVersion != nil {
if currentRequirement.Range(selVersion.Version) {
return all.Resolve(selectedVersions, stillOpen)
}
return nil, fmt.Errorf("unable to find a matching version for \"%s\"", currentRequirement.Name)
}
availableVersions := all.GetAllVersions(currentRequirement.Name)
sort.Sort(availableVersions)
for _, version := range availableVersions {
if currentRequirement.Range(version.Version) {
resolved, err := all.Resolve(append(selectedVersions, version), stillOpen.Join(version.Require))
if err == nil {
return resolved, nil
}
}
}
return nil, fmt.Errorf("unable to find a matching version for \"%s\"", currentRequirement.Name)
}
return selectedVersions, nil
}
func (pv PluginVersions) install() {
anyInstalled := false
currentlyInstalled := GetInstalledVersions(true)
for _, sel := range pv {
if sel.pack.Name != CorePluginName {
shouldInstall := true
if pv := currentlyInstalled.find(sel.pack.Name); pv != nil {
if pv.Version.NE(sel.Version) {
messenger.AddLog("Uninstalling", sel.pack.Name)
UninstallPlugin(sel.pack.Name)
} else {
shouldInstall = false
}
}
if shouldInstall {
if err := sel.DownloadAndInstall(); err != nil {
messenger.Error(err)
return
}
anyInstalled = true
}
}
}
if anyInstalled {
messenger.Message("One or more plugins installed. Please restart micro.")
} else {
messenger.AddLog("Nothing to install / update")
}
}
// UninstallPlugin deletes the plugin folder of the given plugin
func UninstallPlugin(name string) {
if err := os.RemoveAll(filepath.Join(configDir, "plugins", name)); err != nil {
messenger.Error(err)
return
}
delete(loadedPlugins, name)
}
// Install installs the plugin
func (pl PluginPackage) Install() {
selected, err := GetAllPluginPackages().Resolve(GetInstalledVersions(true), PluginDependencies{
&PluginDependency{
Name: pl.Name,
Range: semver.Range(func(v semver.Version) bool { return true }),
}})
if err != nil {
TermMessage(err)
return
}
selected.install()
}
// UpdatePlugins updates the given plugins
func UpdatePlugins(plugins []string) {
// if no plugins are specified, update all installed plugins.
if len(plugins) == 0 {
for name := range loadedPlugins {
plugins = append(plugins, name)
}
}
messenger.AddLog("Checking for plugin updates")
microVersion := PluginVersions{
newStaticPluginVersion(CorePluginName, Version),
}
var updates = make(PluginDependencies, 0)
for _, name := range plugins {
pv := GetInstalledPluginVersion(name)
r, err := semver.ParseRange(">=" + pv) // Try to get newer versions.
if err == nil {
updates = append(updates, &PluginDependency{
Name: name,
Range: r,
})
}
}
selected, err := GetAllPluginPackages().Resolve(microVersion, updates)
if err != nil {
TermMessage(err)
return
}
selected.install()
}

View File

@ -1,56 +0,0 @@
package main
import (
"testing"
"github.com/blang/semver"
"github.com/flynn/json5"
)
func TestDependencyResolving(t *testing.T) {
js := `
[{
"Name": "Foo",
"Versions": [{ "Version": "1.0.0" }, { "Version": "1.5.0" },{ "Version": "2.0.0" }]
}, {
"Name": "Bar",
"Versions": [{ "Version": "1.0.0", "Require": {"Foo": ">1.0.0 <2.0.0"} }]
}, {
"Name": "Unresolvable",
"Versions": [{ "Version": "1.0.0", "Require": {"Foo": "<=1.0.0", "Bar": ">0.0.0"} }]
}]
`
var all PluginPackages
err := json5.Unmarshal([]byte(js), &all)
if err != nil {
t.Error(err)
}
selected, err := all.Resolve(PluginVersions{}, PluginDependencies{
&PluginDependency{"Bar", semver.MustParseRange(">=1.0.0")},
})
check := func(name, version string) {
v := selected.find(name)
expected := semver.MustParse(version)
if v == nil {
t.Errorf("Failed to resolve %s", name)
} else if expected.NE(v.Version) {
t.Errorf("%s resolved in wrong version %v", name, v)
}
}
if err != nil {
t.Error(err)
} else {
check("Foo", "1.5.0")
check("Bar", "1.0.0")
}
selected, err = all.Resolve(PluginVersions{}, PluginDependencies{
&PluginDependency{"Unresolvable", semver.MustParseRange(">0.0.0")},
})
if err == nil {
t.Error("Unresolvable package resolved:", selected)
}
}

14
cmd/micro/profile.go Normal file
View File

@ -0,0 +1,14 @@
package main
import (
"fmt"
"runtime"
humanize "github.com/dustin/go-humanize"
)
func GetMemStats() string {
var memstats runtime.MemStats
runtime.ReadMemStats(&memstats)
return fmt.Sprintf("Alloc: %s, Sys: %s, GC: %d, PauseTotalNs: %dns", humanize.Bytes(memstats.Alloc), humanize.Bytes(memstats.Sys), memstats.NumGC, memstats.PauseTotalNs)
}

View File

@ -8,12 +8,15 @@ import (
)
const (
RTColorscheme = "colorscheme"
RTSyntax = "syntax"
RTHelp = "help"
RTPlugin = "plugin"
RTColorscheme = 0
RTSyntax = 1
RTHelp = 2
RTPlugin = 3
NumTypes = 4 // How many filetypes are there
)
type RTFiletype byte
// RuntimeFile allows the program to read runtime data like colorschemes or syntax files
type RuntimeFile interface {
// Name returns a name of the file without paths or extensions
@ -23,7 +26,7 @@ type RuntimeFile interface {
}
// allFiles contains all available files, mapped by filetype
var allFiles map[string][]RuntimeFile
var allFiles [NumTypes][]RuntimeFile
// some file on filesystem
type realFile string
@ -73,16 +76,13 @@ func (nf namedFile) Name() string {
}
// AddRuntimeFile registers a file for the given filetype
func AddRuntimeFile(fileType string, file RuntimeFile) {
if allFiles == nil {
allFiles = make(map[string][]RuntimeFile)
}
func AddRuntimeFile(fileType RTFiletype, file RuntimeFile) {
allFiles[fileType] = append(allFiles[fileType], file)
}
// AddRuntimeFilesFromDirectory registers each file from the given directory for
// the filetype which matches the file-pattern
func AddRuntimeFilesFromDirectory(fileType, directory, pattern string) {
func AddRuntimeFilesFromDirectory(fileType RTFiletype, directory, pattern string) {
files, _ := ioutil.ReadDir(directory)
for _, f := range files {
if ok, _ := filepath.Match(pattern, f.Name()); !f.IsDir() && ok {
@ -94,7 +94,7 @@ func AddRuntimeFilesFromDirectory(fileType, directory, pattern string) {
// AddRuntimeFilesFromAssets registers each file from the given asset-directory for
// the filetype which matches the file-pattern
func AddRuntimeFilesFromAssets(fileType, directory, pattern string) {
func AddRuntimeFilesFromAssets(fileType RTFiletype, directory, pattern string) {
files, err := AssetDir(directory)
if err != nil {
return
@ -108,7 +108,7 @@ func AddRuntimeFilesFromAssets(fileType, directory, pattern string) {
// FindRuntimeFile finds a runtime file of the given filetype and name
// will return nil if no file was found
func FindRuntimeFile(fileType, name string) RuntimeFile {
func FindRuntimeFile(fileType RTFiletype, name string) RuntimeFile {
for _, f := range ListRuntimeFiles(fileType) {
if f.Name() == name {
return f
@ -118,16 +118,13 @@ func FindRuntimeFile(fileType, name string) RuntimeFile {
}
// ListRuntimeFiles lists all known runtime files for the given filetype
func ListRuntimeFiles(fileType string) []RuntimeFile {
if files, ok := allFiles[fileType]; ok {
return files
}
return []RuntimeFile{}
func ListRuntimeFiles(fileType RTFiletype) []RuntimeFile {
return allFiles[fileType]
}
// InitRuntimeFiles initializes all assets file and the config directory
func InitRuntimeFiles() {
add := func(fileType, dir, pattern string) {
add := func(fileType RTFiletype, dir, pattern string) {
AddRuntimeFilesFromDirectory(fileType, filepath.Join(configDir, dir), pattern)
AddRuntimeFilesFromAssets(fileType, path.Join("runtime", dir), pattern)
}
@ -160,7 +157,7 @@ func InitRuntimeFiles() {
}
// PluginReadRuntimeFile allows plugin scripts to read the content of a runtime file
func PluginReadRuntimeFile(fileType, name string) string {
func PluginReadRuntimeFile(fileType RTFiletype, name string) string {
if file := FindRuntimeFile(fileType, name); file != nil {
if data, err := file.Data(); err == nil {
return string(data)
@ -170,7 +167,7 @@ func PluginReadRuntimeFile(fileType, name string) string {
}
// PluginListRuntimeFiles allows plugins to lists all runtime files of the given type
func PluginListRuntimeFiles(fileType string) []string {
func PluginListRuntimeFiles(fileType RTFiletype) []string {
files := ListRuntimeFiles(fileType)
result := make([]string, len(files))
for i, f := range files {
@ -180,7 +177,7 @@ func PluginListRuntimeFiles(fileType string) []string {
}
// PluginAddRuntimeFile adds a file to the runtime files for a plugin
func PluginAddRuntimeFile(plugin, filetype, filePath string) {
func PluginAddRuntimeFile(plugin string, filetype RTFiletype, filePath string) {
fullpath := filepath.Join(configDir, "plugins", plugin, filePath)
if _, err := os.Stat(fullpath); err == nil {
AddRuntimeFile(filetype, realFile(fullpath))
@ -191,7 +188,7 @@ func PluginAddRuntimeFile(plugin, filetype, filePath string) {
}
// PluginAddRuntimeFilesFromDirectory adds files from a directory to the runtime files for a plugin
func PluginAddRuntimeFilesFromDirectory(plugin, filetype, directory, pattern string) {
func PluginAddRuntimeFilesFromDirectory(plugin string, filetype RTFiletype, directory, pattern string) {
fullpath := filepath.Join(configDir, "plugins", plugin, directory)
if _, err := os.Stat(fullpath); err == nil {
AddRuntimeFilesFromDirectory(filetype, fullpath, pattern)
@ -202,6 +199,6 @@ func PluginAddRuntimeFilesFromDirectory(plugin, filetype, directory, pattern str
}
// PluginAddRuntimeFileFromMemory adds a file to the runtime files for a plugin from a given string
func PluginAddRuntimeFileFromMemory(plugin, filetype, filename, data string) {
func PluginAddRuntimeFileFromMemory(plugin string, filetype RTFiletype, filename, data string) {
AddRuntimeFile(filetype, memoryFile{filename, []byte(data)})
}

42
cmd/micro/rtfiles_test.go Normal file
View File

@ -0,0 +1,42 @@
package main
import (
"testing"
"github.com/stretchr/testify/assert"
)
func init() {
InitRuntimeFiles()
}
func TestAddFile(t *testing.T) {
AddRuntimeFile(RTPlugin, memoryFile{"foo.lua", []byte("hello world\n")})
AddRuntimeFile(RTSyntax, memoryFile{"bar", []byte("some syntax file\n")})
f1 := FindRuntimeFile(RTPlugin, "foo.lua")
assert.NotNil(t, f1)
assert.Equal(t, "foo.lua", f1.Name())
data, err := f1.Data()
assert.Nil(t, err)
assert.Equal(t, []byte("hello world\n"), data)
f2 := FindRuntimeFile(RTSyntax, "bar")
assert.NotNil(t, f2)
assert.Equal(t, "bar", f2.Name())
data, err = f2.Data()
assert.Nil(t, err)
assert.Equal(t, []byte("some syntax file\n"), data)
}
func TestFindFile(t *testing.T) {
f := FindRuntimeFile(RTSyntax, "go")
assert.NotNil(t, f)
assert.Equal(t, "go", f.Name())
data, err := f.Data()
assert.Nil(t, err)
assert.Equal(t, []byte("filetype: go"), data[:12])
e := FindRuntimeFile(RTSyntax, "foobar")
assert.Nil(t, e)
}

View File

@ -3176,7 +3176,7 @@ func runtimeSyntaxSedYaml() (*asset, error) {
return a, nil
}
var _runtimeSyntaxShYaml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x94\x54\x7f\x73\x1b\x45\x0c\xfd\x3f\x9f\xe2\xea\x64\xa8\xdd\x62\xd3\x96\xb6\x03\xe6\x87\x29\x2d\x30\x9d\x02\xed\x0c\x30\x93\x21\x9b\x96\xf5\xae\xce\xb7\xdc\xfe\xb8\xee\xea\xe2\x84\xbe\x7c\x77\x46\x67\x3b\x69\x4d\xa7\x40\x26\xb7\x3a\x49\x96\xf4\xa4\x7d\xa7\xda\x79\xe2\x8b\x8e\xe6\x55\x69\xc8\xfb\x83\x03\x4b\x4c\x86\xe7\x07\x55\x55\x55\xe2\x8c\x3a\xd0\xbc\x1a\x8d\x95\x9a\x95\xe6\x08\x4a\xcd\x96\xba\x34\x22\xb7\x42\xd4\x6c\xb0\x15\x5b\xc3\x2b\xed\x9d\x2e\x54\xf0\x8e\xb2\x73\xd6\x7d\x34\xec\x52\xdc\xba\xaf\xd5\xdd\x0f\xba\x9c\xa4\x36\xde\x51\x94\x9a\xed\x5e\x77\xf2\x45\xbb\xda\xe8\xed\x2a\xb4\x33\x93\x62\x7d\xe5\xca\x66\xa3\xbf\x78\xf6\xc3\xb7\xbf\x3d\xfd\xf1\x09\x66\xb4\xec\x9d\xb7\x4a\x1d\xe1\xd1\x8b\x67\x83\x6d\x32\x1a\xda\x6c\x48\x5b\xca\xf3\x6a\xf4\xf2\xf0\xc6\xec\xd6\x27\x63\x8a\x67\xd5\xed\xc9\x62\xbc\xd4\x93\xc5\x58\x4f\x16\xa5\x19\x57\x38\x9a\x8c\x0e\x0e\x72\xef\xa9\x6c\x66\x73\x58\xfd\xdc\x87\x25\xe5\x32\x68\xd3\xca\xa4\x58\x58\x47\x9e\xc5\xc1\x3c\xaf\x46\x4a\x2d\x4f\xee\x4c\x3f\x3f\xbd\xad\xd4\x72\x53\x48\xa2\x1e\xa7\x68\x9d\x74\xab\x7d\xa9\x74\xb4\x12\xc8\x39\xf9\xaa\xf6\x69\xbd\xcd\x55\x58\x33\x05\x8a\xbc\xc9\x32\x36\xba\x10\x6c\x82\x4d\x91\x40\xde\xd5\x20\x5f\x08\x54\xb4\x01\x9d\x3b\x46\xed\x50\xa7\x8c\xdd\x20\xe1\x6a\xb8\x08\x9f\x8c\xf6\xc8\xa4\x2d\x32\x71\x9f\x23\x0a\x79\x32\x8c\xd2\xb8\x9a\xc1\x0d\x45\xb0\x0b\x84\x3e\xb2\xf3\x58\x37\xce\xd3\xe4\x0a\xec\xb4\x2a\x1d\x19\xa7\xfd\xe6\xf6\xdf\x40\xa9\x4b\x28\x35\x86\x52\x13\x28\xf5\x05\x94\x3a\x85\x52\x27\xf8\x03\x4a\x29\x05\x19\xec\x97\xf8\x1a\x37\xf0\x15\x3e\x82\x52\x98\x5c\x37\xfd\x8b\x70\xab\x32\x29\x04\x1d\xed\x6e\x62\x1b\xda\x6d\x1a\xb4\x20\xd3\x24\xd0\x79\x97\x32\xc3\x13\xa3\x10\xa3\x0f\xba\xb4\xe8\x63\x21\x9e\xec\x0d\x31\x84\x14\x2b\xef\x62\x7f\xfe\x81\xb4\xe3\x15\xdc\x6a\xb2\xd0\xeb\x76\x60\x12\xac\x1c\xb5\x8b\x16\x4a\xad\xdf\xdc\xf9\xf8\xfe\xe5\x2a\x53\x87\xd6\x79\x3f\x1c\xda\xfb\x2b\x8f\xa7\x52\x10\x74\x2b\xe4\x12\x7f\x21\x8b\xd2\x80\x75\xde\x87\x92\xa9\x67\xe7\xcb\x07\x70\x2c\x75\xa1\x87\xf7\x05\xc3\xf0\x39\xc1\x68\x86\x69\x4c\x8a\x30\xcd\x2a\x77\x30\x4d\x48\x16\xa6\x49\x6b\xb1\xe4\x94\x18\xa6\x2d\x7d\x80\xa4\x84\xe9\x60\x4a\xe7\x1d\xc3\xf4\x0c\xab\x99\x60\x2d\x6c\x0d\xeb\xb2\x3c\x26\xf9\x94\x8b\xbc\x0d\xc9\x6d\x0f\x8a\x67\x32\x4a\x1d\xad\x88\x8c\x5a\x1b\x16\x7a\x68\x61\x4d\x1d\x18\x75\xf2\x16\x42\x7b\x34\xa9\xb0\xb3\x90\x7f\xe1\xaf\xf7\xf8\x33\x09\x75\x5c\x6c\xe1\x85\x42\xab\x21\xa9\x2f\x08\xf6\x81\x40\x0a\xad\x94\x0d\x6d\xed\xea\x84\xd0\xc6\x64\x11\x5a\xa6\xd0\x21\x9c\x21\x3a\x43\x88\x1e\x31\x35\x7d\x87\xd8\xe5\x64\x10\xfb\x20\x25\x93\x45\xa7\x0b\x13\x3a\xcd\x8d\x69\x5a\x74\x2e\xb6\x17\xe8\x32\xba\xec\x22\x0b\xe4\xe1\xa5\x46\xc7\xe7\xe8\xd6\x76\x20\xee\x80\x23\x93\xf6\x12\x85\x1c\x90\x83\x94\xcf\x7d\x94\xe9\x15\x7a\x8d\x71\x69\xf4\x5d\x94\x46\xdf\xbb\x77\x7f\x10\x0f\x1e\x8a\xf8\xf4\xb3\x41\x7b\x70\xf7\xde\x44\x50\x97\x26\x0f\xf7\xd7\xd7\x28\x9e\xa8\x43\x11\x9e\x6d\xa6\x2a\x5f\x1b\x0a\xdb\xa5\x38\x99\x2f\x30\x04\x5c\x44\x03\xd6\xf2\x38\x0f\x26\x02\x53\xe1\xcd\xe7\x22\x47\xea\x19\x9c\x7a\xd3\x80\x33\x38\xf7\x24\x47\x34\x72\x39\x3c\xe4\x96\x44\xfd\x30\xbb\x3e\x6e\xef\xa2\x8f\xee\x35\xfa\x38\xf4\xd4\x17\xca\x05\x67\xd2\xcc\xda\x60\xdd\x24\x79\x74\x70\xb8\xa0\xb2\x4f\xb0\xab\x85\x51\xd5\x5e\xaf\xca\x7b\x96\xc4\x74\x7a\xa2\xa7\x7f\x4d\x4f\x6f\x8f\xde\xbb\x41\xaa\xc1\x2d\xde\xad\xdb\x59\x8a\xec\x6a\xb7\xdd\x53\x47\x4a\xbd\x59\xc8\xae\x7a\x34\xfd\xfd\xd5\x8d\x6f\x0e\x8f\x6e\x2d\xa6\xb2\xb6\x2e\x17\xa3\xff\x1f\xb0\xbf\x0f\x0b\x67\x17\x57\xf3\x6d\x3f\x95\x40\xcb\x03\xac\xd1\xe8\xca\x46\xd1\xee\x59\x4a\xeb\xba\xa1\x94\x52\xb3\x6b\xeb\x5b\xfb\x77\xf7\xf7\x76\xa5\xcd\xbe\x7a\xdc\xe8\x7c\x1d\xfa\x5f\xe1\xdc\xdc\x47\x73\x73\xbf\x6c\x75\x72\x7a\x9d\x2d\x0c\xc3\xfd\x47\x96\xf1\x4b\x28\x55\x26\x87\xfb\xc9\x8e\xfe\xa5\x07\x4e\x36\x49\xf8\xaf\xcf\x9f\x3c\xc7\xf1\xf1\x31\xbe\x7f\x7a\xfc\xd3\x77\x93\xb9\xcc\xf3\xef\x00\x00\x00\xff\xff\xef\x45\x76\x90\xa3\x07\x00\x00")
var _runtimeSyntaxShYaml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x94\x54\x7f\x73\x1b\x45\x0c\xfd\x3f\x9f\xe2\xea\x64\xa8\xdd\x62\xd3\x96\xb6\x03\xe6\x87\x29\x2d\x30\x9d\x02\xed\x0c\x30\x93\x21\x9b\x96\xf5\xae\xce\xb7\xdc\xfe\xb8\xee\xea\xe2\x84\xbe\x7c\x77\x46\x67\x3b\x69\x4d\xa7\x40\x26\xb7\x3a\x49\x96\xf4\xa4\x7d\xa7\xda\x79\xe2\x8b\x8e\xe6\x55\x69\xc8\xfb\x83\x03\x4b\x4c\x86\xe7\x07\x55\x55\x55\xe2\x8c\x3a\xd0\xbc\x1a\x8d\x95\x9a\x95\xe6\x08\x4a\xcd\x96\xba\x34\x3b\x99\x0d\xb6\x62\x6b\x78\xa5\xbd\xd3\x85\x0a\xde\x51\x76\xce\xba\x8f\x86\x5d\x8a\x5b\xf7\xb5\xba\xfb\x41\x97\x93\x14\xc5\x3b\xca\x8b\x76\x35\xc8\xae\x5d\x85\x76\x66\x52\xac\xb1\x73\x65\xb3\xd1\x5f\x3c\xfb\xe1\xdb\xdf\x9e\xfe\xf8\x04\x33\x5a\xf6\xce\x5b\xa5\x8e\xf0\xe8\xc5\xb3\xc1\x36\x19\x0d\xcd\x34\xa4\x2d\xe5\x79\x35\x7a\x79\x78\x63\x76\xeb\x93\x31\xc5\xb3\xea\xf6\x64\x31\x5e\xea\xc9\xa2\x34\xe3\x0a\x47\x93\xd1\xc1\x41\xee\x3d\x95\x4d\xf7\x87\xd5\xcf\x7d\x58\x52\x2e\x83\x36\xad\x4c\x8a\x85\x75\xe4\x59\x1c\xcc\xf3\x6a\xa4\xd4\xf2\xe4\xce\xf4\xf3\xd3\xdb\x4a\x2d\x37\x45\x24\xea\x71\x8a\xd6\x49\x5b\xda\x97\x4a\x47\x2b\x81\x9c\x93\xaf\x6a\x9f\xd6\xdb\x5c\x85\x35\x53\xa0\xc8\x9b\x2c\x63\xa3\x0b\xc1\x26\xd8\x14\x09\xe4\x5d\x0d\xf2\x85\x40\x45\x1b\xd0\xb9\x63\xd4\x0e\x75\xca\xd8\x4d\x0c\xae\x86\x8b\xf0\xc9\x68\x8f\x4c\xda\x22\x13\xf7\x39\xa2\x90\x27\xc3\x28\x8d\xab\x19\xdc\x50\x04\xbb\x40\xe8\x23\x3b\x8f\x75\xe3\x3c\x4d\xae\xc0\x4e\xab\xd2\x91\x71\xda\x6f\xee\xf7\x0d\x94\xba\x84\x52\x63\x28\x35\x81\x52\x5f\x40\xa9\x53\x28\x75\x82\x3f\xa0\x94\x52\x90\xa1\x7e\x89\xaf\x71\x03\x5f\xe1\x23\x28\x85\xc9\x75\xd3\xbf\x08\x7b\x2a\x93\x42\xd0\xd1\xee\x26\xb6\x21\xd6\xa6\x41\x0b\x32\x4d\x02\x9d\x77\x29\x33\x3c\x31\x0a\x31\xfa\xa0\x4b\x8b\x3e\x16\xe2\xc9\xde\x10\x43\x48\xb1\xf2\x2e\xf6\xe7\x1f\x48\x3b\x5e\xc1\xad\x26\x0b\xbd\x6e\x07\xca\xc0\xca\x51\xbb\x68\xa1\xd4\xfa\xcd\x9d\x8f\xef\x5f\xae\x32\x75\x68\x9d\xf7\xc3\xa1\xbd\xbf\xf2\x78\x2a\x05\x41\xb7\x42\x2c\xf1\x17\xb2\x28\x0d\x58\xe7\x7d\x28\x99\x7a\x76\xbe\x7c\x00\xc7\x52\x17\x7a\x78\x5f\x30\x0c\x1f\x0c\x8c\x66\x98\xc6\xa4\x08\xd3\xac\x72\x07\xd3\x84\x64\x61\x9a\xb4\x16\x4b\x4e\x89\x61\xda\xd2\x07\x48\x4a\x98\x0e\xa6\x74\xde\x31\x4c\xcf\xb0\x9a\x09\xd6\xc2\xd6\xb0\x2e\xcb\x63\x92\x4f\xb9\xc8\xdb\x90\xdc\xf6\xa0\x78\x26\xa3\xd4\xd1\x8a\xc8\xa8\xb5\x61\xa1\x87\x16\xd6\xd4\x81\x51\x27\x6f\x21\x94\x47\x93\x0a\x3b\x0b\xf9\x17\xfe\x7a\x8f\x3f\x93\x50\xc7\xc5\x16\x5e\x28\xb4\x1a\x92\xfa\x82\x60\x1f\x08\xa4\xd0\x4a\xd9\xd0\xd6\xae\x4e\x08\x6d\x4c\x16\xa1\x65\x0a\x1d\xc2\x19\xa2\x33\x84\xe8\x11\x53\xd3\x77\x88\x5d\x4e\x06\xb1\x0f\x52\x32\x59\x74\xba\x30\xa1\xd3\xdc\x98\xa6\x45\xe7\x62\x7b\x81\x2e\xa3\xcb\x2e\xb2\x40\x1e\x5e\x6a\x74\x7c\x8e\x6e\x6d\x07\xe2\x0e\x38\x32\x69\x2f\x51\xc8\x01\x39\x48\xf9\xdc\x47\x99\x5e\xa1\xd7\x18\x97\x46\xdf\x45\x69\xf4\xbd\x7b\xf7\x07\xf1\xe0\xa1\x88\x4f\x3f\x1b\xb4\x07\x77\xef\x4d\x04\x75\x69\xf2\x70\x7f\x7d\x8d\xe2\x89\x3a\x14\xe1\xd9\x66\xaa\xf2\xb5\xa1\xb0\x5d\x8a\x93\xf9\x02\x43\xc0\x45\x34\x60\x2d\x8f\xf3\x60\x22\x30\x15\xde\x7c\x2e\x72\xa4\x9e\xc1\xa9\x37\x0d\x38\x83\x73\x4f\x72\x44\x23\x97\xc3\x43\x6e\x49\xd4\x0f\xb3\xeb\xe3\xf6\x2e\xfa\xe8\x5e\xa3\x8f\x43\x4f\x7d\xa1\x5c\x70\x26\xcd\xac\x0d\xd6\x4d\x92\x47\x07\x87\x0b\x2a\xfb\x04\xbb\x5a\x18\x55\xed\xf5\xaa\xbc\x67\x49\x4c\xa7\x27\x7a\xfa\xd7\xf4\xf4\xf6\xe8\xbd\x1b\xa4\x1a\xdc\xe2\xdd\xba\x9d\xa5\xc8\xae\x76\xdb\x3d\x75\xa4\xd4\x9b\x85\xec\xaa\x47\xd3\xdf\x5f\xdd\xf8\xe6\xf0\xe8\xd6\x62\x2a\x6b\xeb\x72\x31\xfa\xff\x01\xfb\xfb\xb0\x70\x76\x71\x35\xdf\xf6\x53\x09\xb4\x3c\xc0\x1a\x8d\xae\x6c\x14\xed\x9e\xa5\xb4\xae\x1b\x4a\x29\x35\xbb\xb6\xbe\xb5\x7f\x77\x7f\x6f\x57\xda\xec\xab\xc7\x8d\xce\xd7\xa1\xff\x15\xce\xcd\x7d\x34\x37\xf7\xcb\x56\x27\xa7\xd7\xd9\xc2\x30\xdc\x7f\x64\x19\xbf\x84\x52\x65\x72\xb8\x9f\xec\xe8\x5f\x7a\xe0\x64\x93\x84\xff\xfa\xfc\xc9\x73\x1c\x1f\x1f\xe3\xfb\xa7\xc7\x3f\x7d\x37\x99\xcb\x3c\xff\x0e\x00\x00\xff\xff\x42\xc3\x8d\xb7\x85\x07\x00\x00")
func runtimeSyntaxShYamlBytes() ([]byte, error) {
return bindataRead(

View File

@ -1,20 +0,0 @@
package main
// ScrollBar represents an optional scrollbar that can be used
type ScrollBar struct {
view *View
}
// Display shows the scrollbar
func (sb *ScrollBar) Display() {
style := defStyle.Reverse(true)
screen.SetContent(sb.view.x+sb.view.Width-1, sb.view.y+sb.pos(), ' ', nil, style)
}
func (sb *ScrollBar) pos() int {
numlines := sb.view.Buf.NumLines
h := sb.view.Height
filepercent := float32(sb.view.Topline) / float32(numlines)
return int(filepercent * float32(h))
}

View File

@ -1,214 +0,0 @@
package main
import (
"regexp"
"strings"
"github.com/zyedidia/tcell"
)
var (
// What was the last search
lastSearch string
// Where should we start the search down from (or up from)
searchStart Loc
// Is there currently a search in progress
searching bool
// Stores the history for searching
searchHistory []string
)
// BeginSearch starts a search
func BeginSearch(searchStr string) {
searchHistory = append(searchHistory, "")
messenger.historyNum = len(searchHistory) - 1
searching = true
messenger.response = searchStr
messenger.cursorx = Count(searchStr)
messenger.Message("Find: ")
messenger.hasPrompt = true
}
// EndSearch stops the current search
func EndSearch() {
searchHistory[len(searchHistory)-1] = messenger.response
searching = false
messenger.hasPrompt = false
messenger.Clear()
messenger.Reset()
if lastSearch != "" {
messenger.Message("^P Previous ^N Next")
}
}
// ExitSearch exits the search mode, reset active search phrase, and clear status bar
func ExitSearch(v *View) {
lastSearch = ""
searching = false
messenger.hasPrompt = false
messenger.Clear()
messenger.Reset()
v.Cursor.ResetSelection()
}
// HandleSearchEvent takes an event and a view and will do a real time match from the messenger's output
// to the current buffer. It searches down the buffer.
func HandleSearchEvent(event tcell.Event, v *View) {
switch e := event.(type) {
case *tcell.EventKey:
switch e.Key() {
case tcell.KeyEscape:
// Exit the search mode
ExitSearch(v)
return
case tcell.KeyEnter:
// If the user has pressed Enter, they want this to be the lastSearch
lastSearch = messenger.response
EndSearch()
return
case tcell.KeyCtrlQ, tcell.KeyCtrlC:
// Done
EndSearch()
return
}
}
messenger.HandleEvent(event, searchHistory)
if messenger.cursorx < 0 {
// Done
EndSearch()
return
}
if messenger.response == "" {
v.Cursor.ResetSelection()
// We don't end the search though
return
}
Search(messenger.response, v, true)
v.Relocate()
return
}
func searchDown(r *regexp.Regexp, v *View, start, end Loc) bool {
if start.Y >= v.Buf.NumLines {
start.Y = v.Buf.NumLines - 1
}
if start.Y < 0 {
start.Y = 0
}
for i := start.Y; i <= end.Y; i++ {
var l []byte
var charPos int
if i == start.Y {
runes := []rune(string(v.Buf.lines[i].data))
if start.X >= len(runes) {
start.X = len(runes) - 1
}
if start.X < 0 {
start.X = 0
}
l = []byte(string(runes[start.X:]))
charPos = start.X
if strings.Contains(r.String(), "^") && start.X != 0 {
continue
}
} else {
l = v.Buf.lines[i].data
}
match := r.FindIndex(l)
if match != nil {
v.Cursor.SetSelectionStart(Loc{charPos + runePos(match[0], string(l)), i})
v.Cursor.SetSelectionEnd(Loc{charPos + runePos(match[1], string(l)), i})
v.Cursor.OrigSelection[0] = v.Cursor.CurSelection[0]
v.Cursor.OrigSelection[1] = v.Cursor.CurSelection[1]
v.Cursor.Loc = v.Cursor.CurSelection[1]
return true
}
}
return false
}
func searchUp(r *regexp.Regexp, v *View, start, end Loc) bool {
if start.Y >= v.Buf.NumLines {
start.Y = v.Buf.NumLines - 1
}
if start.Y < 0 {
start.Y = 0
}
for i := start.Y; i >= end.Y; i-- {
var l []byte
if i == start.Y {
runes := []rune(string(v.Buf.lines[i].data))
if start.X >= len(runes) {
start.X = len(runes) - 1
}
if start.X < 0 {
start.X = 0
}
l = []byte(string(runes[:start.X]))
if strings.Contains(r.String(), "$") && start.X != Count(string(l)) {
continue
}
} else {
l = v.Buf.lines[i].data
}
match := r.FindIndex(l)
if match != nil {
v.Cursor.SetSelectionStart(Loc{runePos(match[0], string(l)), i})
v.Cursor.SetSelectionEnd(Loc{runePos(match[1], string(l)), i})
v.Cursor.OrigSelection[0] = v.Cursor.CurSelection[0]
v.Cursor.OrigSelection[1] = v.Cursor.CurSelection[1]
v.Cursor.Loc = v.Cursor.CurSelection[1]
return true
}
}
return false
}
// Search searches in the view for the given regex. The down bool
// specifies whether it should search down from the searchStart position
// or up from there
func Search(searchStr string, v *View, down bool) {
if searchStr == "" {
return
}
r, err := regexp.Compile(searchStr)
if v.Buf.Settings["ignorecase"].(bool) {
r, err = regexp.Compile("(?i)" + searchStr)
}
if err != nil {
return
}
var found bool
if down {
found = searchDown(r, v, searchStart, v.Buf.End())
if !found {
found = searchDown(r, v, v.Buf.Start(), searchStart)
}
} else {
found = searchUp(r, v, searchStart, v.Buf.Start())
if !found {
found = searchUp(r, v, v.Buf.End(), searchStart)
}
}
if !found {
v.Cursor.ResetSelection()
}
}

View File

@ -1,16 +1,12 @@
package main
import (
"crypto/md5"
"encoding/json"
"errors"
"io"
"io/ioutil"
"os"
"reflect"
"strconv"
"strings"
"sync"
"github.com/flynn/json5"
"github.com/zyedidia/glob"
@ -21,7 +17,8 @@ type optionValidator func(string, interface{}) error
// The options that the user can set
var globalSettings map[string]interface{}
var invalidSettings bool
// This is the raw parsed json
var parsedSettings map[string]interface{}
// Options with validators
var optionValidators = map[string]optionValidator{
@ -33,74 +30,42 @@ var optionValidators = map[string]optionValidator{
"fileformat": validateLineEnding,
}
// InitGlobalSettings initializes the options map and sets all options to their default values
func InitGlobalSettings() {
invalidSettings = false
defaults := DefaultGlobalSettings()
var parsed map[string]interface{}
func ReadSettings() error {
filename := configDir + "/settings.json"
writeSettings := false
if _, e := os.Stat(filename); e == nil {
input, err := ioutil.ReadFile(filename)
if err != nil {
return errors.New("Error reading settings.json file: " + err.Error())
}
if !strings.HasPrefix(string(input), "null") {
// Unmarshal the input into the parsed map
err = json5.Unmarshal(input, &parsedSettings)
if err != nil {
TermMessage("Error reading settings.json file: " + err.Error())
invalidSettings = true
return
return errors.New("Error reading settings.json: " + err.Error())
}
err = json5.Unmarshal(input, &parsed)
if err != nil {
TermMessage("Error reading settings.json:", err.Error())
invalidSettings = true
}
} else {
writeSettings = true
}
}
return nil
}
globalSettings = make(map[string]interface{})
for k, v := range defaults {
globalSettings[k] = v
}
for k, v := range parsed {
// InitGlobalSettings initializes the options map and sets all options to their default values
// Must be called after ReadSettings
func InitGlobalSettings() {
globalSettings = DefaultGlobalSettings()
for k, v := range parsedSettings {
if !strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
globalSettings[k] = v
}
}
if _, err := os.Stat(filename); os.IsNotExist(err) || writeSettings {
err := WriteSettings(filename)
if err != nil {
TermMessage("Error writing settings.json file: " + err.Error())
}
}
}
// InitLocalSettings scans the json in settings.json and sets the options locally based
// on whether the buffer matches the glob
func InitLocalSettings(buf *Buffer) {
invalidSettings = false
var parsed map[string]interface{}
filename := configDir + "/settings.json"
if _, e := os.Stat(filename); e == nil {
input, err := ioutil.ReadFile(filename)
if err != nil {
TermMessage("Error reading settings.json file: " + err.Error())
invalidSettings = true
return
}
err = json5.Unmarshal(input, &parsed)
if err != nil {
TermMessage("Error reading settings.json:", err.Error())
invalidSettings = true
}
}
for k, v := range parsed {
// Must be called after ReadSettings
func InitLocalSettings(buf *Buffer) error {
var parseError error
for k, v := range parsedSettings {
if strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
if strings.HasPrefix(k, "ft:") {
if buf.Settings["filetype"].(string) == k[3:] {
@ -111,7 +76,7 @@ func InitLocalSettings(buf *Buffer) {
} else {
g, err := glob.Compile(k)
if err != nil {
TermMessage("Error with glob setting ", k, ": ", err)
parseError = errors.New("Error with glob setting " + k + ": " + err.Error())
continue
}
@ -123,59 +88,31 @@ func InitLocalSettings(buf *Buffer) {
}
}
}
return parseError
}
// WriteSettings writes the settings to the specified filename as JSON
func WriteSettings(filename string) error {
if invalidSettings {
// Do not write the settings if there was an error when reading them
return nil
}
var err error
if _, e := os.Stat(configDir); e == nil {
parsed := make(map[string]interface{})
filename := configDir + "/settings.json"
for k, v := range globalSettings {
parsed[k] = v
}
if _, e := os.Stat(filename); e == nil {
input, err := ioutil.ReadFile(filename)
if string(input) != "null" {
if err != nil {
return err
}
err = json5.Unmarshal(input, &parsed)
if err != nil {
TermMessage("Error reading settings.json:", err.Error())
invalidSettings = true
}
for k, v := range parsed {
if !strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
if _, ok := globalSettings[k]; ok {
parsed[k] = globalSettings[k]
}
}
}
}
parsedSettings[k] = v
}
txt, _ := json.MarshalIndent(parsed, "", " ")
txt, _ := json.MarshalIndent(parsedSettings, "", " ")
err = ioutil.WriteFile(filename, append(txt, '\n'), 0644)
}
return err
}
// AddOption creates a new option. This is meant to be called by plugins to add options.
func AddOption(name string, value interface{}) {
func AddOption(name string, value interface{}) error {
globalSettings[name] = value
err := WriteSettings(configDir + "/settings.json")
if err != nil {
TermMessage("Error writing settings.json file: " + err.Error())
return errors.New("Error writing settings.json file: " + err.Error())
}
return nil
}
// GetGlobalOption returns the global value of the given option
@ -188,291 +125,268 @@ func GetLocalOption(name string, buf *Buffer) interface{} {
return buf.Settings[name]
}
// TODO: get option for current buffer
// GetOption returns the value of the given option
// If there is a local version of the option, it returns that
// otherwise it will return the global version
func GetOption(name string) interface{} {
if GetLocalOption(name, CurView().Buf) != nil {
return GetLocalOption(name, CurView().Buf)
// func GetOption(name string) interface{} {
// if GetLocalOption(name, CurView().Buf) != nil {
// return GetLocalOption(name, CurView().Buf)
// }
// return GetGlobalOption(name)
// }
func DefaultCommonSettings() map[string]interface{} {
return map[string]interface{}{
"autoindent": true,
"autosave": false,
"basename": false,
"colorcolumn": float64(0),
"cursorline": true,
"eofnewline": false,
"fastdirty": true,
"fileformat": "unix",
"hidehelp": false,
"ignorecase": false,
"indentchar": " ",
"keepautoindent": false,
"matchbrace": false,
"matchbraceleft": false,
"rmtrailingws": false,
"ruler": true,
"savecursor": false,
"saveundo": false,
"scrollbar": false,
"scrollmargin": float64(3),
"scrollspeed": float64(2),
"softwrap": false,
"smartpaste": true,
"splitbottom": true,
"splitright": true,
"statusline": true,
"syntax": true,
"tabmovement": false,
"tabsize": float64(4),
"tabstospaces": false,
"useprimary": true,
}
return GetGlobalOption(name)
}
// DefaultGlobalSettings returns the default global settings for micro
// Note that colorscheme is a global only option
func DefaultGlobalSettings() map[string]interface{} {
return map[string]interface{}{
"autoindent": true,
"autosave": false,
"basename": false,
"colorcolumn": float64(0),
"colorscheme": "default",
"cursorline": true,
"eofnewline": false,
"fastdirty": true,
"fileformat": "unix",
"hidehelp": false,
"ignorecase": false,
"indentchar": " ",
"infobar": true,
"keepautoindent": false,
"keymenu": false,
"matchbrace": false,
"matchbraceleft": false,
"mouse": true,
"pluginchannels": []string{"https://raw.githubusercontent.com/micro-editor/plugin-channel/master/channel.json"},
"pluginrepos": []string{},
"rmtrailingws": false,
"ruler": true,
"savecursor": false,
"savehistory": true,
"saveundo": false,
"scrollbar": false,
"scrollmargin": float64(3),
"scrollspeed": float64(2),
"softwrap": false,
"smartpaste": true,
"splitbottom": true,
"splitright": true,
"statusline": true,
"sucmd": "sudo",
"syntax": true,
"tabmovement": false,
"tabsize": float64(4),
"tabstospaces": false,
"termtitle": false,
"useprimary": true,
}
common := DefaultCommonSettings()
common["colorscheme"] = "default"
common["infobar"] = true
common["keymenu"] = false
common["mouse"] = true
common["pluginchannels"] = []string{"https://raw.githubusercontent.com/micro-editor/plugin-channel/master/channel.json"}
common["pluginrepos"] = []string{}
common["savehistory"] = true
common["sucmd"] = "sudo"
common["termtitle"] = false
return common
}
// DefaultLocalSettings returns the default local settings
// Note that filetype is a local only option
func DefaultLocalSettings() map[string]interface{} {
return map[string]interface{}{
"autoindent": true,
"autosave": false,
"basename": false,
"colorcolumn": float64(0),
"cursorline": true,
"eofnewline": false,
"fastdirty": true,
"fileformat": "unix",
"filetype": "Unknown",
"hidehelp": false,
"ignorecase": false,
"indentchar": " ",
"keepautoindent": false,
"matchbrace": false,
"matchbraceleft": false,
"rmtrailingws": false,
"ruler": true,
"savecursor": false,
"saveundo": false,
"scrollbar": false,
"scrollmargin": float64(3),
"scrollspeed": float64(2),
"softwrap": false,
"smartpaste": true,
"splitbottom": true,
"splitright": true,
"statusline": true,
"syntax": true,
"tabmovement": false,
"tabsize": float64(4),
"tabstospaces": false,
"useprimary": true,
}
common := DefaultCommonSettings()
common["filetype"] = "Unknown"
return common
}
// TODO: everything else
// SetOption attempts to set the given option to the value
// By default it will set the option as global, but if the option
// is local only it will set the local version
// Use setlocal to force an option to be set locally
func SetOption(option, value string) error {
if _, ok := globalSettings[option]; !ok {
if _, ok := CurView().Buf.Settings[option]; !ok {
return errors.New("Invalid option")
}
SetLocalOption(option, value, CurView())
return nil
}
var nativeValue interface{}
kind := reflect.TypeOf(globalSettings[option]).Kind()
if kind == reflect.Bool {
b, err := ParseBool(value)
if err != nil {
return errors.New("Invalid value")
}
nativeValue = b
} else if kind == reflect.String {
nativeValue = value
} else if kind == reflect.Float64 {
i, err := strconv.Atoi(value)
if err != nil {
return errors.New("Invalid value")
}
nativeValue = float64(i)
} else {
return errors.New("Option has unsupported value type")
}
if err := optionIsValid(option, nativeValue); err != nil {
return err
}
globalSettings[option] = nativeValue
if option == "colorscheme" {
// LoadSyntaxFiles()
InitColorscheme()
for _, tab := range tabs {
for _, view := range tab.Views {
view.Buf.UpdateRules()
}
}
}
if option == "infobar" || option == "keymenu" {
for _, tab := range tabs {
tab.Resize()
}
}
if option == "mouse" {
if !nativeValue.(bool) {
screen.DisableMouse()
} else {
screen.EnableMouse()
}
}
if len(tabs) != 0 {
if _, ok := CurView().Buf.Settings[option]; ok {
for _, tab := range tabs {
for _, view := range tab.Views {
SetLocalOption(option, value, view)
}
}
}
}
return nil
}
// SetLocalOption sets the local version of this option
func SetLocalOption(option, value string, view *View) error {
buf := view.Buf
if _, ok := buf.Settings[option]; !ok {
return errors.New("Invalid option")
}
var nativeValue interface{}
kind := reflect.TypeOf(buf.Settings[option]).Kind()
if kind == reflect.Bool {
b, err := ParseBool(value)
if err != nil {
return errors.New("Invalid value")
}
nativeValue = b
} else if kind == reflect.String {
nativeValue = value
} else if kind == reflect.Float64 {
i, err := strconv.Atoi(value)
if err != nil {
return errors.New("Invalid value")
}
nativeValue = float64(i)
} else {
return errors.New("Option has unsupported value type")
}
if err := optionIsValid(option, nativeValue); err != nil {
return err
}
if option == "fastdirty" {
// If it is being turned off, we have to hash every open buffer
var empty [md5.Size]byte
var wg sync.WaitGroup
for _, tab := range tabs {
for _, v := range tab.Views {
if !nativeValue.(bool) {
if v.Buf.origHash == empty {
wg.Add(1)
go func(b *Buffer) { // calculate md5 hash of the file
defer wg.Done()
if file, e := os.Open(b.AbsPath); e == nil {
defer file.Close()
h := md5.New()
if _, e = io.Copy(h, file); e == nil {
h.Sum(b.origHash[:0])
}
}
}(v.Buf)
}
} else {
v.Buf.IsModified = v.Buf.Modified()
}
}
}
wg.Wait()
}
buf.Settings[option] = nativeValue
if option == "statusline" {
view.ToggleStatusLine()
}
if option == "filetype" {
// LoadSyntaxFiles()
InitColorscheme()
buf.UpdateRules()
}
if option == "fileformat" {
buf.IsModified = true
}
if option == "syntax" {
if !nativeValue.(bool) {
buf.ClearMatches()
} else {
if buf.highlighter != nil {
buf.highlighter.HighlightStates(buf)
}
}
}
return nil
}
// SetOptionAndSettings sets the given option and saves the option setting to the settings config file
func SetOptionAndSettings(option, value string) {
filename := configDir + "/settings.json"
err := SetOption(option, value)
if err != nil {
messenger.Error(err.Error())
return
}
err = WriteSettings(filename)
if err != nil {
messenger.Error("Error writing to settings.json: " + err.Error())
return
}
}
// func SetOption(option, value string) error {
// if _, ok := globalSettings[option]; !ok {
// if _, ok := CurView().Buf.Settings[option]; !ok {
// return errors.New("Invalid option")
// }
// SetLocalOption(option, value, CurView())
// return nil
// }
//
// var nativeValue interface{}
//
// kind := reflect.TypeOf(globalSettings[option]).Kind()
// if kind == reflect.Bool {
// b, err := ParseBool(value)
// if err != nil {
// return errors.New("Invalid value")
// }
// nativeValue = b
// } else if kind == reflect.String {
// nativeValue = value
// } else if kind == reflect.Float64 {
// i, err := strconv.Atoi(value)
// if err != nil {
// return errors.New("Invalid value")
// }
// nativeValue = float64(i)
// } else {
// return errors.New("Option has unsupported value type")
// }
//
// if err := optionIsValid(option, nativeValue); err != nil {
// return err
// }
//
// globalSettings[option] = nativeValue
//
// if option == "colorscheme" {
// // LoadSyntaxFiles()
// InitColorscheme()
// for _, tab := range tabs {
// for _, view := range tab.Views {
// view.Buf.UpdateRules()
// }
// }
// }
//
// if option == "infobar" || option == "keymenu" {
// for _, tab := range tabs {
// tab.Resize()
// }
// }
//
// if option == "mouse" {
// if !nativeValue.(bool) {
// screen.DisableMouse()
// } else {
// screen.EnableMouse()
// }
// }
//
// if len(tabs) != 0 {
// if _, ok := CurView().Buf.Settings[option]; ok {
// for _, tab := range tabs {
// for _, view := range tab.Views {
// SetLocalOption(option, value, view)
// }
// }
// }
// }
//
// return nil
// }
//
// // SetLocalOption sets the local version of this option
// func SetLocalOption(option, value string, view *View) error {
// buf := view.Buf
// if _, ok := buf.Settings[option]; !ok {
// return errors.New("Invalid option")
// }
//
// var nativeValue interface{}
//
// kind := reflect.TypeOf(buf.Settings[option]).Kind()
// if kind == reflect.Bool {
// b, err := ParseBool(value)
// if err != nil {
// return errors.New("Invalid value")
// }
// nativeValue = b
// } else if kind == reflect.String {
// nativeValue = value
// } else if kind == reflect.Float64 {
// i, err := strconv.Atoi(value)
// if err != nil {
// return errors.New("Invalid value")
// }
// nativeValue = float64(i)
// } else {
// return errors.New("Option has unsupported value type")
// }
//
// if err := optionIsValid(option, nativeValue); err != nil {
// return err
// }
//
// if option == "fastdirty" {
// // If it is being turned off, we have to hash every open buffer
// var empty [md5.Size]byte
// var wg sync.WaitGroup
//
// for _, tab := range tabs {
// for _, v := range tab.Views {
// if !nativeValue.(bool) {
// if v.Buf.origHash == empty {
// wg.Add(1)
//
// go func(b *Buffer) { // calculate md5 hash of the file
// defer wg.Done()
//
// if file, e := os.Open(b.AbsPath); e == nil {
// defer file.Close()
//
// h := md5.New()
//
// if _, e = io.Copy(h, file); e == nil {
// h.Sum(b.origHash[:0])
// }
// }
// }(v.Buf)
// }
// } else {
// v.Buf.IsModified = v.Buf.Modified()
// }
// }
// }
//
// wg.Wait()
// }
//
// buf.Settings[option] = nativeValue
//
// if option == "statusline" {
// view.ToggleStatusLine()
// }
//
// if option == "filetype" {
// // LoadSyntaxFiles()
// InitColorscheme()
// buf.UpdateRules()
// }
//
// if option == "fileformat" {
// buf.IsModified = true
// }
//
// if option == "syntax" {
// if !nativeValue.(bool) {
// buf.ClearMatches()
// } else {
// if buf.highlighter != nil {
// buf.highlighter.HighlightStates(buf)
// }
// }
// }
//
// return nil
// }
//
// // SetOptionAndSettings sets the given option and saves the option setting to the settings config file
// func SetOptionAndSettings(option, value string) {
// filename := configDir + "/settings.json"
//
// err := SetOption(option, value)
//
// if err != nil {
// messenger.Error(err.Error())
// return
// }
//
// err = WriteSettings(filename)
// if err != nil {
// messenger.Error("Error writing to settings.json: " + err.Error())
// return
// }
// }
func optionIsValid(option string, value interface{}) error {
if validator, ok := optionValidators[option]; ok {

View File

@ -1,129 +0,0 @@
package main
import (
"bytes"
"io"
"os"
"os/exec"
"os/signal"
"strings"
"github.com/zyedidia/micro/cmd/micro/shellwords"
)
// ExecCommand executes a command using exec
// It returns any output/errors
func ExecCommand(name string, arg ...string) (string, error) {
var err error
cmd := exec.Command(name, arg...)
outputBytes := &bytes.Buffer{}
cmd.Stdout = outputBytes
cmd.Stderr = outputBytes
err = cmd.Start()
if err != nil {
return "", err
}
err = cmd.Wait() // wait for command to finish
outstring := outputBytes.String()
return outstring, err
}
// RunShellCommand executes a shell command and returns the output/error
func RunShellCommand(input string) (string, error) {
args, err := shellwords.Split(input)
if err != nil {
return "", err
}
inputCmd := args[0]
return ExecCommand(inputCmd, args[1:]...)
}
func RunBackgroundShell(input string) {
args, err := shellwords.Split(input)
if err != nil {
messenger.Error(err)
return
}
inputCmd := args[0]
messenger.Message("Running...")
go func() {
output, err := RunShellCommand(input)
totalLines := strings.Split(output, "\n")
if len(totalLines) < 3 {
if err == nil {
messenger.Message(inputCmd, " exited without error")
} else {
messenger.Message(inputCmd, " exited with error: ", err, ": ", output)
}
} else {
messenger.Message(output)
}
// We have to make sure to redraw
RedrawAll()
}()
}
func RunInteractiveShell(input string, wait bool, getOutput bool) (string, error) {
args, err := shellwords.Split(input)
if err != nil {
return "", err
}
inputCmd := args[0]
// Shut down the screen because we're going to interact directly with the shell
screen.Fini()
screen = nil
args = args[1:]
// Set up everything for the command
outputBytes := &bytes.Buffer{}
cmd := exec.Command(inputCmd, args...)
cmd.Stdin = os.Stdin
if getOutput {
cmd.Stdout = io.MultiWriter(os.Stdout, outputBytes)
} else {
cmd.Stdout = os.Stdout
}
cmd.Stderr = os.Stderr
// This is a trap for Ctrl-C so that it doesn't kill micro
// Instead we trap Ctrl-C to kill the program we're running
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func() {
for range c {
cmd.Process.Kill()
}
}()
cmd.Start()
err = cmd.Wait()
output := outputBytes.String()
if wait {
// This is just so we don't return right away and let the user press enter to return
TermMessage("")
}
// Start the screen back up
InitScreen()
return output, err
}
// HandleShellCommand runs the shell command
// The openTerm argument specifies whether a terminal should be opened (for viewing output
// or interacting with stdin)
func HandleShellCommand(input string, openTerm bool, waitToFinish bool) string {
if !openTerm {
RunBackgroundShell(input)
return ""
} else {
output, _ := RunInteractiveShell(input, waitToFinish, false)
return output
}
}

View File

@ -1,18 +0,0 @@
// +build linux darwin dragonfly openbsd_amd64 freebsd
package main
import (
"github.com/zyedidia/micro/cmd/micro/shellwords"
)
const TermEmuSupported = true
func RunTermEmulator(input string, wait bool, getOutput bool, callback string) error {
args, err := shellwords.Split(input)
if err != nil {
return err
}
err = CurView().StartTerminal(args, wait, getOutput, callback)
return err
}

View File

@ -1,11 +0,0 @@
// +build !linux,!darwin,!freebsd,!dragonfly,!openbsd_amd64
package main
import "errors"
const TermEmuSupported = false
func RunTermEmulator(input string, wait bool, getOutput bool) error {
return errors.New("Unsupported operating system")
}

View File

@ -1,317 +0,0 @@
package main
// SplitType specifies whether a split is horizontal or vertical
type SplitType bool
const (
// VerticalSplit type
VerticalSplit = false
// HorizontalSplit type
HorizontalSplit = true
)
// A Node on the split tree
type Node interface {
VSplit(buf *Buffer, splitIndex int)
HSplit(buf *Buffer, splitIndex int)
String() string
}
// A LeafNode is an actual split so it contains a view
type LeafNode struct {
view *View
parent *SplitTree
}
// NewLeafNode returns a new leaf node containing the given view
func NewLeafNode(v *View, parent *SplitTree) *LeafNode {
n := new(LeafNode)
n.view = v
n.view.splitNode = n
n.parent = parent
return n
}
// A SplitTree is a Node itself and it contains other nodes
type SplitTree struct {
kind SplitType
parent *SplitTree
children []Node
x int
y int
width int
height int
lockWidth bool
lockHeight bool
tabNum int
}
// VSplit creates a vertical split
func (l *LeafNode) VSplit(buf *Buffer, splitIndex int) {
if splitIndex < 0 {
splitIndex = 0
}
tab := tabs[l.parent.tabNum]
if l.parent.kind == VerticalSplit {
if splitIndex > len(l.parent.children) {
splitIndex = len(l.parent.children)
}
newView := NewView(buf)
newView.TabNum = l.parent.tabNum
l.parent.children = append(l.parent.children, nil)
copy(l.parent.children[splitIndex+1:], l.parent.children[splitIndex:])
l.parent.children[splitIndex] = NewLeafNode(newView, l.parent)
tab.Views = append(tab.Views, nil)
copy(tab.Views[splitIndex+1:], tab.Views[splitIndex:])
tab.Views[splitIndex] = newView
tab.CurView = splitIndex
} else {
if splitIndex > 1 {
splitIndex = 1
}
s := new(SplitTree)
s.kind = VerticalSplit
s.parent = l.parent
s.tabNum = l.parent.tabNum
newView := NewView(buf)
newView.TabNum = l.parent.tabNum
if splitIndex == 1 {
s.children = []Node{l, NewLeafNode(newView, s)}
} else {
s.children = []Node{NewLeafNode(newView, s), l}
}
l.parent.children[search(l.parent.children, l)] = s
l.parent = s
tab.Views = append(tab.Views, nil)
copy(tab.Views[splitIndex+1:], tab.Views[splitIndex:])
tab.Views[splitIndex] = newView
tab.CurView = splitIndex
}
tab.Resize()
}
// HSplit creates a horizontal split
func (l *LeafNode) HSplit(buf *Buffer, splitIndex int) {
if splitIndex < 0 {
splitIndex = 0
}
tab := tabs[l.parent.tabNum]
if l.parent.kind == HorizontalSplit {
if splitIndex > len(l.parent.children) {
splitIndex = len(l.parent.children)
}
newView := NewView(buf)
newView.TabNum = l.parent.tabNum
l.parent.children = append(l.parent.children, nil)
copy(l.parent.children[splitIndex+1:], l.parent.children[splitIndex:])
l.parent.children[splitIndex] = NewLeafNode(newView, l.parent)
tab.Views = append(tab.Views, nil)
copy(tab.Views[splitIndex+1:], tab.Views[splitIndex:])
tab.Views[splitIndex] = newView
tab.CurView = splitIndex
} else {
if splitIndex > 1 {
splitIndex = 1
}
s := new(SplitTree)
s.kind = HorizontalSplit
s.tabNum = l.parent.tabNum
s.parent = l.parent
newView := NewView(buf)
newView.TabNum = l.parent.tabNum
newView.Num = len(tab.Views)
if splitIndex == 1 {
s.children = []Node{l, NewLeafNode(newView, s)}
} else {
s.children = []Node{NewLeafNode(newView, s), l}
}
l.parent.children[search(l.parent.children, l)] = s
l.parent = s
tab.Views = append(tab.Views, nil)
copy(tab.Views[splitIndex+1:], tab.Views[splitIndex:])
tab.Views[splitIndex] = newView
tab.CurView = splitIndex
}
tab.Resize()
}
// Delete deletes a split
func (l *LeafNode) Delete() {
i := search(l.parent.children, l)
copy(l.parent.children[i:], l.parent.children[i+1:])
l.parent.children[len(l.parent.children)-1] = nil
l.parent.children = l.parent.children[:len(l.parent.children)-1]
tab := tabs[l.parent.tabNum]
j := findView(tab.Views, l.view)
copy(tab.Views[j:], tab.Views[j+1:])
tab.Views[len(tab.Views)-1] = nil // or the zero value of T
tab.Views = tab.Views[:len(tab.Views)-1]
for i, v := range tab.Views {
v.Num = i
}
if tab.CurView > 0 {
tab.CurView--
}
}
// Cleanup rearranges all the parents after a split has been deleted
func (s *SplitTree) Cleanup() {
for i, node := range s.children {
if n, ok := node.(*SplitTree); ok {
if len(n.children) == 1 {
if child, ok := n.children[0].(*LeafNode); ok {
s.children[i] = child
child.parent = s
continue
}
}
n.Cleanup()
}
}
}
// ResizeSplits resizes all the splits correctly
func (s *SplitTree) ResizeSplits() {
lockedWidth := 0
lockedHeight := 0
lockedChildren := 0
for _, node := range s.children {
if n, ok := node.(*LeafNode); ok {
if s.kind == VerticalSplit {
if n.view.LockWidth {
lockedWidth += n.view.Width
lockedChildren++
}
} else {
if n.view.LockHeight {
lockedHeight += n.view.Height
lockedChildren++
}
}
} else if n, ok := node.(*SplitTree); ok {
if s.kind == VerticalSplit {
if n.lockWidth {
lockedWidth += n.width
lockedChildren++
}
} else {
if n.lockHeight {
lockedHeight += n.height
lockedChildren++
}
}
}
}
x, y := 0, 0
for _, node := range s.children {
if n, ok := node.(*LeafNode); ok {
if s.kind == VerticalSplit {
if !n.view.LockWidth {
n.view.Width = (s.width - lockedWidth) / (len(s.children) - lockedChildren)
}
n.view.Height = s.height
n.view.x = s.x + x
n.view.y = s.y
x += n.view.Width
} else {
if !n.view.LockHeight {
n.view.Height = (s.height - lockedHeight) / (len(s.children) - lockedChildren)
}
n.view.Width = s.width
n.view.y = s.y + y
n.view.x = s.x
y += n.view.Height
}
if n.view.Buf.Settings["statusline"].(bool) {
n.view.Height--
}
n.view.ToggleTabbar()
} else if n, ok := node.(*SplitTree); ok {
if s.kind == VerticalSplit {
if !n.lockWidth {
n.width = (s.width - lockedWidth) / (len(s.children) - lockedChildren)
}
n.height = s.height
n.x = s.x + x
n.y = s.y
x += n.width
} else {
if !n.lockHeight {
n.height = (s.height - lockedHeight) / (len(s.children) - lockedChildren)
}
n.width = s.width
n.y = s.y + y
n.x = s.x
y += n.height
}
n.ResizeSplits()
}
}
}
func (l *LeafNode) String() string {
return l.view.Buf.GetName()
}
func search(haystack []Node, needle Node) int {
for i, x := range haystack {
if x == needle {
return i
}
}
return 0
}
func findView(haystack []*View, needle *View) int {
for i, x := range haystack {
if x == needle {
return i
}
}
return 0
}
// VSplit is here just to make SplitTree fit the Node interface
func (s *SplitTree) VSplit(buf *Buffer, splitIndex int) {}
// HSplit is here just to make SplitTree fit the Node interface
func (s *SplitTree) HSplit(buf *Buffer, splitIndex int) {}
func (s *SplitTree) String() string {
str := "["
for _, child := range s.children {
str += child.String() + ", "
}
return str + "]"
}

View File

@ -1,7 +1,7 @@
package main
// Stack is a simple implementation of a LIFO stack for text events
type Stack struct {
// TEStack is a simple implementation of a LIFO stack for text events
type TEStack struct {
Top *Element
Size int
}
@ -13,19 +13,19 @@ type Element struct {
}
// Len returns the stack's length
func (s *Stack) Len() int {
func (s *TEStack) Len() int {
return s.Size
}
// Push a new element onto the stack
func (s *Stack) Push(value *TextEvent) {
func (s *TEStack) Push(value *TextEvent) {
s.Top = &Element{value, s.Top}
s.Size++
}
// Pop removes the top element from the stack and returns its value
// If the stack is empty, return nil
func (s *Stack) Pop() (value *TextEvent) {
func (s *TEStack) Pop() (value *TextEvent) {
if s.Size > 0 {
value, s.Top = s.Top.Value, s.Top.Next
s.Size--
@ -35,7 +35,7 @@ func (s *Stack) Pop() (value *TextEvent) {
}
// Peek returns the top element of the stack without removing it
func (s *Stack) Peek() *TextEvent {
func (s *TEStack) Peek() *TextEvent {
if s.Size > 0 {
return s.Top.Value
}

35
cmd/micro/stack_test.go Normal file
View File

@ -0,0 +1,35 @@
package main
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestStack(t *testing.T) {
s := new(TEStack)
e1 := &TextEvent{
EventType: TextEventReplace,
Time: time.Now(),
}
e2 := &TextEvent{
EventType: TextEventInsert,
Time: time.Now(),
}
s.Push(e1)
s.Push(e2)
p := s.Peek()
assert.Equal(t, p.EventType, TextEventInsert)
p = s.Pop()
assert.Equal(t, p.EventType, TextEventInsert)
p = s.Peek()
assert.Equal(t, p.EventType, TextEventReplace)
p = s.Pop()
assert.Equal(t, p.EventType, TextEventReplace)
p = s.Pop()
assert.Nil(t, p)
p = s.Peek()
assert.Nil(t, p)
}

View File

@ -1,98 +1,116 @@
package main
import (
"bytes"
"fmt"
"path"
"regexp"
"strconv"
"unicode/utf8"
)
// Statusline represents the information line at the bottom
// of each view
// StatusLine represents the information line at the bottom
// of each window
// It gives information such as filename, whether the file has been
// modified, filetype, cursor location
type Statusline struct {
view *View
type StatusLine struct {
FormatLeft string
FormatRight string
Info map[string]func(*Buffer) string
win *Window
}
// TODO: plugin modify status line formatter
// NewStatusLine returns a statusline bound to a window
func NewStatusLine(win *Window) *StatusLine {
s := new(StatusLine)
// s.FormatLeft = "$(filename) $(modified)($(line),$(col)) $(opt:filetype) $(opt:fileformat)"
s.FormatLeft = "$(filename) $(modified)(line,col) $(opt:filetype) $(opt:fileformat)"
s.FormatRight = "$(bind:ToggleKeyMenu): show bindings, $(bind:ToggleHelp): open help"
s.Info = map[string]func(*Buffer) string{
"filename": func(b *Buffer) string {
if b.Settings["basename"].(bool) {
return path.Base(b.GetName())
}
return b.GetName()
},
"line": func(b *Buffer) string {
return strconv.Itoa(b.GetActiveCursor().Y)
},
"col": func(b *Buffer) string {
return strconv.Itoa(b.GetActiveCursor().X)
},
"modified": func(b *Buffer) string {
if b.Modified() {
return "+ "
}
return ""
},
}
s.win = win
return s
}
// FindOpt finds a given option in the current buffer's settings
func (s *StatusLine) FindOpt(opt string) interface{} {
if val, ok := s.win.Buf.Settings[opt]; ok {
return val
}
return "null"
}
var formatParser = regexp.MustCompile(`\$\(.+?\)`)
// Display draws the statusline to the screen
func (sline *Statusline) Display() {
if messenger.hasPrompt && !GetGlobalOption("infobar").(bool) {
return
}
func (s *StatusLine) Display() {
// TODO: don't display if infobar off and has message
// if !GetGlobalOption("infobar").(bool) {
// return
// }
// We'll draw the line at the lowest line in the view
y := sline.view.Height + sline.view.y
// We'll draw the line at the lowest line in the window
y := s.win.Height + s.win.Y
file := sline.view.Buf.GetName()
if sline.view.Buf.Settings["basename"].(bool) {
file = path.Base(file)
}
// If the buffer is dirty (has been modified) write a little '+'
if sline.view.Buf.Modified() {
file += " +"
}
// Add one to cursor.x and cursor.y because (0,0) is the top left,
// but users will be used to (1,1) (first line,first column)
// We use GetVisualX() here because otherwise we get the column number in runes
// so a '\t' is only 1, when it should be tabSize
columnNum := strconv.Itoa(sline.view.Cursor.GetVisualX() + 1)
lineNum := strconv.Itoa(sline.view.Cursor.Y + 1)
file += " (" + lineNum + "," + columnNum + ")"
// Add the filetype
file += " " + sline.view.Buf.FileType()
file += " " + sline.view.Buf.Settings["fileformat"].(string)
rightText := ""
if !sline.view.Buf.Settings["hidehelp"].(bool) {
if len(kmenuBinding) > 0 {
if globalSettings["keymenu"].(bool) {
rightText += kmenuBinding + ": hide bindings"
} else {
rightText += kmenuBinding + ": show bindings"
}
formatter := func(match []byte) []byte {
name := match[2 : len(match)-1]
if bytes.HasPrefix(name, []byte("opt")) {
option := name[4:]
return []byte(fmt.Sprint(s.FindOpt(string(option))))
} else if bytes.HasPrefix(name, []byte("bind")) {
// TODO: status line bindings
return []byte("TODO")
} else {
return []byte(s.Info[string(name)](s.win.Buf))
}
if len(helpBinding) > 0 {
if len(kmenuBinding) > 0 {
rightText += ", "
}
if sline.view.Type == vtHelp {
rightText += helpBinding + ": close help"
} else {
rightText += helpBinding + ": open help"
}
}
rightText += " "
}
leftText := []byte(s.FormatLeft)
leftText = formatParser.ReplaceAllFunc([]byte(s.FormatLeft), formatter)
rightText := []byte(s.FormatRight)
rightText = formatParser.ReplaceAllFunc([]byte(s.FormatRight), formatter)
statusLineStyle := defStyle.Reverse(true)
if style, ok := colorscheme["statusline"]; ok {
statusLineStyle = style
}
// Maybe there is a unicode filename?
fileRunes := []rune(file)
leftLen := utf8.RuneCount(leftText)
rightLen := utf8.RuneCount(rightText)
if sline.view.Type == vtTerm {
fileRunes = []rune(sline.view.term.title)
rightText = ""
}
viewX := sline.view.x
if viewX != 0 {
screen.SetContent(viewX, y, ' ', nil, statusLineStyle)
viewX++
}
for x := 0; x < sline.view.Width; x++ {
if x < len(fileRunes) {
screen.SetContent(viewX+x, y, fileRunes[x], nil, statusLineStyle)
} else if x >= sline.view.Width-len(rightText) && x < len(rightText)+sline.view.Width-len(rightText) {
screen.SetContent(viewX+x, y, []rune(rightText)[x-sline.view.Width+len(rightText)], nil, statusLineStyle)
winX := s.win.X
for x := 0; x < s.win.Width; x++ {
if x < leftLen {
r, size := utf8.DecodeRune(leftText)
leftText = leftText[size:]
screen.SetContent(winX+x, y, r, nil, statusLineStyle)
} else if x >= s.win.Width-rightLen && x < rightLen+s.win.Width-rightLen {
r, size := utf8.DecodeRune(rightText)
rightText = rightText[size:]
screen.SetContent(winX+x, y, r, nil, statusLineStyle)
} else {
screen.SetContent(viewX+x, y, ' ', nil, statusLineStyle)
screen.SetContent(winX+x, y, ' ', nil, statusLineStyle)
}
}
}

View File

@ -1,293 +0,0 @@
package main
import (
"path/filepath"
"sort"
"github.com/zyedidia/tcell"
)
var tabBarOffset int
// A Tab holds an array of views and a splitTree to determine how the
// views should be arranged
type Tab struct {
// This contains all the views in this tab
// There is generally only one view per tab, but you can have
// multiple views with splits
Views []*View
// This is the current view for this tab
CurView int
tree *SplitTree
}
// NewTabFromView creates a new tab and puts the given view in the tab
func NewTabFromView(v *View) *Tab {
t := new(Tab)
t.Views = append(t.Views, v)
t.Views[0].Num = 0
t.tree = new(SplitTree)
t.tree.kind = VerticalSplit
t.tree.children = []Node{NewLeafNode(t.Views[0], t.tree)}
w, h := screen.Size()
t.tree.width = w
t.tree.height = h
if globalSettings["infobar"].(bool) {
t.tree.height--
}
if globalSettings["keymenu"].(bool) {
t.tree.height -= 2
}
t.Resize()
return t
}
// SetNum sets all this tab's views to have the correct tab number
func (t *Tab) SetNum(num int) {
t.tree.tabNum = num
for _, v := range t.Views {
v.TabNum = num
}
}
// Cleanup cleans up the tree (for example if views have closed)
func (t *Tab) Cleanup() {
t.tree.Cleanup()
}
// Resize handles a resize event from the terminal and resizes
// all child views correctly
func (t *Tab) Resize() {
w, h := screen.Size()
t.tree.width = w
t.tree.height = h
if globalSettings["infobar"].(bool) {
t.tree.height--
}
if globalSettings["keymenu"].(bool) {
t.tree.height -= 2
}
t.tree.ResizeSplits()
for i, v := range t.Views {
v.Num = i
if v.Type == vtTerm {
v.term.Resize(v.Width, v.Height)
}
}
}
// CurView returns the current view
func CurView() *View {
curTab := tabs[curTab]
return curTab.Views[curTab.CurView]
}
// TabbarString returns the string that should be displayed in the tabbar
// It also returns a map containing which indicies correspond to which tab number
// This is useful when we know that the mouse click has occurred at an x location
// but need to know which tab that corresponds to to accurately change the tab
func TabbarString() (string, map[int]int) {
str := ""
indicies := make(map[int]int)
unique := make(map[string]int)
for _, t := range tabs {
unique[filepath.Base(t.Views[t.CurView].Buf.GetName())]++
}
for i, t := range tabs {
buf := t.Views[t.CurView].Buf
name := filepath.Base(buf.GetName())
if i == curTab {
str += "["
} else {
str += " "
}
if unique[name] == 1 {
str += name
} else {
str += buf.GetName()
}
if buf.Modified() {
str += " +"
}
if i == curTab {
str += "]"
} else {
str += " "
}
str += " "
indicies[Count(str)-2] = i + 1
}
return str, indicies
}
// TabbarHandleMouseEvent checks the given mouse event if it is clicking on the tabbar
// If it is it changes the current tab accordingly
// This function returns true if the tab is changed
func TabbarHandleMouseEvent(event tcell.Event) bool {
// There is no tabbar displayed if there are less than 2 tabs
if len(tabs) <= 1 {
return false
}
switch e := event.(type) {
case *tcell.EventMouse:
button := e.Buttons()
// Must be a left click
if button == tcell.Button1 {
x, y := e.Position()
if y != 0 {
return false
}
str, indicies := TabbarString()
if x+tabBarOffset >= len(str) {
return false
}
var tabnum int
var keys []int
for k := range indicies {
keys = append(keys, k)
}
sort.Ints(keys)
for _, k := range keys {
if x+tabBarOffset <= k {
tabnum = indicies[k] - 1
break
}
}
curTab = tabnum
return true
}
}
return false
}
// DisplayTabs displays the tabbar at the top of the editor if there are multiple tabs
func DisplayTabs() {
if len(tabs) <= 1 {
return
}
str, indicies := TabbarString()
tabBarStyle := defStyle.Reverse(true)
if style, ok := colorscheme["tabbar"]; ok {
tabBarStyle = style
}
// Maybe there is a unicode filename?
fileRunes := []rune(str)
w, _ := screen.Size()
tooWide := (w < len(fileRunes))
// if the entire tab-bar is longer than the screen is wide,
// then it should be truncated appropriately to keep the
// active tab visible on the UI.
if tooWide == true {
// first we have to work out where the selected tab is
// out of the total length of the tab bar. this is done
// by extracting the hit-areas from the indicies map
// that was constructed by `TabbarString()`
var keys []int
for offset := range indicies {
keys = append(keys, offset)
}
// sort them to be in ascending order so that values will
// correctly reflect the displayed ordering of the tabs
sort.Ints(keys)
// record the offset of each tab and the previous tab so
// we can find the position of the tab's hit-box.
previousTabOffset := 0
currentTabOffset := 0
for _, k := range keys {
tabIndex := indicies[k] - 1
if tabIndex == curTab {
currentTabOffset = k
break
}
// this is +2 because there are two padding spaces that aren't accounted
// for in the display. please note that this is for cosmetic purposes only.
previousTabOffset = k + 2
}
// get the width of the hitbox of the active tab, from there calculate the offsets
// to the left and right of it to approximately center it on the tab bar display.
centeringOffset := (w - (currentTabOffset - previousTabOffset))
leftBuffer := previousTabOffset - (centeringOffset / 2)
rightBuffer := currentTabOffset + (centeringOffset / 2)
// check to make sure we haven't overshot the bounds of the string,
// if we have, then take that remainder and put it on the left side
overshotRight := rightBuffer - len(fileRunes)
if overshotRight > 0 {
leftBuffer = leftBuffer + overshotRight
}
overshotLeft := leftBuffer - 0
if overshotLeft < 0 {
leftBuffer = 0
rightBuffer = leftBuffer + (w - 1)
} else {
rightBuffer = leftBuffer + (w - 2)
}
if rightBuffer > len(fileRunes)-1 {
rightBuffer = len(fileRunes) - 1
}
// construct a new buffer of text to put the
// newly formatted tab bar text into.
var displayText []rune
// if the left-side of the tab bar isn't at the start
// of the constructed tab bar text, then show that are
// more tabs to the left by displaying a "+"
if leftBuffer != 0 {
displayText = append(displayText, '+')
}
// copy the runes in from the original tab bar text string
// into the new display buffer
for x := leftBuffer; x < rightBuffer; x++ {
displayText = append(displayText, fileRunes[x])
}
// if there is more text to the right of the right-most
// column in the tab bar text, then indicate there are more
// tabs to the right by displaying a "+"
if rightBuffer < len(fileRunes)-1 {
displayText = append(displayText, '+')
}
// now store the offset from zero of the left-most text
// that is being displayed. This is to ensure that when
// clicking on the tab bar, the correct tab gets selected.
tabBarOffset = leftBuffer
// use the constructed buffer as the display buffer to print
// onscreen.
fileRunes = displayText
} else {
tabBarOffset = 0
}
// iterate over the width of the terminal display and for each column,
// write a character into the tab display area with the appropriate style.
for x := 0; x < w; x++ {
if x < len(fileRunes) {
screen.SetContent(x, 0, fileRunes[x], nil, tabBarStyle)
} else {
screen.SetContent(x, 0, ' ', nil, tabBarStyle)
}
}
}

View File

@ -1,228 +0,0 @@
package main
import (
"bytes"
"fmt"
"os"
"os/exec"
"strconv"
"strings"
"github.com/zyedidia/clipboard"
"github.com/zyedidia/tcell"
"github.com/zyedidia/terminal"
)
const (
VTIdle = iota // Waiting for a new command
VTRunning // Currently running a command
VTDone // Finished running a command
)
// A Terminal holds information for the terminal emulator
type Terminal struct {
state terminal.State
view *View
vtOld ViewType
term *terminal.VT
title string
status int
selection [2]Loc
wait bool
getOutput bool
output *bytes.Buffer
callback string
}
// HasSelection returns whether this terminal has a valid selection
func (t *Terminal) HasSelection() bool {
return t.selection[0] != t.selection[1]
}
// GetSelection returns the selected text
func (t *Terminal) GetSelection(width int) string {
start := t.selection[0]
end := t.selection[1]
if start.GreaterThan(end) {
start, end = end, start
}
var ret string
var l Loc
for y := start.Y; y <= end.Y; y++ {
for x := 0; x < width; x++ {
l.X, l.Y = x, y
if l.GreaterEqual(start) && l.LessThan(end) {
c, _, _ := t.state.Cell(x, y)
ret += string(c)
}
}
}
return ret
}
// Start begins a new command in this terminal with a given view
func (t *Terminal) Start(execCmd []string, view *View, getOutput bool) error {
if len(execCmd) <= 0 {
return nil
}
cmd := exec.Command(execCmd[0], execCmd[1:]...)
t.output = nil
if getOutput {
t.output = bytes.NewBuffer([]byte{})
}
term, _, err := terminal.Start(&t.state, cmd, t.output)
if err != nil {
return err
}
t.term = term
t.view = view
t.getOutput = getOutput
t.vtOld = view.Type
t.status = VTRunning
t.title = execCmd[0] + ":" + strconv.Itoa(cmd.Process.Pid)
go func() {
for {
err := term.Parse()
if err != nil {
fmt.Fprintln(os.Stderr, "[Press enter to close]")
break
}
updateterm <- true
}
closeterm <- view.Num
}()
return nil
}
// Resize informs the terminal of a resize event
func (t *Terminal) Resize(width, height int) {
t.term.Resize(width, height)
}
// HandleEvent handles a tcell event by forwarding it to the terminal emulator
// If the event is a mouse event and the program running in the emulator
// does not have mouse support, the emulator will support selections and
// copy-paste
func (t *Terminal) HandleEvent(event tcell.Event) {
if e, ok := event.(*tcell.EventKey); ok {
if t.status == VTDone {
switch e.Key() {
case tcell.KeyEscape, tcell.KeyCtrlQ, tcell.KeyEnter:
t.Close()
t.view.Type = vtDefault
default:
}
}
if e.Key() == tcell.KeyCtrlC && t.HasSelection() {
clipboard.WriteAll(t.GetSelection(t.view.Width), "clipboard")
messenger.Message("Copied selection to clipboard")
} else if t.status != VTDone {
t.WriteString(event.EscSeq())
}
} else if e, ok := event.(*tcell.EventMouse); !ok || t.state.Mode(terminal.ModeMouseMask) {
t.WriteString(event.EscSeq())
} else {
x, y := e.Position()
x -= t.view.x
y += t.view.y
if e.Buttons() == tcell.Button1 {
if !t.view.mouseReleased {
// drag
t.selection[1].X = x
t.selection[1].Y = y
} else {
t.selection[0].X = x
t.selection[0].Y = y
t.selection[1].X = x
t.selection[1].Y = y
}
t.view.mouseReleased = false
} else if e.Buttons() == tcell.ButtonNone {
if !t.view.mouseReleased {
t.selection[1].X = x
t.selection[1].Y = y
}
t.view.mouseReleased = true
}
}
}
// Stop stops execution of the terminal and sets the status
// to VTDone
func (t *Terminal) Stop() {
t.term.File().Close()
t.term.Close()
if t.wait {
t.status = VTDone
} else {
t.Close()
t.view.Type = t.vtOld
}
}
// Close sets the status to VTIdle indicating that the terminal
// is ready for a new command to execute
func (t *Terminal) Close() {
t.status = VTIdle
// call the lua function that the user has given as a callback
if t.getOutput {
_, err := Call(t.callback, t.output.String())
if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") {
TermMessage(err)
}
}
}
// WriteString writes a given string to this terminal's pty
func (t *Terminal) WriteString(str string) {
t.term.File().WriteString(str)
}
// Display displays this terminal in a view
func (t *Terminal) Display() {
divider := 0
if t.view.x != 0 {
divider = 1
dividerStyle := defStyle
if style, ok := colorscheme["divider"]; ok {
dividerStyle = style
}
for i := 0; i < t.view.Height; i++ {
screen.SetContent(t.view.x, t.view.y+i, '|', nil, dividerStyle.Reverse(true))
}
}
t.state.Lock()
defer t.state.Unlock()
var l Loc
for y := 0; y < t.view.Height; y++ {
for x := 0; x < t.view.Width; x++ {
l.X, l.Y = x, y
c, f, b := t.state.Cell(x, y)
fg, bg := int(f), int(b)
if f == terminal.DefaultFG {
fg = int(tcell.ColorDefault)
}
if b == terminal.DefaultBG {
bg = int(tcell.ColorDefault)
}
st := tcell.StyleDefault.Foreground(GetColor256(int(fg))).Background(GetColor256(int(bg)))
if l.LessThan(t.selection[1]) && l.GreaterEqual(t.selection[0]) || l.LessThan(t.selection[0]) && l.GreaterEqual(t.selection[1]) {
st = st.Reverse(true)
}
screen.SetContent(t.view.x+x+divider, t.view.y+y, c, nil, st)
}
}
if t.state.CursorVisible() && tabs[curTab].CurView == t.view.Num {
curx, cury := t.state.Cursor()
screen.ShowCursor(curx+t.view.x+divider, cury+t.view.y)
}
}

View File

@ -1,45 +1,20 @@
package main
import (
"errors"
"os"
"os/user"
"path/filepath"
"reflect"
"runtime"
"strconv"
"regexp"
"strings"
"time"
"unicode/utf8"
"github.com/go-errors/errors"
"github.com/mattn/go-runewidth"
"regexp"
runewidth "github.com/mattn/go-runewidth"
)
// Util.go is a collection of utility functions that are used throughout
// the program
// Count returns the length of a string in runes
// This is exactly equivalent to utf8.RuneCountInString(), just less characters
func Count(s string) int {
return utf8.RuneCountInString(s)
}
// Convert byte array to rune array
func toRunes(b []byte) []rune {
runes := make([]rune, 0, utf8.RuneCount(b))
for len(b) > 0 {
r, size := utf8.DecodeRune(b)
runes = append(runes, r)
b = b[size:]
}
return runes
}
func sliceStart(slc []byte, index int) []byte {
// SliceEnd returns a byte slice where the index is a rune index
// Slices off the start of the slice
func SliceEnd(slc []byte, index int) []byte {
len := len(slc)
i := 0
totalSize := 0
@ -56,7 +31,9 @@ func sliceStart(slc []byte, index int) []byte {
return slc[totalSize:]
}
func sliceEnd(slc []byte, index int) []byte {
// SliceStart returns a byte slice where the index is a rune index
// Slices off the end of the slice
func SliceStart(slc []byte, index int) []byte {
len := len(slc)
i := 0
totalSize := 0
@ -73,20 +50,63 @@ func sliceEnd(slc []byte, index int) []byte {
return slc[:totalSize]
}
// NumOccurrences counts the number of occurrences of a byte in a string
func NumOccurrences(s string, c byte) int {
var n int
for i := 0; i < len(s); i++ {
if s[i] == c {
n++
// SliceVisualEnd will take a byte slice and slice off the start
// up to a given visual index. If the index is in the middle of a
// rune the number of visual columns into the rune will be returned
func SliceVisualEnd(b []byte, n, tabsize int) ([]byte, int) {
width := 0
for len(b) > 0 {
r, size := utf8.DecodeRune(b)
w := 0
switch r {
case '\t':
ts := tabsize - (width % tabsize)
w = ts
default:
w = runewidth.RuneWidth(r)
}
if width+w > n {
return b, n - width
}
width += w
b = b[size:]
}
return b, width
}
// Abs is a simple absolute value function for ints
func Abs(n int) int {
if n < 0 {
return -n
}
return n
}
// Spaces returns a string with n spaces
func Spaces(n int) string {
return strings.Repeat(" ", n)
// StringWidth returns the visual width of a byte array indexed from 0 to n (rune index)
// with a given tabsize
func StringWidth(b []byte, n, tabsize int) int {
i := 0
width := 0
for len(b) > 0 {
r, size := utf8.DecodeRune(b)
b = b[size:]
switch r {
case '\t':
ts := tabsize - (width % tabsize)
width += ts
default:
width += runewidth.RuneWidth(r)
}
i++
if i == n {
return width
}
}
return width
}
// Min takes the min of two ints
@ -108,7 +128,6 @@ func Max(a, b int) int {
// FSize gets the size of a file
func FSize(f *os.File) int64 {
fi, _ := f.Stat()
// get the size
return fi.Size()
}
@ -131,6 +150,7 @@ func IsWhitespace(c rune) bool {
// IsStrWhitespace returns true if the given string is all whitespace
func IsStrWhitespace(str string) bool {
// Range loop for unicode correctness
for _, c := range str {
if !IsWhitespace(c) {
return false
@ -139,197 +159,12 @@ func IsStrWhitespace(str string) bool {
return true
}
// Contains returns whether or not a string array contains a given string
func Contains(list []string, a string) bool {
for _, b := range list {
if b == a {
return true
}
}
return false
}
// Insert makes a simple insert into a string at the given position
func Insert(str string, pos int, value string) string {
return string([]rune(str)[:pos]) + value + string([]rune(str)[pos:])
}
// MakeRelative will attempt to make a relative path between path and base
func MakeRelative(path, base string) (string, error) {
if len(path) > 0 {
rel, err := filepath.Rel(base, path)
if err != nil {
return path, err
}
return rel, nil
}
return path, nil
}
// GetLeadingWhitespace returns the leading whitespace of the given string
func GetLeadingWhitespace(str string) string {
ws := ""
for _, c := range str {
if c == ' ' || c == '\t' {
ws += string(c)
} else {
break
}
}
return ws
}
// IsSpaces checks if a given string is only spaces
func IsSpaces(str []byte) bool {
for _, c := range str {
if c != ' ' {
return false
}
}
return true
}
// IsSpacesOrTabs checks if a given string contains only spaces and tabs
func IsSpacesOrTabs(str string) bool {
for _, c := range str {
if c != ' ' && c != '\t' {
return false
}
}
return true
}
// ParseBool is almost exactly like strconv.ParseBool, except it also accepts 'on' and 'off'
// as 'true' and 'false' respectively
func ParseBool(str string) (bool, error) {
if str == "on" {
return true, nil
}
if str == "off" {
return false, nil
}
return strconv.ParseBool(str)
}
// EscapePath replaces every path separator in a given path with a %
func EscapePath(path string) string {
path = filepath.ToSlash(path)
return strings.Replace(path, "/", "%", -1)
}
// GetModTime returns the last modification time for a given file
// It also returns a boolean if there was a problem accessing the file
func GetModTime(path string) (time.Time, bool) {
info, err := os.Stat(path)
if err != nil {
return time.Now(), false
}
return info.ModTime(), true
}
// StringWidth returns the width of a string where tabs count as `tabsize` width
func StringWidth(str string, tabsize int) int {
sw := runewidth.StringWidth(str)
lineIdx := 0
for _, ch := range str {
switch ch {
case '\t':
ts := tabsize - (lineIdx % tabsize)
sw += ts
lineIdx += ts
case '\n':
lineIdx = 0
default:
lineIdx++
}
}
return sw
}
// WidthOfLargeRunes searches all the runes in a string and counts up all the widths of runes
// that have a width larger than 1 (this also counts tabs as `tabsize` width)
func WidthOfLargeRunes(str string, tabsize int) int {
count := 0
lineIdx := 0
for _, ch := range str {
var w int
if ch == '\t' {
w = tabsize - (lineIdx % tabsize)
} else {
w = runewidth.RuneWidth(ch)
}
if w > 1 {
count += (w - 1)
}
if ch == '\n' {
lineIdx = 0
} else {
lineIdx += w
}
}
return count
}
// RunePos returns the rune index of a given byte index
// This could cause problems if the byte index is between code points
func runePos(p int, str string) int {
return utf8.RuneCountInString(str[:p])
}
func lcs(a, b string) string {
arunes := []rune(a)
brunes := []rune(b)
lcs := ""
for i, r := range arunes {
if i >= len(brunes) {
break
}
if r == brunes[i] {
lcs += string(r)
} else {
break
}
}
return lcs
}
// CommonSubstring gets a common substring among the inputs
func CommonSubstring(arr ...string) string {
commonStr := arr[0]
for _, str := range arr[1:] {
commonStr = lcs(commonStr, str)
}
return commonStr
}
// Abs is a simple absolute value function for ints
func Abs(n int) int {
if n < 0 {
return -n
}
return n
}
// FuncName returns the full name of a given function object
func FuncName(i interface{}) string {
return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
}
// ShortFuncName returns the name only of a given function object
func ShortFuncName(i interface{}) string {
return strings.TrimPrefix(runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name(), "main.(*View).")
}
// TODO: consider changing because of snap segfault
// ReplaceHome takes a path as input and replaces ~ at the start of the path with the user's
// home directory. Does nothing if the path does not start with '~'.
func ReplaceHome(path string) string {
func ReplaceHome(path string) (string, error) {
if !strings.HasPrefix(path, "~") {
return path
return path, nil
}
var userData *user.User
@ -339,23 +174,18 @@ func ReplaceHome(path string) string {
if homeString == "~" {
userData, err = user.Current()
if err != nil {
messenger.Error("Could not find user: ", err)
return "", errors.New("Could not find user: " + err.Error())
}
} else {
userData, err = user.Lookup(homeString[1:])
if err != nil {
if messenger != nil {
messenger.Error("Could not find user: ", err)
} else {
TermMessage("Could not find user: ", err)
}
return ""
return "", errors.New("Could not find user: " + err.Error())
}
}
home := userData.HomeDir
return strings.Replace(path, homeString, home, 1)
return strings.Replace(path, homeString, home, 1), nil
}
// GetPathAndCursorPosition returns a filename without everything following a `:`
@ -375,26 +205,11 @@ func GetPathAndCursorPosition(path string) (string, []string) {
return match[1], []string{match[2], "0"}
}
func ParseCursorLocation(cursorPositions []string) (Loc, error) {
startpos := Loc{0, 0}
var err error
// if no positions are available exit early
if cursorPositions == nil {
return startpos, errors.New("No cursor positions were provided.")
}
startpos.Y, err = strconv.Atoi(cursorPositions[0])
// GetModTime returns the last modification time for a given file
func GetModTime(path string) (time.Time, error) {
info, err := os.Stat(path)
if err != nil {
messenger.Error("Error parsing cursor position: ", err)
} else {
if len(cursorPositions) > 1 {
startpos.X, err = strconv.Atoi(cursorPositions[1])
if err != nil {
messenger.Error("Error parsing cursor position: ", err)
}
}
return time.Now(), err
}
return startpos, err
return info.ModTime(), nil
}

View File

@ -2,330 +2,32 @@ package main
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestNumOccurences(t *testing.T) {
var tests = []struct {
inputStr string
inputChar byte
want int
}{
{"aaaa", 'a', 4},
{"\trfd\ta", '\t', 2},
{"∆ƒ\tø ® \t\t", '\t', 3},
}
for _, test := range tests {
if got := NumOccurrences(test.inputStr, test.inputChar); got != test.want {
t.Errorf("NumOccurences(%s, %c) = %d", test.inputStr, test.inputChar, got)
}
}
}
func TestSpaces(t *testing.T) {
var tests = []struct {
input int
want string
}{
{4, " "},
{0, ""},
}
for _, test := range tests {
if got := Spaces(test.input); got != test.want {
t.Errorf("Spaces(%d) = \"%s\"", test.input, got)
}
}
}
func TestIsWordChar(t *testing.T) {
if IsWordChar("t") == false {
t.Errorf("IsWordChar(t) = false")
}
if IsWordChar("T") == false {
t.Errorf("IsWordChar(T) = false")
}
if IsWordChar("5") == false {
t.Errorf("IsWordChar(5) = false")
}
if IsWordChar("_") == false {
t.Errorf("IsWordChar(_) = false")
}
if IsWordChar("ß") == false {
t.Errorf("IsWordChar(ß) = false")
}
if IsWordChar("~") == true {
t.Errorf("IsWordChar(~) = true")
}
if IsWordChar(" ") == true {
t.Errorf("IsWordChar( ) = true")
}
if IsWordChar(")") == true {
t.Errorf("IsWordChar()) = true")
}
if IsWordChar("\n") == true {
t.Errorf("IsWordChar(\n)) = true")
}
}
func TestStringWidth(t *testing.T) {
tabsize := 4
if w := StringWidth("1\t2", tabsize); w != 5 {
t.Error("StringWidth 1 Failed. Got", w)
}
if w := StringWidth("\t", tabsize); w != 4 {
t.Error("StringWidth 2 Failed. Got", w)
}
if w := StringWidth("1\t", tabsize); w != 4 {
t.Error("StringWidth 3 Failed. Got", w)
}
if w := StringWidth("\t\t", tabsize); w != 8 {
t.Error("StringWidth 4 Failed. Got", w)
}
if w := StringWidth("12\t2\t", tabsize); w != 8 {
t.Error("StringWidth 5 Failed. Got", w)
}
bytes := []byte("\tPot să \tmănânc sticlă și ea nu mă rănește.")
n := StringWidth(bytes, 23, 4)
assert.Equal(t, 26, n)
}
func TestWidthOfLargeRunes(t *testing.T) {
tabsize := 4
if w := WidthOfLargeRunes("1\t2", tabsize); w != 2 {
t.Error("WidthOfLargeRunes 1 Failed. Got", w)
}
if w := WidthOfLargeRunes("\t", tabsize); w != 3 {
t.Error("WidthOfLargeRunes 2 Failed. Got", w)
}
if w := WidthOfLargeRunes("1\t", tabsize); w != 2 {
t.Error("WidthOfLargeRunes 3 Failed. Got", w)
}
if w := WidthOfLargeRunes("\t\t", tabsize); w != 6 {
t.Error("WidthOfLargeRunes 4 Failed. Got", w)
}
if w := WidthOfLargeRunes("12\t2\t", tabsize); w != 3 {
t.Error("WidthOfLargeRunes 5 Failed. Got", w)
}
}
func assertEqual(t *testing.T, expected interface{}, result interface{}) {
if expected != result {
t.Fatalf("Expected: %d != Got: %d", expected, result)
}
}
func assertTrue(t *testing.T, condition bool) {
if !condition {
t.Fatalf("Condition was not true. Got false")
}
}
func TestGetPathRelativeWithDot(t *testing.T) {
path, cursorPosition := GetPathAndCursorPosition("./myfile:10:5")
assertEqual(t, path, "./myfile")
assertEqual(t, "10", cursorPosition[0])
assertEqual(t, "5", cursorPosition[1])
}
func TestGetPathRelativeWithDotWindows(t *testing.T) {
path, cursorPosition := GetPathAndCursorPosition(".\\myfile:10:5")
assertEqual(t, path, ".\\myfile")
assertEqual(t, "10", cursorPosition[0])
assertEqual(t, cursorPosition[1], "5")
}
func TestGetPathRelativeNoDot(t *testing.T) {
path, cursorPosition := GetPathAndCursorPosition("myfile:10:5")
assertEqual(t, path, "myfile")
assertEqual(t, "10", cursorPosition[0])
assertEqual(t, cursorPosition[1], "5")
}
func TestGetPathAbsoluteWindows(t *testing.T) {
path, cursorPosition := GetPathAndCursorPosition("C:\\myfile:10:5")
assertEqual(t, path, "C:\\myfile")
assertEqual(t, "10", cursorPosition[0])
assertEqual(t, cursorPosition[1], "5")
path, cursorPosition = GetPathAndCursorPosition("C:/myfile:10:5")
assertEqual(t, path, "C:/myfile")
assertEqual(t, "10", cursorPosition[0])
assertEqual(t, "5", cursorPosition[1])
}
func TestGetPathAbsoluteUnix(t *testing.T) {
path, cursorPosition := GetPathAndCursorPosition("/home/user/myfile:10:5")
assertEqual(t, path, "/home/user/myfile")
assertEqual(t, "10", cursorPosition[0])
assertEqual(t, "5", cursorPosition[1])
}
func TestGetPathRelativeWithDotWithoutLineAndColumn(t *testing.T) {
path, cursorPosition := GetPathAndCursorPosition("./myfile")
assertEqual(t, path, "./myfile")
// no cursor position in filename, nil should be returned
assertTrue(t, cursorPosition == nil)
}
func TestGetPathRelativeWithDotWindowsWithoutLineAndColumn(t *testing.T) {
path, cursorPosition := GetPathAndCursorPosition(".\\myfile")
assertEqual(t, path, ".\\myfile")
assertTrue(t, cursorPosition == nil)
}
func TestGetPathRelativeNoDotWithoutLineAndColumn(t *testing.T) {
path, cursorPosition := GetPathAndCursorPosition("myfile")
assertEqual(t, path, "myfile")
assertTrue(t, cursorPosition == nil)
}
func TestGetPathAbsoluteWindowsWithoutLineAndColumn(t *testing.T) {
path, cursorPosition := GetPathAndCursorPosition("C:\\myfile")
assertEqual(t, path, "C:\\myfile")
assertTrue(t, cursorPosition == nil)
path, cursorPosition = GetPathAndCursorPosition("C:/myfile")
assertEqual(t, path, "C:/myfile")
assertTrue(t, cursorPosition == nil)
}
func TestGetPathAbsoluteUnixWithoutLineAndColumn(t *testing.T) {
path, cursorPosition := GetPathAndCursorPosition("/home/user/myfile")
assertEqual(t, path, "/home/user/myfile")
assertTrue(t, cursorPosition == nil)
}
func TestGetPathSingleLetterFileRelativePath(t *testing.T) {
path, cursorPosition := GetPathAndCursorPosition("a:5:6")
assertEqual(t, path, "a")
assertEqual(t, "5", cursorPosition[0])
assertEqual(t, "6", cursorPosition[1])
}
func TestGetPathSingleLetterFileAbsolutePathWindows(t *testing.T) {
path, cursorPosition := GetPathAndCursorPosition("C:\\a:5:6")
assertEqual(t, path, "C:\\a")
assertEqual(t, "5", cursorPosition[0])
assertEqual(t, "6", cursorPosition[1])
path, cursorPosition = GetPathAndCursorPosition("C:/a:5:6")
assertEqual(t, path, "C:/a")
assertEqual(t, "5", cursorPosition[0])
assertEqual(t, "6", cursorPosition[1])
}
func TestGetPathSingleLetterFileAbsolutePathUnix(t *testing.T) {
path, cursorPosition := GetPathAndCursorPosition("/home/user/a:5:6")
assertEqual(t, path, "/home/user/a")
assertEqual(t, "5", cursorPosition[0])
assertEqual(t, "6", cursorPosition[1])
}
func TestGetPathSingleLetterFileAbsolutePathWindowsWithoutLineAndColumn(t *testing.T) {
path, cursorPosition := GetPathAndCursorPosition("C:\\a")
assertEqual(t, path, "C:\\a")
assertTrue(t, cursorPosition == nil)
path, cursorPosition = GetPathAndCursorPosition("C:/a")
assertEqual(t, path, "C:/a")
assertTrue(t, cursorPosition == nil)
}
func TestGetPathSingleLetterFileAbsolutePathUnixWithoutLineAndColumn(t *testing.T) {
path, cursorPosition := GetPathAndCursorPosition("/home/user/a")
assertEqual(t, path, "/home/user/a")
assertTrue(t, cursorPosition == nil)
}
func TestGetPathRelativeWithDotOnlyLine(t *testing.T) {
path, cursorPosition := GetPathAndCursorPosition("./myfile:10")
assertEqual(t, path, "./myfile")
assertEqual(t, "10", cursorPosition[0])
assertEqual(t, "0", cursorPosition[1])
}
func TestGetPathRelativeWithDotWindowsOnlyLine(t *testing.T) {
path, cursorPosition := GetPathAndCursorPosition(".\\myfile:10")
assertEqual(t, path, ".\\myfile")
assertEqual(t, "10", cursorPosition[0])
assertEqual(t, "0", cursorPosition[1])
}
func TestGetPathRelativeNoDotOnlyLine(t *testing.T) {
path, cursorPosition := GetPathAndCursorPosition("myfile:10")
assertEqual(t, path, "myfile")
assertEqual(t, "10", cursorPosition[0])
assertEqual(t, "0", cursorPosition[1])
}
func TestGetPathAbsoluteWindowsOnlyLine(t *testing.T) {
path, cursorPosition := GetPathAndCursorPosition("C:\\myfile:10")
assertEqual(t, path, "C:\\myfile")
assertEqual(t, "10", cursorPosition[0])
assertEqual(t, "0", cursorPosition[1])
path, cursorPosition = GetPathAndCursorPosition("C:/myfile:10")
assertEqual(t, path, "C:/myfile")
assertEqual(t, "10", cursorPosition[0])
assertEqual(t, "0", cursorPosition[1])
}
func TestGetPathAbsoluteUnixOnlyLine(t *testing.T) {
path, cursorPosition := GetPathAndCursorPosition("/home/user/myfile:10")
assertEqual(t, path, "/home/user/myfile")
assertEqual(t, "10", cursorPosition[0])
assertEqual(t, "0", cursorPosition[1])
}
func TestParseCursorLocationOneArg(t *testing.T) {
location, err := ParseCursorLocation([]string{"3"})
assertEqual(t, 3, location.Y)
assertEqual(t, 0, location.X)
assertEqual(t, nil, err)
}
func TestParseCursorLocationTwoArgs(t *testing.T) {
location, err := ParseCursorLocation([]string{"3", "15"})
assertEqual(t, 3, location.Y)
assertEqual(t, 15, location.X)
assertEqual(t, nil, err)
}
func TestParseCursorLocationNoArgs(t *testing.T) {
location, err := ParseCursorLocation(nil)
// the expected result is the start position - 0, 0
assertEqual(t, 0, location.Y)
assertEqual(t, 0, location.X)
// an error will be present here as the positions we're parsing are a nil
assertTrue(t, err != nil)
}
func TestParseCursorLocationFirstArgNotValidNumber(t *testing.T) {
// the messenger is necessary as ParseCursorLocation
// puts a message in it on error
messenger = new(Messenger)
_, err := ParseCursorLocation([]string{"apples", "1"})
// the expected result is the start position - 0, 0
assertTrue(t, messenger.hasMessage)
assertTrue(t, err != nil)
}
func TestParseCursorLocationSecondArgNotValidNumber(t *testing.T) {
// the messenger is necessary as ParseCursorLocation
// puts a message in it on error
messenger = new(Messenger)
_, err := ParseCursorLocation([]string{"1", "apples"})
// the expected result is the start position - 0, 0
assertTrue(t, messenger.hasMessage)
assertTrue(t, err != nil)
func TestSliceVisualEnd(t *testing.T) {
s := []byte("\thello")
slc, n := SliceVisualEnd(s, 2, 4)
assert.Equal(t, []byte("\thello"), slc)
assert.Equal(t, 2, n)
slc, n = SliceVisualEnd(s, 1, 4)
assert.Equal(t, []byte("\thello"), slc)
assert.Equal(t, 1, n)
slc, n = SliceVisualEnd(s, 4, 4)
assert.Equal(t, []byte("hello"), slc)
assert.Equal(t, 0, n)
slc, n = SliceVisualEnd(s, 5, 4)
assert.Equal(t, []byte("ello"), slc)
assert.Equal(t, 0, n)
}

File diff suppressed because it is too large Load Diff

187
cmd/micro/window.go Normal file
View File

@ -0,0 +1,187 @@
package main
import (
"strconv"
"unicode/utf8"
runewidth "github.com/mattn/go-runewidth"
"github.com/zyedidia/tcell"
)
type Window struct {
// X and Y coordinates for the top left of the window
X int
Y int
// Width and Height for the window
Width int
Height int
// Which line in the buffer to start displaying at (vertical scroll)
StartLine int
// Which visual column in the to start displaying at (horizontal scroll)
StartCol int
// Buffer being shown in this window
Buf *Buffer
sline *StatusLine
}
func NewWindow(x, y, width, height int, buf *Buffer) *Window {
w := new(Window)
w.X, w.Y, w.Width, w.Height, w.Buf = x, y, width, height, buf
w.sline = NewStatusLine(w)
return w
}
func (w *Window) DrawLineNum(lineNumStyle tcell.Style, softwrapped bool, maxLineNumLength int, vloc *Loc, bloc *Loc) {
lineNum := strconv.Itoa(bloc.Y + 1)
// Write the spaces before the line number if necessary
for i := 0; i < maxLineNumLength-len(lineNum); i++ {
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ' ', nil, lineNumStyle)
vloc.X++
}
// Write the actual line number
for _, ch := range lineNum {
if softwrapped {
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ' ', nil, lineNumStyle)
} else {
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ch, nil, lineNumStyle)
}
vloc.X++
}
// Write the extra space
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ' ', nil, lineNumStyle)
vloc.X++
}
// GetStyle returns the highlight style for the given character position
// If there is no change to the current highlight style it just returns that
func (w *Window) GetStyle(style tcell.Style, bloc Loc, r rune) tcell.Style {
if group, ok := w.Buf.Match(bloc.Y)[bloc.X]; ok {
s := GetColor(group.String())
return s
}
return style
}
// DisplayBuffer draws the buffer being shown in this window on the screen
func (w *Window) DisplayBuffer() {
b := w.Buf
bufHeight := w.Height
if b.Settings["statusline"].(bool) {
bufHeight--
}
// TODO: Rehighlighting
// start := w.StartLine
if b.Settings["syntax"].(bool) && b.syntaxDef != nil {
// if start > 0 && b.lines[start-1].rehighlight {
// b.highlighter.ReHighlightLine(b, start-1)
// b.lines[start-1].rehighlight = false
// }
//
// b.highlighter.ReHighlightStates(b, start)
//
b.highlighter.HighlightMatches(b, w.StartLine, w.StartLine+bufHeight)
}
lineNumStyle := defStyle
if style, ok := colorscheme["line-number"]; ok {
lineNumStyle = style
}
// We need to know the string length of the largest line number
// so we can pad appropriately when displaying line numbers
maxLineNumLength := len(strconv.Itoa(len(b.lines)))
tabsize := int(b.Settings["tabsize"].(float64))
softwrap := b.Settings["softwrap"].(bool)
// this represents the current draw position
// within the current window
vloc := Loc{0, 0}
// this represents the current draw position in the buffer (char positions)
bloc := Loc{w.StartCol, w.StartLine}
curStyle := defStyle
for vloc.Y = 0; vloc.Y < bufHeight; vloc.Y++ {
vloc.X = 0
if b.Settings["ruler"].(bool) {
w.DrawLineNum(lineNumStyle, false, maxLineNumLength, &vloc, &bloc)
}
line := b.LineBytes(bloc.Y)
line, nColsBeforeStart := SliceVisualEnd(line, bloc.X, tabsize)
totalwidth := bloc.X - nColsBeforeStart
for len(line) > 0 {
r, size := utf8.DecodeRune(line)
curStyle = w.GetStyle(curStyle, bloc, r)
if nColsBeforeStart <= 0 {
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, r, nil, curStyle)
vloc.X++
}
nColsBeforeStart--
width := 0
char := ' '
switch r {
case '\t':
ts := tabsize - (totalwidth % tabsize)
width = ts
default:
width = runewidth.RuneWidth(r)
char = '@'
}
bloc.X++
line = line[size:]
// Draw any extra characters either spaces for tabs or @ for incomplete wide runes
if width > 1 {
for i := 1; i < width; i++ {
if nColsBeforeStart <= 0 {
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, char, nil, curStyle)
vloc.X++
}
nColsBeforeStart--
}
}
totalwidth += width
// If we reach the end of the window then we either stop or we wrap for softwrap
if vloc.X >= w.Width {
if !softwrap {
break
} else {
vloc.Y++
if vloc.Y >= bufHeight {
break
}
vloc.X = 0
// This will draw an empty line number because the current line is wrapped
w.DrawLineNum(lineNumStyle, true, maxLineNumLength, &vloc, &bloc)
}
}
}
bloc.X = w.StartCol
bloc.Y++
if bloc.Y >= len(b.lines) {
break
}
}
}
func (w *Window) DisplayStatusLine() {
w.sline.Display()
}