mirror of
https://github.com/Hopiu/micro.git
synced 2026-03-17 06:20:28 +00:00
Merge branch 'master' into patch-1
This commit is contained in:
commit
532f932712
194 changed files with 20619 additions and 19125 deletions
9
.gitignore
vendored
9
.gitignore
vendored
|
|
@ -7,3 +7,12 @@ tmp.sh
|
|||
test/
|
||||
.idea/
|
||||
packages/
|
||||
todo.txt
|
||||
test.txt
|
||||
log.txt
|
||||
*.old
|
||||
tools/build-version
|
||||
tools/build-date
|
||||
tools/info-plist
|
||||
tools/bindata
|
||||
*.hdr
|
||||
|
|
|
|||
75
.gitmodules
vendored
75
.gitmodules
vendored
|
|
@ -1,72 +1,3 @@
|
|||
[submodule "cmd/micro/vendor/github.com/blang/semver"]
|
||||
path = cmd/micro/vendor/github.com/blang/semver
|
||||
url = https://github.com/blang/semver
|
||||
[submodule "cmd/micro/vendor/github.com/dustin/go-humanize"]
|
||||
path = cmd/micro/vendor/github.com/dustin/go-humanize
|
||||
url = https://github.com/dustin/go-humanize
|
||||
[submodule "cmd/micro/vendor/github.com/go-errors/errors"]
|
||||
path = cmd/micro/vendor/github.com/go-errors/errors
|
||||
url = https://github.com/go-errors/errors
|
||||
[submodule "cmd/micro/vendor/github.com/mattn/go-isatty"]
|
||||
path = cmd/micro/vendor/github.com/mattn/go-isatty
|
||||
url = https://github.com/mattn/go-isatty
|
||||
[submodule "cmd/micro/vendor/github.com/mattn/go-runewidth"]
|
||||
path = cmd/micro/vendor/github.com/mattn/go-runewidth
|
||||
url = https://github.com/mattn/go-runewidth
|
||||
[submodule "cmd/micro/vendor/github.com/mitchellh/go-homedir"]
|
||||
path = cmd/micro/vendor/github.com/mitchellh/go-homedir
|
||||
url = https://github.com/mitchellh/go-homedir
|
||||
[submodule "cmd/micro/vendor/github.com/sergi/go-diff"]
|
||||
path = cmd/micro/vendor/github.com/sergi/go-diff
|
||||
url = https://github.com/sergi/go-diff
|
||||
[submodule "cmd/micro/vendor/github.com/yuin/gopher-lua"]
|
||||
path = cmd/micro/vendor/github.com/yuin/gopher-lua
|
||||
url = https://github.com/yuin/gopher-lua
|
||||
[submodule "cmd/micro/vendor/golang.org/x/net"]
|
||||
path = cmd/micro/vendor/golang.org/x/net
|
||||
url = https://go.googlesource.com/net
|
||||
[submodule "cmd/micro/vendor/github.com/zyedidia/clipboard"]
|
||||
path = cmd/micro/vendor/github.com/zyedidia/clipboard
|
||||
url = https://github.com/zyedidia/clipboard
|
||||
[submodule "cmd/micro/vendor/github.com/zyedidia/glob"]
|
||||
path = cmd/micro/vendor/github.com/zyedidia/glob
|
||||
url = https://github.com/zyedidia/glob
|
||||
[submodule "cmd/micro/vendor/github.com/zyedidia/tcell"]
|
||||
path = cmd/micro/vendor/github.com/zyedidia/tcell
|
||||
url = https://github.com/zyedidia/tcell
|
||||
[submodule "cmd/micro/vendor/github.com/gdamore/encoding"]
|
||||
path = cmd/micro/vendor/github.com/gdamore/encoding
|
||||
url = https://github.com/gdamore/encoding
|
||||
[submodule "cmd/micro/vendor/golang.org/x/text"]
|
||||
path = cmd/micro/vendor/golang.org/x/text
|
||||
url = https://go.googlesource.com/text
|
||||
[submodule "cmd/micro/vendor/github.com/lucasb-eyer/go-colorful"]
|
||||
path = cmd/micro/vendor/github.com/lucasb-eyer/go-colorful
|
||||
url = https://github.com/lucasb-eyer/go-colorful
|
||||
[submodule "cmd/micro/vendor/layeh.com/gopher-luar"]
|
||||
path = cmd/micro/vendor/layeh.com/gopher-luar
|
||||
url = https://github.com/layeh/gopher-luar
|
||||
[submodule "cmd/micro/vendor/gopkg.in/yaml.v2"]
|
||||
path = cmd/micro/vendor/gopkg.in/yaml.v2
|
||||
url = https://gopkg.in/yaml.v2
|
||||
[submodule "cmd/micro/vendor/github.com/zyedidia/poller"]
|
||||
path = cmd/micro/vendor/github.com/zyedidia/poller
|
||||
url = https://github.com/zyedidia/poller
|
||||
[submodule "cmd/micro/vendor/github.com/flynn/json5"]
|
||||
path = cmd/micro/vendor/github.com/flynn/json5
|
||||
url = https://github.com/flynn/json5
|
||||
[submodule "cmd/micro/vendor/github.com/zyedidia/terminal"]
|
||||
path = cmd/micro/vendor/github.com/zyedidia/terminal
|
||||
url = https://github.com/zyedidia/terminal
|
||||
[submodule "cmd/micro/vendor/github.com/zyedidia/pty"]
|
||||
path = cmd/micro/vendor/github.com/zyedidia/pty
|
||||
url = https://github.com/zyedidia/pty
|
||||
[submodule "cmd/micro/vendor/github.com/smartystreets/goconvey"]
|
||||
path = cmd/micro/vendor/github.com/smartystreets/goconvey
|
||||
url = https://github.com/smartystreets/goconvey
|
||||
[submodule "cmd/micro/vendor/github.com/jtolds/gls"]
|
||||
path = cmd/micro/vendor/github.com/jtolds/gls
|
||||
url = https://github.com/jtolds/gls
|
||||
[submodule "cmd/micro/vendor/github.com/smartystreets/assertions"]
|
||||
path = cmd/micro/vendor/github.com/smartystreets/assertions
|
||||
url = https://github.com/smartystreets/assertions
|
||||
[submodule "tools/go-bindata"]
|
||||
path = tools/go-bindata
|
||||
url = https://github.com/zyedidia/go-bindata
|
||||
|
|
|
|||
|
|
@ -1,2 +1,9 @@
|
|||
language: go
|
||||
script: make test
|
||||
go:
|
||||
- "1.11.x"
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
script:
|
||||
- env GO111MODULE=on make build
|
||||
- env GO111MODULE=on make test
|
||||
|
|
|
|||
46
Makefile
46
Makefile
|
|
@ -1,50 +1,54 @@
|
|||
.PHONY: runtime
|
||||
|
||||
VERSION := $(shell GOOS=$(shell go env GOHOSTOS) GOARCH=$(shell go env GOHOSTARCH) \
|
||||
VERSION = $(shell GOOS=$(shell go env GOHOSTOS) GOARCH=$(shell go env GOHOSTARCH) \
|
||||
go run tools/build-version.go)
|
||||
HASH := $(shell git rev-parse --short HEAD)
|
||||
DATE := $(shell GOOS=$(shell go env GOHOSTOS) GOARCH=$(shell go env GOHOSTARCH) \
|
||||
HASH = $(shell git rev-parse --short HEAD)
|
||||
DATE = $(shell GOOS=$(shell go env GOHOSTOS) GOARCH=$(shell go env GOHOSTARCH) \
|
||||
go run tools/build-date.go)
|
||||
ADDITIONAL_GO_LINKER_FLAGS := $(shell GOOS=$(shell go env GOHOSTOS) \
|
||||
GOARCH=$(shell go env GOHOSTARCH) \
|
||||
go run tools/info-plist.go "$(VERSION)")
|
||||
ADDITIONAL_GO_LINKER_FLAGS = $(shell GOOS=$(shell go env GOHOSTOS) \
|
||||
GOARCH=$(shell go env GOHOSTARCH))
|
||||
GOBIN ?= $(shell go env GOPATH)/bin
|
||||
GOVARS = -X github.com/zyedidia/micro/internal/util.Version=$(VERSION) -X github.com/zyedidia/micro/internal/util.CommitHash=$(HASH) -X 'github.com/zyedidia/micro/internal/util.CompileDate=$(DATE)' -X github.com/zyedidia/micro/internal/util.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
|
||||
build:
|
||||
go build -mod=readonly -ldflags "-s -w $(GOVARS) $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro
|
||||
|
||||
build-dbg:
|
||||
go build -mod=readonly -ldflags "-s -w $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro
|
||||
|
||||
build-mod:
|
||||
go build -ldflags "-s -w $(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 -mod=readonly -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
|
||||
install:
|
||||
go install -mod=readonly -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
|
||||
|
||||
update:
|
||||
git pull
|
||||
git submodule update --init
|
||||
go install -mod=readonly -ldflags "-s -w $(GOVARS) $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro
|
||||
|
||||
# Builds the runtime
|
||||
runtime:
|
||||
go get -u github.com/jteeuwen/go-bindata/...
|
||||
$(GOBIN)/go-bindata -nometadata -o runtime.go runtime/...
|
||||
mv runtime.go cmd/micro
|
||||
gofmt -w cmd/micro/runtime.go
|
||||
git submodule update --init
|
||||
go run runtime/syntax/make_headers.go runtime/syntax
|
||||
go build -o tools/bindata ./tools/go-bindata
|
||||
tools/bindata -pkg config -nomemcopy -nometadata -o runtime.go runtime/...
|
||||
mv runtime.go internal/config
|
||||
gofmt -w internal/config/runtime.go
|
||||
|
||||
test:
|
||||
go test ./cmd/micro
|
||||
go test ./internal/...
|
||||
|
||||
clean:
|
||||
rm -f micro
|
||||
|
|
|
|||
2328
cmd/micro/actions.go
2328
cmd/micro/actions.go
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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",
|
||||
}
|
||||
}
|
||||
|
|
@ -1,853 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/zyedidia/micro/cmd/micro/highlight"
|
||||
)
|
||||
|
||||
const LargeFileThreshold = 50000
|
||||
|
||||
var (
|
||||
// 0 - no line type detected
|
||||
// 1 - lf detected
|
||||
// 2 - crlf detected
|
||||
fileformat = 0
|
||||
)
|
||||
|
||||
// Buffer stores the text for files that are loaded into the text editor
|
||||
// It uses a rope to efficiently store the string and contains some
|
||||
// simple functions for saving and wrapper functions for modifying the rope
|
||||
type Buffer struct {
|
||||
// The eventhandler for undo/redo
|
||||
*EventHandler
|
||||
// This stores all the text in the buffer as an array of lines
|
||||
*LineArray
|
||||
|
||||
Cursor Cursor
|
||||
cursors []*Cursor // for multiple cursors
|
||||
curCursor int // the current cursor
|
||||
|
||||
// Path to the file on disk
|
||||
Path string
|
||||
// Absolute path to the file on disk
|
||||
AbsPath string
|
||||
// Name of the buffer on the status line
|
||||
name string
|
||||
|
||||
// Whether or not the buffer has been modified since it was opened
|
||||
IsModified bool
|
||||
|
||||
// Stores the last modification time of the file the buffer is pointing to
|
||||
ModTime time.Time
|
||||
|
||||
// NumLines is the number of lines in the buffer
|
||||
NumLines int
|
||||
|
||||
syntaxDef *highlight.Def
|
||||
highlighter *highlight.Highlighter
|
||||
|
||||
// Hash of the original buffer -- empty if fastdirty is on
|
||||
origHash [md5.Size]byte
|
||||
|
||||
// Buffer local settings
|
||||
Settings map[string]interface{}
|
||||
}
|
||||
|
||||
// The SerializedBuffer holds the types that get serialized when a buffer is saved
|
||||
// These are used for the savecursor and saveundo options
|
||||
type SerializedBuffer struct {
|
||||
EventHandler *EventHandler
|
||||
Cursor Cursor
|
||||
ModTime time.Time
|
||||
}
|
||||
|
||||
// NewBufferFromFile opens a new buffer using the given path
|
||||
// It will also automatically handle `~`, and line/column with filename:l:c
|
||||
// It will return an empty buffer if the path does not exist
|
||||
// and an error if the file is a directory
|
||||
func NewBufferFromFile(path string) (*Buffer, error) {
|
||||
filename, cursorPosition := GetPathAndCursorPosition(path)
|
||||
filename = ReplaceHome(filename)
|
||||
file, err := os.Open(filename)
|
||||
fileInfo, _ := os.Stat(filename)
|
||||
|
||||
if err == nil && fileInfo.IsDir() {
|
||||
return nil, errors.New(filename + " is a directory")
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
var buf *Buffer
|
||||
if err != nil {
|
||||
// File does not exist -- create an empty buffer with that name
|
||||
buf = NewBufferFromString("", filename)
|
||||
} else {
|
||||
buf = NewBuffer(file, FSize(file), filename, cursorPosition)
|
||||
}
|
||||
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
// NewBufferFromString creates a new buffer containing the given string
|
||||
func NewBufferFromString(text, path string) *Buffer {
|
||||
return NewBuffer(strings.NewReader(text), int64(len(text)), path, nil)
|
||||
}
|
||||
|
||||
// NewBuffer creates a new buffer from a given reader with a given path
|
||||
func NewBuffer(reader io.Reader, size int64, path string, cursorPosition []string) *Buffer {
|
||||
// check if the file is already open in a tab. If it's open return the buffer to that tab
|
||||
if path != "" {
|
||||
for _, tab := range tabs {
|
||||
for _, view := range tab.Views {
|
||||
if view.Buf.Path == path {
|
||||
return view.Buf
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
b := new(Buffer)
|
||||
b.LineArray = NewLineArray(size, reader)
|
||||
|
||||
b.Settings = DefaultLocalSettings()
|
||||
for k, v := range globalSettings {
|
||||
if _, ok := b.Settings[k]; ok {
|
||||
b.Settings[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
if fileformat == 1 {
|
||||
b.Settings["fileformat"] = "unix"
|
||||
} else if fileformat == 2 {
|
||||
b.Settings["fileformat"] = "dos"
|
||||
}
|
||||
|
||||
absPath, _ := filepath.Abs(path)
|
||||
|
||||
b.Path = path
|
||||
b.AbsPath = absPath
|
||||
|
||||
// The last time this file was modified
|
||||
b.ModTime, _ = GetModTime(b.Path)
|
||||
|
||||
b.EventHandler = NewEventHandler(b)
|
||||
|
||||
b.Update()
|
||||
b.UpdateRules()
|
||||
|
||||
if _, err := os.Stat(configDir + "/buffers/"); os.IsNotExist(err) {
|
||||
os.Mkdir(configDir+"/buffers/", os.ModePerm)
|
||||
}
|
||||
|
||||
cursorLocation, cursorLocationError := GetBufferCursorLocation(cursorPosition, b)
|
||||
b.Cursor = Cursor{
|
||||
Loc: cursorLocation,
|
||||
buf: b,
|
||||
}
|
||||
|
||||
InitLocalSettings(b)
|
||||
|
||||
if cursorLocationError != nil && len(*flagStartPos) == 0 && (b.Settings["savecursor"].(bool) || b.Settings["saveundo"].(bool)) {
|
||||
// If either savecursor or saveundo is turned on, we need to load the serialized information
|
||||
// from ~/.config/micro/buffers
|
||||
file, err := os.Open(configDir + "/buffers/" + EscapePath(b.AbsPath))
|
||||
defer file.Close()
|
||||
if err == nil {
|
||||
var buffer SerializedBuffer
|
||||
decoder := gob.NewDecoder(file)
|
||||
gob.Register(TextEvent{})
|
||||
err = decoder.Decode(&buffer)
|
||||
if err != nil {
|
||||
TermMessage(err.Error(), "\n", "You may want to remove the files in ~/.config/micro/buffers (these files store the information for the 'saveundo' and 'savecursor' options) if this problem persists.")
|
||||
}
|
||||
if b.Settings["savecursor"].(bool) {
|
||||
b.Cursor = buffer.Cursor
|
||||
b.Cursor.buf = b
|
||||
b.Cursor.Relocate()
|
||||
}
|
||||
|
||||
if b.Settings["saveundo"].(bool) {
|
||||
// We should only use last time's eventhandler if the file wasn't modified by someone else in the meantime
|
||||
if b.ModTime == buffer.ModTime {
|
||||
b.EventHandler = buffer.EventHandler
|
||||
b.EventHandler.buf = b
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !b.Settings["fastdirty"].(bool) {
|
||||
if size > LargeFileThreshold {
|
||||
// If the file is larger than a megabyte fastdirty needs to be on
|
||||
b.Settings["fastdirty"] = true
|
||||
} else {
|
||||
calcHash(b, &b.origHash)
|
||||
}
|
||||
}
|
||||
|
||||
b.cursors = []*Cursor{&b.Cursor}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func GetBufferCursorLocation(cursorPosition []string, b *Buffer) (Loc, error) {
|
||||
// parse the cursor position. The cursor location is ALWAYS initialised to 0, 0 even when
|
||||
// an error occurs due to lack of arguments or because the arguments are not numbers
|
||||
cursorLocation, cursorLocationError := ParseCursorLocation(cursorPosition)
|
||||
|
||||
// Put the cursor at the first spot. In the logic for cursor position the -startpos
|
||||
// flag is processed first and will overwrite any line/col parameters with colons after the filename
|
||||
if len(*flagStartPos) > 0 || cursorLocationError == nil {
|
||||
var lineNum, colNum int
|
||||
var errPos1, errPos2 error
|
||||
|
||||
positions := strings.Split(*flagStartPos, ",")
|
||||
|
||||
// if the -startpos flag contains enough args use them for the cursor location.
|
||||
// In this case args passed at the end of the filename will be ignored
|
||||
if len(positions) == 2 {
|
||||
lineNum, errPos1 = strconv.Atoi(positions[0])
|
||||
colNum, errPos2 = strconv.Atoi(positions[1])
|
||||
}
|
||||
// if -startpos has invalid arguments, use the arguments from filename.
|
||||
// This will have a default value (0, 0) even when the filename arguments are invalid
|
||||
if errPos1 != nil || errPos2 != nil || len(*flagStartPos) == 0 {
|
||||
// otherwise check if there are any arguments after the filename and use them
|
||||
lineNum = cursorLocation.Y
|
||||
colNum = cursorLocation.X
|
||||
}
|
||||
|
||||
// if some arguments were found make sure they don't go outside the file and cause overflows
|
||||
cursorLocation.Y = lineNum - 1
|
||||
cursorLocation.X = colNum
|
||||
// Check to avoid line overflow
|
||||
if cursorLocation.Y > b.NumLines-1 {
|
||||
cursorLocation.Y = b.NumLines - 1
|
||||
} else if cursorLocation.Y < 0 {
|
||||
cursorLocation.Y = 0
|
||||
}
|
||||
// Check to avoid column overflow
|
||||
if cursorLocation.X > len(b.Line(cursorLocation.Y)) {
|
||||
cursorLocation.X = len(b.Line(cursorLocation.Y))
|
||||
} else if cursorLocation.X < 0 {
|
||||
cursorLocation.X = 0
|
||||
}
|
||||
}
|
||||
return cursorLocation, cursorLocationError
|
||||
}
|
||||
|
||||
// GetName returns the name that should be displayed in the statusline
|
||||
// for this buffer
|
||||
func (b *Buffer) GetName() string {
|
||||
if b.name == "" {
|
||||
if b.Path == "" {
|
||||
return "No name"
|
||||
}
|
||||
return b.Path
|
||||
}
|
||||
return b.name
|
||||
}
|
||||
|
||||
// UpdateRules updates the syntax rules and filetype for this buffer
|
||||
// This is called when the colorscheme changes
|
||||
func (b *Buffer) UpdateRules() {
|
||||
rehighlight := false
|
||||
var files []*highlight.File
|
||||
for _, f := range ListRuntimeFiles(RTSyntax) {
|
||||
data, err := f.Data()
|
||||
if err != nil {
|
||||
TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
|
||||
} else {
|
||||
file, err := highlight.ParseFile(data)
|
||||
if err != nil {
|
||||
TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
|
||||
continue
|
||||
}
|
||||
ftdetect, err := highlight.ParseFtDetect(file)
|
||||
if err != nil {
|
||||
TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
ft := b.Settings["filetype"].(string)
|
||||
if (ft == "Unknown" || ft == "") && !rehighlight {
|
||||
if highlight.MatchFiletype(ftdetect, b.Path, b.lines[0].data) {
|
||||
header := new(highlight.Header)
|
||||
header.FileType = file.FileType
|
||||
header.FtDetect = ftdetect
|
||||
b.syntaxDef, err = highlight.ParseDef(file, header)
|
||||
if err != nil {
|
||||
TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
|
||||
continue
|
||||
}
|
||||
rehighlight = true
|
||||
}
|
||||
} else {
|
||||
if file.FileType == ft && !rehighlight {
|
||||
header := new(highlight.Header)
|
||||
header.FileType = file.FileType
|
||||
header.FtDetect = ftdetect
|
||||
b.syntaxDef, err = highlight.ParseDef(file, header)
|
||||
if err != nil {
|
||||
TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
|
||||
continue
|
||||
}
|
||||
rehighlight = true
|
||||
}
|
||||
}
|
||||
files = append(files, file)
|
||||
}
|
||||
}
|
||||
|
||||
if b.syntaxDef != nil {
|
||||
highlight.ResolveIncludes(b.syntaxDef, files)
|
||||
}
|
||||
|
||||
if b.highlighter == nil || rehighlight {
|
||||
if b.syntaxDef != nil {
|
||||
b.Settings["filetype"] = b.syntaxDef.FileType
|
||||
b.highlighter = highlight.NewHighlighter(b.syntaxDef)
|
||||
if b.Settings["syntax"].(bool) {
|
||||
b.highlighter.HighlightStates(b)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FileType returns the buffer's filetype
|
||||
func (b *Buffer) FileType() string {
|
||||
return b.Settings["filetype"].(string)
|
||||
}
|
||||
|
||||
// IndentString returns a string representing one level of indentation
|
||||
func (b *Buffer) IndentString() string {
|
||||
if b.Settings["tabstospaces"].(bool) {
|
||||
return Spaces(int(b.Settings["tabsize"].(float64)))
|
||||
}
|
||||
return "\t"
|
||||
}
|
||||
|
||||
// CheckModTime makes sure that the file this buffer points to hasn't been updated
|
||||
// by an external program since it was last read
|
||||
// If it has, we ask the user if they would like to reload the file
|
||||
func (b *Buffer) CheckModTime() {
|
||||
modTime, ok := GetModTime(b.Path)
|
||||
if ok {
|
||||
if modTime != b.ModTime {
|
||||
choice, canceled := messenger.YesNoPrompt("The file has changed since it was last read. Reload file? (y,n)")
|
||||
messenger.Reset()
|
||||
messenger.Clear()
|
||||
if !choice || canceled {
|
||||
// Don't load new changes -- do nothing
|
||||
b.ModTime, _ = GetModTime(b.Path)
|
||||
} else {
|
||||
// Load new changes
|
||||
b.ReOpen()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ReOpen reloads the current buffer from disk
|
||||
func (b *Buffer) ReOpen() {
|
||||
data, err := ioutil.ReadFile(b.Path)
|
||||
txt := string(data)
|
||||
|
||||
if err != nil {
|
||||
messenger.Error(err.Error())
|
||||
return
|
||||
}
|
||||
b.EventHandler.ApplyDiff(txt)
|
||||
|
||||
b.ModTime, _ = GetModTime(b.Path)
|
||||
b.IsModified = false
|
||||
b.Update()
|
||||
b.Cursor.Relocate()
|
||||
}
|
||||
|
||||
// Update fetches the string from the rope and updates the `text` and `lines` in the buffer
|
||||
func (b *Buffer) Update() {
|
||||
b.NumLines = len(b.lines)
|
||||
}
|
||||
|
||||
// MergeCursors merges any cursors that are at the same position
|
||||
// into one cursor
|
||||
func (b *Buffer) MergeCursors() {
|
||||
var cursors []*Cursor
|
||||
for i := 0; i < len(b.cursors); i++ {
|
||||
c1 := b.cursors[i]
|
||||
if c1 != nil {
|
||||
for j := 0; j < len(b.cursors); j++ {
|
||||
c2 := b.cursors[j]
|
||||
if c2 != nil && i != j && c1.Loc == c2.Loc {
|
||||
b.cursors[j] = nil
|
||||
}
|
||||
}
|
||||
cursors = append(cursors, c1)
|
||||
}
|
||||
}
|
||||
|
||||
b.cursors = cursors
|
||||
|
||||
for i := range b.cursors {
|
||||
b.cursors[i].Num = i
|
||||
}
|
||||
|
||||
if b.curCursor >= len(b.cursors) {
|
||||
b.curCursor = len(b.cursors) - 1
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateCursors updates all the cursors indicies
|
||||
func (b *Buffer) UpdateCursors() {
|
||||
for i, c := range b.cursors {
|
||||
c.Num = i
|
||||
}
|
||||
}
|
||||
|
||||
// Save saves the buffer to its default path
|
||||
func (b *Buffer) Save() error {
|
||||
return b.SaveAs(b.Path)
|
||||
}
|
||||
|
||||
// SaveWithSudo saves the buffer to the default path with sudo
|
||||
func (b *Buffer) SaveWithSudo() error {
|
||||
return b.SaveAsWithSudo(b.Path)
|
||||
}
|
||||
|
||||
// Serialize serializes the buffer to configDir/buffers
|
||||
func (b *Buffer) Serialize() error {
|
||||
if !b.Settings["savecursor"].(bool) && !b.Settings["saveundo"].(bool) {
|
||||
return nil
|
||||
}
|
||||
|
||||
name := configDir + "/buffers/" + EscapePath(b.AbsPath)
|
||||
|
||||
return overwriteFile(name, func(file io.Writer) error {
|
||||
return gob.NewEncoder(file).Encode(SerializedBuffer{
|
||||
b.EventHandler,
|
||||
b.Cursor,
|
||||
b.ModTime,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func init() {
|
||||
gob.Register(TextEvent{})
|
||||
gob.Register(SerializedBuffer{})
|
||||
}
|
||||
|
||||
// SaveAs saves the buffer to a specified path (filename), creating the file if it does not exist
|
||||
func (b *Buffer) SaveAs(filename string) error {
|
||||
b.UpdateRules()
|
||||
if b.Settings["rmtrailingws"].(bool) {
|
||||
for i, l := range b.lines {
|
||||
pos := len(bytes.TrimRightFunc(l.data, unicode.IsSpace))
|
||||
|
||||
if pos < len(l.data) {
|
||||
b.deleteToEnd(Loc{pos, i})
|
||||
}
|
||||
}
|
||||
|
||||
b.Cursor.Relocate()
|
||||
}
|
||||
|
||||
if b.Settings["eofnewline"].(bool) {
|
||||
end := b.End()
|
||||
if b.RuneAt(Loc{end.X - 1, end.Y}) != '\n' {
|
||||
b.Insert(end, "\n")
|
||||
}
|
||||
}
|
||||
|
||||
defer func() {
|
||||
b.ModTime, _ = GetModTime(filename)
|
||||
}()
|
||||
|
||||
// Removes any tilde and replaces with the absolute path to home
|
||||
absFilename := ReplaceHome(filename)
|
||||
|
||||
// Get the leading path to the file | "." is returned if there's no leading path provided
|
||||
if dirname := filepath.Dir(absFilename); dirname != "." {
|
||||
// Check if the parent dirs don't exist
|
||||
if _, statErr := os.Stat(dirname); os.IsNotExist(statErr) {
|
||||
// Prompt to make sure they want to create the dirs that are missing
|
||||
if yes, canceled := messenger.YesNoPrompt("Parent folders \"" + dirname + "\" do not exist. Create them? (y,n)"); yes && !canceled {
|
||||
// Create all leading dir(s) since they don't exist
|
||||
if mkdirallErr := os.MkdirAll(dirname, os.ModePerm); mkdirallErr != nil {
|
||||
// If there was an error creating the dirs
|
||||
return mkdirallErr
|
||||
}
|
||||
} else {
|
||||
// If they canceled the creation of leading dirs
|
||||
return errors.New("Save aborted")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var fileSize int
|
||||
|
||||
err := overwriteFile(absFilename, func(file io.Writer) (e error) {
|
||||
if len(b.lines) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// end of line
|
||||
var eol []byte
|
||||
|
||||
if b.Settings["fileformat"] == "dos" {
|
||||
eol = []byte{'\r', '\n'}
|
||||
} else {
|
||||
eol = []byte{'\n'}
|
||||
}
|
||||
|
||||
// write lines
|
||||
if fileSize, e = file.Write(b.lines[0].data); e != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, l := range b.lines[1:] {
|
||||
if _, e = file.Write(eol); e != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if _, e = file.Write(l.data); e != nil {
|
||||
return
|
||||
}
|
||||
|
||||
fileSize += len(eol) + len(l.data)
|
||||
}
|
||||
|
||||
return
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !b.Settings["fastdirty"].(bool) {
|
||||
if fileSize > LargeFileThreshold {
|
||||
// For large files 'fastdirty' needs to be on
|
||||
b.Settings["fastdirty"] = true
|
||||
} else {
|
||||
calcHash(b, &b.origHash)
|
||||
}
|
||||
}
|
||||
|
||||
b.Path = filename
|
||||
b.IsModified = false
|
||||
return b.Serialize()
|
||||
}
|
||||
|
||||
// overwriteFile opens the given file for writing, truncating if one exists, and then calls
|
||||
// the supplied function with the file as io.Writer object, also making sure the file is
|
||||
// closed afterwards.
|
||||
func overwriteFile(name string, fn func(io.Writer) error) (err error) {
|
||||
var file *os.File
|
||||
|
||||
if file, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if e := file.Close(); e != nil && err == nil {
|
||||
err = e
|
||||
}
|
||||
}()
|
||||
|
||||
w := bufio.NewWriter(file)
|
||||
|
||||
if err = fn(w); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = w.Flush()
|
||||
return
|
||||
}
|
||||
|
||||
// calcHash calculates md5 hash of all lines in the buffer
|
||||
func calcHash(b *Buffer, out *[md5.Size]byte) {
|
||||
h := md5.New()
|
||||
|
||||
if len(b.lines) > 0 {
|
||||
h.Write(b.lines[0].data)
|
||||
|
||||
for _, l := range b.lines[1:] {
|
||||
h.Write([]byte{'\n'})
|
||||
h.Write(l.data)
|
||||
}
|
||||
}
|
||||
|
||||
h.Sum((*out)[:0])
|
||||
}
|
||||
|
||||
// SaveAsWithSudo is the same as SaveAs except it uses a neat trick
|
||||
// with tee to use sudo so the user doesn't have to reopen micro with sudo
|
||||
func (b *Buffer) SaveAsWithSudo(filename string) error {
|
||||
b.UpdateRules()
|
||||
b.Path = filename
|
||||
|
||||
// Shut down the screen because we're going to interact directly with the shell
|
||||
screen.Fini()
|
||||
screen = nil
|
||||
|
||||
// Set up everything for the command
|
||||
cmd := exec.Command(globalSettings["sucmd"].(string), "tee", filename)
|
||||
cmd.Stdin = bytes.NewBufferString(b.SaveString(b.Settings["fileformat"] == "dos"))
|
||||
|
||||
// This is a trap for Ctrl-C so that it doesn't kill micro
|
||||
// Instead we trap Ctrl-C to kill the program we're running
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt)
|
||||
go func() {
|
||||
for range c {
|
||||
cmd.Process.Kill()
|
||||
}
|
||||
}()
|
||||
|
||||
// Start the command
|
||||
cmd.Start()
|
||||
err := cmd.Wait()
|
||||
|
||||
// Start the screen back up
|
||||
InitScreen()
|
||||
if err == nil {
|
||||
b.IsModified = false
|
||||
b.ModTime, _ = GetModTime(filename)
|
||||
b.Serialize()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Modified returns if this buffer has been modified since
|
||||
// being opened
|
||||
func (b *Buffer) Modified() bool {
|
||||
if b.Settings["fastdirty"].(bool) {
|
||||
return b.IsModified
|
||||
}
|
||||
|
||||
var buff [md5.Size]byte
|
||||
|
||||
calcHash(b, &buff)
|
||||
return buff != b.origHash
|
||||
}
|
||||
|
||||
func (b *Buffer) insert(pos Loc, value []byte) {
|
||||
b.IsModified = true
|
||||
b.LineArray.insert(pos, value)
|
||||
b.Update()
|
||||
}
|
||||
func (b *Buffer) remove(start, end Loc) string {
|
||||
b.IsModified = true
|
||||
sub := b.LineArray.remove(start, end)
|
||||
b.Update()
|
||||
return sub
|
||||
}
|
||||
func (b *Buffer) deleteToEnd(start Loc) {
|
||||
b.IsModified = true
|
||||
b.LineArray.DeleteToEnd(start)
|
||||
b.Update()
|
||||
}
|
||||
|
||||
// Start returns the location of the first character in the buffer
|
||||
func (b *Buffer) Start() Loc {
|
||||
return Loc{0, 0}
|
||||
}
|
||||
|
||||
// End returns the location of the last character in the buffer
|
||||
func (b *Buffer) End() Loc {
|
||||
return Loc{utf8.RuneCount(b.lines[b.NumLines-1].data), b.NumLines - 1}
|
||||
}
|
||||
|
||||
// RuneAt returns the rune at a given location in the buffer
|
||||
func (b *Buffer) RuneAt(loc Loc) rune {
|
||||
line := b.LineRunes(loc.Y)
|
||||
if len(line) > 0 {
|
||||
return line[loc.X]
|
||||
}
|
||||
return '\n'
|
||||
}
|
||||
|
||||
// LineBytes returns a single line as an array of runes
|
||||
func (b *Buffer) LineBytes(n int) []byte {
|
||||
if n >= len(b.lines) {
|
||||
return []byte{}
|
||||
}
|
||||
return b.lines[n].data
|
||||
}
|
||||
|
||||
// LineRunes returns a single line as an array of runes
|
||||
func (b *Buffer) LineRunes(n int) []rune {
|
||||
if n >= len(b.lines) {
|
||||
return []rune{}
|
||||
}
|
||||
return toRunes(b.lines[n].data)
|
||||
}
|
||||
|
||||
// Line returns a single line
|
||||
func (b *Buffer) Line(n int) string {
|
||||
if n >= len(b.lines) {
|
||||
return ""
|
||||
}
|
||||
return string(b.lines[n].data)
|
||||
}
|
||||
|
||||
// LinesNum returns the number of lines in the buffer
|
||||
func (b *Buffer) LinesNum() int {
|
||||
return len(b.lines)
|
||||
}
|
||||
|
||||
// Lines returns an array of strings containing the lines from start to end
|
||||
func (b *Buffer) Lines(start, end int) []string {
|
||||
lines := b.lines[start:end]
|
||||
var slice []string
|
||||
for _, line := range lines {
|
||||
slice = append(slice, string(line.data))
|
||||
}
|
||||
return slice
|
||||
}
|
||||
|
||||
// Len gives the length of the buffer
|
||||
func (b *Buffer) Len() (n int) {
|
||||
for _, l := range b.lines {
|
||||
n += utf8.RuneCount(l.data)
|
||||
}
|
||||
|
||||
if len(b.lines) > 1 {
|
||||
n += len(b.lines) - 1 // account for newlines
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// MoveLinesUp moves the range of lines up one row
|
||||
func (b *Buffer) MoveLinesUp(start int, end int) {
|
||||
// 0 < start < end <= len(b.lines)
|
||||
if start < 1 || start >= end || end > len(b.lines) {
|
||||
return // what to do? FIXME
|
||||
}
|
||||
if end == len(b.lines) {
|
||||
b.Insert(
|
||||
Loc{
|
||||
utf8.RuneCount(b.lines[end-1].data),
|
||||
end - 1,
|
||||
},
|
||||
"\n"+b.Line(start-1),
|
||||
)
|
||||
} else {
|
||||
b.Insert(
|
||||
Loc{0, end},
|
||||
b.Line(start-1)+"\n",
|
||||
)
|
||||
}
|
||||
b.Remove(
|
||||
Loc{0, start - 1},
|
||||
Loc{0, start},
|
||||
)
|
||||
}
|
||||
|
||||
// MoveLinesDown moves the range of lines down one row
|
||||
func (b *Buffer) MoveLinesDown(start int, end int) {
|
||||
// 0 <= start < end < len(b.lines)
|
||||
// if end == len(b.lines), we can't do anything here because the
|
||||
// last line is unaccessible, FIXME
|
||||
if start < 0 || start >= end || end >= len(b.lines)-1 {
|
||||
return // what to do? FIXME
|
||||
}
|
||||
b.Insert(
|
||||
Loc{0, start},
|
||||
b.Line(end)+"\n",
|
||||
)
|
||||
end++
|
||||
b.Remove(
|
||||
Loc{0, end},
|
||||
Loc{0, end + 1},
|
||||
)
|
||||
}
|
||||
|
||||
// ClearMatches clears all of the syntax highlighting for this buffer
|
||||
func (b *Buffer) ClearMatches() {
|
||||
for i := range b.lines {
|
||||
b.SetMatch(i, nil)
|
||||
b.SetState(i, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Buffer) clearCursors() {
|
||||
for i := 1; i < len(b.cursors); i++ {
|
||||
b.cursors[i] = nil
|
||||
}
|
||||
b.cursors = b.cursors[:1]
|
||||
b.UpdateCursors()
|
||||
b.Cursor.ResetSelection()
|
||||
}
|
||||
|
||||
var bracePairs = [][2]rune{
|
||||
{'(', ')'},
|
||||
{'{', '}'},
|
||||
{'[', ']'},
|
||||
}
|
||||
|
||||
// FindMatchingBrace returns the location in the buffer of the matching bracket
|
||||
// It is given a brace type containing the open and closing character, (for example
|
||||
// '{' and '}') as well as the location to match from
|
||||
func (b *Buffer) FindMatchingBrace(braceType [2]rune, start Loc) Loc {
|
||||
curLine := b.LineRunes(start.Y)
|
||||
startChar := curLine[start.X]
|
||||
var i int
|
||||
if startChar == braceType[0] {
|
||||
for y := start.Y; y < b.NumLines; y++ {
|
||||
l := b.LineRunes(y)
|
||||
xInit := 0
|
||||
if y == start.Y {
|
||||
xInit = start.X
|
||||
}
|
||||
for x := xInit; x < len(l); x++ {
|
||||
r := l[x]
|
||||
if r == braceType[0] {
|
||||
i++
|
||||
} else if r == braceType[1] {
|
||||
i--
|
||||
if i == 0 {
|
||||
return Loc{x, y}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if startChar == braceType[1] {
|
||||
for y := start.Y; y >= 0; y-- {
|
||||
l := []rune(string(b.lines[y].data))
|
||||
xInit := len(l) - 1
|
||||
if y == start.Y {
|
||||
xInit = start.X
|
||||
}
|
||||
for x := xInit; x >= 0; x-- {
|
||||
r := l[x]
|
||||
if r == braceType[0] {
|
||||
i--
|
||||
if i == 0 {
|
||||
return Loc{x, y}
|
||||
}
|
||||
} else if r == braceType[1] {
|
||||
i++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return start
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
||||
//}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,692 +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,
|
||||
}
|
||||
}
|
||||
|
||||
// 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}},
|
||||
}
|
||||
}
|
||||
|
||||
// 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:])
|
||||
}
|
||||
}
|
||||
31
cmd/micro/debug.go
Normal file
31
cmd/micro/debug.go
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/zyedidia/micro/internal/util"
|
||||
)
|
||||
|
||||
// NullWriter simply sends writes into the void
|
||||
type NullWriter struct{}
|
||||
|
||||
// Write is empty
|
||||
func (NullWriter) Write(data []byte) (n int, err error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// InitLog sets up the debug log system for micro if it has been enabled by compile-time variables
|
||||
func InitLog() {
|
||||
if util.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{})
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
132
cmd/micro/initlua.go
Normal file
132
cmd/micro/initlua.go
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
luar "layeh.com/gopher-luar"
|
||||
|
||||
"github.com/zyedidia/micro/internal/action"
|
||||
"github.com/zyedidia/micro/internal/buffer"
|
||||
"github.com/zyedidia/micro/internal/config"
|
||||
"github.com/zyedidia/micro/internal/display"
|
||||
ulua "github.com/zyedidia/micro/internal/lua"
|
||||
"github.com/zyedidia/micro/internal/screen"
|
||||
"github.com/zyedidia/micro/internal/shell"
|
||||
"github.com/zyedidia/micro/internal/util"
|
||||
)
|
||||
|
||||
func init() {
|
||||
ulua.L = lua.NewState()
|
||||
ulua.L.SetGlobal("import", luar.New(ulua.L, LuaImport))
|
||||
}
|
||||
|
||||
func LuaImport(pkg string) *lua.LTable {
|
||||
switch pkg {
|
||||
case "micro":
|
||||
return luaImportMicro()
|
||||
case "micro/shell":
|
||||
return luaImportMicroShell()
|
||||
case "micro/buffer":
|
||||
return luaImportMicroBuffer()
|
||||
case "micro/config":
|
||||
return luaImportMicroConfig()
|
||||
case "micro/util":
|
||||
return luaImportMicroUtil()
|
||||
default:
|
||||
return ulua.Import(pkg)
|
||||
}
|
||||
}
|
||||
|
||||
func luaImportMicro() *lua.LTable {
|
||||
pkg := ulua.L.NewTable()
|
||||
|
||||
ulua.L.SetField(pkg, "TermMessage", luar.New(ulua.L, screen.TermMessage))
|
||||
ulua.L.SetField(pkg, "TermError", luar.New(ulua.L, screen.TermError))
|
||||
ulua.L.SetField(pkg, "InfoBar", luar.New(ulua.L, action.GetInfoBar))
|
||||
ulua.L.SetField(pkg, "Log", luar.New(ulua.L, log.Println))
|
||||
ulua.L.SetField(pkg, "SetStatusInfoFn", luar.New(ulua.L, display.SetStatusInfoFnLua))
|
||||
|
||||
return pkg
|
||||
}
|
||||
|
||||
func luaImportMicroConfig() *lua.LTable {
|
||||
pkg := ulua.L.NewTable()
|
||||
|
||||
ulua.L.SetField(pkg, "MakeCommand", luar.New(ulua.L, action.LuaMakeCommand))
|
||||
ulua.L.SetField(pkg, "FileComplete", luar.New(ulua.L, buffer.FileComplete))
|
||||
ulua.L.SetField(pkg, "HelpComplete", luar.New(ulua.L, action.HelpComplete))
|
||||
ulua.L.SetField(pkg, "OptionComplete", luar.New(ulua.L, action.OptionComplete))
|
||||
ulua.L.SetField(pkg, "OptionValueComplete", luar.New(ulua.L, action.OptionValueComplete))
|
||||
ulua.L.SetField(pkg, "NoComplete", luar.New(ulua.L, nil))
|
||||
ulua.L.SetField(pkg, "TryBindKey", luar.New(ulua.L, action.TryBindKey))
|
||||
ulua.L.SetField(pkg, "Reload", luar.New(ulua.L, action.ReloadConfig))
|
||||
ulua.L.SetField(pkg, "AddRuntimeFileFromMemory", luar.New(ulua.L, config.PluginAddRuntimeFileFromMemory))
|
||||
ulua.L.SetField(pkg, "AddRuntimeFilesFromDirectory", luar.New(ulua.L, config.PluginAddRuntimeFileFromMemory))
|
||||
ulua.L.SetField(pkg, "AddRuntimeFile", luar.New(ulua.L, config.PluginAddRuntimeFile))
|
||||
ulua.L.SetField(pkg, "ListRuntimeFiles", luar.New(ulua.L, config.PluginListRuntimeFiles))
|
||||
ulua.L.SetField(pkg, "ReadRuntimeFile", luar.New(ulua.L, config.PluginReadRuntimeFile))
|
||||
ulua.L.SetField(pkg, "RTColorscheme", luar.New(ulua.L, config.RTColorscheme))
|
||||
ulua.L.SetField(pkg, "RTSyntax", luar.New(ulua.L, config.RTSyntax))
|
||||
ulua.L.SetField(pkg, "RTHelp", luar.New(ulua.L, config.RTHelp))
|
||||
ulua.L.SetField(pkg, "RTPlugin", luar.New(ulua.L, config.RTPlugin))
|
||||
ulua.L.SetField(pkg, "RegisterCommonOption", luar.New(ulua.L, config.RegisterCommonOption))
|
||||
ulua.L.SetField(pkg, "RegisterGlobalOption", luar.New(ulua.L, config.RegisterGlobalOption))
|
||||
ulua.L.SetField(pkg, "GetGlobalOption", luar.New(ulua.L, config.GetGlobalOption))
|
||||
ulua.L.SetField(pkg, "SetGlobalOption", luar.New(ulua.L, action.SetGlobalOption))
|
||||
ulua.L.SetField(pkg, "SetGlobalOptionNative", luar.New(ulua.L, action.SetGlobalOptionNative))
|
||||
|
||||
return pkg
|
||||
}
|
||||
|
||||
func luaImportMicroShell() *lua.LTable {
|
||||
pkg := ulua.L.NewTable()
|
||||
|
||||
ulua.L.SetField(pkg, "ExecCommand", luar.New(ulua.L, shell.ExecCommand))
|
||||
ulua.L.SetField(pkg, "RunCommand", luar.New(ulua.L, shell.RunCommand))
|
||||
ulua.L.SetField(pkg, "RunBackgroundShell", luar.New(ulua.L, shell.RunBackgroundShell))
|
||||
ulua.L.SetField(pkg, "RunInteractiveShell", luar.New(ulua.L, shell.RunInteractiveShell))
|
||||
ulua.L.SetField(pkg, "JobStart", luar.New(ulua.L, shell.JobStart))
|
||||
ulua.L.SetField(pkg, "JobSpawn", luar.New(ulua.L, shell.JobSpawn))
|
||||
ulua.L.SetField(pkg, "JobStop", luar.New(ulua.L, shell.JobStop))
|
||||
ulua.L.SetField(pkg, "JobSend", luar.New(ulua.L, shell.JobSend))
|
||||
ulua.L.SetField(pkg, "RunTermEmulator", luar.New(ulua.L, action.RunTermEmulator))
|
||||
ulua.L.SetField(pkg, "TermEmuSupported", luar.New(ulua.L, action.TermEmuSupported))
|
||||
|
||||
return pkg
|
||||
}
|
||||
|
||||
func luaImportMicroBuffer() *lua.LTable {
|
||||
pkg := ulua.L.NewTable()
|
||||
|
||||
ulua.L.SetField(pkg, "NewMessage", luar.New(ulua.L, buffer.NewMessage))
|
||||
ulua.L.SetField(pkg, "NewMessageAtLine", luar.New(ulua.L, buffer.NewMessageAtLine))
|
||||
ulua.L.SetField(pkg, "MTInfo", luar.New(ulua.L, buffer.MTInfo))
|
||||
ulua.L.SetField(pkg, "MTWarning", luar.New(ulua.L, buffer.MTWarning))
|
||||
ulua.L.SetField(pkg, "MTError", luar.New(ulua.L, buffer.MTError))
|
||||
ulua.L.SetField(pkg, "Loc", luar.New(ulua.L, func(x, y int) buffer.Loc {
|
||||
return buffer.Loc{x, y}
|
||||
}))
|
||||
ulua.L.SetField(pkg, "BTDefault", luar.New(ulua.L, buffer.BTDefault.Kind))
|
||||
ulua.L.SetField(pkg, "BTHelp", luar.New(ulua.L, buffer.BTHelp.Kind))
|
||||
ulua.L.SetField(pkg, "BTLog", luar.New(ulua.L, buffer.BTLog.Kind))
|
||||
ulua.L.SetField(pkg, "BTScratch", luar.New(ulua.L, buffer.BTScratch.Kind))
|
||||
ulua.L.SetField(pkg, "BTRaw", luar.New(ulua.L, buffer.BTRaw.Kind))
|
||||
ulua.L.SetField(pkg, "BTInfo", luar.New(ulua.L, buffer.BTInfo.Kind))
|
||||
ulua.L.SetField(pkg, "NewBufferFromFile", luar.New(ulua.L, func(path string) (*buffer.Buffer, error) {
|
||||
return buffer.NewBufferFromFile(path, buffer.BTDefault)
|
||||
}))
|
||||
ulua.L.SetField(pkg, "ByteOffset", luar.New(ulua.L, buffer.ByteOffset))
|
||||
|
||||
return pkg
|
||||
}
|
||||
|
||||
func luaImportMicroUtil() *lua.LTable {
|
||||
pkg := ulua.L.NewTable()
|
||||
|
||||
ulua.L.SetField(pkg, "RuneAt", luar.New(ulua.L, util.LuaRuneAt))
|
||||
ulua.L.SetField(pkg, "GetLeadingWhitespace", luar.New(ulua.L, util.LuaGetLeadingWhitespace))
|
||||
ulua.L.SetField(pkg, "IsWordChar", luar.New(ulua.L, util.LuaIsWordChar))
|
||||
|
||||
return pkg
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -5,76 +5,87 @@ import (
|
|||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
"sort"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/yuin/gopher-lua"
|
||||
"github.com/zyedidia/clipboard"
|
||||
"github.com/zyedidia/micro/cmd/micro/terminfo"
|
||||
isatty "github.com/mattn/go-isatty"
|
||||
"github.com/zyedidia/micro/internal/action"
|
||||
"github.com/zyedidia/micro/internal/buffer"
|
||||
"github.com/zyedidia/micro/internal/config"
|
||||
"github.com/zyedidia/micro/internal/screen"
|
||||
"github.com/zyedidia/micro/internal/shell"
|
||||
"github.com/zyedidia/micro/internal/util"
|
||||
"github.com/zyedidia/tcell"
|
||||
"github.com/zyedidia/tcell/encoding"
|
||||
"layeh.com/gopher-luar"
|
||||
)
|
||||
|
||||
const (
|
||||
doubleClickThreshold = 400 // How many milliseconds to wait before a second click is not a double click
|
||||
undoThreshold = 500 // If two events are less than n milliseconds apart, undo both of them
|
||||
autosaveTime = 8 // Number of seconds to wait before autosaving
|
||||
)
|
||||
|
||||
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
|
||||
configDir string
|
||||
|
||||
// Version is the version number or commit hash
|
||||
// These variables should be set by the linker when compiling
|
||||
Version = "0.0.0-unknown"
|
||||
// CommitHash is the commit this version was built on
|
||||
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
|
||||
|
||||
// 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")
|
||||
flagConfigDir = flag.String("config-dir", "", "Specify a custom location for the configuration directory")
|
||||
flagOptions = flag.Bool("options", false, "Show all option help")
|
||||
optionFlags map[string]*string
|
||||
)
|
||||
|
||||
func InitFlags() {
|
||||
flag.Usage = func() {
|
||||
fmt.Println("Usage: micro [OPTIONS] [FILE]...")
|
||||
fmt.Println("-config-dir dir")
|
||||
fmt.Println(" \tSpecify a custom location for the configuration directory")
|
||||
fmt.Println("[FILE]:LINE:COL")
|
||||
fmt.Println(" \tSpecify a line and column to start the cursor at when opening a buffer")
|
||||
fmt.Println(" \tThis can also be done by opening file:LINE:COL")
|
||||
fmt.Println("-options")
|
||||
fmt.Println(" \tShow all option help")
|
||||
fmt.Println("-version")
|
||||
fmt.Println(" \tShow the version number and information")
|
||||
|
||||
fmt.Print("\nMicro's options can also be set via command line arguments for quick\nadjustments. For real configuration, please use the settings.json\nfile (see 'help options').\n\n")
|
||||
fmt.Println("-option value")
|
||||
fmt.Println(" \tSet `option` to `value` for this session")
|
||||
fmt.Println(" \tFor example: `micro -syntax off file.c`")
|
||||
fmt.Println("\nUse `micro -options` to see the full list of configuration options")
|
||||
}
|
||||
|
||||
optionFlags = make(map[string]*string)
|
||||
|
||||
for k, v := range config.DefaultAllSettings() {
|
||||
optionFlags[k] = flag.String(k, "", fmt.Sprintf("The %s option. Default value: '%v'.", k, v))
|
||||
}
|
||||
|
||||
flag.Parse()
|
||||
|
||||
if *flagVersion {
|
||||
// If -version was passed
|
||||
fmt.Println("Version:", util.Version)
|
||||
fmt.Println("Commit hash:", util.CommitHash)
|
||||
fmt.Println("Compiled on", util.CompileDate)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if *flagOptions {
|
||||
// If -options was passed
|
||||
var keys []string
|
||||
m := config.DefaultAllSettings()
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
v := m[k]
|
||||
fmt.Printf("-%s value\n", k)
|
||||
fmt.Printf(" \tDefault value: '%v'\n", v)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
// LoadInput determines which files should be loaded into buffers
|
||||
// based on the input stored in flag.Args()
|
||||
func LoadInput() []*Buffer {
|
||||
func LoadInput() []*buffer.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
|
||||
|
|
@ -90,25 +101,15 @@ func LoadInput() []*Buffer {
|
|||
var input []byte
|
||||
var err error
|
||||
args := flag.Args()
|
||||
buffers := make([]*Buffer, 0, len(args))
|
||||
buffers := make([]*buffer.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])
|
||||
buf, err := buffer.NewBufferFromFile(args[i], buffer.BTDefault)
|
||||
if err != nil {
|
||||
TermMessage(err)
|
||||
screen.TermMessage(err)
|
||||
continue
|
||||
}
|
||||
// If the file didn't exist, input will be empty, and we'll open an empty buffer
|
||||
|
|
@ -120,475 +121,142 @@ func LoadInput() []*Buffer {
|
|||
// and we should read from stdin
|
||||
input, err = ioutil.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
TermMessage("Error reading from stdin: ", err)
|
||||
screen.TermMessage("Error reading from stdin: ", err)
|
||||
input = []byte{}
|
||||
}
|
||||
buffers = append(buffers, NewBufferFromString(string(input), filename))
|
||||
buffers = append(buffers, buffer.NewBufferFromString(string(input), filename, buffer.BTDefault))
|
||||
} else {
|
||||
// Option 3, just open an empty buffer
|
||||
buffers = append(buffers, NewBufferFromString(string(input), filename))
|
||||
buffers = append(buffers, buffer.NewBufferFromString(string(input), filename, buffer.BTDefault))
|
||||
}
|
||||
|
||||
return buffers
|
||||
}
|
||||
|
||||
// InitConfigDir finds the configuration directory for micro according to the XDG spec.
|
||||
// If no directory is found, it creates one.
|
||||
func InitConfigDir() {
|
||||
xdgHome := os.Getenv("XDG_CONFIG_HOME")
|
||||
if xdgHome == "" {
|
||||
// The user has not set $XDG_CONFIG_HOME so we should act like it was set to ~/.config
|
||||
home, err := homedir.Dir()
|
||||
if err != nil {
|
||||
TermMessage("Error finding your home directory\nCan't load config files")
|
||||
return
|
||||
}
|
||||
xdgHome = home + "/.config"
|
||||
}
|
||||
configDir = xdgHome + "/micro"
|
||||
|
||||
if len(*flagConfigDir) > 0 {
|
||||
if _, err := os.Stat(*flagConfigDir); os.IsNotExist(err) {
|
||||
TermMessage("Error: " + *flagConfigDir + " does not exist. Defaulting to " + configDir + ".")
|
||||
} else {
|
||||
configDir = *flagConfigDir
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := os.Stat(xdgHome); os.IsNotExist(err) {
|
||||
// If the xdgHome doesn't exist we should create it
|
||||
err = os.Mkdir(xdgHome, os.ModePerm)
|
||||
if err != nil {
|
||||
TermMessage("Error creating XDG_CONFIG_HOME directory: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := os.Stat(configDir); os.IsNotExist(err) {
|
||||
// If the micro specific config directory doesn't exist we should create that too
|
||||
err = os.Mkdir(configDir, os.ModePerm)
|
||||
if err != nil {
|
||||
TermMessage("Error creating configuration directory: " + err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// InitScreen creates and initializes the tcell screen
|
||||
func InitScreen() {
|
||||
// Should we enable true color?
|
||||
truecolor := os.Getenv("MICRO_TRUECOLOR") == "1"
|
||||
|
||||
tcelldb := os.Getenv("TCELLDB")
|
||||
os.Setenv("TCELLDB", configDir+"/.tcelldb")
|
||||
|
||||
// In order to enable true color, we have to set the TERM to `xterm-truecolor` when
|
||||
// initializing tcell, but after that, we can set the TERM back to whatever it was
|
||||
oldTerm := os.Getenv("TERM")
|
||||
if truecolor {
|
||||
os.Setenv("TERM", "xterm-truecolor")
|
||||
}
|
||||
|
||||
// Initilize tcell
|
||||
var err error
|
||||
screen, err = tcell.NewScreen()
|
||||
if err != nil {
|
||||
if err == tcell.ErrTermNotFound {
|
||||
err = terminfo.WriteDB(configDir + "/.tcelldb")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
fmt.Println("Fatal: Micro could not create tcelldb")
|
||||
os.Exit(1)
|
||||
}
|
||||
screen, err = tcell.NewScreen()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
fmt.Println("Fatal: Micro could not initialize a screen.")
|
||||
os.Exit(1)
|
||||
}
|
||||
} else {
|
||||
fmt.Println(err)
|
||||
fmt.Println("Fatal: Micro could not initialize a screen.")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
if err = screen.Init(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Now we can put the TERM back to what it was before
|
||||
if truecolor {
|
||||
os.Setenv("TERM", oldTerm)
|
||||
}
|
||||
|
||||
if GetGlobalOption("mouse").(bool) {
|
||||
screen.EnableMouse()
|
||||
}
|
||||
|
||||
os.Setenv("TCELLDB", tcelldb)
|
||||
|
||||
// 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() {
|
||||
flag.Usage = func() {
|
||||
fmt.Println("Usage: micro [OPTIONS] [FILE]...")
|
||||
fmt.Println("-config-dir dir")
|
||||
fmt.Println(" \tSpecify a custom location for the configuration directory")
|
||||
fmt.Println("-startpos LINE,COL")
|
||||
fmt.Println("+LINE:COL")
|
||||
fmt.Println(" \tSpecify a line and column to start the cursor at when opening a buffer")
|
||||
fmt.Println(" \tThis can also be done by opening file:LINE:COL")
|
||||
fmt.Println("-options")
|
||||
fmt.Println(" \tShow all option help")
|
||||
fmt.Println("-version")
|
||||
fmt.Println(" \tShow the version number and information")
|
||||
defer os.Exit(0)
|
||||
|
||||
fmt.Print("\nMicro's options can also be set via command line arguments for quick\nadjustments. For real configuration, please use the settings.json\nfile (see 'help options').\n\n")
|
||||
fmt.Println("-option value")
|
||||
fmt.Println(" \tSet `option` to `value` for this session")
|
||||
fmt.Println(" \tFor example: `micro -syntax off file.c`")
|
||||
fmt.Println("\nUse `micro -options` to see the full list of configuration options")
|
||||
// runtime.SetCPUProfileRate(400)
|
||||
// f, _ := os.Create("micro.prof")
|
||||
// pprof.StartCPUProfile(f)
|
||||
// defer pprof.StopCPUProfile()
|
||||
|
||||
var err error
|
||||
|
||||
InitLog()
|
||||
|
||||
InitFlags()
|
||||
|
||||
err = config.InitConfigDir(*flagConfigDir)
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
|
||||
optionFlags := make(map[string]*string)
|
||||
|
||||
for k, v := range DefaultGlobalSettings() {
|
||||
optionFlags[k] = flag.String(k, "", fmt.Sprintf("The %s option. Default value: '%v'", k, v))
|
||||
config.InitRuntimeFiles()
|
||||
err = config.ReadSettings()
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
config.InitGlobalSettings()
|
||||
|
||||
flag.Parse()
|
||||
|
||||
if *flagVersion {
|
||||
// If -version was passed
|
||||
fmt.Println("Version:", Version)
|
||||
fmt.Println("Commit hash:", CommitHash)
|
||||
fmt.Println("Compiled on", CompileDate)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if *flagOptions {
|
||||
// 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)
|
||||
// flag options
|
||||
for k, v := range optionFlags {
|
||||
if *v != "" {
|
||||
nativeValue, err := config.GetNativeValue(k, config.DefaultAllSettings()[k], *v)
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
continue
|
||||
}
|
||||
config.GlobalSettings[k] = nativeValue
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Start the Lua VM for running plugins
|
||||
L = lua.NewState()
|
||||
defer L.Close()
|
||||
action.InitBindings()
|
||||
action.InitCommands()
|
||||
|
||||
// Some encoding stuff in case the user isn't using UTF-8
|
||||
encoding.Register()
|
||||
tcell.SetEncodingFallback(tcell.EncodingFallbackASCII)
|
||||
err = config.InitColorscheme()
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
|
||||
// Find the user's configuration directory (probably $XDG_CONFIG_HOME/micro)
|
||||
InitConfigDir()
|
||||
err = config.LoadAllPlugins()
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
err = config.RunPluginFn("init")
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
|
||||
// Build a list of available Extensions (Syntax, Colorscheme etc.)
|
||||
InitRuntimeFiles()
|
||||
screen.Init()
|
||||
|
||||
// Load the user's settings
|
||||
InitGlobalSettings()
|
||||
|
||||
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() {
|
||||
if err := recover(); err != nil {
|
||||
screen.Fini()
|
||||
screen.Screen.Fini()
|
||||
fmt.Println("Micro encountered an error:", err)
|
||||
// backup all open buffers
|
||||
for _, b := range buffer.OpenBuffers {
|
||||
b.Backup(false)
|
||||
}
|
||||
// Print the stack trace too
|
||||
fmt.Print(errors.Wrap(err, 2).ErrorStack())
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
|
||||
// Create a new messenger
|
||||
// This is used for sending the user messages in the bottom of the editor
|
||||
messenger = new(Messenger)
|
||||
messenger.LoadHistory()
|
||||
b := LoadInput()
|
||||
action.InitTabs(b)
|
||||
action.InitGlobals()
|
||||
|
||||
// Now we load the input
|
||||
buffers := LoadInput()
|
||||
if len(buffers) == 0 {
|
||||
screen.Fini()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
for _, buf := range buffers {
|
||||
// For each buffer we create a new tab and place the view in that tab
|
||||
tab := NewTabFromView(NewView(buf))
|
||||
tab.SetNum(len(tabs))
|
||||
tabs = append(tabs, tab)
|
||||
for _, t := range tabs {
|
||||
for _, v := range t.Views {
|
||||
v.Center(false)
|
||||
}
|
||||
|
||||
t.Resize()
|
||||
}
|
||||
}
|
||||
|
||||
for k, v := range optionFlags {
|
||||
if *v != "" {
|
||||
SetOption(k, *v)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
events = make(chan tcell.Event)
|
||||
|
||||
// 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
|
||||
screen.Lock()
|
||||
e := screen.Screen.PollEvent()
|
||||
screen.Unlock()
|
||||
if e != nil {
|
||||
events <- e
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
// Display everything
|
||||
RedrawAll()
|
||||
screen.Screen.Fill(' ', config.DefStyle)
|
||||
screen.Screen.HideCursor()
|
||||
action.Tabs.Display()
|
||||
for _, ep := range action.MainTab().Panes {
|
||||
ep.Display()
|
||||
}
|
||||
action.MainTab().Display()
|
||||
action.InfoBar.Display()
|
||||
screen.Screen.Show()
|
||||
|
||||
var event tcell.Event
|
||||
|
||||
// Check for new events
|
||||
select {
|
||||
case f := <-jobs:
|
||||
case f := <-shell.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)
|
||||
f.Function(f.Output, f.Args...)
|
||||
case <-config.Autosave:
|
||||
for _, b := range buffer.OpenBuffers {
|
||||
b.Save()
|
||||
}
|
||||
case <-updateterm:
|
||||
continue
|
||||
case vnum := <-closeterm:
|
||||
tabs[curTab].Views[vnum].CloseTerminal()
|
||||
case <-shell.CloseTerms:
|
||||
case event = <-events:
|
||||
case <-screen.DrawChan:
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
if action.InfoBar.HasPrompt {
|
||||
action.InfoBar.HandleEvent(event)
|
||||
} else {
|
||||
action.Tabs.HandleEvent(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,207 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
const (
|
||||
RTColorscheme = "colorscheme"
|
||||
RTSyntax = "syntax"
|
||||
RTHelp = "help"
|
||||
RTPlugin = "plugin"
|
||||
)
|
||||
|
||||
// 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
|
||||
Name() string
|
||||
// Data returns the content of the file.
|
||||
Data() ([]byte, error)
|
||||
}
|
||||
|
||||
// allFiles contains all available files, mapped by filetype
|
||||
var allFiles map[string][]RuntimeFile
|
||||
|
||||
// some file on filesystem
|
||||
type realFile string
|
||||
|
||||
// some asset file
|
||||
type assetFile string
|
||||
|
||||
// some file on filesystem but with a different name
|
||||
type namedFile struct {
|
||||
realFile
|
||||
name string
|
||||
}
|
||||
|
||||
// a file with the data stored in memory
|
||||
type memoryFile struct {
|
||||
name string
|
||||
data []byte
|
||||
}
|
||||
|
||||
func (mf memoryFile) Name() string {
|
||||
return mf.name
|
||||
}
|
||||
func (mf memoryFile) Data() ([]byte, error) {
|
||||
return mf.data, nil
|
||||
}
|
||||
|
||||
func (rf realFile) Name() string {
|
||||
fn := filepath.Base(string(rf))
|
||||
return fn[:len(fn)-len(filepath.Ext(fn))]
|
||||
}
|
||||
|
||||
func (rf realFile) Data() ([]byte, error) {
|
||||
return ioutil.ReadFile(string(rf))
|
||||
}
|
||||
|
||||
func (af assetFile) Name() string {
|
||||
fn := path.Base(string(af))
|
||||
return fn[:len(fn)-len(path.Ext(fn))]
|
||||
}
|
||||
|
||||
func (af assetFile) Data() ([]byte, error) {
|
||||
return Asset(string(af))
|
||||
}
|
||||
|
||||
func (nf namedFile) Name() string {
|
||||
return nf.name
|
||||
}
|
||||
|
||||
// AddRuntimeFile registers a file for the given filetype
|
||||
func AddRuntimeFile(fileType string, file RuntimeFile) {
|
||||
if allFiles == nil {
|
||||
allFiles = make(map[string][]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) {
|
||||
files, _ := ioutil.ReadDir(directory)
|
||||
for _, f := range files {
|
||||
if ok, _ := filepath.Match(pattern, f.Name()); !f.IsDir() && ok {
|
||||
fullPath := filepath.Join(directory, f.Name())
|
||||
AddRuntimeFile(fileType, realFile(fullPath))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AddRuntimeFilesFromAssets registers each file from the given asset-directory for
|
||||
// the filetype which matches the file-pattern
|
||||
func AddRuntimeFilesFromAssets(fileType, directory, pattern string) {
|
||||
files, err := AssetDir(directory)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, f := range files {
|
||||
if ok, _ := path.Match(pattern, f); ok {
|
||||
AddRuntimeFile(fileType, assetFile(path.Join(directory, f)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
for _, f := range ListRuntimeFiles(fileType) {
|
||||
if f.Name() == name {
|
||||
return f
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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{}
|
||||
}
|
||||
|
||||
// InitRuntimeFiles initializes all assets file and the config directory
|
||||
func InitRuntimeFiles() {
|
||||
add := func(fileType, dir, pattern string) {
|
||||
AddRuntimeFilesFromDirectory(fileType, filepath.Join(configDir, dir), pattern)
|
||||
AddRuntimeFilesFromAssets(fileType, path.Join("runtime", dir), pattern)
|
||||
}
|
||||
|
||||
add(RTColorscheme, "colorschemes", "*.micro")
|
||||
add(RTSyntax, "syntax", "*.yaml")
|
||||
add(RTHelp, "help", "*.md")
|
||||
|
||||
// Search configDir for plugin-scripts
|
||||
files, _ := ioutil.ReadDir(filepath.Join(configDir, "plugins"))
|
||||
for _, f := range files {
|
||||
realpath, _ := filepath.EvalSymlinks(filepath.Join(configDir, "plugins", f.Name()))
|
||||
realpathStat, _ := os.Stat(realpath)
|
||||
if realpathStat.IsDir() {
|
||||
scriptPath := filepath.Join(configDir, "plugins", f.Name(), f.Name()+".lua")
|
||||
if _, err := os.Stat(scriptPath); err == nil {
|
||||
AddRuntimeFile(RTPlugin, realFile(scriptPath))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if files, err := AssetDir("runtime/plugins"); err == nil {
|
||||
for _, f := range files {
|
||||
scriptPath := path.Join("runtime/plugins", f, f+".lua")
|
||||
if _, err := AssetInfo(scriptPath); err == nil {
|
||||
AddRuntimeFile(RTPlugin, assetFile(scriptPath))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PluginReadRuntimeFile allows plugin scripts to read the content of a runtime file
|
||||
func PluginReadRuntimeFile(fileType, name string) string {
|
||||
if file := FindRuntimeFile(fileType, name); file != nil {
|
||||
if data, err := file.Data(); err == nil {
|
||||
return string(data)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// PluginListRuntimeFiles allows plugins to lists all runtime files of the given type
|
||||
func PluginListRuntimeFiles(fileType string) []string {
|
||||
files := ListRuntimeFiles(fileType)
|
||||
result := make([]string, len(files))
|
||||
for i, f := range files {
|
||||
result[i] = f.Name()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// PluginAddRuntimeFile adds a file to the runtime files for a plugin
|
||||
func PluginAddRuntimeFile(plugin, filetype, filePath string) {
|
||||
fullpath := filepath.Join(configDir, "plugins", plugin, filePath)
|
||||
if _, err := os.Stat(fullpath); err == nil {
|
||||
AddRuntimeFile(filetype, realFile(fullpath))
|
||||
} else {
|
||||
fullpath = path.Join("runtime", "plugins", plugin, filePath)
|
||||
AddRuntimeFile(filetype, assetFile(fullpath))
|
||||
}
|
||||
}
|
||||
|
||||
// PluginAddRuntimeFilesFromDirectory adds files from a directory to the runtime files for a plugin
|
||||
func PluginAddRuntimeFilesFromDirectory(plugin, filetype, directory, pattern string) {
|
||||
fullpath := filepath.Join(configDir, "plugins", plugin, directory)
|
||||
if _, err := os.Stat(fullpath); err == nil {
|
||||
AddRuntimeFilesFromDirectory(filetype, fullpath, pattern)
|
||||
} else {
|
||||
fullpath = path.Join("runtime", "plugins", plugin, directory)
|
||||
AddRuntimeFilesFromAssets(filetype, fullpath, pattern)
|
||||
}
|
||||
}
|
||||
|
||||
// PluginAddRuntimeFileFromMemory adds a file to the runtime files for a plugin from a given string
|
||||
func PluginAddRuntimeFileFromMemory(plugin, filetype, filename, data string) {
|
||||
AddRuntimeFile(filetype, memoryFile{filename, []byte(data)})
|
||||
}
|
||||
4119
cmd/micro/runtime.go
4119
cmd/micro/runtime.go
File diff suppressed because one or more lines are too long
|
|
@ -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))
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,541 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/flynn/json5"
|
||||
"github.com/zyedidia/glob"
|
||||
)
|
||||
|
||||
type optionValidator func(string, interface{}) error
|
||||
|
||||
// The options that the user can set
|
||||
var globalSettings map[string]interface{}
|
||||
|
||||
var invalidSettings bool
|
||||
|
||||
// Options with validators
|
||||
var optionValidators = map[string]optionValidator{
|
||||
"tabsize": validatePositiveValue,
|
||||
"scrollmargin": validateNonNegativeValue,
|
||||
"scrollspeed": validateNonNegativeValue,
|
||||
"colorscheme": validateColorscheme,
|
||||
"colorcolumn": validateNonNegativeValue,
|
||||
"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{}
|
||||
|
||||
filename := configDir + "/settings.json"
|
||||
writeSettings := false
|
||||
if _, e := os.Stat(filename); e == nil {
|
||||
input, err := ioutil.ReadFile(filename)
|
||||
if !strings.HasPrefix(string(input), "null") {
|
||||
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
|
||||
}
|
||||
} else {
|
||||
writeSettings = true
|
||||
}
|
||||
}
|
||||
|
||||
globalSettings = make(map[string]interface{})
|
||||
for k, v := range defaults {
|
||||
globalSettings[k] = v
|
||||
}
|
||||
for k, v := range parsed {
|
||||
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 {
|
||||
if strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
|
||||
if strings.HasPrefix(k, "ft:") {
|
||||
if buf.Settings["filetype"].(string) == k[3:] {
|
||||
for k1, v1 := range v.(map[string]interface{}) {
|
||||
buf.Settings[k1] = v1
|
||||
}
|
||||
}
|
||||
} else {
|
||||
g, err := glob.Compile(k)
|
||||
if err != nil {
|
||||
TermMessage("Error with glob setting ", k, ": ", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if g.MatchString(buf.Path) {
|
||||
for k1, v1 := range v.(map[string]interface{}) {
|
||||
buf.Settings[k1] = v1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
txt, _ := json.MarshalIndent(parsed, "", " ")
|
||||
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{}) {
|
||||
globalSettings[name] = value
|
||||
err := WriteSettings(configDir + "/settings.json")
|
||||
if err != nil {
|
||||
TermMessage("Error writing settings.json file: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// GetGlobalOption returns the global value of the given option
|
||||
func GetGlobalOption(name string) interface{} {
|
||||
return globalSettings[name]
|
||||
}
|
||||
|
||||
// GetLocalOption returns the local value of the given option
|
||||
func GetLocalOption(name string, buf *Buffer) interface{} {
|
||||
return buf.Settings[name]
|
||||
}
|
||||
|
||||
// GetOption returns the value of the given option
|
||||
// If there is a local version of the option, it returns that
|
||||
// otherwise it will return the global version
|
||||
func GetOption(name string) interface{} {
|
||||
if GetLocalOption(name, CurView().Buf) != nil {
|
||||
return GetLocalOption(name, CurView().Buf)
|
||||
}
|
||||
return GetGlobalOption(name)
|
||||
}
|
||||
|
||||
// DefaultGlobalSettings returns the default global settings for micro
|
||||
// Note that colorscheme is a global only option
|
||||
func DefaultGlobalSettings() map[string]interface{} {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
|
||||
// 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 optionIsValid(option string, value interface{}) error {
|
||||
if validator, ok := optionValidators[option]; ok {
|
||||
return validator(option, value)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Option validators
|
||||
|
||||
func validatePositiveValue(option string, value interface{}) error {
|
||||
tabsize, ok := value.(float64)
|
||||
|
||||
if !ok {
|
||||
return errors.New("Expected numeric type for " + option)
|
||||
}
|
||||
|
||||
if tabsize < 1 {
|
||||
return errors.New(option + " must be greater than 0")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateNonNegativeValue(option string, value interface{}) error {
|
||||
nativeValue, ok := value.(float64)
|
||||
|
||||
if !ok {
|
||||
return errors.New("Expected numeric type for " + option)
|
||||
}
|
||||
|
||||
if nativeValue < 0 {
|
||||
return errors.New(option + " must be non-negative")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateColorscheme(option string, value interface{}) error {
|
||||
colorscheme, ok := value.(string)
|
||||
|
||||
if !ok {
|
||||
return errors.New("Expected string type for colorscheme")
|
||||
}
|
||||
|
||||
if !ColorschemeExists(colorscheme) {
|
||||
return errors.New(colorscheme + " is not a valid colorscheme")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateLineEnding(option string, value interface{}) error {
|
||||
endingType, ok := value.(string)
|
||||
|
||||
if !ok {
|
||||
return errors.New("Expected string type for file format")
|
||||
}
|
||||
|
||||
if endingType != "unix" && endingType != "dos" {
|
||||
return errors.New("File format must be either 'unix' or 'dos'")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -1,229 +0,0 @@
|
|||
package shellwords
|
||||
|
||||
import (
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var testcases = []struct {
|
||||
line string
|
||||
expected []string
|
||||
}{
|
||||
{`var --bar=baz`, []string{`var`, `--bar=baz`}},
|
||||
{`var --bar="baz"`, []string{`var`, `--bar=baz`}},
|
||||
{`var "--bar=baz"`, []string{`var`, `--bar=baz`}},
|
||||
{`var "--bar='baz'"`, []string{`var`, `--bar='baz'`}},
|
||||
{"var --bar=`baz`", []string{`var`, "--bar=`baz`"}},
|
||||
{`var "--bar=\"baz'"`, []string{`var`, `--bar="baz'`}},
|
||||
{`var "--bar=\'baz\'"`, []string{`var`, `--bar='baz'`}},
|
||||
{`var --bar='\'`, []string{`var`, `--bar=\`}},
|
||||
{`var "--bar baz"`, []string{`var`, `--bar baz`}},
|
||||
{`var --"bar baz"`, []string{`var`, `--bar baz`}},
|
||||
{`var --"bar baz"`, []string{`var`, `--bar baz`}},
|
||||
}
|
||||
|
||||
func TestSimple(t *testing.T) {
|
||||
for _, testcase := range testcases {
|
||||
args, err := Parse(testcase.line)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(args, testcase.expected) {
|
||||
t.Fatalf("Expected %#v, but %#v:", testcase.expected, args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestError(t *testing.T) {
|
||||
_, err := Parse("foo '")
|
||||
if err == nil {
|
||||
t.Fatal("Should be an error")
|
||||
}
|
||||
_, err = Parse(`foo "`)
|
||||
if err == nil {
|
||||
t.Fatal("Should be an error")
|
||||
}
|
||||
|
||||
_, err = Parse("foo `")
|
||||
if err == nil {
|
||||
t.Fatal("Should be an error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLastSpace(t *testing.T) {
|
||||
args, err := Parse("foo bar\\ ")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(args) != 2 {
|
||||
t.Fatal("Should have two elements")
|
||||
}
|
||||
if args[0] != "foo" {
|
||||
t.Fatal("1st element should be `foo`")
|
||||
}
|
||||
if args[1] != "bar " {
|
||||
t.Fatal("1st element should be `bar `")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBacktick(t *testing.T) {
|
||||
goversion, err := shellRun("go version")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
parser := NewParser()
|
||||
parser.ParseBacktick = true
|
||||
args, err := parser.Parse("echo `go version`")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expected := []string{"echo", goversion}
|
||||
if !reflect.DeepEqual(args, expected) {
|
||||
t.Fatalf("Expected %#v, but %#v:", expected, args)
|
||||
}
|
||||
|
||||
args, err = parser.Parse(`echo $(echo foo)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expected = []string{"echo", "foo"}
|
||||
if !reflect.DeepEqual(args, expected) {
|
||||
t.Fatalf("Expected %#v, but %#v:", expected, args)
|
||||
}
|
||||
|
||||
parser.ParseBacktick = false
|
||||
args, err = parser.Parse(`echo $(echo "foo")`)
|
||||
expected = []string{"echo", `$(echo "foo")`}
|
||||
if !reflect.DeepEqual(args, expected) {
|
||||
t.Fatalf("Expected %#v, but %#v:", expected, args)
|
||||
}
|
||||
args, err = parser.Parse("echo $(`echo1)")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expected = []string{"echo", "$(`echo1)"}
|
||||
if !reflect.DeepEqual(args, expected) {
|
||||
t.Fatalf("Expected %#v, but %#v:", expected, args)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBacktickError(t *testing.T) {
|
||||
parser := NewParser()
|
||||
parser.ParseBacktick = true
|
||||
_, err := parser.Parse("echo `go Version`")
|
||||
if err == nil {
|
||||
t.Fatal("Should be an error")
|
||||
}
|
||||
expected := "exit status 2:go: unknown subcommand \"Version\"\nRun 'go help' for usage.\n"
|
||||
if expected != err.Error() {
|
||||
t.Fatalf("Expected %q, but %q", expected, err.Error())
|
||||
}
|
||||
_, err = parser.Parse(`echo $(echo1)`)
|
||||
if err == nil {
|
||||
t.Fatal("Should be an error")
|
||||
}
|
||||
_, err = parser.Parse(`echo $(echo1`)
|
||||
if err == nil {
|
||||
t.Fatal("Should be an error")
|
||||
}
|
||||
_, err = parser.Parse(`echo $ (echo1`)
|
||||
if err == nil {
|
||||
t.Fatal("Should be an error")
|
||||
}
|
||||
_, err = parser.Parse(`echo (echo1`)
|
||||
if err == nil {
|
||||
t.Fatal("Should be an error")
|
||||
}
|
||||
_, err = parser.Parse(`echo )echo1`)
|
||||
if err == nil {
|
||||
t.Fatal("Should be an error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnv(t *testing.T) {
|
||||
os.Setenv("FOO", "bar")
|
||||
|
||||
parser := NewParser()
|
||||
parser.ParseEnv = true
|
||||
args, err := parser.Parse("echo $FOO")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expected := []string{"echo", "bar"}
|
||||
if !reflect.DeepEqual(args, expected) {
|
||||
t.Fatalf("Expected %#v, but %#v:", expected, args)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoEnv(t *testing.T) {
|
||||
parser := NewParser()
|
||||
parser.ParseEnv = true
|
||||
args, err := parser.Parse("echo $BAR")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expected := []string{"echo", ""}
|
||||
if !reflect.DeepEqual(args, expected) {
|
||||
t.Fatalf("Expected %#v, but %#v:", expected, args)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDupEnv(t *testing.T) {
|
||||
os.Setenv("FOO", "bar")
|
||||
os.Setenv("FOO_BAR", "baz")
|
||||
|
||||
parser := NewParser()
|
||||
parser.ParseEnv = true
|
||||
args, err := parser.Parse("echo $$FOO$")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expected := []string{"echo", "$bar$"}
|
||||
if !reflect.DeepEqual(args, expected) {
|
||||
t.Fatalf("Expected %#v, but %#v:", expected, args)
|
||||
}
|
||||
|
||||
args, err = parser.Parse("echo $${FOO_BAR}$")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expected = []string{"echo", "$baz$"}
|
||||
if !reflect.DeepEqual(args, expected) {
|
||||
t.Fatalf("Expected %#v, but %#v:", expected, args)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHaveMore(t *testing.T) {
|
||||
parser := NewParser()
|
||||
parser.ParseEnv = true
|
||||
|
||||
line := "echo foo; seq 1 10"
|
||||
args, err := parser.Parse(line)
|
||||
if err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
expected := []string{"echo", "foo"}
|
||||
if !reflect.DeepEqual(args, expected) {
|
||||
t.Fatalf("Expected %#v, but %#v:", expected, args)
|
||||
}
|
||||
|
||||
if parser.Position == 0 {
|
||||
t.Fatalf("Commands should be remaining")
|
||||
}
|
||||
|
||||
line = string([]rune(line)[parser.Position+1:])
|
||||
args, err = parser.Parse(line)
|
||||
if err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
expected = []string{"seq", "1", "10"}
|
||||
if !reflect.DeepEqual(args, expected) {
|
||||
t.Fatalf("Expected %#v, but %#v:", expected, args)
|
||||
}
|
||||
|
||||
if parser.Position > 0 {
|
||||
t.Fatalf("Commands should not be remaining")
|
||||
}
|
||||
}
|
||||
|
|
@ -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 + "]"
|
||||
}
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"path"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Statusline represents the information line at the bottom
|
||||
// of each view
|
||||
// It gives information such as filename, whether the file has been
|
||||
// modified, filetype, cursor location
|
||||
type Statusline struct {
|
||||
view *View
|
||||
}
|
||||
|
||||
// Display draws the statusline to the screen
|
||||
func (sline *Statusline) Display() {
|
||||
if messenger.hasPrompt && !GetGlobalOption("infobar").(bool) {
|
||||
return
|
||||
}
|
||||
|
||||
// We'll draw the line at the lowest line in the view
|
||||
y := sline.view.Height + sline.view.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"
|
||||
}
|
||||
}
|
||||
if len(helpBinding) > 0 {
|
||||
if len(kmenuBinding) > 0 {
|
||||
rightText += ", "
|
||||
}
|
||||
if sline.view.Type == vtHelp {
|
||||
rightText += helpBinding + ": close help"
|
||||
} else {
|
||||
rightText += helpBinding + ": open help"
|
||||
}
|
||||
}
|
||||
rightText += " "
|
||||
}
|
||||
|
||||
statusLineStyle := defStyle.Reverse(true)
|
||||
if style, ok := colorscheme["statusline"]; ok {
|
||||
statusLineStyle = style
|
||||
}
|
||||
|
||||
// Maybe there is a unicode filename?
|
||||
fileRunes := []rune(file)
|
||||
|
||||
if sline.view.Type == vtTerm {
|
||||
fileRunes = []rune(sline.view.term.title)
|
||||
rightText = ""
|
||||
}
|
||||
|
||||
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)
|
||||
} else {
|
||||
screen.SetContent(viewX+x, y, ' ', nil, statusLineStyle)
|
||||
}
|
||||
}
|
||||
}
|
||||
293
cmd/micro/tab.go
293
cmd/micro/tab.go
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,194 +0,0 @@
|
|||
// Copyright 2016 The TCell Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use file except in compliance with the License.
|
||||
// You may obtain a copy of the license at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package terminfo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
// This terminfo entry is a stripped down version from
|
||||
// xterm-256color, but I've added some of my own entries.
|
||||
var testTerminfo = &Terminfo{
|
||||
Name: "simulation_test",
|
||||
Columns: 80,
|
||||
Lines: 24,
|
||||
Colors: 256,
|
||||
Bell: "\a",
|
||||
Blink: "\x1b2ms$<2>",
|
||||
Reverse: "\x1b[7m",
|
||||
SetFg: "\x1b[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;m",
|
||||
SetBg: "\x1b[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e48;5;%p1%d%;m",
|
||||
AltChars: "``aaffggiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~",
|
||||
Mouse: "\x1b[M",
|
||||
MouseMode: "%?%p1%{1}%=%t%'h'%Pa%e%'l'%Pa%;\x1b[?1000%ga%c\x1b[?1003%ga%c\x1b[?1006%ga%c",
|
||||
SetCursor: "\x1b[%i%p1%d;%p2%dH",
|
||||
PadChar: "\x00",
|
||||
}
|
||||
|
||||
func TestTerminfo(t *testing.T) {
|
||||
|
||||
ti := testTerminfo
|
||||
|
||||
Convey("Terminfo parameter processing", t, func() {
|
||||
// This tests %i, and basic parameter strings too
|
||||
Convey("TGoto works", func() {
|
||||
s := ti.TGoto(7, 9)
|
||||
So(s, ShouldEqual, "\x1b[10;8H")
|
||||
})
|
||||
|
||||
// This tests some conditionals
|
||||
Convey("TParm extended formats work", func() {
|
||||
s := ti.TParm("A[%p1%2.2X]B", 47)
|
||||
So(s, ShouldEqual, "A[2F]B")
|
||||
})
|
||||
|
||||
// This tests some conditionals
|
||||
Convey("TParm colors work", func() {
|
||||
s := ti.TParm(ti.SetFg, 7)
|
||||
So(s, ShouldEqual, "\x1b[37m")
|
||||
|
||||
s = ti.TParm(ti.SetFg, 15)
|
||||
So(s, ShouldEqual, "\x1b[97m")
|
||||
|
||||
s = ti.TParm(ti.SetFg, 200)
|
||||
So(s, ShouldEqual, "\x1b[38;5;200m")
|
||||
})
|
||||
|
||||
// This tests variables
|
||||
Convey("TParm mouse mode works", func() {
|
||||
s := ti.TParm(ti.MouseMode, 1)
|
||||
So(s, ShouldEqual, "\x1b[?1000h\x1b[?1003h\x1b[?1006h")
|
||||
s = ti.TParm(ti.MouseMode, 0)
|
||||
So(s, ShouldEqual, "\x1b[?1000l\x1b[?1003l\x1b[?1006l")
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
Convey("Terminfo delay handling", t, func() {
|
||||
|
||||
Convey("19200 baud", func() {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
ti.TPuts(buf, ti.Blink, 19200)
|
||||
s := string(buf.Bytes())
|
||||
So(s, ShouldEqual, "\x1b2ms\x00\x00\x00\x00")
|
||||
})
|
||||
|
||||
Convey("50 baud", func() {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
ti.TPuts(buf, ti.Blink, 50)
|
||||
s := string(buf.Bytes())
|
||||
So(s, ShouldEqual, "\x1b2ms")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestTerminfoDatabase(t *testing.T) {
|
||||
|
||||
Convey("Database Lookups work", t, func() {
|
||||
Convey("Basic lookup works", func() {
|
||||
os.Setenv("TCELLDB", "testdata/test1")
|
||||
ti, err := LookupTerminfo("test1")
|
||||
So(err, ShouldBeNil)
|
||||
So(ti, ShouldNotBeNil)
|
||||
So(ti.Columns, ShouldEqual, 80)
|
||||
|
||||
ti, err = LookupTerminfo("alias1")
|
||||
So(err, ShouldBeNil)
|
||||
So(ti, ShouldNotBeNil)
|
||||
So(ti.Columns, ShouldEqual, 80)
|
||||
|
||||
os.Setenv("TCELLDB", "testdata")
|
||||
ti, err = LookupTerminfo("test2")
|
||||
So(err, ShouldBeNil)
|
||||
So(ti, ShouldNotBeNil)
|
||||
So(ti.Columns, ShouldEqual, 80)
|
||||
So(len(ti.Aliases), ShouldEqual, 1)
|
||||
So(ti.Aliases[0], ShouldEqual, "alias2")
|
||||
})
|
||||
|
||||
Convey("Incorrect primary name works", func() {
|
||||
os.Setenv("TCELLDB", "testdata")
|
||||
ti, err := LookupTerminfo("test3")
|
||||
So(err, ShouldNotBeNil)
|
||||
So(ti, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("Loops fail", func() {
|
||||
os.Setenv("TCELLDB", "testdata")
|
||||
ti, err := LookupTerminfo("loop1")
|
||||
So(ti, ShouldBeNil)
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("Gzip database works", func() {
|
||||
os.Setenv("TCELLDB", "testdata")
|
||||
ti, err := LookupTerminfo("test-gzip")
|
||||
So(err, ShouldBeNil)
|
||||
So(ti, ShouldNotBeNil)
|
||||
So(ti.Columns, ShouldEqual, 80)
|
||||
})
|
||||
|
||||
Convey("Gzip alias lookup works", func() {
|
||||
os.Setenv("TCELLDB", "testdata")
|
||||
ti, err := LookupTerminfo("alias-gzip")
|
||||
So(err, ShouldBeNil)
|
||||
So(ti, ShouldNotBeNil)
|
||||
So(ti.Columns, ShouldEqual, 80)
|
||||
})
|
||||
|
||||
Convey("Broken alias works", func() {
|
||||
os.Setenv("TCELLDB", "testdata")
|
||||
ti, err := LookupTerminfo("alias-none")
|
||||
So(err, ShouldNotBeNil)
|
||||
So(ti, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("Combined database works", func() {
|
||||
os.Setenv("TCELLDB", "testdata/combined")
|
||||
ti, err := LookupTerminfo("combined2")
|
||||
So(err, ShouldBeNil)
|
||||
So(ti, ShouldNotBeNil)
|
||||
So(ti.Lines, ShouldEqual, 102)
|
||||
|
||||
ti, err = LookupTerminfo("alias-comb1")
|
||||
So(err, ShouldBeNil)
|
||||
So(ti, ShouldNotBeNil)
|
||||
So(ti.Lines, ShouldEqual, 101)
|
||||
|
||||
ti, err = LookupTerminfo("combined3")
|
||||
So(err, ShouldBeNil)
|
||||
So(ti, ShouldNotBeNil)
|
||||
So(ti.Lines, ShouldEqual, 103)
|
||||
|
||||
ti, err = LookupTerminfo("combined1")
|
||||
So(err, ShouldBeNil)
|
||||
So(ti, ShouldNotBeNil)
|
||||
So(ti.Lines, ShouldEqual, 101)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkSetFgBg(b *testing.B) {
|
||||
ti := testTerminfo
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
ti.TParm(ti.SetFg, 100, 200)
|
||||
ti.TParm(ti.SetBg, 100, 200)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,400 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/mattn/go-runewidth"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
len := len(slc)
|
||||
i := 0
|
||||
totalSize := 0
|
||||
for totalSize < len {
|
||||
if i >= index {
|
||||
return slc[totalSize:]
|
||||
}
|
||||
|
||||
_, size := utf8.DecodeRune(slc[totalSize:])
|
||||
totalSize += size
|
||||
i++
|
||||
}
|
||||
|
||||
return slc[totalSize:]
|
||||
}
|
||||
|
||||
func sliceEnd(slc []byte, index int) []byte {
|
||||
len := len(slc)
|
||||
i := 0
|
||||
totalSize := 0
|
||||
for totalSize < len {
|
||||
if i >= index {
|
||||
return slc[:totalSize]
|
||||
}
|
||||
|
||||
_, size := utf8.DecodeRune(slc[totalSize:])
|
||||
totalSize += size
|
||||
i++
|
||||
}
|
||||
|
||||
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++
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// Spaces returns a string with n spaces
|
||||
func Spaces(n int) string {
|
||||
return strings.Repeat(" ", n)
|
||||
}
|
||||
|
||||
// Min takes the min of two ints
|
||||
func Min(a, b int) int {
|
||||
if a > b {
|
||||
return b
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// Max takes the max of two ints
|
||||
func Max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// FSize gets the size of a file
|
||||
func FSize(f *os.File) int64 {
|
||||
fi, _ := f.Stat()
|
||||
// get the size
|
||||
return fi.Size()
|
||||
}
|
||||
|
||||
// IsWordChar returns whether or not the string is a 'word character'
|
||||
// If it is a unicode character, then it does not match
|
||||
// Word characters are defined as [A-Za-z0-9_]
|
||||
func IsWordChar(str string) bool {
|
||||
if len(str) > 1 {
|
||||
// Unicode
|
||||
return true
|
||||
}
|
||||
c := str[0]
|
||||
return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c == '_')
|
||||
}
|
||||
|
||||
// IsWhitespace returns true if the given rune is a space, tab, or newline
|
||||
func IsWhitespace(c rune) bool {
|
||||
return c == ' ' || c == '\t' || c == '\n'
|
||||
}
|
||||
|
||||
// IsStrWhitespace returns true if the given string is all whitespace
|
||||
func IsStrWhitespace(str string) bool {
|
||||
for _, c := range str {
|
||||
if !IsWhitespace(c) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Contains returns whether or not a string array contains a given string
|
||||
func Contains(list []string, a string) bool {
|
||||
for _, b := range list {
|
||||
if b == a {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Insert makes a simple insert into a string at the given position
|
||||
func Insert(str string, pos int, value string) string {
|
||||
return string([]rune(str)[:pos]) + value + string([]rune(str)[pos:])
|
||||
}
|
||||
|
||||
// MakeRelative will attempt to make a relative path between path and base
|
||||
func MakeRelative(path, base string) (string, error) {
|
||||
if len(path) > 0 {
|
||||
rel, err := filepath.Rel(base, path)
|
||||
if err != nil {
|
||||
return path, err
|
||||
}
|
||||
return rel, nil
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// GetLeadingWhitespace returns the leading whitespace of the given string
|
||||
func GetLeadingWhitespace(str string) string {
|
||||
ws := ""
|
||||
for _, c := range str {
|
||||
if c == ' ' || c == '\t' {
|
||||
ws += string(c)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return ws
|
||||
}
|
||||
|
||||
// IsSpaces checks if a given string is only spaces
|
||||
func IsSpaces(str []byte) bool {
|
||||
for _, c := range str {
|
||||
if c != ' ' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// IsSpacesOrTabs checks if a given string contains only spaces and tabs
|
||||
func IsSpacesOrTabs(str string) bool {
|
||||
for _, c := range str {
|
||||
if c != ' ' && c != '\t' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// ParseBool is almost exactly like strconv.ParseBool, except it also accepts 'on' and 'off'
|
||||
// as 'true' and 'false' respectively
|
||||
func ParseBool(str string) (bool, error) {
|
||||
if str == "on" {
|
||||
return true, nil
|
||||
}
|
||||
if str == "off" {
|
||||
return false, nil
|
||||
}
|
||||
return strconv.ParseBool(str)
|
||||
}
|
||||
|
||||
// EscapePath replaces every path separator in a given path with a %
|
||||
func EscapePath(path string) string {
|
||||
path = filepath.ToSlash(path)
|
||||
return strings.Replace(path, "/", "%", -1)
|
||||
}
|
||||
|
||||
// GetModTime returns the last modification time for a given file
|
||||
// It also returns a boolean if there was a problem accessing the file
|
||||
func GetModTime(path string) (time.Time, bool) {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return time.Now(), false
|
||||
}
|
||||
return info.ModTime(), true
|
||||
}
|
||||
|
||||
// StringWidth returns the width of a string where tabs count as `tabsize` width
|
||||
func StringWidth(str string, tabsize int) int {
|
||||
sw := runewidth.StringWidth(str)
|
||||
lineIdx := 0
|
||||
for _, ch := range str {
|
||||
switch ch {
|
||||
case '\t':
|
||||
ts := tabsize - (lineIdx % tabsize)
|
||||
sw += ts
|
||||
lineIdx += ts
|
||||
case '\n':
|
||||
lineIdx = 0
|
||||
default:
|
||||
lineIdx++
|
||||
}
|
||||
}
|
||||
return sw
|
||||
}
|
||||
|
||||
// WidthOfLargeRunes searches all the runes in a string and counts up all the widths of runes
|
||||
// that have a width larger than 1 (this also counts tabs as `tabsize` width)
|
||||
func WidthOfLargeRunes(str string, tabsize int) int {
|
||||
count := 0
|
||||
lineIdx := 0
|
||||
for _, ch := range str {
|
||||
var w int
|
||||
if ch == '\t' {
|
||||
w = tabsize - (lineIdx % tabsize)
|
||||
} else {
|
||||
w = runewidth.RuneWidth(ch)
|
||||
}
|
||||
if w > 1 {
|
||||
count += (w - 1)
|
||||
}
|
||||
if ch == '\n' {
|
||||
lineIdx = 0
|
||||
} else {
|
||||
lineIdx += w
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// RunePos returns the rune index of a given byte index
|
||||
// This could cause problems if the byte index is between code points
|
||||
func runePos(p int, str string) int {
|
||||
return utf8.RuneCountInString(str[:p])
|
||||
}
|
||||
|
||||
func lcs(a, b string) string {
|
||||
arunes := []rune(a)
|
||||
brunes := []rune(b)
|
||||
|
||||
lcs := ""
|
||||
for i, r := range arunes {
|
||||
if i >= len(brunes) {
|
||||
break
|
||||
}
|
||||
if r == brunes[i] {
|
||||
lcs += string(r)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return lcs
|
||||
}
|
||||
|
||||
// CommonSubstring gets a common substring among the inputs
|
||||
func CommonSubstring(arr ...string) string {
|
||||
commonStr := arr[0]
|
||||
|
||||
for _, str := range arr[1:] {
|
||||
commonStr = lcs(commonStr, str)
|
||||
}
|
||||
|
||||
return commonStr
|
||||
}
|
||||
|
||||
// Abs is a simple absolute value function for ints
|
||||
func Abs(n int) int {
|
||||
if n < 0 {
|
||||
return -n
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// FuncName returns the full name of a given function object
|
||||
func FuncName(i interface{}) string {
|
||||
return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
|
||||
}
|
||||
|
||||
// ShortFuncName returns the name only of a given function object
|
||||
func ShortFuncName(i interface{}) string {
|
||||
return strings.TrimPrefix(runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name(), "main.(*View).")
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if !strings.HasPrefix(path, "~") {
|
||||
return path
|
||||
}
|
||||
|
||||
var userData *user.User
|
||||
var err error
|
||||
|
||||
homeString := strings.Split(path, "/")[0]
|
||||
if homeString == "~" {
|
||||
userData, err = user.Current()
|
||||
if err != nil {
|
||||
messenger.Error("Could not find user: ", err)
|
||||
}
|
||||
} 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 ""
|
||||
}
|
||||
}
|
||||
|
||||
home := userData.HomeDir
|
||||
|
||||
return strings.Replace(path, homeString, home, 1)
|
||||
}
|
||||
|
||||
// GetPathAndCursorPosition returns a filename without everything following a `:`
|
||||
// This is used for opening files like util.go:10:5 to specify a line and column
|
||||
// Special cases like Windows Absolute path (C:\myfile.txt:10:5) are handled correctly.
|
||||
func GetPathAndCursorPosition(path string) (string, []string) {
|
||||
re := regexp.MustCompile(`([\s\S]+?)(?::(\d+))(?::(\d+))?`)
|
||||
match := re.FindStringSubmatch(path)
|
||||
// no lines/columns were specified in the path, return just the path with no cursor location
|
||||
if len(match) == 0 {
|
||||
return path, nil
|
||||
} else if match[len(match)-1] != "" {
|
||||
// if the last capture group match isn't empty then both line and column were provided
|
||||
return match[1], match[2:]
|
||||
}
|
||||
// if it was empty, then only a line was provided, so default to column 0
|
||||
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])
|
||||
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 startpos, err
|
||||
}
|
||||
|
|
@ -1,331 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNumOccurences(t *testing.T) {
|
||||
var tests = []struct {
|
||||
inputStr string
|
||||
inputChar byte
|
||||
want int
|
||||
}{
|
||||
{"aaaa", 'a', 4},
|
||||
{"\trfd\ta", '\t', 2},
|
||||
{"∆ƒ\tø ® \t\t", '\t', 3},
|
||||
}
|
||||
for _, test := range tests {
|
||||
if got := NumOccurrences(test.inputStr, test.inputChar); got != test.want {
|
||||
t.Errorf("NumOccurences(%s, %c) = %d", test.inputStr, test.inputChar, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpaces(t *testing.T) {
|
||||
var tests = []struct {
|
||||
input int
|
||||
want string
|
||||
}{
|
||||
{4, " "},
|
||||
{0, ""},
|
||||
}
|
||||
for _, test := range tests {
|
||||
if got := Spaces(test.input); got != test.want {
|
||||
t.Errorf("Spaces(%d) = \"%s\"", test.input, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsWordChar(t *testing.T) {
|
||||
if IsWordChar("t") == false {
|
||||
t.Errorf("IsWordChar(t) = false")
|
||||
}
|
||||
if IsWordChar("T") == false {
|
||||
t.Errorf("IsWordChar(T) = false")
|
||||
}
|
||||
if IsWordChar("5") == false {
|
||||
t.Errorf("IsWordChar(5) = false")
|
||||
}
|
||||
if IsWordChar("_") == false {
|
||||
t.Errorf("IsWordChar(_) = false")
|
||||
}
|
||||
if IsWordChar("ß") == false {
|
||||
t.Errorf("IsWordChar(ß) = false")
|
||||
}
|
||||
if IsWordChar("~") == true {
|
||||
t.Errorf("IsWordChar(~) = true")
|
||||
}
|
||||
if IsWordChar(" ") == true {
|
||||
t.Errorf("IsWordChar( ) = true")
|
||||
}
|
||||
if IsWordChar(")") == true {
|
||||
t.Errorf("IsWordChar()) = true")
|
||||
}
|
||||
if IsWordChar("\n") == true {
|
||||
t.Errorf("IsWordChar(\n)) = true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringWidth(t *testing.T) {
|
||||
tabsize := 4
|
||||
if w := StringWidth("1\t2", tabsize); w != 5 {
|
||||
t.Error("StringWidth 1 Failed. Got", w)
|
||||
}
|
||||
if w := StringWidth("\t", tabsize); w != 4 {
|
||||
t.Error("StringWidth 2 Failed. Got", w)
|
||||
}
|
||||
if w := StringWidth("1\t", tabsize); w != 4 {
|
||||
t.Error("StringWidth 3 Failed. Got", w)
|
||||
}
|
||||
if w := StringWidth("\t\t", tabsize); w != 8 {
|
||||
t.Error("StringWidth 4 Failed. Got", w)
|
||||
}
|
||||
if w := StringWidth("12\t2\t", tabsize); w != 8 {
|
||||
t.Error("StringWidth 5 Failed. Got", w)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWidthOfLargeRunes(t *testing.T) {
|
||||
tabsize := 4
|
||||
if w := WidthOfLargeRunes("1\t2", tabsize); w != 2 {
|
||||
t.Error("WidthOfLargeRunes 1 Failed. Got", w)
|
||||
}
|
||||
if w := WidthOfLargeRunes("\t", tabsize); w != 3 {
|
||||
t.Error("WidthOfLargeRunes 2 Failed. Got", w)
|
||||
}
|
||||
if w := WidthOfLargeRunes("1\t", tabsize); w != 2 {
|
||||
t.Error("WidthOfLargeRunes 3 Failed. Got", w)
|
||||
}
|
||||
if w := WidthOfLargeRunes("\t\t", tabsize); w != 6 {
|
||||
t.Error("WidthOfLargeRunes 4 Failed. Got", w)
|
||||
}
|
||||
if w := WidthOfLargeRunes("12\t2\t", tabsize); w != 3 {
|
||||
t.Error("WidthOfLargeRunes 5 Failed. Got", w)
|
||||
}
|
||||
}
|
||||
|
||||
func assertEqual(t *testing.T, expected interface{}, result interface{}) {
|
||||
if expected != result {
|
||||
t.Fatalf("Expected: %d != Got: %d", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func assertTrue(t *testing.T, condition bool) {
|
||||
if !condition {
|
||||
t.Fatalf("Condition was not true. Got false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPathRelativeWithDot(t *testing.T) {
|
||||
path, cursorPosition := GetPathAndCursorPosition("./myfile:10:5")
|
||||
|
||||
assertEqual(t, path, "./myfile")
|
||||
assertEqual(t, "10", cursorPosition[0])
|
||||
assertEqual(t, "5", cursorPosition[1])
|
||||
}
|
||||
func TestGetPathRelativeWithDotWindows(t *testing.T) {
|
||||
path, cursorPosition := GetPathAndCursorPosition(".\\myfile:10:5")
|
||||
|
||||
assertEqual(t, path, ".\\myfile")
|
||||
assertEqual(t, "10", cursorPosition[0])
|
||||
assertEqual(t, cursorPosition[1], "5")
|
||||
}
|
||||
func TestGetPathRelativeNoDot(t *testing.T) {
|
||||
path, cursorPosition := GetPathAndCursorPosition("myfile:10:5")
|
||||
|
||||
assertEqual(t, path, "myfile")
|
||||
assertEqual(t, "10", cursorPosition[0])
|
||||
|
||||
assertEqual(t, cursorPosition[1], "5")
|
||||
}
|
||||
func TestGetPathAbsoluteWindows(t *testing.T) {
|
||||
path, cursorPosition := GetPathAndCursorPosition("C:\\myfile:10:5")
|
||||
|
||||
assertEqual(t, path, "C:\\myfile")
|
||||
assertEqual(t, "10", cursorPosition[0])
|
||||
|
||||
assertEqual(t, cursorPosition[1], "5")
|
||||
|
||||
path, cursorPosition = GetPathAndCursorPosition("C:/myfile:10:5")
|
||||
|
||||
assertEqual(t, path, "C:/myfile")
|
||||
assertEqual(t, "10", cursorPosition[0])
|
||||
assertEqual(t, "5", cursorPosition[1])
|
||||
}
|
||||
|
||||
func TestGetPathAbsoluteUnix(t *testing.T) {
|
||||
path, cursorPosition := GetPathAndCursorPosition("/home/user/myfile:10:5")
|
||||
|
||||
assertEqual(t, path, "/home/user/myfile")
|
||||
assertEqual(t, "10", cursorPosition[0])
|
||||
assertEqual(t, "5", cursorPosition[1])
|
||||
}
|
||||
|
||||
func TestGetPathRelativeWithDotWithoutLineAndColumn(t *testing.T) {
|
||||
path, cursorPosition := GetPathAndCursorPosition("./myfile")
|
||||
|
||||
assertEqual(t, path, "./myfile")
|
||||
// no cursor position in filename, nil should be returned
|
||||
assertTrue(t, cursorPosition == nil)
|
||||
}
|
||||
func TestGetPathRelativeWithDotWindowsWithoutLineAndColumn(t *testing.T) {
|
||||
path, cursorPosition := GetPathAndCursorPosition(".\\myfile")
|
||||
|
||||
assertEqual(t, path, ".\\myfile")
|
||||
assertTrue(t, cursorPosition == nil)
|
||||
|
||||
}
|
||||
func TestGetPathRelativeNoDotWithoutLineAndColumn(t *testing.T) {
|
||||
path, cursorPosition := GetPathAndCursorPosition("myfile")
|
||||
|
||||
assertEqual(t, path, "myfile")
|
||||
assertTrue(t, cursorPosition == nil)
|
||||
|
||||
}
|
||||
func TestGetPathAbsoluteWindowsWithoutLineAndColumn(t *testing.T) {
|
||||
path, cursorPosition := GetPathAndCursorPosition("C:\\myfile")
|
||||
|
||||
assertEqual(t, path, "C:\\myfile")
|
||||
assertTrue(t, cursorPosition == nil)
|
||||
|
||||
path, cursorPosition = GetPathAndCursorPosition("C:/myfile")
|
||||
|
||||
assertEqual(t, path, "C:/myfile")
|
||||
assertTrue(t, cursorPosition == nil)
|
||||
|
||||
}
|
||||
func TestGetPathAbsoluteUnixWithoutLineAndColumn(t *testing.T) {
|
||||
path, cursorPosition := GetPathAndCursorPosition("/home/user/myfile")
|
||||
|
||||
assertEqual(t, path, "/home/user/myfile")
|
||||
assertTrue(t, cursorPosition == nil)
|
||||
|
||||
}
|
||||
func TestGetPathSingleLetterFileRelativePath(t *testing.T) {
|
||||
path, cursorPosition := GetPathAndCursorPosition("a:5:6")
|
||||
|
||||
assertEqual(t, path, "a")
|
||||
assertEqual(t, "5", cursorPosition[0])
|
||||
assertEqual(t, "6", cursorPosition[1])
|
||||
}
|
||||
func TestGetPathSingleLetterFileAbsolutePathWindows(t *testing.T) {
|
||||
path, cursorPosition := GetPathAndCursorPosition("C:\\a:5:6")
|
||||
|
||||
assertEqual(t, path, "C:\\a")
|
||||
assertEqual(t, "5", cursorPosition[0])
|
||||
assertEqual(t, "6", cursorPosition[1])
|
||||
|
||||
path, cursorPosition = GetPathAndCursorPosition("C:/a:5:6")
|
||||
|
||||
assertEqual(t, path, "C:/a")
|
||||
assertEqual(t, "5", cursorPosition[0])
|
||||
assertEqual(t, "6", cursorPosition[1])
|
||||
}
|
||||
func TestGetPathSingleLetterFileAbsolutePathUnix(t *testing.T) {
|
||||
path, cursorPosition := GetPathAndCursorPosition("/home/user/a:5:6")
|
||||
|
||||
assertEqual(t, path, "/home/user/a")
|
||||
assertEqual(t, "5", cursorPosition[0])
|
||||
assertEqual(t, "6", cursorPosition[1])
|
||||
}
|
||||
func TestGetPathSingleLetterFileAbsolutePathWindowsWithoutLineAndColumn(t *testing.T) {
|
||||
path, cursorPosition := GetPathAndCursorPosition("C:\\a")
|
||||
|
||||
assertEqual(t, path, "C:\\a")
|
||||
assertTrue(t, cursorPosition == nil)
|
||||
|
||||
path, cursorPosition = GetPathAndCursorPosition("C:/a")
|
||||
|
||||
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)
|
||||
}
|
||||
1
cmd/micro/vendor/github.com/blang/semver
generated
vendored
1
cmd/micro/vendor/github.com/blang/semver
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 4a1e882c79dcf4ec00d2e29fac74b9c8938d5052
|
||||
1
cmd/micro/vendor/github.com/dustin/go-humanize
generated
vendored
1
cmd/micro/vendor/github.com/dustin/go-humanize
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 259d2a102b871d17f30e3cd9881a642961a1e486
|
||||
1
cmd/micro/vendor/github.com/flynn/json5
generated
vendored
1
cmd/micro/vendor/github.com/flynn/json5
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 7620272ed63390e979cf5882d2fa0506fe2a8db5
|
||||
1
cmd/micro/vendor/github.com/gdamore/encoding
generated
vendored
1
cmd/micro/vendor/github.com/gdamore/encoding
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
Subproject commit b23993cbb6353f0e6aa98d0ee318a34728f628b9
|
||||
1
cmd/micro/vendor/github.com/go-errors/errors
generated
vendored
1
cmd/micro/vendor/github.com/go-errors/errors
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 8fa88b06e5974e97fbf9899a7f86a344bfd1f105
|
||||
1
cmd/micro/vendor/github.com/jtolds/gls
generated
vendored
1
cmd/micro/vendor/github.com/jtolds/gls
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 77f18212c9c7edc9bd6a33d383a7b545ce62f064
|
||||
1
cmd/micro/vendor/github.com/lucasb-eyer/go-colorful
generated
vendored
1
cmd/micro/vendor/github.com/lucasb-eyer/go-colorful
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
Subproject commit c900de9dbbc73129068f5af6a823068fc5f2308c
|
||||
1
cmd/micro/vendor/github.com/mattn/go-isatty
generated
vendored
1
cmd/micro/vendor/github.com/mattn/go-isatty
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
Subproject commit fc9e8d8ef48496124e79ae0df75490096eccf6fe
|
||||
1
cmd/micro/vendor/github.com/mattn/go-runewidth
generated
vendored
1
cmd/micro/vendor/github.com/mattn/go-runewidth
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 97311d9f7767e3d6f422ea06661bc2c7a19e8a5d
|
||||
1
cmd/micro/vendor/github.com/mitchellh/go-homedir
generated
vendored
1
cmd/micro/vendor/github.com/mitchellh/go-homedir
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
Subproject commit b8bc1bf767474819792c23f32d8286a45736f1c6
|
||||
1
cmd/micro/vendor/github.com/sergi/go-diff
generated
vendored
1
cmd/micro/vendor/github.com/sergi/go-diff
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
Subproject commit feef008d51ad2b3778f85d387ccf91735543008d
|
||||
1
cmd/micro/vendor/github.com/smartystreets/assertions
generated
vendored
1
cmd/micro/vendor/github.com/smartystreets/assertions
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 0b37b35ec7434b77e77a4bb29b79677cced992ea
|
||||
1
cmd/micro/vendor/github.com/smartystreets/goconvey
generated
vendored
1
cmd/micro/vendor/github.com/smartystreets/goconvey
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
Subproject commit e5b2b7c9111590d019a696c7800593f666e1a7f4
|
||||
1
cmd/micro/vendor/github.com/yuin/gopher-lua
generated
vendored
1
cmd/micro/vendor/github.com/yuin/gopher-lua
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
Subproject commit b402f3114ec730d8bddb074a6c137309f561aa78
|
||||
1
cmd/micro/vendor/github.com/zyedidia/clipboard
generated
vendored
1
cmd/micro/vendor/github.com/zyedidia/clipboard
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 4611e809d8b1a3051c11d11f4b610c44df73fa38
|
||||
1
cmd/micro/vendor/github.com/zyedidia/glob
generated
vendored
1
cmd/micro/vendor/github.com/zyedidia/glob
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
Subproject commit dd4023a66dc351ae26e592d21cd133b5b143f3d8
|
||||
1
cmd/micro/vendor/github.com/zyedidia/poller
generated
vendored
1
cmd/micro/vendor/github.com/zyedidia/poller
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
Subproject commit ab09682913b79f402713d1df1977dedc19eb25ac
|
||||
1
cmd/micro/vendor/github.com/zyedidia/pty
generated
vendored
1
cmd/micro/vendor/github.com/zyedidia/pty
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 30364665a2445782b12bcb2db75f16ce684da907
|
||||
1
cmd/micro/vendor/github.com/zyedidia/tcell
generated
vendored
1
cmd/micro/vendor/github.com/zyedidia/tcell
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 208b6e8f2f8d763a4accac33f3bdbd7dc9c71c01
|
||||
1
cmd/micro/vendor/github.com/zyedidia/terminal
generated
vendored
1
cmd/micro/vendor/github.com/zyedidia/terminal
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 1760577dbc0058809dd115a6bfe5999b6b24ab53
|
||||
1
cmd/micro/vendor/golang.org/x/net
generated
vendored
1
cmd/micro/vendor/golang.org/x/net
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 1a68b1313cf4ad7778376e82641197b60c02f65c
|
||||
1
cmd/micro/vendor/golang.org/x/text
generated
vendored
1
cmd/micro/vendor/golang.org/x/text
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 210eee5cf7323015d097341bcf7166130d001cd8
|
||||
1
cmd/micro/vendor/gopkg.in/yaml.v2
generated
vendored
1
cmd/micro/vendor/gopkg.in/yaml.v2
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
Subproject commit cd8b52f8269e0feb286dfeef29f8fe4d5b397e0b
|
||||
1
cmd/micro/vendor/layeh.com/gopher-luar
generated
vendored
1
cmd/micro/vendor/layeh.com/gopher-luar
generated
vendored
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 16281577dff29557258daa8ca9bc9b2e05f451f2
|
||||
1117
cmd/micro/view.go
1117
cmd/micro/view.go
File diff suppressed because it is too large
Load diff
24
go.mod
Normal file
24
go.mod
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
module github.com/zyedidia/micro
|
||||
|
||||
require (
|
||||
github.com/blang/semver v3.5.1+incompatible
|
||||
github.com/dustin/go-humanize v1.0.0
|
||||
github.com/flynn/json5 v0.0.0-20160717195620-7620272ed633
|
||||
github.com/gdamore/encoding v1.0.0 // indirect
|
||||
github.com/go-errors/errors v1.0.1
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3 // indirect
|
||||
github.com/mattn/go-isatty v0.0.11
|
||||
github.com/mattn/go-runewidth v0.0.7
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/sergi/go-diff v1.1.0
|
||||
github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb
|
||||
github.com/zyedidia/clipboard v0.0.0-20190823154308-241f98e9b197
|
||||
github.com/zyedidia/glob v0.0.0-20170209203856-dd4023a66dc3
|
||||
github.com/zyedidia/poller v0.0.0-20170616160828-ab09682913b7 // indirect
|
||||
github.com/zyedidia/pty v2.0.0+incompatible // indirect
|
||||
github.com/zyedidia/tcell v0.0.0-20191228235154-5b9bbc0d56c7
|
||||
github.com/zyedidia/terminal v0.0.0-20180726154117-533c623e2415
|
||||
golang.org/x/text v0.3.2
|
||||
gopkg.in/yaml.v2 v2.2.7
|
||||
layeh.com/gopher-luar v1.0.7
|
||||
)
|
||||
69
go.sum
Normal file
69
go.sum
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
|
||||
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/flynn/json5 v0.0.0-20160717195620-7620272ed633 h1:xJMmr4GMYIbALX5edyoDIOQpc2bOQTeJiWMeCl9lX/8=
|
||||
github.com/flynn/json5 v0.0.0-20160717195620-7620272ed633/go.mod h1:NJDK3/o7abx6PP54EOe0G0n0RLmhCo9xv61gUYpI0EY=
|
||||
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
|
||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||
github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54=
|
||||
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
|
||||
github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb h1:ZkM6LRnq40pR1Ox0hTHlnpkcOTuFIDQpZ1IN8rKKhX0=
|
||||
github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
|
||||
github.com/zyedidia/clipboard v0.0.0-20190823154308-241f98e9b197 h1:gYTNnAW6azuB3BbA6QYWO/H4F2ABSOjjw3Z03tlXd2c=
|
||||
github.com/zyedidia/clipboard v0.0.0-20190823154308-241f98e9b197/go.mod h1:WDk3p8GiZV9+xFWlSo8qreeoLhW6Ik692rqXk+cNeRY=
|
||||
github.com/zyedidia/glob v0.0.0-20170209203856-dd4023a66dc3 h1:oMHjjTLfGXVuyOQBYj5/td9WC0mw4g1xDBPovIqmHew=
|
||||
github.com/zyedidia/glob v0.0.0-20170209203856-dd4023a66dc3/go.mod h1:YKbIYP//Eln8eDgAJGI3IDvR3s4Tv9Z9TGIOumiyQ5c=
|
||||
github.com/zyedidia/poller v0.0.0-20170616160828-ab09682913b7 h1:G2kKl91VH6VynNSOqOPL9E58iFHWIamXp+feOi7KOyo=
|
||||
github.com/zyedidia/poller v0.0.0-20170616160828-ab09682913b7/go.mod h1:vZXJOHGDcuK08GXhF6IAY0ZFd2WcgOR5DOTp84Uk5eE=
|
||||
github.com/zyedidia/poller v2.0.0+incompatible h1:DMOvB0EXz2JTokqOKfxPWN/8xXFJbO+m4vNhMkOY7Lo=
|
||||
github.com/zyedidia/poller v2.0.0+incompatible/go.mod h1:vZXJOHGDcuK08GXhF6IAY0ZFd2WcgOR5DOTp84Uk5eE=
|
||||
github.com/zyedidia/pty v1.1.1 h1:SGOF3egDZGCGbpB5DPlg+yabbAuVkUtXVeNTDFeEytM=
|
||||
github.com/zyedidia/pty v1.1.1/go.mod h1:4y9l9yJZNxRa7GB/fB+mmDmGkG3CqmzLf4vUxGGotEA=
|
||||
github.com/zyedidia/pty v2.0.0+incompatible h1:Ou5vXL6tvjst+RV8sUFISbuKDnUJPhnpygApMFGweqw=
|
||||
github.com/zyedidia/pty v2.0.0+incompatible/go.mod h1:4y9l9yJZNxRa7GB/fB+mmDmGkG3CqmzLf4vUxGGotEA=
|
||||
github.com/zyedidia/tcell v0.0.0-20191219170756-59b50b23fa9b h1:cryFENlMxJJrkimVx/CUMFDCxC4vpmey2x3A3tAgTNM=
|
||||
github.com/zyedidia/tcell v0.0.0-20191219170756-59b50b23fa9b/go.mod h1:yXgdp23+aW8OMENYVBvpKoeiBtjaVWJ9HhpPDu6LBfM=
|
||||
github.com/zyedidia/tcell v0.0.0-20191227234059-2c574ec1b972 h1:tqsPH3tvIF9LQMWQODln09mJEV1ZlwC1qc5dTplj+eI=
|
||||
github.com/zyedidia/tcell v0.0.0-20191227234059-2c574ec1b972/go.mod h1:yXgdp23+aW8OMENYVBvpKoeiBtjaVWJ9HhpPDu6LBfM=
|
||||
github.com/zyedidia/tcell v0.0.0-20191228235154-5b9bbc0d56c7 h1:Qw5255OIirY741L/Kt5KLitfzfhpk29x+A193zByTS0=
|
||||
github.com/zyedidia/tcell v0.0.0-20191228235154-5b9bbc0d56c7/go.mod h1:yXgdp23+aW8OMENYVBvpKoeiBtjaVWJ9HhpPDu6LBfM=
|
||||
github.com/zyedidia/terminal v0.0.0-20180726154117-533c623e2415 h1:752dTQ5OatJ9M5ULK2+9lor+nzyZz+LYDo3WGngg3Rc=
|
||||
github.com/zyedidia/terminal v0.0.0-20180726154117-533c623e2415/go.mod h1:8leT8G0Cm8NoJHdrrKHyR9MirWoF4YW7pZh06B6H+1E=
|
||||
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
|
||||
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
layeh.com/gopher-luar v1.0.7 h1:53iv6CCkRs5wyofZ+qVXcyAYQOIG52s6pt4xkqZdq7k=
|
||||
layeh.com/gopher-luar v1.0.7/go.mod h1:TPnIVCZ2RJBndm7ohXyaqfhzjlZ+OA2SZR/YwL8tECk=
|
||||
1544
internal/action/actions.go
Normal file
1544
internal/action/actions.go
Normal file
File diff suppressed because it is too large
Load diff
8
internal/action/actions_other.go
Normal file
8
internal/action/actions_other.go
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
// +build plan9 nacl windows
|
||||
|
||||
package action
|
||||
|
||||
func (*BufPane) Suspend() bool {
|
||||
InfoBar.Error("Suspend is only supported on BSD/Linux")
|
||||
return false
|
||||
}
|
||||
|
|
@ -1,37 +1,27 @@
|
|||
// +build linux darwin dragonfly solaris openbsd netbsd freebsd
|
||||
|
||||
package main
|
||||
package action
|
||||
|
||||
import "syscall"
|
||||
import (
|
||||
"syscall"
|
||||
|
||||
"github.com/zyedidia/micro/internal/screen"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
func (*BufPane) Suspend() bool {
|
||||
screenb := screen.TempFini()
|
||||
|
||||
// suspend the process
|
||||
pid := syscall.Getpid()
|
||||
err := syscall.Kill(pid, syscall.SIGSTOP)
|
||||
if err != nil {
|
||||
TermMessage(err)
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
|
||||
if !screenWasNil {
|
||||
InitScreen()
|
||||
}
|
||||
screen.TempStart(screenb)
|
||||
|
||||
if usePlugin {
|
||||
return PostActionCall("Suspend", v)
|
||||
}
|
||||
return true
|
||||
return false
|
||||
}
|
||||
493
internal/action/bindings.go
Normal file
493
internal/action/bindings.go
Normal file
|
|
@ -0,0 +1,493 @@
|
|||
package action
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/flynn/json5"
|
||||
"github.com/zyedidia/micro/internal/config"
|
||||
"github.com/zyedidia/micro/internal/screen"
|
||||
"github.com/zyedidia/tcell"
|
||||
)
|
||||
|
||||
func InitBindings() {
|
||||
config.Bindings = DefaultBindings()
|
||||
|
||||
var parsed map[string]string
|
||||
defaults := DefaultBindings()
|
||||
|
||||
filename := config.ConfigDir + "/bindings.json"
|
||||
if _, e := os.Stat(filename); e == nil {
|
||||
input, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
screen.TermMessage("Error reading bindings.json file: " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
err = json5.Unmarshal(input, &parsed)
|
||||
if err != nil {
|
||||
screen.TermMessage("Error reading bindings.json:", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
for k, v := range defaults {
|
||||
BindKey(k, v)
|
||||
}
|
||||
for k, v := range parsed {
|
||||
BindKey(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
func BindKey(k, v string) {
|
||||
event, ok := findEvent(k)
|
||||
if !ok {
|
||||
screen.TermMessage(k, "is not a bindable event")
|
||||
}
|
||||
|
||||
switch e := event.(type) {
|
||||
case KeyEvent:
|
||||
BufMapKey(e, v)
|
||||
case MouseEvent:
|
||||
BufMapMouse(e, v)
|
||||
case RawEvent:
|
||||
BufMapKey(e, v)
|
||||
}
|
||||
|
||||
config.Bindings[k] = v
|
||||
}
|
||||
|
||||
// findEvent will find binding Key 'b' using string 'k'
|
||||
func findEvent(k string) (b Event, 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 RawEvent{
|
||||
esc: k,
|
||||
}, true
|
||||
default:
|
||||
break modSearch
|
||||
}
|
||||
}
|
||||
|
||||
if len(k) == 0 {
|
||||
return KeyEvent{}, false
|
||||
}
|
||||
|
||||
// Control is handled in a special way, since the terminal sends explicitly
|
||||
// marked escape sequences for control keys
|
||||
// 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 := keyEvents["Ctrl"+k]; ok {
|
||||
var r tcell.Key
|
||||
// Special case for escape, for some reason tcell doesn't send it with the esc character
|
||||
if code < 256 && code != 27 {
|
||||
r = code
|
||||
}
|
||||
// It is, we're done.
|
||||
return KeyEvent{
|
||||
code: code,
|
||||
mod: modifiers,
|
||||
r: rune(r),
|
||||
}, true
|
||||
}
|
||||
}
|
||||
|
||||
// See if we can find the key in bindingKeys
|
||||
if code, ok := keyEvents[k]; ok {
|
||||
var r tcell.Key
|
||||
// Special case for escape, for some reason tcell doesn't send it with the esc character
|
||||
if code < 256 && code != 27 {
|
||||
r = code
|
||||
}
|
||||
return KeyEvent{
|
||||
code: code,
|
||||
mod: modifiers,
|
||||
r: rune(r),
|
||||
}, true
|
||||
}
|
||||
|
||||
// See if we can find the key in bindingMouse
|
||||
if code, ok := mouseEvents[k]; ok {
|
||||
return MouseEvent{
|
||||
btn: code,
|
||||
mod: modifiers,
|
||||
}, true
|
||||
}
|
||||
|
||||
// If we were given one character, then we've got a rune.
|
||||
if len(k) == 1 {
|
||||
return KeyEvent{
|
||||
code: tcell.KeyRune,
|
||||
mod: modifiers,
|
||||
r: rune(k[0]),
|
||||
}, true
|
||||
}
|
||||
|
||||
// We don't know what happened.
|
||||
return KeyEvent{}, false
|
||||
}
|
||||
|
||||
// TryBindKey tries to bind a key by writing to config.ConfigDir/bindings.json
|
||||
// Returns true if the keybinding already existed and a possible error
|
||||
func TryBindKey(k, v string, overwrite bool) (bool, error) {
|
||||
var e error
|
||||
var parsed map[string]string
|
||||
|
||||
filename := config.ConfigDir + "/bindings.json"
|
||||
if _, e = os.Stat(filename); e == nil {
|
||||
input, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return false, errors.New("Error reading bindings.json file: " + err.Error())
|
||||
}
|
||||
|
||||
err = json5.Unmarshal(input, &parsed)
|
||||
if err != nil {
|
||||
return false, errors.New("Error reading bindings.json: " + err.Error())
|
||||
}
|
||||
|
||||
key, ok := findEvent(k)
|
||||
if !ok {
|
||||
return false, errors.New("Invalid event " + k)
|
||||
}
|
||||
|
||||
found := false
|
||||
for ev := range parsed {
|
||||
if e, ok := findEvent(ev); ok {
|
||||
if e == key {
|
||||
if overwrite {
|
||||
parsed[ev] = v
|
||||
}
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if found && !overwrite {
|
||||
return true, nil
|
||||
} else if !found {
|
||||
parsed[k] = v
|
||||
}
|
||||
|
||||
BindKey(k, v)
|
||||
|
||||
txt, _ := json.MarshalIndent(parsed, "", " ")
|
||||
return true, ioutil.WriteFile(filename, append(txt, '\n'), 0644)
|
||||
}
|
||||
return false, e
|
||||
}
|
||||
|
||||
// UnbindKey removes the binding for a key from the bindings.json file
|
||||
func UnbindKey(k string) error {
|
||||
var e error
|
||||
var parsed map[string]string
|
||||
|
||||
filename := config.ConfigDir + "/bindings.json"
|
||||
if _, e = os.Stat(filename); e == nil {
|
||||
input, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return errors.New("Error reading bindings.json file: " + err.Error())
|
||||
}
|
||||
|
||||
err = json5.Unmarshal(input, &parsed)
|
||||
if err != nil {
|
||||
return errors.New("Error reading bindings.json: " + err.Error())
|
||||
}
|
||||
|
||||
key, ok := findEvent(k)
|
||||
if !ok {
|
||||
return errors.New("Invalid event " + k)
|
||||
}
|
||||
|
||||
for ev := range parsed {
|
||||
if e, ok := findEvent(ev); ok {
|
||||
if e == key {
|
||||
delete(parsed, ev)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defaults := DefaultBindings()
|
||||
if a, ok := defaults[k]; ok {
|
||||
BindKey(k, a)
|
||||
} else if _, ok := config.Bindings[k]; ok {
|
||||
delete(config.Bindings, k)
|
||||
}
|
||||
|
||||
txt, _ := json.MarshalIndent(parsed, "", " ")
|
||||
return ioutil.WriteFile(filename, append(txt, '\n'), 0644)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
var mouseEvents = 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 keyEvents = 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,
|
||||
}
|
||||
|
||||
// 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": "Autocomplete|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": "command-edit:goto ",
|
||||
"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",
|
||||
}
|
||||
}
|
||||
636
internal/action/bufpane.go
Normal file
636
internal/action/bufpane.go
Normal file
|
|
@ -0,0 +1,636 @@
|
|||
package action
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
luar "layeh.com/gopher-luar"
|
||||
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
"github.com/zyedidia/micro/internal/buffer"
|
||||
"github.com/zyedidia/micro/internal/config"
|
||||
"github.com/zyedidia/micro/internal/display"
|
||||
ulua "github.com/zyedidia/micro/internal/lua"
|
||||
"github.com/zyedidia/micro/internal/screen"
|
||||
"github.com/zyedidia/tcell"
|
||||
)
|
||||
|
||||
type BufKeyAction func(*BufPane) bool
|
||||
type BufMouseAction func(*BufPane, *tcell.EventMouse) bool
|
||||
|
||||
var BufKeyBindings map[Event]BufKeyAction
|
||||
var BufKeyStrings map[Event]string
|
||||
var BufMouseBindings map[MouseEvent]BufMouseAction
|
||||
|
||||
func init() {
|
||||
BufKeyBindings = make(map[Event]BufKeyAction)
|
||||
BufKeyStrings = make(map[Event]string)
|
||||
BufMouseBindings = make(map[MouseEvent]BufMouseAction)
|
||||
}
|
||||
|
||||
func LuaAction(fn string) func(*BufPane) bool {
|
||||
luaFn := strings.Split(fn, ".")
|
||||
if len(luaFn) <= 1 {
|
||||
return nil
|
||||
}
|
||||
plName, plFn := luaFn[0], luaFn[1]
|
||||
pl := config.FindPlugin(plName)
|
||||
if pl == nil {
|
||||
return nil
|
||||
}
|
||||
return func(h *BufPane) bool {
|
||||
val, err := pl.Call(plFn, luar.New(ulua.L, h))
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
if v, ok := val.(lua.LBool); !ok {
|
||||
return false
|
||||
} else {
|
||||
return bool(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BufMapKey maps a key event to an action
|
||||
func BufMapKey(k Event, action string) {
|
||||
BufKeyStrings[k] = action
|
||||
var actionfns []func(*BufPane) bool
|
||||
var names []string
|
||||
var types []byte
|
||||
for i := 0; ; i++ {
|
||||
if action == "" {
|
||||
break
|
||||
}
|
||||
|
||||
// TODO: fix problem when complex bindings have these
|
||||
// characters (escape them?)
|
||||
idx := strings.IndexAny(action, "&|,")
|
||||
a := action
|
||||
if idx >= 0 {
|
||||
a = action[:idx]
|
||||
types = append(types, action[idx])
|
||||
action = action[idx+1:]
|
||||
} else {
|
||||
types = append(types, ' ')
|
||||
action = ""
|
||||
}
|
||||
|
||||
var afn func(*BufPane) bool
|
||||
if strings.HasPrefix(a, "command:") {
|
||||
a = strings.SplitN(a, ":", 2)[1]
|
||||
afn = CommandAction(a)
|
||||
names = append(names, "")
|
||||
} else if strings.HasPrefix(a, "command-edit:") {
|
||||
a = strings.SplitN(a, ":", 2)[1]
|
||||
afn = CommandEditAction(a)
|
||||
names = append(names, "")
|
||||
} else if strings.HasPrefix(a, "lua:") {
|
||||
a = strings.SplitN(a, ":", 2)[1]
|
||||
afn = LuaAction(a)
|
||||
if afn == nil {
|
||||
screen.TermMessage("Lua Error:", a, "does not exist")
|
||||
continue
|
||||
}
|
||||
names = append(names, "")
|
||||
} else if f, ok := BufKeyActions[a]; ok {
|
||||
afn = f
|
||||
names = append(names, a)
|
||||
} else {
|
||||
screen.TermMessage("Error:", a, "does not exist")
|
||||
continue
|
||||
}
|
||||
actionfns = append(actionfns, afn)
|
||||
}
|
||||
BufKeyBindings[k] = func(h *BufPane) bool {
|
||||
cursors := h.Buf.GetCursors()
|
||||
success := true
|
||||
for i, a := range actionfns {
|
||||
for j, c := range cursors {
|
||||
h.Buf.SetCurCursor(c.Num)
|
||||
h.Cursor = c
|
||||
if i == 0 || (success && types[i-1] == '&') || (!success && types[i-1] == '|') || (types[i-1] == ',') {
|
||||
success = h.execAction(a, names[i], j)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// BufMapMouse maps a mouse event to an action
|
||||
func BufMapMouse(k MouseEvent, action string) {
|
||||
if f, ok := BufMouseActions[action]; ok {
|
||||
BufMouseBindings[k] = f
|
||||
} else {
|
||||
delete(BufMouseBindings, k)
|
||||
BufMapKey(k, action)
|
||||
}
|
||||
}
|
||||
|
||||
// The BufPane 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 BufPane struct {
|
||||
display.BWindow
|
||||
|
||||
Buf *buffer.Buffer
|
||||
|
||||
Cursor *buffer.Cursor // the active cursor
|
||||
|
||||
// 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 buffer.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
|
||||
|
||||
// Last search stores the last successful search for FindNext and FindPrev
|
||||
lastSearch string
|
||||
// Should the current multiple cursor selection search based on word or
|
||||
// based on selection (false for selection, true for word)
|
||||
multiWord bool
|
||||
|
||||
splitID uint64
|
||||
|
||||
// remember original location of a search in case the search is canceled
|
||||
searchOrig buffer.Loc
|
||||
}
|
||||
|
||||
func NewBufPane(buf *buffer.Buffer, win display.BWindow) *BufPane {
|
||||
h := new(BufPane)
|
||||
h.Buf = buf
|
||||
h.BWindow = win
|
||||
|
||||
h.Cursor = h.Buf.GetActiveCursor()
|
||||
h.mouseReleased = true
|
||||
|
||||
config.RunPluginFn("onBufPaneOpen", luar.New(ulua.L, h))
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func NewBufPaneFromBuf(buf *buffer.Buffer) *BufPane {
|
||||
w := display.NewBufWindow(0, 0, 0, 0, buf)
|
||||
return NewBufPane(buf, w)
|
||||
}
|
||||
|
||||
// PluginCB calls all plugin callbacks with a certain name and
|
||||
// displays an error if there is one and returns the aggregrate
|
||||
// boolean response
|
||||
func (h *BufPane) PluginCB(cb string) bool {
|
||||
b, err := config.RunPluginFnBool(cb, luar.New(ulua.L, h))
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// PluginCBRune is the same as PluginCB but also passes a rune to
|
||||
// the plugins
|
||||
func (h *BufPane) PluginCBRune(cb string, r rune) bool {
|
||||
b, err := config.RunPluginFnBool(cb, luar.New(ulua.L, h), luar.New(ulua.L, string(r)))
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func (h *BufPane) OpenBuffer(b *buffer.Buffer) {
|
||||
h.Buf.Close()
|
||||
h.Buf = b
|
||||
h.BWindow.SetBuffer(b)
|
||||
h.Cursor = b.GetActiveCursor()
|
||||
h.Resize(h.GetView().Width, h.GetView().Height)
|
||||
v := new(display.View)
|
||||
h.SetView(v)
|
||||
h.Relocate()
|
||||
// Set mouseReleased to true because we assume the mouse is not being pressed when
|
||||
// the editor is opened
|
||||
h.mouseReleased = true
|
||||
// Set isOverwriteMode to false, because we assume we are in the default mode when editor
|
||||
// is opened
|
||||
h.isOverwriteMode = false
|
||||
h.lastClickTime = time.Time{}
|
||||
}
|
||||
|
||||
func (h *BufPane) ID() uint64 {
|
||||
return h.splitID
|
||||
}
|
||||
|
||||
func (h *BufPane) SetID(i uint64) {
|
||||
h.splitID = i
|
||||
}
|
||||
|
||||
func (h *BufPane) Name() string {
|
||||
return h.Buf.GetName()
|
||||
}
|
||||
|
||||
// HandleEvent executes the tcell event properly
|
||||
func (h *BufPane) HandleEvent(event tcell.Event) {
|
||||
if h.Buf.ExternallyModified() {
|
||||
InfoBar.YNPrompt("The file on disk has changed. Reload file? (y,n)", func(yes, canceled bool) {
|
||||
if !yes || canceled {
|
||||
h.Buf.UpdateModTime()
|
||||
} else {
|
||||
h.Buf.ReOpen()
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
switch e := event.(type) {
|
||||
case *tcell.EventRaw:
|
||||
re := RawEvent{
|
||||
esc: e.EscSeq(),
|
||||
}
|
||||
h.DoKeyEvent(re)
|
||||
case *tcell.EventPaste:
|
||||
h.paste(e.Text())
|
||||
h.Relocate()
|
||||
case *tcell.EventKey:
|
||||
ke := KeyEvent{
|
||||
code: e.Key(),
|
||||
mod: e.Modifiers(),
|
||||
r: e.Rune(),
|
||||
}
|
||||
|
||||
done := h.DoKeyEvent(ke)
|
||||
if !done && e.Key() == tcell.KeyRune {
|
||||
h.DoRuneInsert(e.Rune())
|
||||
}
|
||||
case *tcell.EventMouse:
|
||||
cancel := false
|
||||
switch e.Buttons() {
|
||||
case tcell.Button1:
|
||||
_, my := e.Position()
|
||||
if h.Buf.Settings["statusline"].(bool) && my >= h.GetView().Y+h.GetView().Height-1 {
|
||||
cancel = true
|
||||
}
|
||||
case tcell.ButtonNone:
|
||||
// Mouse event with no click
|
||||
if !h.mouseReleased {
|
||||
// Mouse was just released
|
||||
|
||||
// mx, my := e.Position()
|
||||
// mouseLoc := h.LocFromVisual(buffer.Loc{X: mx, Y: my})
|
||||
|
||||
// we could finish the selection based on the release location as described
|
||||
// below but when the mouse click is within the scroll margin this will
|
||||
// cause a scroll and selection even for a simple mouse click which is
|
||||
// not good
|
||||
// for terminals that don't support mouse motion events, selection via
|
||||
// the mouse won't work but this is ok
|
||||
|
||||
// Relocating here isn't really necessary because the cursor will
|
||||
// be in the right place from the last mouse event
|
||||
// However, if we are running in a terminal that doesn't support mouse motion
|
||||
// events, this still allows the user to make selections, except only after they
|
||||
// release the mouse
|
||||
|
||||
// if !h.doubleClick && !h.tripleClick {
|
||||
// h.Cursor.Loc = mouseLoc
|
||||
// h.Cursor.SetSelectionEnd(h.Cursor.Loc)
|
||||
// h.Cursor.CopySelection("primary")
|
||||
// }
|
||||
h.mouseReleased = true
|
||||
}
|
||||
}
|
||||
|
||||
if !cancel {
|
||||
me := MouseEvent{
|
||||
btn: e.Buttons(),
|
||||
mod: e.Modifiers(),
|
||||
}
|
||||
h.DoMouseEvent(me, e)
|
||||
}
|
||||
}
|
||||
h.Buf.MergeCursors()
|
||||
|
||||
if h.IsActive() {
|
||||
// Display any gutter messages for this line
|
||||
c := h.Buf.GetActiveCursor()
|
||||
none := true
|
||||
for _, m := range h.Buf.Messages {
|
||||
if c.Y == m.Start.Y || c.Y == m.End.Y {
|
||||
InfoBar.GutterMessage(m.Msg)
|
||||
none = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if none && InfoBar.HasGutter {
|
||||
InfoBar.ClearGutter()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DoKeyEvent executes a key event by finding the action it is bound
|
||||
// to and executing it (possibly multiple times for multiple cursors)
|
||||
func (h *BufPane) DoKeyEvent(e Event) bool {
|
||||
if action, ok := BufKeyBindings[e]; ok {
|
||||
return action(h)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (h *BufPane) execAction(action func(*BufPane) bool, name string, cursor int) bool {
|
||||
if name != "Autocomplete" {
|
||||
h.Buf.HasSuggestions = false
|
||||
}
|
||||
|
||||
_, isMulti := MultiActions[name]
|
||||
if (!isMulti && cursor == 0) || isMulti {
|
||||
if h.PluginCB("pre" + name) {
|
||||
success := action(h)
|
||||
success = success && h.PluginCB("on"+name)
|
||||
|
||||
if isMulti {
|
||||
if recording_macro {
|
||||
if name != "ToggleMacro" && name != "PlayMacro" {
|
||||
curmacro = append(curmacro, action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return success
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (h *BufPane) completeAction(action string) {
|
||||
h.PluginCB("on" + action)
|
||||
}
|
||||
|
||||
func (h *BufPane) HasKeyEvent(e Event) bool {
|
||||
_, ok := BufKeyBindings[e]
|
||||
return ok
|
||||
}
|
||||
|
||||
// DoMouseEvent executes a mouse event by finding the action it is bound
|
||||
// to and executing it
|
||||
func (h *BufPane) DoMouseEvent(e MouseEvent, te *tcell.EventMouse) bool {
|
||||
if action, ok := BufMouseBindings[e]; ok {
|
||||
if action(h, te) {
|
||||
h.Relocate()
|
||||
}
|
||||
return true
|
||||
} else if h.HasKeyEvent(e) {
|
||||
return h.DoKeyEvent(e)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// DoRuneInsert inserts a given rune into the current buffer
|
||||
// (possibly multiple times for multiple cursors)
|
||||
func (h *BufPane) DoRuneInsert(r rune) {
|
||||
cursors := h.Buf.GetCursors()
|
||||
for _, c := range cursors {
|
||||
// Insert a character
|
||||
h.Buf.SetCurCursor(c.Num)
|
||||
h.Cursor = c
|
||||
if !h.PluginCBRune("preRune", r) {
|
||||
continue
|
||||
}
|
||||
if c.HasSelection() {
|
||||
c.DeleteSelection()
|
||||
c.ResetSelection()
|
||||
}
|
||||
|
||||
if h.isOverwriteMode {
|
||||
next := c.Loc
|
||||
next.X++
|
||||
h.Buf.Replace(c.Loc, next, string(r))
|
||||
} else {
|
||||
h.Buf.Insert(c.Loc, string(r))
|
||||
}
|
||||
if recording_macro {
|
||||
curmacro = append(curmacro, r)
|
||||
}
|
||||
h.PluginCBRune("onRune", r)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *BufPane) VSplitBuf(buf *buffer.Buffer) *BufPane {
|
||||
e := NewBufPaneFromBuf(buf)
|
||||
e.splitID = MainTab().GetNode(h.splitID).VSplit(h.Buf.Settings["splitright"].(bool))
|
||||
MainTab().Panes = append(MainTab().Panes, e)
|
||||
MainTab().Resize()
|
||||
MainTab().SetActive(len(MainTab().Panes) - 1)
|
||||
return e
|
||||
}
|
||||
func (h *BufPane) HSplitBuf(buf *buffer.Buffer) *BufPane {
|
||||
e := NewBufPaneFromBuf(buf)
|
||||
e.splitID = MainTab().GetNode(h.splitID).HSplit(h.Buf.Settings["splitbottom"].(bool))
|
||||
MainTab().Panes = append(MainTab().Panes, e)
|
||||
MainTab().Resize()
|
||||
MainTab().SetActive(len(MainTab().Panes) - 1)
|
||||
return e
|
||||
}
|
||||
func (h *BufPane) Close() {
|
||||
h.Buf.Close()
|
||||
}
|
||||
|
||||
func (h *BufPane) SetActive(b bool) {
|
||||
h.BWindow.SetActive(b)
|
||||
if b {
|
||||
// Display any gutter messages for this line
|
||||
c := h.Buf.GetActiveCursor()
|
||||
none := true
|
||||
for _, m := range h.Buf.Messages {
|
||||
if c.Y == m.Start.Y || c.Y == m.End.Y {
|
||||
InfoBar.GutterMessage(m.Msg)
|
||||
none = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if none && InfoBar.HasGutter {
|
||||
InfoBar.ClearGutter()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// BufKeyActions contains the list of all possible key actions the bufhandler could execute
|
||||
var BufKeyActions = map[string]BufKeyAction{
|
||||
"CursorUp": (*BufPane).CursorUp,
|
||||
"CursorDown": (*BufPane).CursorDown,
|
||||
"CursorPageUp": (*BufPane).CursorPageUp,
|
||||
"CursorPageDown": (*BufPane).CursorPageDown,
|
||||
"CursorLeft": (*BufPane).CursorLeft,
|
||||
"CursorRight": (*BufPane).CursorRight,
|
||||
"CursorStart": (*BufPane).CursorStart,
|
||||
"CursorEnd": (*BufPane).CursorEnd,
|
||||
"SelectToStart": (*BufPane).SelectToStart,
|
||||
"SelectToEnd": (*BufPane).SelectToEnd,
|
||||
"SelectUp": (*BufPane).SelectUp,
|
||||
"SelectDown": (*BufPane).SelectDown,
|
||||
"SelectLeft": (*BufPane).SelectLeft,
|
||||
"SelectRight": (*BufPane).SelectRight,
|
||||
"WordRight": (*BufPane).WordRight,
|
||||
"WordLeft": (*BufPane).WordLeft,
|
||||
"SelectWordRight": (*BufPane).SelectWordRight,
|
||||
"SelectWordLeft": (*BufPane).SelectWordLeft,
|
||||
"DeleteWordRight": (*BufPane).DeleteWordRight,
|
||||
"DeleteWordLeft": (*BufPane).DeleteWordLeft,
|
||||
"SelectLine": (*BufPane).SelectLine,
|
||||
"SelectToStartOfLine": (*BufPane).SelectToStartOfLine,
|
||||
"SelectToEndOfLine": (*BufPane).SelectToEndOfLine,
|
||||
"ParagraphPrevious": (*BufPane).ParagraphPrevious,
|
||||
"ParagraphNext": (*BufPane).ParagraphNext,
|
||||
"InsertNewline": (*BufPane).InsertNewline,
|
||||
"Backspace": (*BufPane).Backspace,
|
||||
"Delete": (*BufPane).Delete,
|
||||
"InsertTab": (*BufPane).InsertTab,
|
||||
"Save": (*BufPane).Save,
|
||||
"SaveAll": (*BufPane).SaveAll,
|
||||
"SaveAs": (*BufPane).SaveAs,
|
||||
"Find": (*BufPane).Find,
|
||||
"FindNext": (*BufPane).FindNext,
|
||||
"FindPrevious": (*BufPane).FindPrevious,
|
||||
"Center": (*BufPane).Center,
|
||||
"Undo": (*BufPane).Undo,
|
||||
"Redo": (*BufPane).Redo,
|
||||
"Copy": (*BufPane).Copy,
|
||||
"Cut": (*BufPane).Cut,
|
||||
"CutLine": (*BufPane).CutLine,
|
||||
"DuplicateLine": (*BufPane).DuplicateLine,
|
||||
"DeleteLine": (*BufPane).DeleteLine,
|
||||
"MoveLinesUp": (*BufPane).MoveLinesUp,
|
||||
"MoveLinesDown": (*BufPane).MoveLinesDown,
|
||||
"IndentSelection": (*BufPane).IndentSelection,
|
||||
"OutdentSelection": (*BufPane).OutdentSelection,
|
||||
"Autocomplete": (*BufPane).Autocomplete,
|
||||
"OutdentLine": (*BufPane).OutdentLine,
|
||||
"Paste": (*BufPane).Paste,
|
||||
"PastePrimary": (*BufPane).PastePrimary,
|
||||
"SelectAll": (*BufPane).SelectAll,
|
||||
"OpenFile": (*BufPane).OpenFile,
|
||||
"Start": (*BufPane).Start,
|
||||
"End": (*BufPane).End,
|
||||
"PageUp": (*BufPane).PageUp,
|
||||
"PageDown": (*BufPane).PageDown,
|
||||
"SelectPageUp": (*BufPane).SelectPageUp,
|
||||
"SelectPageDown": (*BufPane).SelectPageDown,
|
||||
"HalfPageUp": (*BufPane).HalfPageUp,
|
||||
"HalfPageDown": (*BufPane).HalfPageDown,
|
||||
"StartOfLine": (*BufPane).StartOfLine,
|
||||
"EndOfLine": (*BufPane).EndOfLine,
|
||||
"ToggleHelp": (*BufPane).ToggleHelp,
|
||||
"ToggleKeyMenu": (*BufPane).ToggleKeyMenu,
|
||||
"ToggleRuler": (*BufPane).ToggleRuler,
|
||||
"ClearStatus": (*BufPane).ClearStatus,
|
||||
"ShellMode": (*BufPane).ShellMode,
|
||||
"CommandMode": (*BufPane).CommandMode,
|
||||
"ToggleOverwriteMode": (*BufPane).ToggleOverwriteMode,
|
||||
"Escape": (*BufPane).Escape,
|
||||
"Quit": (*BufPane).Quit,
|
||||
"QuitAll": (*BufPane).QuitAll,
|
||||
"AddTab": (*BufPane).AddTab,
|
||||
"PreviousTab": (*BufPane).PreviousTab,
|
||||
"NextTab": (*BufPane).NextTab,
|
||||
"NextSplit": (*BufPane).NextSplit,
|
||||
"PreviousSplit": (*BufPane).PreviousSplit,
|
||||
"Unsplit": (*BufPane).Unsplit,
|
||||
"VSplit": (*BufPane).VSplitAction,
|
||||
"HSplit": (*BufPane).HSplitAction,
|
||||
"ToggleMacro": (*BufPane).ToggleMacro,
|
||||
"PlayMacro": (*BufPane).PlayMacro,
|
||||
"Suspend": (*BufPane).Suspend,
|
||||
"ScrollUp": (*BufPane).ScrollUpAction,
|
||||
"ScrollDown": (*BufPane).ScrollDownAction,
|
||||
"SpawnMultiCursor": (*BufPane).SpawnMultiCursor,
|
||||
"SpawnMultiCursorSelect": (*BufPane).SpawnMultiCursorSelect,
|
||||
"RemoveMultiCursor": (*BufPane).RemoveMultiCursor,
|
||||
"RemoveAllMultiCursors": (*BufPane).RemoveAllMultiCursors,
|
||||
"SkipMultiCursor": (*BufPane).SkipMultiCursor,
|
||||
"JumpToMatchingBrace": (*BufPane).JumpToMatchingBrace,
|
||||
"None": (*BufPane).None,
|
||||
|
||||
// This was changed to InsertNewline but I don't want to break backwards compatibility
|
||||
"InsertEnter": (*BufPane).InsertNewline,
|
||||
}
|
||||
|
||||
// BufMouseActions contains the list of all possible mouse actions the bufhandler could execute
|
||||
var BufMouseActions = map[string]BufMouseAction{
|
||||
"MousePress": (*BufPane).MousePress,
|
||||
"MouseMultiCursor": (*BufPane).MouseMultiCursor,
|
||||
}
|
||||
|
||||
// MultiActions is a list of actions that should be executed multiple
|
||||
// times if there are multiple cursors (one per cursor)
|
||||
// Generally actions that modify global editor state like quitting or
|
||||
// saving should not be included in this list
|
||||
var MultiActions = map[string]bool{
|
||||
"CursorUp": true,
|
||||
"CursorDown": true,
|
||||
"CursorPageUp": true,
|
||||
"CursorPageDown": true,
|
||||
"CursorLeft": true,
|
||||
"CursorRight": true,
|
||||
"CursorStart": true,
|
||||
"CursorEnd": true,
|
||||
"SelectToStart": true,
|
||||
"SelectToEnd": true,
|
||||
"SelectUp": true,
|
||||
"SelectDown": true,
|
||||
"SelectLeft": true,
|
||||
"SelectRight": true,
|
||||
"WordRight": true,
|
||||
"WordLeft": true,
|
||||
"SelectWordRight": true,
|
||||
"SelectWordLeft": true,
|
||||
"DeleteWordRight": true,
|
||||
"DeleteWordLeft": true,
|
||||
"SelectLine": true,
|
||||
"SelectToStartOfLine": true,
|
||||
"SelectToEndOfLine": true,
|
||||
"ParagraphPrevious": true,
|
||||
"ParagraphNext": true,
|
||||
"InsertNewline": true,
|
||||
"Backspace": true,
|
||||
"Delete": true,
|
||||
"InsertTab": true,
|
||||
"FindNext": true,
|
||||
"FindPrevious": true,
|
||||
"Cut": true,
|
||||
"CutLine": true,
|
||||
"DuplicateLine": true,
|
||||
"DeleteLine": true,
|
||||
"MoveLinesUp": true,
|
||||
"MoveLinesDown": true,
|
||||
"IndentSelection": true,
|
||||
"OutdentSelection": true,
|
||||
"OutdentLine": true,
|
||||
"Paste": true,
|
||||
"PastePrimary": true,
|
||||
"SelectPageUp": true,
|
||||
"SelectPageDown": true,
|
||||
"StartOfLine": true,
|
||||
"EndOfLine": true,
|
||||
"JumpToMatchingBrace": true,
|
||||
}
|
||||
968
internal/action/command.go
Normal file
968
internal/action/command.go
Normal file
|
|
@ -0,0 +1,968 @@
|
|||
package action
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
luar "layeh.com/gopher-luar"
|
||||
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
"github.com/zyedidia/micro/internal/buffer"
|
||||
"github.com/zyedidia/micro/internal/config"
|
||||
ulua "github.com/zyedidia/micro/internal/lua"
|
||||
"github.com/zyedidia/micro/internal/screen"
|
||||
"github.com/zyedidia/micro/internal/shell"
|
||||
"github.com/zyedidia/micro/internal/util"
|
||||
"github.com/zyedidia/micro/pkg/shellwords"
|
||||
)
|
||||
|
||||
// A Command contains information about how to execute a command
|
||||
// It has the action for that command as well as a completer function
|
||||
type Command struct {
|
||||
action func(*BufPane, []string)
|
||||
completer buffer.Completer
|
||||
}
|
||||
|
||||
var commands map[string]Command
|
||||
|
||||
func InitCommands() {
|
||||
commands = map[string]Command{
|
||||
"set": {(*BufPane).SetCmd, OptionValueComplete},
|
||||
"reset": {(*BufPane).ResetCmd, OptionValueComplete},
|
||||
"setlocal": {(*BufPane).SetLocalCmd, OptionValueComplete},
|
||||
"show": {(*BufPane).ShowCmd, OptionComplete},
|
||||
"showkey": {(*BufPane).ShowKeyCmd, nil},
|
||||
"run": {(*BufPane).RunCmd, nil},
|
||||
"bind": {(*BufPane).BindCmd, nil},
|
||||
"unbind": {(*BufPane).UnbindCmd, nil},
|
||||
"quit": {(*BufPane).QuitCmd, nil},
|
||||
"goto": {(*BufPane).GotoCmd, nil},
|
||||
"save": {(*BufPane).SaveCmd, nil},
|
||||
"replace": {(*BufPane).ReplaceCmd, nil},
|
||||
"replaceall": {(*BufPane).ReplaceAllCmd, nil},
|
||||
"vsplit": {(*BufPane).VSplitCmd, buffer.FileComplete},
|
||||
"hsplit": {(*BufPane).HSplitCmd, buffer.FileComplete},
|
||||
"tab": {(*BufPane).NewTabCmd, buffer.FileComplete},
|
||||
"help": {(*BufPane).HelpCmd, HelpComplete},
|
||||
"eval": {(*BufPane).EvalCmd, nil},
|
||||
"log": {(*BufPane).ToggleLogCmd, nil},
|
||||
"plugin": {(*BufPane).PluginCmd, PluginComplete},
|
||||
"reload": {(*BufPane).ReloadCmd, nil},
|
||||
"reopen": {(*BufPane).ReopenCmd, nil},
|
||||
"cd": {(*BufPane).CdCmd, buffer.FileComplete},
|
||||
"pwd": {(*BufPane).PwdCmd, nil},
|
||||
"open": {(*BufPane).OpenCmd, buffer.FileComplete},
|
||||
"tabswitch": {(*BufPane).TabSwitchCmd, nil},
|
||||
"term": {(*BufPane).TermCmd, nil},
|
||||
"memusage": {(*BufPane).MemUsageCmd, nil},
|
||||
"retab": {(*BufPane).RetabCmd, nil},
|
||||
"raw": {(*BufPane).RawCmd, nil},
|
||||
"textfilter": {(*BufPane).TextFilterCmd, nil},
|
||||
}
|
||||
}
|
||||
|
||||
// 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 LuaMakeCommand(name, function string, completer buffer.Completer) {
|
||||
action := LuaFunctionCommand(function)
|
||||
commands[name] = Command{action, completer}
|
||||
}
|
||||
|
||||
// LuaFunctionCommand returns a normal function
|
||||
// so that a command can be bound to a lua function
|
||||
func LuaFunctionCommand(fn string) func(*BufPane, []string) {
|
||||
luaFn := strings.Split(fn, ".")
|
||||
if len(luaFn) <= 1 {
|
||||
return nil
|
||||
}
|
||||
plName, plFn := luaFn[0], luaFn[1]
|
||||
pl := config.FindPlugin(plName)
|
||||
if pl == nil {
|
||||
return nil
|
||||
}
|
||||
return func(bp *BufPane, args []string) {
|
||||
luaArgs := []lua.LValue{luar.New(ulua.L, bp), luar.New(ulua.L, args)}
|
||||
_, err := pl.Call(plFn, luaArgs...)
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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) BufKeyAction {
|
||||
return func(h *BufPane) bool {
|
||||
InfoBar.Prompt("> ", prompt, "Command", nil, func(resp string, canceled bool) {
|
||||
if !canceled {
|
||||
MainTab().CurPane().HandleCommand(resp)
|
||||
}
|
||||
})
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// CommandAction returns a bindable function which executes the
|
||||
// given command
|
||||
func CommandAction(cmd string) BufKeyAction {
|
||||
return func(h *BufPane) bool {
|
||||
MainTab().CurPane().HandleCommand(cmd)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var PluginCmds = []string{"list", "info", "version"}
|
||||
|
||||
// PluginCmd installs, removes, updates, lists, or searches for given plugins
|
||||
func (h *BufPane) PluginCmd(args []string) {
|
||||
if len(args) <= 0 {
|
||||
InfoBar.Error("Not enough arguments, see 'help commands'")
|
||||
return
|
||||
}
|
||||
|
||||
valid := true
|
||||
switch args[0] {
|
||||
case "list":
|
||||
for _, pl := range config.Plugins {
|
||||
var en string
|
||||
if pl.IsEnabled() {
|
||||
en = "enabled"
|
||||
} else {
|
||||
en = "disabled"
|
||||
}
|
||||
WriteLog(fmt.Sprintf("%s: %s", pl.Name, en))
|
||||
if pl.Default {
|
||||
WriteLog(" (default)\n")
|
||||
} else {
|
||||
WriteLog("\n")
|
||||
}
|
||||
}
|
||||
WriteLog("Default plugins come pre-installed with micro.")
|
||||
case "version":
|
||||
if len(args) <= 1 {
|
||||
InfoBar.Error("No plugin provided to give info for")
|
||||
return
|
||||
}
|
||||
found := false
|
||||
for _, pl := range config.Plugins {
|
||||
if pl.Name == args[1] {
|
||||
found = true
|
||||
if pl.Info == nil {
|
||||
InfoBar.Message("Sorry no version for", pl.Name)
|
||||
return
|
||||
}
|
||||
|
||||
WriteLog("Version: " + pl.Info.Vstr + "\n")
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
InfoBar.Message(args[1], "is not installed")
|
||||
}
|
||||
case "info":
|
||||
if len(args) <= 1 {
|
||||
InfoBar.Error("No plugin provided to give info for")
|
||||
return
|
||||
}
|
||||
found := false
|
||||
for _, pl := range config.Plugins {
|
||||
if pl.Name == args[1] {
|
||||
found = true
|
||||
if pl.Info == nil {
|
||||
InfoBar.Message("Sorry no info for ", pl.Name)
|
||||
return
|
||||
}
|
||||
|
||||
var buffer bytes.Buffer
|
||||
buffer.WriteString("Name: ")
|
||||
buffer.WriteString(pl.Info.Name)
|
||||
buffer.WriteString("\n")
|
||||
buffer.WriteString("Description: ")
|
||||
buffer.WriteString(pl.Info.Desc)
|
||||
buffer.WriteString("\n")
|
||||
buffer.WriteString("Website: ")
|
||||
buffer.WriteString(pl.Info.Site)
|
||||
buffer.WriteString("\n")
|
||||
buffer.WriteString("Installation link: ")
|
||||
buffer.WriteString(pl.Info.Install)
|
||||
buffer.WriteString("\n")
|
||||
buffer.WriteString("Version: ")
|
||||
buffer.WriteString(pl.Info.Vstr)
|
||||
buffer.WriteString("\n")
|
||||
buffer.WriteString("Requirements:")
|
||||
buffer.WriteString("\n")
|
||||
for _, r := range pl.Info.Require {
|
||||
buffer.WriteString(" - ")
|
||||
buffer.WriteString(r)
|
||||
buffer.WriteString("\n")
|
||||
}
|
||||
|
||||
WriteLog(buffer.String())
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
InfoBar.Message(args[1], "is not installed")
|
||||
return
|
||||
}
|
||||
default:
|
||||
InfoBar.Error("Not a valid plugin command")
|
||||
return
|
||||
}
|
||||
|
||||
if valid && h.Buf.Type != buffer.BTLog {
|
||||
OpenLogBuf(h)
|
||||
}
|
||||
}
|
||||
|
||||
// RetabCmd changes all spaces to tabs or all tabs to spaces
|
||||
// depending on the user's settings
|
||||
func (h *BufPane) RetabCmd(args []string) {
|
||||
h.Buf.Retab()
|
||||
}
|
||||
|
||||
// RawCmd opens a new raw view which displays the escape sequences micro
|
||||
// is receiving in real-time
|
||||
func (h *BufPane) RawCmd(args []string) {
|
||||
width, height := screen.Screen.Size()
|
||||
iOffset := config.GetInfoBarOffset()
|
||||
tp := NewTabFromPane(0, 0, width, height-iOffset, NewRawPane())
|
||||
Tabs.AddTab(tp)
|
||||
Tabs.SetActive(len(Tabs.List) - 1)
|
||||
}
|
||||
|
||||
// TextFilterCmd filters the selection through the command.
|
||||
// Selection goes to the command input.
|
||||
// On successful run command output replaces the current selection.
|
||||
func (h *BufPane) TextFilterCmd(args []string) {
|
||||
if len(args) == 0 {
|
||||
InfoBar.Error("usage: textfilter arguments")
|
||||
return
|
||||
}
|
||||
sel := h.Cursor.GetSelection()
|
||||
if len(sel) == 0 {
|
||||
h.Cursor.SelectWord()
|
||||
sel = h.Cursor.GetSelection()
|
||||
}
|
||||
var bout, berr bytes.Buffer
|
||||
cmd := exec.Command(args[0], args[1:]...)
|
||||
cmd.Stdin = strings.NewReader(string(sel))
|
||||
cmd.Stderr = &berr
|
||||
cmd.Stdout = &bout
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
InfoBar.Error(err.Error() + " " + berr.String())
|
||||
return
|
||||
}
|
||||
h.Cursor.DeleteSelection()
|
||||
h.Buf.Insert(h.Cursor.Loc, bout.String())
|
||||
}
|
||||
|
||||
// TabSwitchCmd switches to a given tab either by name or by number
|
||||
func (h *BufPane) TabSwitchCmd(args []string) {
|
||||
if len(args) > 0 {
|
||||
num, err := strconv.Atoi(args[0])
|
||||
if err != nil {
|
||||
// Check for tab with this name
|
||||
|
||||
found := false
|
||||
for i, t := range Tabs.List {
|
||||
if t.Panes[t.active].Name() == args[0] {
|
||||
Tabs.SetActive(i)
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
InfoBar.Error("Could not find tab: ", err)
|
||||
}
|
||||
} else {
|
||||
num--
|
||||
if num >= 0 && num < len(Tabs.List) {
|
||||
Tabs.SetActive(num)
|
||||
} else {
|
||||
InfoBar.Error("Invalid tab index")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CdCmd changes the current working directory
|
||||
func (h *BufPane) CdCmd(args []string) {
|
||||
if len(args) > 0 {
|
||||
path, err := util.ReplaceHome(args[0])
|
||||
if err != nil {
|
||||
InfoBar.Error(err)
|
||||
return
|
||||
}
|
||||
err = os.Chdir(path)
|
||||
if err != nil {
|
||||
InfoBar.Error(err)
|
||||
return
|
||||
}
|
||||
wd, _ := os.Getwd()
|
||||
for _, b := range buffer.OpenBuffers {
|
||||
if len(b.Path) > 0 {
|
||||
b.Path, _ = util.MakeRelative(b.AbsPath, wd)
|
||||
if p, _ := filepath.Abs(b.Path); !strings.Contains(p, wd) {
|
||||
b.Path = b.AbsPath
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MemUsageCmd 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 (h *BufPane) MemUsageCmd(args []string) {
|
||||
InfoBar.Message(util.GetMemStats())
|
||||
}
|
||||
|
||||
// PwdCmd prints the current working directory
|
||||
func (h *BufPane) PwdCmd(args []string) {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
InfoBar.Message(err.Error())
|
||||
} else {
|
||||
InfoBar.Message(wd)
|
||||
}
|
||||
}
|
||||
|
||||
// OpenCmd opens a new buffer with a given filename
|
||||
func (h *BufPane) OpenCmd(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 {
|
||||
InfoBar.Error("Error parsing args ", err)
|
||||
return
|
||||
}
|
||||
filename = strings.Join(args, " ")
|
||||
|
||||
open := func() {
|
||||
b, err := buffer.NewBufferFromFile(filename, buffer.BTDefault)
|
||||
if err != nil {
|
||||
InfoBar.Error(err)
|
||||
return
|
||||
}
|
||||
h.OpenBuffer(b)
|
||||
}
|
||||
if h.Buf.Modified() {
|
||||
InfoBar.YNPrompt("Save changes to "+h.Buf.GetName()+" before closing? (y,n,esc)", func(yes, canceled bool) {
|
||||
if !canceled && !yes {
|
||||
open()
|
||||
} else if !canceled && yes {
|
||||
h.Save()
|
||||
open()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
open()
|
||||
}
|
||||
} else {
|
||||
InfoBar.Error("No filename")
|
||||
}
|
||||
}
|
||||
|
||||
// ToggleLogCmd toggles the log view
|
||||
func (h *BufPane) ToggleLogCmd(args []string) {
|
||||
if h.Buf.Type != buffer.BTLog {
|
||||
OpenLogBuf(h)
|
||||
} else {
|
||||
h.Quit()
|
||||
}
|
||||
}
|
||||
|
||||
// ReloadCmd reloads all files (syntax files, colorschemes...)
|
||||
func (h *BufPane) ReloadCmd(args []string) {
|
||||
ReloadConfig()
|
||||
}
|
||||
|
||||
func ReloadConfig() {
|
||||
config.InitRuntimeFiles()
|
||||
err := config.ReadSettings()
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
config.InitGlobalSettings()
|
||||
InitBindings()
|
||||
InitCommands()
|
||||
|
||||
err = config.InitColorscheme()
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
|
||||
for _, b := range buffer.OpenBuffers {
|
||||
b.UpdateRules()
|
||||
}
|
||||
}
|
||||
|
||||
// ReopenCmd reopens the buffer (reload from disk)
|
||||
func (h *BufPane) ReopenCmd(args []string) {
|
||||
if h.Buf.Modified() {
|
||||
InfoBar.YNPrompt("Save file before reopen?", func(yes, canceled bool) {
|
||||
if !canceled && yes {
|
||||
h.Save()
|
||||
h.Buf.ReOpen()
|
||||
} else if !canceled {
|
||||
h.Buf.ReOpen()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
h.Buf.ReOpen()
|
||||
}
|
||||
}
|
||||
|
||||
func (h *BufPane) openHelp(page string) error {
|
||||
if data, err := config.FindRuntimeFile(config.RTHelp, page).Data(); err != nil {
|
||||
return errors.New(fmt.Sprint("Unable to load help text", page, "\n", err))
|
||||
} else {
|
||||
helpBuffer := buffer.NewBufferFromString(string(data), page+".md", buffer.BTHelp)
|
||||
helpBuffer.SetName("Help " + page)
|
||||
|
||||
if h.Buf.Type == buffer.BTHelp {
|
||||
h.OpenBuffer(helpBuffer)
|
||||
} else {
|
||||
h.HSplitBuf(helpBuffer)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// HelpCmd tries to open the given help page in a horizontal split
|
||||
func (h *BufPane) HelpCmd(args []string) {
|
||||
if len(args) < 1 {
|
||||
// Open the default help if the user just typed "> help"
|
||||
h.openHelp("help")
|
||||
} else {
|
||||
if config.FindRuntimeFile(config.RTHelp, args[0]) != nil {
|
||||
err := h.openHelp(args[0])
|
||||
if err != nil {
|
||||
InfoBar.Error(err)
|
||||
}
|
||||
} else {
|
||||
InfoBar.Error("Sorry, no help for ", args[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// VSplitCmd 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 (h *BufPane) VSplitCmd(args []string) {
|
||||
if len(args) == 0 {
|
||||
// Open an empty vertical split
|
||||
h.VSplitAction()
|
||||
return
|
||||
}
|
||||
|
||||
buf, err := buffer.NewBufferFromFile(args[0], buffer.BTDefault)
|
||||
if err != nil {
|
||||
InfoBar.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
h.VSplitBuf(buf)
|
||||
}
|
||||
|
||||
// HSplitCmd 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 (h *BufPane) HSplitCmd(args []string) {
|
||||
if len(args) == 0 {
|
||||
// Open an empty horizontal split
|
||||
h.HSplitAction()
|
||||
return
|
||||
}
|
||||
|
||||
buf, err := buffer.NewBufferFromFile(args[0], buffer.BTDefault)
|
||||
if err != nil {
|
||||
InfoBar.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
h.HSplitBuf(buf)
|
||||
}
|
||||
|
||||
// EvalCmd evaluates a lua expression
|
||||
func (h *BufPane) EvalCmd(args []string) {
|
||||
}
|
||||
|
||||
// NewTabCmd opens the given file in a new tab
|
||||
func (h *BufPane) NewTabCmd(args []string) {
|
||||
width, height := screen.Screen.Size()
|
||||
iOffset := config.GetInfoBarOffset()
|
||||
if len(args) > 0 {
|
||||
for _, a := range args {
|
||||
b, err := buffer.NewBufferFromFile(a, buffer.BTDefault)
|
||||
if err != nil {
|
||||
InfoBar.Error(err)
|
||||
return
|
||||
}
|
||||
tp := NewTabFromBuffer(0, 0, width, height-1-iOffset, b)
|
||||
Tabs.AddTab(tp)
|
||||
Tabs.SetActive(len(Tabs.List) - 1)
|
||||
}
|
||||
} else {
|
||||
b := buffer.NewBufferFromString("", "", buffer.BTDefault)
|
||||
tp := NewTabFromBuffer(0, 0, width, height-iOffset, b)
|
||||
Tabs.AddTab(tp)
|
||||
Tabs.SetActive(len(Tabs.List) - 1)
|
||||
}
|
||||
}
|
||||
|
||||
func SetGlobalOptionNative(option string, nativeValue interface{}) error {
|
||||
config.GlobalSettings[option] = nativeValue
|
||||
|
||||
if option == "colorscheme" {
|
||||
// LoadSyntaxFiles()
|
||||
config.InitColorscheme()
|
||||
for _, b := range buffer.OpenBuffers {
|
||||
b.UpdateRules()
|
||||
}
|
||||
} else if option == "infobar" || option == "keymenu" {
|
||||
Tabs.Resize()
|
||||
} else if option == "mouse" {
|
||||
if !nativeValue.(bool) {
|
||||
screen.Screen.DisableMouse()
|
||||
} else {
|
||||
screen.Screen.EnableMouse()
|
||||
}
|
||||
// } else if option == "autosave" {
|
||||
// if nativeValue.(float64) > 0 {
|
||||
// config.SetAutoTime(int(nativeValue.(float64)))
|
||||
// config.StartAutoSave()
|
||||
// } else {
|
||||
// config.SetAutoTime(0)
|
||||
// }
|
||||
} else {
|
||||
for _, pl := range config.Plugins {
|
||||
if option == pl.Name {
|
||||
if nativeValue.(bool) && !pl.Loaded {
|
||||
pl.Load()
|
||||
pl.Call("init")
|
||||
} else if !nativeValue.(bool) && pl.Loaded {
|
||||
pl.Call("deinit")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, b := range buffer.OpenBuffers {
|
||||
b.SetOptionNative(option, nativeValue)
|
||||
}
|
||||
|
||||
return config.WriteSettings(config.ConfigDir + "/settings.json")
|
||||
}
|
||||
|
||||
func SetGlobalOption(option, value string) error {
|
||||
if _, ok := config.GlobalSettings[option]; !ok {
|
||||
return config.ErrInvalidOption
|
||||
}
|
||||
|
||||
nativeValue, err := config.GetNativeValue(option, config.GlobalSettings[option], value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return SetGlobalOptionNative(option, nativeValue)
|
||||
}
|
||||
|
||||
// ResetCmd resets a setting to its default value
|
||||
func (h *BufPane) ResetCmd(args []string) {
|
||||
if len(args) < 1 {
|
||||
InfoBar.Error("Not enough arguments")
|
||||
return
|
||||
}
|
||||
|
||||
option := args[0]
|
||||
|
||||
defaultGlobals := config.DefaultGlobalSettings()
|
||||
defaultLocals := config.DefaultCommonSettings()
|
||||
|
||||
if _, ok := defaultGlobals[option]; ok {
|
||||
SetGlobalOptionNative(option, defaultGlobals[option])
|
||||
return
|
||||
}
|
||||
if _, ok := defaultLocals[option]; ok {
|
||||
h.Buf.SetOptionNative(option, defaultLocals[option])
|
||||
return
|
||||
}
|
||||
InfoBar.Error(config.ErrInvalidOption)
|
||||
}
|
||||
|
||||
// SetCmd sets an option
|
||||
func (h *BufPane) SetCmd(args []string) {
|
||||
if len(args) < 2 {
|
||||
InfoBar.Error("Not enough arguments")
|
||||
return
|
||||
}
|
||||
|
||||
option := args[0]
|
||||
value := args[1]
|
||||
|
||||
err := SetGlobalOption(option, value)
|
||||
if err == config.ErrInvalidOption {
|
||||
err := h.Buf.SetOption(option, value)
|
||||
if err != nil {
|
||||
InfoBar.Error(err)
|
||||
}
|
||||
} else if err != nil {
|
||||
InfoBar.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// SetLocalCmd sets an option local to the buffer
|
||||
func (h *BufPane) SetLocalCmd(args []string) {
|
||||
if len(args) < 2 {
|
||||
InfoBar.Error("Not enough arguments")
|
||||
return
|
||||
}
|
||||
|
||||
option := args[0]
|
||||
value := args[1]
|
||||
|
||||
err := h.Buf.SetOption(option, value)
|
||||
if err != nil {
|
||||
InfoBar.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// ShowCmd shows the value of the given option
|
||||
func (h *BufPane) ShowCmd(args []string) {
|
||||
if len(args) < 1 {
|
||||
InfoBar.Error("Please provide an option to show")
|
||||
return
|
||||
}
|
||||
|
||||
var option interface{}
|
||||
if opt, ok := h.Buf.Settings[args[0]]; ok {
|
||||
option = opt
|
||||
} else if opt, ok := config.GlobalSettings[args[0]]; ok {
|
||||
option = opt
|
||||
}
|
||||
|
||||
if option == nil {
|
||||
InfoBar.Error(args[0], " is not a valid option")
|
||||
return
|
||||
}
|
||||
|
||||
InfoBar.Message(option)
|
||||
}
|
||||
|
||||
// ShowKeyCmd displays the action that a key is bound to
|
||||
func (h *BufPane) ShowKeyCmd(args []string) {
|
||||
if len(args) < 1 {
|
||||
InfoBar.Error("Please provide a key to show")
|
||||
return
|
||||
}
|
||||
|
||||
if action, ok := config.Bindings[args[0]]; ok {
|
||||
InfoBar.Message(action)
|
||||
} else {
|
||||
InfoBar.Message(args[0], " has no binding")
|
||||
}
|
||||
}
|
||||
|
||||
// BindCmd creates a new keybinding
|
||||
func (h *BufPane) BindCmd(args []string) {
|
||||
if len(args) < 2 {
|
||||
InfoBar.Error("Not enough arguments")
|
||||
return
|
||||
}
|
||||
|
||||
_, err := TryBindKey(args[0], args[1], true)
|
||||
if err != nil {
|
||||
InfoBar.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// UnbindCmd binds a key to its default action
|
||||
func (h *BufPane) UnbindCmd(args []string) {
|
||||
if len(args) < 1 {
|
||||
InfoBar.Error("Not enough arguments")
|
||||
return
|
||||
}
|
||||
|
||||
err := UnbindKey(args[0])
|
||||
if err != nil {
|
||||
InfoBar.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// RunCmd runs a shell command in the background
|
||||
func (h *BufPane) RunCmd(args []string) {
|
||||
runf, err := shell.RunBackgroundShell(shellwords.Join(args...))
|
||||
if err != nil {
|
||||
InfoBar.Error(err)
|
||||
} else {
|
||||
go func() {
|
||||
InfoBar.Message(runf())
|
||||
screen.Redraw()
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// QuitCmd closes the main view
|
||||
func (h *BufPane) QuitCmd(args []string) {
|
||||
h.Quit()
|
||||
}
|
||||
|
||||
// GotoCmd is a command that will send the cursor to a certain
|
||||
// position in the buffer
|
||||
// For example: `goto line`, or `goto line:col`
|
||||
func (h *BufPane) GotoCmd(args []string) {
|
||||
if len(args) <= 0 {
|
||||
InfoBar.Error("Not enough arguments")
|
||||
} else {
|
||||
h.RemoveAllMultiCursors()
|
||||
if strings.Contains(args[0], ":") {
|
||||
parts := strings.SplitN(args[0], ":", 2)
|
||||
line, err := strconv.Atoi(parts[0])
|
||||
if err != nil {
|
||||
InfoBar.Error(err)
|
||||
return
|
||||
}
|
||||
col, err := strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
InfoBar.Error(err)
|
||||
return
|
||||
}
|
||||
line = util.Clamp(line-1, 0, h.Buf.LinesNum()-1)
|
||||
col = util.Clamp(col-1, 0, utf8.RuneCount(h.Buf.LineBytes(line)))
|
||||
h.Cursor.GotoLoc(buffer.Loc{col, line})
|
||||
} else {
|
||||
line, err := strconv.Atoi(args[0])
|
||||
if err != nil {
|
||||
InfoBar.Error(err)
|
||||
return
|
||||
}
|
||||
line = util.Clamp(line-1, 0, h.Buf.LinesNum()-1)
|
||||
h.Cursor.GotoLoc(buffer.Loc{0, line})
|
||||
}
|
||||
h.Relocate()
|
||||
}
|
||||
}
|
||||
|
||||
// SaveCmd saves the buffer optionally with an argument file name
|
||||
func (h *BufPane) SaveCmd(args []string) {
|
||||
if len(args) == 0 {
|
||||
h.Save()
|
||||
} else {
|
||||
h.Buf.SaveAs(args[0])
|
||||
}
|
||||
}
|
||||
|
||||
// ReplaceCmd runs search and replace
|
||||
func (h *BufPane) ReplaceCmd(args []string) {
|
||||
if len(args) < 2 || len(args) > 4 {
|
||||
// We need to find both a search and replace expression
|
||||
InfoBar.Error("Invalid replace statement: " + strings.Join(args, " "))
|
||||
return
|
||||
}
|
||||
|
||||
all := false
|
||||
noRegex := false
|
||||
|
||||
foundSearch := false
|
||||
foundReplace := false
|
||||
var search string
|
||||
var replaceStr string
|
||||
for _, arg := range args {
|
||||
switch arg {
|
||||
case "-a":
|
||||
all = true
|
||||
case "-l":
|
||||
noRegex = true
|
||||
default:
|
||||
if !foundSearch {
|
||||
foundSearch = true
|
||||
search = arg
|
||||
} else if !foundReplace {
|
||||
foundReplace = true
|
||||
replaceStr = arg
|
||||
} else {
|
||||
InfoBar.Error("Invalid flag: " + arg)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if noRegex {
|
||||
search = regexp.QuoteMeta(search)
|
||||
}
|
||||
|
||||
replace := []byte(replaceStr)
|
||||
|
||||
var regex *regexp.Regexp
|
||||
var err error
|
||||
if h.Buf.Settings["ignorecase"].(bool) {
|
||||
regex, err = regexp.Compile("(?im)" + search)
|
||||
} else {
|
||||
regex, err = regexp.Compile("(?m)" + search)
|
||||
}
|
||||
if err != nil {
|
||||
// There was an error with the user's regex
|
||||
InfoBar.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
nreplaced := 0
|
||||
start := h.Buf.Start()
|
||||
// end := h.Buf.End()
|
||||
// if h.Cursor.HasSelection() {
|
||||
// start = h.Cursor.CurSelection[0]
|
||||
// end = h.Cursor.CurSelection[1]
|
||||
// }
|
||||
if all {
|
||||
nreplaced = h.Buf.ReplaceRegex(start, h.Buf.End(), regex, replace)
|
||||
} else {
|
||||
inRange := func(l buffer.Loc) bool {
|
||||
return l.GreaterEqual(start) && l.LessEqual(h.Buf.End())
|
||||
}
|
||||
|
||||
searchLoc := start
|
||||
searching := true
|
||||
var doReplacement func()
|
||||
doReplacement = func() {
|
||||
locs, found, err := h.Buf.FindNext(search, start, h.Buf.End(), searchLoc, true, !noRegex)
|
||||
if err != nil {
|
||||
InfoBar.Error(err)
|
||||
return
|
||||
}
|
||||
if !found || !inRange(locs[0]) || !inRange(locs[1]) {
|
||||
h.Cursor.ResetSelection()
|
||||
h.Buf.RelocateCursors()
|
||||
return
|
||||
}
|
||||
|
||||
h.Cursor.SetSelectionStart(locs[0])
|
||||
h.Cursor.SetSelectionEnd(locs[1])
|
||||
|
||||
InfoBar.YNPrompt("Perform replacement (y,n,esc)", func(yes, canceled bool) {
|
||||
if !canceled && yes {
|
||||
h.Buf.Replace(locs[0], locs[1], replaceStr)
|
||||
|
||||
searchLoc = locs[0]
|
||||
searchLoc.X += utf8.RuneCount(replace)
|
||||
h.Cursor.Loc = searchLoc
|
||||
nreplaced++
|
||||
} else if !canceled && !yes {
|
||||
searchLoc = locs[0]
|
||||
searchLoc.X += utf8.RuneCount(replace)
|
||||
} else if canceled {
|
||||
h.Cursor.ResetSelection()
|
||||
h.Buf.RelocateCursors()
|
||||
return
|
||||
}
|
||||
if searching {
|
||||
doReplacement()
|
||||
}
|
||||
})
|
||||
}
|
||||
doReplacement()
|
||||
}
|
||||
|
||||
h.Buf.RelocateCursors()
|
||||
|
||||
if nreplaced > 1 {
|
||||
InfoBar.Message("Replaced ", nreplaced, " occurrences of ", search)
|
||||
} else if nreplaced == 1 {
|
||||
InfoBar.Message("Replaced ", nreplaced, " occurrence of ", search)
|
||||
} else {
|
||||
InfoBar.Message("Nothing matched ", search)
|
||||
}
|
||||
}
|
||||
|
||||
// ReplaceAllCmd replaces search term all at once
|
||||
func (h *BufPane) ReplaceAllCmd(args []string) {
|
||||
// aliased to Replace command
|
||||
h.ReplaceCmd(append(args, "-a"))
|
||||
}
|
||||
|
||||
// TermCmd opens a terminal in the current view
|
||||
func (h *BufPane) TermCmd(args []string) {
|
||||
ps := MainTab().Panes
|
||||
|
||||
if len(args) == 0 {
|
||||
sh := os.Getenv("SHELL")
|
||||
if sh == "" {
|
||||
InfoBar.Error("Shell environment not found")
|
||||
return
|
||||
}
|
||||
args = []string{sh}
|
||||
}
|
||||
|
||||
term := func(i int, newtab bool) {
|
||||
t := new(shell.Terminal)
|
||||
t.Start(args, false, true, "", nil)
|
||||
|
||||
id := h.ID()
|
||||
if newtab {
|
||||
h.AddTab()
|
||||
i = 0
|
||||
id = MainTab().Panes[0].ID()
|
||||
} else {
|
||||
MainTab().Panes[i].Close()
|
||||
}
|
||||
|
||||
v := h.GetView()
|
||||
MainTab().Panes[i] = NewTermPane(v.X, v.Y, v.Width, v.Height, t, id)
|
||||
MainTab().SetActive(i)
|
||||
}
|
||||
|
||||
// If there is only one open file we make a new tab instead of overwriting it
|
||||
newtab := len(MainTab().Panes) == 1 && len(Tabs.List) == 1
|
||||
|
||||
if newtab {
|
||||
term(0, true)
|
||||
return
|
||||
}
|
||||
|
||||
for i, p := range ps {
|
||||
if p.ID() == h.ID() {
|
||||
if h.Buf.Modified() {
|
||||
InfoBar.YNPrompt("Save changes to "+h.Buf.GetName()+" before closing? (y,n,esc)", func(yes, canceled bool) {
|
||||
if !canceled && !yes {
|
||||
term(i, false)
|
||||
} else if !canceled && yes {
|
||||
h.Save()
|
||||
term(i, false)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
term(i, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HandleCommand handles input from the user
|
||||
func (h *BufPane) HandleCommand(input string) {
|
||||
args, err := shellwords.Split(input)
|
||||
if err != nil {
|
||||
InfoBar.Error("Error parsing args ", err)
|
||||
return
|
||||
}
|
||||
|
||||
inputCmd := args[0]
|
||||
|
||||
if _, ok := commands[inputCmd]; !ok {
|
||||
InfoBar.Error("Unknown command ", inputCmd)
|
||||
} else {
|
||||
WriteLog("> " + input + "\n")
|
||||
commands[inputCmd].action(h, args[1:])
|
||||
WriteLog("\n")
|
||||
}
|
||||
}
|
||||
42
internal/action/events.go
Normal file
42
internal/action/events.go
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
package action
|
||||
|
||||
import (
|
||||
"github.com/zyedidia/tcell"
|
||||
)
|
||||
|
||||
type Event interface{}
|
||||
|
||||
// RawEvent is simply an escape code
|
||||
// We allow users to directly bind escape codes
|
||||
// to get around some of a limitations of terminals
|
||||
type RawEvent struct {
|
||||
esc string
|
||||
}
|
||||
|
||||
// KeyEvent is a key event containing a key code,
|
||||
// some possible modifiers (alt, ctrl, etc...) and
|
||||
// a rune if it was simply a character press
|
||||
// Note: to be compatible with tcell events,
|
||||
// for ctrl keys r=code
|
||||
type KeyEvent struct {
|
||||
code tcell.Key
|
||||
mod tcell.ModMask
|
||||
r rune
|
||||
}
|
||||
|
||||
// MouseEvent is a mouse event with a mouse button and
|
||||
// any possible key modifiers
|
||||
type MouseEvent struct {
|
||||
btn tcell.ButtonMask
|
||||
mod tcell.ModMask
|
||||
}
|
||||
|
||||
type KeyAction func(Handler) bool
|
||||
type MouseAction func(Handler, tcell.EventMouse) bool
|
||||
|
||||
// A Handler will take a tcell event and execute it
|
||||
// appropriately
|
||||
type Handler interface {
|
||||
HandleEvent(tcell.Event)
|
||||
HandleCommand(string)
|
||||
}
|
||||
42
internal/action/globals.go
Normal file
42
internal/action/globals.go
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
package action
|
||||
|
||||
import "github.com/zyedidia/micro/internal/buffer"
|
||||
|
||||
var InfoBar *InfoPane
|
||||
var LogBufPane *BufPane
|
||||
|
||||
func InitGlobals() {
|
||||
InfoBar = NewInfoBar()
|
||||
buffer.LogBuf = buffer.NewBufferFromString("", "Log", buffer.BTLog)
|
||||
}
|
||||
|
||||
func GetInfoBar() *InfoPane {
|
||||
return InfoBar
|
||||
}
|
||||
|
||||
func WriteLog(s string) {
|
||||
buffer.WriteLog(s)
|
||||
if LogBufPane != nil {
|
||||
LogBufPane.CursorEnd()
|
||||
v := LogBufPane.GetView()
|
||||
endY := buffer.LogBuf.End().Y
|
||||
|
||||
if endY > v.StartLine+v.Height {
|
||||
v.StartLine = buffer.LogBuf.End().Y - v.Height + 2
|
||||
LogBufPane.SetView(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func OpenLogBuf(h *BufPane) {
|
||||
LogBufPane = h.HSplitBuf(buffer.LogBuf)
|
||||
LogBufPane.CursorEnd()
|
||||
|
||||
v := LogBufPane.GetView()
|
||||
endY := buffer.LogBuf.End().Y
|
||||
|
||||
if endY > v.StartLine+v.Height {
|
||||
v.StartLine = buffer.LogBuf.End().Y - v.Height + 2
|
||||
LogBufPane.SetView(v)
|
||||
}
|
||||
}
|
||||
303
internal/action/infocomplete.go
Normal file
303
internal/action/infocomplete.go
Normal file
|
|
@ -0,0 +1,303 @@
|
|||
package action
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/zyedidia/micro/internal/buffer"
|
||||
"github.com/zyedidia/micro/internal/config"
|
||||
"github.com/zyedidia/micro/internal/util"
|
||||
)
|
||||
|
||||
// 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`.
|
||||
|
||||
// CommandComplete autocompletes commands
|
||||
func CommandComplete(b *buffer.Buffer) ([]string, []string) {
|
||||
c := b.GetActiveCursor()
|
||||
input, argstart := buffer.GetArg(b)
|
||||
|
||||
var suggestions []string
|
||||
for cmd := range commands {
|
||||
if strings.HasPrefix(cmd, input) {
|
||||
suggestions = append(suggestions, cmd)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(suggestions)
|
||||
completions := make([]string, len(suggestions))
|
||||
for i := range suggestions {
|
||||
completions[i] = util.SliceEndStr(suggestions[i], c.X-argstart)
|
||||
}
|
||||
|
||||
return completions, suggestions
|
||||
}
|
||||
|
||||
// HelpComplete autocompletes help topics
|
||||
func HelpComplete(b *buffer.Buffer) ([]string, []string) {
|
||||
c := b.GetActiveCursor()
|
||||
input, argstart := buffer.GetArg(b)
|
||||
|
||||
var suggestions []string
|
||||
|
||||
for _, file := range config.ListRuntimeFiles(config.RTHelp) {
|
||||
topic := file.Name()
|
||||
if strings.HasPrefix(topic, input) {
|
||||
suggestions = append(suggestions, topic)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(suggestions)
|
||||
completions := make([]string, len(suggestions))
|
||||
for i := range suggestions {
|
||||
completions[i] = util.SliceEndStr(suggestions[i], c.X-argstart)
|
||||
}
|
||||
return completions, suggestions
|
||||
}
|
||||
|
||||
// colorschemeComplete tab-completes names of colorschemes.
|
||||
// This is just a heper value for OptionValueComplete
|
||||
func colorschemeComplete(input string) (string, []string) {
|
||||
var suggestions []string
|
||||
files := config.ListRuntimeFiles(config.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(b *buffer.Buffer) ([]string, []string) {
|
||||
c := b.GetActiveCursor()
|
||||
input, argstart := buffer.GetArg(b)
|
||||
|
||||
var suggestions []string
|
||||
for option := range config.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)
|
||||
// }
|
||||
// }
|
||||
|
||||
sort.Strings(suggestions)
|
||||
completions := make([]string, len(suggestions))
|
||||
for i := range suggestions {
|
||||
completions[i] = util.SliceEndStr(suggestions[i], c.X-argstart)
|
||||
}
|
||||
return completions, suggestions
|
||||
}
|
||||
|
||||
// OptionValueComplete completes values for various options
|
||||
func OptionValueComplete(b *buffer.Buffer) ([]string, []string) {
|
||||
c := b.GetActiveCursor()
|
||||
l := b.LineBytes(c.Y)
|
||||
l = util.SliceStart(l, c.X)
|
||||
input, argstart := buffer.GetArg(b)
|
||||
|
||||
completeValue := false
|
||||
args := bytes.Split(l, []byte{' '})
|
||||
if len(args) >= 2 {
|
||||
// localSettings := config.DefaultLocalSettings()
|
||||
for option := range config.GlobalSettings {
|
||||
if option == string(args[len(args)-2]) {
|
||||
completeValue = true
|
||||
break
|
||||
}
|
||||
}
|
||||
// for option := range localSettings {
|
||||
// if option == string(args[len(args)-2]) {
|
||||
// completeValue = true
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
}
|
||||
if !completeValue {
|
||||
return OptionComplete(b)
|
||||
}
|
||||
|
||||
inputOpt := string(args[len(args)-2])
|
||||
|
||||
inputOpt = strings.TrimSpace(inputOpt)
|
||||
var suggestions []string
|
||||
// localSettings := config.DefaultLocalSettings()
|
||||
var optionVal interface{}
|
||||
for k, option := range config.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")
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Strings(suggestions)
|
||||
|
||||
completions := make([]string, len(suggestions))
|
||||
for i := range suggestions {
|
||||
completions[i] = util.SliceEndStr(suggestions[i], c.X-argstart)
|
||||
}
|
||||
return completions, suggestions
|
||||
}
|
||||
|
||||
// OptionComplete autocompletes options
|
||||
func PluginCmdComplete(b *buffer.Buffer) ([]string, []string) {
|
||||
c := b.GetActiveCursor()
|
||||
input, argstart := buffer.GetArg(b)
|
||||
|
||||
var suggestions []string
|
||||
for _, cmd := range PluginCmds {
|
||||
if strings.HasPrefix(cmd, input) {
|
||||
suggestions = append(suggestions, cmd)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(suggestions)
|
||||
completions := make([]string, len(suggestions))
|
||||
for i := range suggestions {
|
||||
completions[i] = util.SliceEndStr(suggestions[i], c.X-argstart)
|
||||
}
|
||||
return completions, suggestions
|
||||
}
|
||||
|
||||
// PluginComplete completes values for the plugin command
|
||||
func PluginComplete(b *buffer.Buffer) ([]string, []string) {
|
||||
c := b.GetActiveCursor()
|
||||
l := b.LineBytes(c.Y)
|
||||
l = util.SliceStart(l, c.X)
|
||||
input, argstart := buffer.GetArg(b)
|
||||
|
||||
completeValue := false
|
||||
args := bytes.Split(l, []byte{' '})
|
||||
if len(args) >= 2 {
|
||||
for _, cmd := range PluginCmds {
|
||||
if cmd == string(args[len(args)-2]) {
|
||||
completeValue = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !completeValue {
|
||||
return PluginCmdComplete(b)
|
||||
}
|
||||
|
||||
var suggestions []string
|
||||
for _, pl := range config.Plugins {
|
||||
if strings.HasPrefix(pl.Name, input) {
|
||||
suggestions = append(suggestions, pl.Name)
|
||||
}
|
||||
}
|
||||
sort.Strings(suggestions)
|
||||
|
||||
completions := make([]string, len(suggestions))
|
||||
for i := range suggestions {
|
||||
completions[i] = util.SliceEndStr(suggestions[i], c.X-argstart)
|
||||
}
|
||||
return completions, 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
|
||||
// }
|
||||
226
internal/action/infopane.go
Normal file
226
internal/action/infopane.go
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
package action
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
|
||||
"github.com/zyedidia/micro/internal/display"
|
||||
"github.com/zyedidia/micro/internal/info"
|
||||
"github.com/zyedidia/micro/internal/util"
|
||||
"github.com/zyedidia/tcell"
|
||||
)
|
||||
|
||||
type InfoKeyAction func(*InfoPane)
|
||||
|
||||
type InfoPane struct {
|
||||
*BufPane
|
||||
*info.InfoBuf
|
||||
}
|
||||
|
||||
func NewInfoPane(ib *info.InfoBuf, w display.BWindow) *InfoPane {
|
||||
ip := new(InfoPane)
|
||||
ip.InfoBuf = ib
|
||||
ip.BufPane = NewBufPane(ib.Buffer, w)
|
||||
|
||||
return ip
|
||||
}
|
||||
|
||||
func NewInfoBar() *InfoPane {
|
||||
ib := info.NewBuffer()
|
||||
w := display.NewInfoWindow(ib)
|
||||
return NewInfoPane(ib, w)
|
||||
}
|
||||
|
||||
func (h *InfoPane) Close() {
|
||||
h.InfoBuf.Close()
|
||||
h.BufPane.Close()
|
||||
}
|
||||
|
||||
func (h *InfoPane) HandleEvent(event tcell.Event) {
|
||||
switch e := event.(type) {
|
||||
case *tcell.EventKey:
|
||||
ke := KeyEvent{
|
||||
code: e.Key(),
|
||||
mod: e.Modifiers(),
|
||||
r: e.Rune(),
|
||||
}
|
||||
|
||||
done := h.DoKeyEvent(ke)
|
||||
hasYN := h.HasYN
|
||||
if e.Key() == tcell.KeyRune && hasYN {
|
||||
if e.Rune() == 'y' && hasYN {
|
||||
h.YNResp = true
|
||||
h.DonePrompt(false)
|
||||
} else if e.Rune() == 'n' && hasYN {
|
||||
h.YNResp = false
|
||||
h.DonePrompt(false)
|
||||
}
|
||||
}
|
||||
if e.Key() == tcell.KeyRune && !done && !hasYN {
|
||||
h.DoRuneInsert(e.Rune())
|
||||
done = true
|
||||
}
|
||||
if done && h.HasPrompt && !hasYN {
|
||||
resp := string(h.LineBytes(0))
|
||||
hist := h.History[h.PromptType]
|
||||
hist[h.HistoryNum] = resp
|
||||
if h.EventCallback != nil {
|
||||
h.EventCallback(resp)
|
||||
}
|
||||
}
|
||||
case *tcell.EventMouse:
|
||||
h.BufPane.HandleEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *InfoPane) DoKeyEvent(e KeyEvent) bool {
|
||||
done := false
|
||||
if action, ok := BufKeyBindings[e]; ok {
|
||||
estr := BufKeyStrings[e]
|
||||
for _, s := range InfoNones {
|
||||
if s == estr {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for s, a := range InfoOverrides {
|
||||
// TODO this is a hack and really we should have support
|
||||
// for having binding overrides for different buffers
|
||||
if strings.Contains(estr, s) {
|
||||
done = true
|
||||
a(h)
|
||||
break
|
||||
}
|
||||
}
|
||||
if !done {
|
||||
done = action(h.BufPane)
|
||||
}
|
||||
}
|
||||
return done
|
||||
}
|
||||
|
||||
// InfoNones is a list of actions that should have no effect when executed
|
||||
// by an infohandler
|
||||
var InfoNones = []string{
|
||||
"Save",
|
||||
"SaveAll",
|
||||
"SaveAs",
|
||||
"Find",
|
||||
"FindNext",
|
||||
"FindPrevious",
|
||||
"Center",
|
||||
"DuplicateLine",
|
||||
"MoveLinesUp",
|
||||
"MoveLinesDown",
|
||||
"OpenFile",
|
||||
"Start",
|
||||
"End",
|
||||
"PageUp",
|
||||
"PageDown",
|
||||
"SelectPageUp",
|
||||
"SelectPageDown",
|
||||
"HalfPageUp",
|
||||
"HalfPageDown",
|
||||
"ToggleHelp",
|
||||
"ToggleKeyMenu",
|
||||
"ToggleRuler",
|
||||
"JumpLine",
|
||||
"ClearStatus",
|
||||
"ShellMode",
|
||||
"CommandMode",
|
||||
"AddTab",
|
||||
"PreviousTab",
|
||||
"NextTab",
|
||||
"NextSplit",
|
||||
"PreviousSplit",
|
||||
"Unsplit",
|
||||
"VSplit",
|
||||
"HSplit",
|
||||
"ToggleMacro",
|
||||
"PlayMacro",
|
||||
"Suspend",
|
||||
"ScrollUp",
|
||||
"ScrollDown",
|
||||
"SpawnMultiCursor",
|
||||
"SpawnMultiCursorSelect",
|
||||
"RemoveMultiCursor",
|
||||
"RemoveAllMultiCursors",
|
||||
"SkipMultiCursor",
|
||||
}
|
||||
|
||||
// InfoOverrides is the list of actions which have been overridden
|
||||
// by the infohandler
|
||||
var InfoOverrides = map[string]InfoKeyAction{
|
||||
"CursorUp": (*InfoPane).CursorUp,
|
||||
"CursorDown": (*InfoPane).CursorDown,
|
||||
"InsertNewline": (*InfoPane).InsertNewline,
|
||||
"Autocomplete": (*InfoPane).Autocomplete,
|
||||
"OutdentLine": (*InfoPane).CycleBack,
|
||||
"Escape": (*InfoPane).Escape,
|
||||
"Quit": (*InfoPane).Quit,
|
||||
"QuitAll": (*InfoPane).QuitAll,
|
||||
}
|
||||
|
||||
// CursorUp cycles history up
|
||||
func (h *InfoPane) CursorUp() {
|
||||
h.UpHistory(h.History[h.PromptType])
|
||||
}
|
||||
|
||||
// CursorDown cycles history down
|
||||
func (h *InfoPane) CursorDown() {
|
||||
h.DownHistory(h.History[h.PromptType])
|
||||
}
|
||||
|
||||
// Autocomplete begins autocompletion
|
||||
func (h *InfoPane) Autocomplete() {
|
||||
b := h.Buf
|
||||
if b.HasSuggestions {
|
||||
b.CycleAutocomplete(true)
|
||||
return
|
||||
}
|
||||
|
||||
c := b.GetActiveCursor()
|
||||
l := b.LineBytes(0)
|
||||
l = util.SliceStart(l, c.X)
|
||||
|
||||
args := bytes.Split(l, []byte{' '})
|
||||
cmd := string(args[0])
|
||||
|
||||
if len(args) == 1 {
|
||||
b.Autocomplete(CommandComplete)
|
||||
} else {
|
||||
if action, ok := commands[cmd]; ok {
|
||||
if action.completer != nil {
|
||||
b.Autocomplete(action.completer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CycleBack cycles back in the autocomplete suggestion list
|
||||
func (h *InfoPane) CycleBack() {
|
||||
if h.Buf.HasSuggestions {
|
||||
h.Buf.CycleAutocomplete(false)
|
||||
}
|
||||
}
|
||||
|
||||
// InsertNewline completes the prompt
|
||||
func (h *InfoPane) InsertNewline() {
|
||||
if !h.HasYN {
|
||||
h.DonePrompt(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Quit cancels the prompt
|
||||
func (h *InfoPane) Quit() {
|
||||
h.DonePrompt(true)
|
||||
}
|
||||
|
||||
// QuitAll cancels the prompt
|
||||
func (h *InfoPane) QuitAll() {
|
||||
h.DonePrompt(true)
|
||||
}
|
||||
|
||||
// Escape cancels the prompt
|
||||
func (h *InfoPane) Escape() {
|
||||
h.DonePrompt(true)
|
||||
}
|
||||
14
internal/action/pane.go
Normal file
14
internal/action/pane.go
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
package action
|
||||
|
||||
import (
|
||||
"github.com/zyedidia/micro/internal/display"
|
||||
)
|
||||
|
||||
type Pane interface {
|
||||
Handler
|
||||
display.Window
|
||||
ID() uint64
|
||||
SetID(i uint64)
|
||||
Name() string
|
||||
Close()
|
||||
}
|
||||
40
internal/action/rawpane.go
Normal file
40
internal/action/rawpane.go
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
package action
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/zyedidia/micro/internal/buffer"
|
||||
"github.com/zyedidia/micro/internal/display"
|
||||
"github.com/zyedidia/tcell"
|
||||
)
|
||||
|
||||
type RawPane struct {
|
||||
*BufPane
|
||||
}
|
||||
|
||||
func NewRawPaneFromWin(b *buffer.Buffer, win display.BWindow) *RawPane {
|
||||
rh := new(RawPane)
|
||||
rh.BufPane = NewBufPane(b, win)
|
||||
|
||||
return rh
|
||||
}
|
||||
|
||||
func NewRawPane() *RawPane {
|
||||
b := buffer.NewBufferFromString("", "", buffer.BTRaw)
|
||||
w := display.NewBufWindow(0, 0, 0, 0, b)
|
||||
return NewRawPaneFromWin(b, w)
|
||||
}
|
||||
|
||||
func (h *RawPane) HandleEvent(event tcell.Event) {
|
||||
switch e := event.(type) {
|
||||
case *tcell.EventKey:
|
||||
if e.Key() == tcell.KeyCtrlQ {
|
||||
h.Quit()
|
||||
}
|
||||
}
|
||||
|
||||
h.Buf.Insert(h.Cursor.Loc, reflect.TypeOf(event).String()[7:])
|
||||
h.Buf.Insert(h.Cursor.Loc, fmt.Sprintf(": %q\n", event.EscSeq()))
|
||||
h.Relocate()
|
||||
}
|
||||
288
internal/action/tab.go
Normal file
288
internal/action/tab.go
Normal file
|
|
@ -0,0 +1,288 @@
|
|||
package action
|
||||
|
||||
import (
|
||||
"github.com/zyedidia/micro/internal/buffer"
|
||||
"github.com/zyedidia/micro/internal/config"
|
||||
"github.com/zyedidia/micro/internal/display"
|
||||
"github.com/zyedidia/micro/internal/screen"
|
||||
"github.com/zyedidia/micro/internal/views"
|
||||
"github.com/zyedidia/tcell"
|
||||
)
|
||||
|
||||
// The TabList is a list of tabs and a window to display the tab bar
|
||||
// at the top of the screen
|
||||
type TabList struct {
|
||||
*display.TabWindow
|
||||
List []*Tab
|
||||
}
|
||||
|
||||
// NewTabList creates a TabList from a list of buffers by creating a Tab
|
||||
// for each buffer
|
||||
func NewTabList(bufs []*buffer.Buffer) *TabList {
|
||||
w, h := screen.Screen.Size()
|
||||
iOffset := config.GetInfoBarOffset()
|
||||
tl := new(TabList)
|
||||
tl.List = make([]*Tab, len(bufs))
|
||||
if len(bufs) > 1 {
|
||||
for i, b := range bufs {
|
||||
tl.List[i] = NewTabFromBuffer(0, 1, w, h-1-iOffset, b)
|
||||
}
|
||||
} else {
|
||||
tl.List[0] = NewTabFromBuffer(0, 0, w, h-iOffset, bufs[0])
|
||||
}
|
||||
tl.TabWindow = display.NewTabWindow(w, 0)
|
||||
tl.Names = make([]string, len(bufs))
|
||||
|
||||
return tl
|
||||
}
|
||||
|
||||
// UpdateNames makes sure that the list of names the tab window has access to is
|
||||
// correct
|
||||
func (t *TabList) UpdateNames() {
|
||||
t.Names = t.Names[:0]
|
||||
for _, p := range t.List {
|
||||
t.Names = append(t.Names, p.Panes[p.active].Name())
|
||||
}
|
||||
}
|
||||
|
||||
// AddTab adds a new tab to this TabList
|
||||
func (t *TabList) AddTab(p *Tab) {
|
||||
t.List = append(t.List, p)
|
||||
t.Resize()
|
||||
t.UpdateNames()
|
||||
}
|
||||
|
||||
// RemoveTab removes a tab with the given id from the TabList
|
||||
func (t *TabList) RemoveTab(id uint64) {
|
||||
for i, p := range t.List {
|
||||
if len(p.Panes) == 0 {
|
||||
continue
|
||||
}
|
||||
if p.Panes[0].ID() == id {
|
||||
copy(t.List[i:], t.List[i+1:])
|
||||
t.List[len(t.List)-1] = nil
|
||||
t.List = t.List[:len(t.List)-1]
|
||||
if t.Active() >= len(t.List) {
|
||||
t.SetActive(len(t.List) - 1)
|
||||
}
|
||||
t.Resize()
|
||||
t.UpdateNames()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Resize resizes all elements within the tab list
|
||||
// One thing to note is that when there is only 1 tab
|
||||
// the tab bar should not be drawn so resizing must take
|
||||
// that into account
|
||||
func (t *TabList) Resize() {
|
||||
w, h := screen.Screen.Size()
|
||||
iOffset := config.GetInfoBarOffset()
|
||||
InfoBar.Resize(w, h-1)
|
||||
if len(t.List) > 1 {
|
||||
for _, p := range t.List {
|
||||
p.Y = 1
|
||||
p.Node.Resize(w, h-1-iOffset)
|
||||
p.Resize()
|
||||
}
|
||||
} else if len(t.List) == 1 {
|
||||
t.List[0].Y = 0
|
||||
t.List[0].Node.Resize(w, h-iOffset)
|
||||
t.List[0].Resize()
|
||||
}
|
||||
}
|
||||
|
||||
// HandleEvent checks for a resize event or a mouse event on the tab bar
|
||||
// otherwise it will forward the event to the currently active tab
|
||||
func (t *TabList) HandleEvent(event tcell.Event) {
|
||||
switch e := event.(type) {
|
||||
case *tcell.EventResize:
|
||||
t.Resize()
|
||||
case *tcell.EventMouse:
|
||||
mx, my := e.Position()
|
||||
switch e.Buttons() {
|
||||
case tcell.Button1:
|
||||
if len(t.List) > 1 {
|
||||
ind := t.LocFromVisual(buffer.Loc{mx, my})
|
||||
if ind != -1 {
|
||||
t.SetActive(ind)
|
||||
return
|
||||
}
|
||||
if my == 0 {
|
||||
return
|
||||
}
|
||||
}
|
||||
case tcell.WheelUp:
|
||||
if my == t.Y {
|
||||
t.Scroll(4)
|
||||
return
|
||||
}
|
||||
case tcell.WheelDown:
|
||||
if my == t.Y {
|
||||
t.Scroll(-4)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
t.List[t.Active()].HandleEvent(event)
|
||||
}
|
||||
|
||||
// Display updates the names and then displays the tab bar
|
||||
func (t *TabList) Display() {
|
||||
t.UpdateNames()
|
||||
if len(t.List) > 1 {
|
||||
t.TabWindow.Display()
|
||||
}
|
||||
}
|
||||
|
||||
// Tabs is the global tab list
|
||||
var Tabs *TabList
|
||||
|
||||
func InitTabs(bufs []*buffer.Buffer) {
|
||||
Tabs = NewTabList(bufs)
|
||||
}
|
||||
|
||||
func MainTab() *Tab {
|
||||
return Tabs.List[Tabs.Active()]
|
||||
}
|
||||
|
||||
// A Tab represents a single tab
|
||||
// It consists of a list of edit panes (the open buffers),
|
||||
// a split tree (stored as just the root node), and a uiwindow
|
||||
// to display the UI elements like the borders between splits
|
||||
type Tab struct {
|
||||
*views.Node
|
||||
*display.UIWindow
|
||||
Panes []Pane
|
||||
active int
|
||||
|
||||
resizing *views.Node // node currently being resized
|
||||
}
|
||||
|
||||
// NewTabFromBuffer creates a new tab from the given buffer
|
||||
func NewTabFromBuffer(x, y, width, height int, b *buffer.Buffer) *Tab {
|
||||
t := new(Tab)
|
||||
t.Node = views.NewRoot(x, y, width, height)
|
||||
t.UIWindow = display.NewUIWindow(t.Node)
|
||||
|
||||
e := NewBufPaneFromBuf(b)
|
||||
e.SetID(t.ID())
|
||||
|
||||
t.Panes = append(t.Panes, e)
|
||||
return t
|
||||
}
|
||||
|
||||
func NewTabFromPane(x, y, width, height int, pane Pane) *Tab {
|
||||
t := new(Tab)
|
||||
t.Node = views.NewRoot(x, y, width, height)
|
||||
t.UIWindow = display.NewUIWindow(t.Node)
|
||||
|
||||
pane.SetID(t.ID())
|
||||
|
||||
t.Panes = append(t.Panes, pane)
|
||||
return t
|
||||
}
|
||||
|
||||
// HandleEvent takes a tcell event and usually dispatches it to the current
|
||||
// active pane. However if the event is a resize or a mouse event where the user
|
||||
// is interacting with the UI (resizing splits) then the event is consumed here
|
||||
// If the event is a mouse event in a pane, that pane will become active and get
|
||||
// the event
|
||||
func (t *Tab) HandleEvent(event tcell.Event) {
|
||||
switch e := event.(type) {
|
||||
case *tcell.EventMouse:
|
||||
mx, my := e.Position()
|
||||
switch e.Buttons() {
|
||||
case tcell.Button1:
|
||||
resizeID := t.GetMouseSplitID(buffer.Loc{mx, my})
|
||||
if t.resizing != nil {
|
||||
var size int
|
||||
if t.resizing.Kind == views.STVert {
|
||||
size = mx - t.resizing.X
|
||||
} else {
|
||||
size = my - t.resizing.Y + 1
|
||||
}
|
||||
t.resizing.ResizeSplit(size)
|
||||
t.Resize()
|
||||
return
|
||||
}
|
||||
|
||||
if resizeID != 0 {
|
||||
t.resizing = t.GetNode(uint64(resizeID))
|
||||
return
|
||||
}
|
||||
|
||||
for i, p := range t.Panes {
|
||||
v := p.GetView()
|
||||
inpane := mx >= v.X && mx < v.X+v.Width && my >= v.Y && my < v.Y+v.Height
|
||||
if inpane {
|
||||
t.SetActive(i)
|
||||
break
|
||||
}
|
||||
}
|
||||
case tcell.ButtonNone:
|
||||
t.resizing = nil
|
||||
default:
|
||||
for _, p := range t.Panes {
|
||||
v := p.GetView()
|
||||
inpane := mx >= v.X && mx < v.X+v.Width && my >= v.Y && my < v.Y+v.Height
|
||||
if inpane {
|
||||
p.HandleEvent(event)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
t.Panes[t.active].HandleEvent(event)
|
||||
}
|
||||
|
||||
// SetActive changes the currently active pane to the specified index
|
||||
func (t *Tab) SetActive(i int) {
|
||||
t.active = i
|
||||
for j, p := range t.Panes {
|
||||
if j == i {
|
||||
p.SetActive(true)
|
||||
} else {
|
||||
p.SetActive(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetPane returns the pane with the given split index
|
||||
func (t *Tab) GetPane(splitid uint64) int {
|
||||
for i, p := range t.Panes {
|
||||
if p.ID() == splitid {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Remove pane removes the pane with the given index
|
||||
func (t *Tab) RemovePane(i int) {
|
||||
copy(t.Panes[i:], t.Panes[i+1:])
|
||||
t.Panes[len(t.Panes)-1] = nil
|
||||
t.Panes = t.Panes[:len(t.Panes)-1]
|
||||
}
|
||||
|
||||
// Resize resizes all panes according to their corresponding split nodes
|
||||
func (t *Tab) Resize() {
|
||||
for _, p := range t.Panes {
|
||||
n := t.GetNode(p.ID())
|
||||
pv := p.GetView()
|
||||
offset := 0
|
||||
if n.X != 0 {
|
||||
offset = 1
|
||||
}
|
||||
pv.X, pv.Y = n.X+offset, n.Y
|
||||
p.SetView(pv)
|
||||
p.Resize(n.W-offset, n.H)
|
||||
}
|
||||
}
|
||||
|
||||
// CurPane returns the currently active pane
|
||||
func (t *Tab) CurPane() Pane {
|
||||
return t.Panes[t.active]
|
||||
}
|
||||
29
internal/action/terminal_supported.go
Normal file
29
internal/action/terminal_supported.go
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
// +build linux darwin dragonfly openbsd_amd64 freebsd
|
||||
|
||||
package action
|
||||
|
||||
import (
|
||||
"github.com/zyedidia/micro/internal/shell"
|
||||
"github.com/zyedidia/micro/pkg/shellwords"
|
||||
)
|
||||
|
||||
const TermEmuSupported = true
|
||||
|
||||
func RunTermEmulator(h *BufPane, input string, wait bool, getOutput bool, callback string, userargs []interface{}) error {
|
||||
args, err := shellwords.Split(input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t := new(shell.Terminal)
|
||||
t.Start(args, getOutput, wait, callback, userargs)
|
||||
|
||||
h.AddTab()
|
||||
id := MainTab().Panes[0].ID()
|
||||
|
||||
v := h.GetView()
|
||||
MainTab().Panes[0] = NewTermPane(v.X, v.Y, v.Width, v.Height, t, id)
|
||||
MainTab().SetActive(0)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
// +build !linux,!darwin,!freebsd,!dragonfly,!openbsd_amd64
|
||||
|
||||
package main
|
||||
package action
|
||||
|
||||
import "errors"
|
||||
|
||||
const TermEmuSupported = false
|
||||
|
||||
func RunTermEmulator(input string, wait bool, getOutput bool) error {
|
||||
func RunTermEmulator(input string, wait bool, getOutput bool, callback string, userargs []interface{}) error {
|
||||
return errors.New("Unsupported operating system")
|
||||
}
|
||||
120
internal/action/termpane.go
Normal file
120
internal/action/termpane.go
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
package action
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
|
||||
"github.com/zyedidia/clipboard"
|
||||
"github.com/zyedidia/micro/internal/display"
|
||||
"github.com/zyedidia/micro/internal/screen"
|
||||
"github.com/zyedidia/micro/internal/shell"
|
||||
"github.com/zyedidia/tcell"
|
||||
"github.com/zyedidia/terminal"
|
||||
)
|
||||
|
||||
type TermPane struct {
|
||||
*shell.Terminal
|
||||
display.Window
|
||||
|
||||
mouseReleased bool
|
||||
id uint64
|
||||
}
|
||||
|
||||
func NewTermPane(x, y, w, h int, t *shell.Terminal, id uint64) *TermPane {
|
||||
th := new(TermPane)
|
||||
th.Terminal = t
|
||||
th.id = id
|
||||
th.mouseReleased = true
|
||||
th.Window = display.NewTermWindow(x, y, w, h, t)
|
||||
return th
|
||||
}
|
||||
|
||||
func (t *TermPane) ID() uint64 {
|
||||
return t.id
|
||||
}
|
||||
|
||||
func (t *TermPane) SetID(i uint64) {
|
||||
t.id = i
|
||||
}
|
||||
|
||||
func (t *TermPane) Close() {}
|
||||
|
||||
func (t *TermPane) Quit() {
|
||||
t.Close()
|
||||
if len(MainTab().Panes) > 1 {
|
||||
t.Unsplit()
|
||||
} else if len(Tabs.List) > 1 {
|
||||
Tabs.RemoveTab(t.id)
|
||||
} else {
|
||||
screen.Screen.Fini()
|
||||
InfoBar.Close()
|
||||
runtime.Goexit()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TermPane) Unsplit() {
|
||||
n := MainTab().GetNode(t.id)
|
||||
n.Unsplit()
|
||||
|
||||
MainTab().RemovePane(MainTab().GetPane(t.id))
|
||||
MainTab().Resize()
|
||||
MainTab().SetActive(len(MainTab().Panes) - 1)
|
||||
}
|
||||
|
||||
// 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 *TermPane) HandleEvent(event tcell.Event) {
|
||||
if e, ok := event.(*tcell.EventKey); ok {
|
||||
if t.Status == shell.TTDone {
|
||||
switch e.Key() {
|
||||
case tcell.KeyEscape, tcell.KeyCtrlQ, tcell.KeyEnter:
|
||||
t.Close()
|
||||
t.Quit()
|
||||
default:
|
||||
}
|
||||
}
|
||||
if e.Key() == tcell.KeyCtrlC && t.HasSelection() {
|
||||
clipboard.WriteAll(t.GetSelection(t.GetView().Width), "clipboard")
|
||||
InfoBar.Message("Copied selection to clipboard")
|
||||
} else if t.Status != shell.TTDone {
|
||||
t.WriteString(event.EscSeq())
|
||||
}
|
||||
} else if e, ok := event.(*tcell.EventMouse); e != nil && (!ok || t.State.Mode(terminal.ModeMouseMask)) {
|
||||
t.WriteString(event.EscSeq())
|
||||
} else if e != nil {
|
||||
x, y := e.Position()
|
||||
v := t.GetView()
|
||||
x -= v.X
|
||||
y -= v.Y
|
||||
|
||||
if e.Buttons() == tcell.Button1 {
|
||||
if !t.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.mouseReleased = false
|
||||
} else if e.Buttons() == tcell.ButtonNone {
|
||||
if !t.mouseReleased {
|
||||
t.Selection[1].X = x
|
||||
t.Selection[1].Y = y
|
||||
}
|
||||
t.mouseReleased = true
|
||||
}
|
||||
}
|
||||
|
||||
if t.Status == shell.TTClose {
|
||||
t.Quit()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TermPane) HandleCommand(input string) {
|
||||
InfoBar.Error("Commands are unsupported in term for now")
|
||||
}
|
||||
206
internal/buffer/autocomplete.go
Normal file
206
internal/buffer/autocomplete.go
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
package buffer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/zyedidia/micro/internal/util"
|
||||
)
|
||||
|
||||
// A Completer is a function that takes a buffer and returns info
|
||||
// describing what autocompletions should be inserted at the current
|
||||
// cursor location
|
||||
// It returns a list of string suggestions which will be inserted at
|
||||
// the current cursor location if selected as well as a list of
|
||||
// suggestion names which can be displayed in an autocomplete box or
|
||||
// other UI element
|
||||
type Completer func(*Buffer) ([]string, []string)
|
||||
|
||||
func (b *Buffer) GetSuggestions() {
|
||||
|
||||
}
|
||||
|
||||
// Autocomplete starts the autocomplete process
|
||||
func (b *Buffer) Autocomplete(c Completer) bool {
|
||||
b.Completions, b.Suggestions = c(b)
|
||||
if len(b.Completions) != len(b.Suggestions) || len(b.Completions) == 0 {
|
||||
return false
|
||||
}
|
||||
b.CurSuggestion = -1
|
||||
b.CycleAutocomplete(true)
|
||||
return true
|
||||
}
|
||||
|
||||
// CycleAutocomplete moves to the next suggestion
|
||||
func (b *Buffer) CycleAutocomplete(forward bool) {
|
||||
prevSuggestion := b.CurSuggestion
|
||||
|
||||
if forward {
|
||||
b.CurSuggestion++
|
||||
} else {
|
||||
b.CurSuggestion--
|
||||
}
|
||||
if b.CurSuggestion >= len(b.Suggestions) {
|
||||
b.CurSuggestion = 0
|
||||
} else if b.CurSuggestion < 0 {
|
||||
b.CurSuggestion = len(b.Suggestions) - 1
|
||||
}
|
||||
|
||||
c := b.GetActiveCursor()
|
||||
start := c.Loc
|
||||
end := c.Loc
|
||||
if prevSuggestion < len(b.Suggestions) && prevSuggestion >= 0 {
|
||||
start = end.Move(-utf8.RuneCountInString(b.Completions[prevSuggestion]), b)
|
||||
} else {
|
||||
// end = start.Move(1, b)
|
||||
}
|
||||
|
||||
b.Replace(start, end, b.Completions[b.CurSuggestion])
|
||||
if len(b.Suggestions) > 1 {
|
||||
b.HasSuggestions = true
|
||||
}
|
||||
}
|
||||
|
||||
// GetWord gets the most recent word separated by any separator
|
||||
// (whitespace, punctuation, any non alphanumeric character)
|
||||
func GetWord(b *Buffer) ([]byte, int) {
|
||||
c := b.GetActiveCursor()
|
||||
l := b.LineBytes(c.Y)
|
||||
l = util.SliceStart(l, c.X)
|
||||
|
||||
if c.X == 0 || util.IsWhitespace(b.RuneAt(c.Loc)) {
|
||||
return []byte{}, -1
|
||||
}
|
||||
|
||||
if util.IsNonAlphaNumeric(b.RuneAt(c.Loc)) {
|
||||
return []byte{}, c.X
|
||||
}
|
||||
|
||||
args := bytes.FieldsFunc(l, util.IsNonAlphaNumeric)
|
||||
input := args[len(args)-1]
|
||||
return input, c.X - utf8.RuneCount(input)
|
||||
}
|
||||
|
||||
// GetArg gets the most recent word (separated by ' ' only)
|
||||
func GetArg(b *Buffer) (string, int) {
|
||||
c := b.GetActiveCursor()
|
||||
l := b.LineBytes(c.Y)
|
||||
l = util.SliceStart(l, c.X)
|
||||
|
||||
args := bytes.Split(l, []byte{' '})
|
||||
input := string(args[len(args)-1])
|
||||
argstart := 0
|
||||
for i, a := range args {
|
||||
if i == len(args)-1 {
|
||||
break
|
||||
}
|
||||
argstart += utf8.RuneCount(a) + 1
|
||||
}
|
||||
|
||||
return input, argstart
|
||||
}
|
||||
|
||||
// FileComplete autocompletes filenames
|
||||
func FileComplete(b *Buffer) ([]string, []string) {
|
||||
c := b.GetActiveCursor()
|
||||
input, argstart := GetArg(b)
|
||||
|
||||
sep := 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, _ = util.ReplaceHome(directories)
|
||||
files, err = ioutil.ReadDir(directories)
|
||||
} else {
|
||||
files, err = ioutil.ReadDir(".")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var suggestions []string
|
||||
for _, f := range files {
|
||||
name := f.Name()
|
||||
if f.IsDir() {
|
||||
name += sep
|
||||
}
|
||||
if strings.HasPrefix(name, dirs[len(dirs)-1]) {
|
||||
suggestions = append(suggestions, name)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(suggestions)
|
||||
completions := make([]string, len(suggestions))
|
||||
for i := range suggestions {
|
||||
var complete string
|
||||
if len(dirs) > 1 {
|
||||
complete = strings.Join(dirs[:len(dirs)-1], sep) + sep + suggestions[i]
|
||||
} else {
|
||||
complete = suggestions[i]
|
||||
}
|
||||
completions[i] = util.SliceEndStr(complete, c.X-argstart)
|
||||
}
|
||||
|
||||
return completions, suggestions
|
||||
}
|
||||
|
||||
// BufferComplete autocompletes based on previous words in the buffer
|
||||
func BufferComplete(b *Buffer) ([]string, []string) {
|
||||
c := b.GetActiveCursor()
|
||||
input, argstart := GetWord(b)
|
||||
|
||||
if argstart == -1 {
|
||||
return []string{}, []string{}
|
||||
}
|
||||
|
||||
inputLen := utf8.RuneCount(input)
|
||||
|
||||
suggestionsSet := make(map[string]struct{})
|
||||
|
||||
var suggestions []string
|
||||
for i := c.Y; i >= 0; i-- {
|
||||
l := b.LineBytes(i)
|
||||
words := bytes.FieldsFunc(l, util.IsNonAlphaNumeric)
|
||||
for _, w := range words {
|
||||
if bytes.HasPrefix(w, input) && utf8.RuneCount(w) > inputLen {
|
||||
strw := string(w)
|
||||
if _, ok := suggestionsSet[strw]; !ok {
|
||||
suggestionsSet[strw] = struct{}{}
|
||||
suggestions = append(suggestions, strw)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for i := c.Y + 1; i < b.LinesNum(); i++ {
|
||||
l := b.LineBytes(i)
|
||||
words := bytes.FieldsFunc(l, util.IsNonAlphaNumeric)
|
||||
for _, w := range words {
|
||||
if bytes.HasPrefix(w, input) && utf8.RuneCount(w) > inputLen {
|
||||
strw := string(w)
|
||||
if _, ok := suggestionsSet[strw]; !ok {
|
||||
suggestionsSet[strw] = struct{}{}
|
||||
suggestions = append(suggestions, strw)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(suggestions) > 1 {
|
||||
suggestions = append(suggestions, string(input))
|
||||
}
|
||||
|
||||
completions := make([]string, len(suggestions))
|
||||
for i := range suggestions {
|
||||
completions[i] = util.SliceEndStr(suggestions[i], c.X-argstart)
|
||||
}
|
||||
|
||||
return completions, suggestions
|
||||
}
|
||||
120
internal/buffer/backup.go
Normal file
120
internal/buffer/backup.go
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
package buffer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/zyedidia/micro/internal/config"
|
||||
"github.com/zyedidia/micro/internal/screen"
|
||||
"github.com/zyedidia/micro/internal/util"
|
||||
"golang.org/x/text/encoding"
|
||||
)
|
||||
|
||||
const backupMsg = `A backup was detected for this file. This likely means that micro
|
||||
crashed while editing this file, or another instance of micro is currently
|
||||
editing this file.
|
||||
|
||||
The backup was created on %s, and the file is
|
||||
|
||||
%s
|
||||
|
||||
* 'recover' will apply the backup as unsaved changes to the current buffer.
|
||||
When the buffer is closed, the backup will be removed.
|
||||
* 'ignore' will ignore the backup, discarding its changes. The backup file
|
||||
will be removed.
|
||||
|
||||
Options: [r]ecover, [i]gnore: `
|
||||
|
||||
// Backup saves the current buffer to ConfigDir/backups
|
||||
func (b *Buffer) Backup(checkTime bool) error {
|
||||
if !b.Settings["backup"].(bool) || b.Path == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if checkTime {
|
||||
sub := time.Now().Sub(b.lastbackup)
|
||||
if sub < time.Duration(backupTime)*time.Millisecond {
|
||||
log.Println("Backup event but not enough time has passed", sub)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
b.lastbackup = time.Now()
|
||||
|
||||
backupdir := config.ConfigDir + "/backups/"
|
||||
if _, err := os.Stat(backupdir); os.IsNotExist(err) {
|
||||
os.Mkdir(backupdir, os.ModePerm)
|
||||
log.Println("Creating backup dir")
|
||||
}
|
||||
|
||||
name := backupdir + util.EscapePath(b.AbsPath)
|
||||
|
||||
log.Println("Backing up to", name)
|
||||
|
||||
err := overwriteFile(name, encoding.Nop, func(file io.Writer) (e error) {
|
||||
if len(b.lines) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// end of line
|
||||
eol := []byte{'\n'}
|
||||
|
||||
// write lines
|
||||
if _, e = file.Write(b.lines[0].data); e != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, l := range b.lines[1:] {
|
||||
if _, e = file.Write(eol); e != nil {
|
||||
return
|
||||
}
|
||||
if _, e = file.Write(l.data); e != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// RemoveBackup removes any backup file associated with this buffer
|
||||
func (b *Buffer) RemoveBackup() {
|
||||
if !b.Settings["backup"].(bool) || b.Path == "" {
|
||||
return
|
||||
}
|
||||
f := config.ConfigDir + "/backups/" + util.EscapePath(b.AbsPath)
|
||||
os.Remove(f)
|
||||
}
|
||||
|
||||
// ApplyBackup applies the corresponding backup file to this buffer (if one exists)
|
||||
// Returns true if a backup was applied
|
||||
func (b *Buffer) ApplyBackup(fsize int64) bool {
|
||||
if b.Settings["backup"].(bool) && len(b.Path) > 0 {
|
||||
backupfile := config.ConfigDir + "/backups/" + util.EscapePath(b.AbsPath)
|
||||
if info, err := os.Stat(backupfile); err == nil {
|
||||
backup, err := os.Open(backupfile)
|
||||
if err == nil {
|
||||
defer backup.Close()
|
||||
t := info.ModTime()
|
||||
msg := fmt.Sprintf(backupMsg, t.Format("Mon Jan _2 at 15:04, 2006"), util.EscapePath(b.AbsPath))
|
||||
choice := screen.TermPrompt(msg, []string{"r", "i", "recover", "ignore"}, true)
|
||||
|
||||
if choice%2 == 0 {
|
||||
// recover
|
||||
b.LineArray = NewLineArray(uint64(fsize), FFAuto, backup)
|
||||
b.isModified = true
|
||||
return true
|
||||
} else if choice%2 == 1 {
|
||||
// delete
|
||||
os.Remove(backupfile)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
878
internal/buffer/buffer.go
Normal file
878
internal/buffer/buffer.go
Normal file
|
|
@ -0,0 +1,878 @@
|
|||
package buffer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
luar "layeh.com/gopher-luar"
|
||||
|
||||
"github.com/zyedidia/micro/internal/config"
|
||||
ulua "github.com/zyedidia/micro/internal/lua"
|
||||
"github.com/zyedidia/micro/internal/screen"
|
||||
"github.com/zyedidia/micro/internal/util"
|
||||
"github.com/zyedidia/micro/pkg/highlight"
|
||||
"golang.org/x/text/encoding/htmlindex"
|
||||
"golang.org/x/text/encoding/unicode"
|
||||
"golang.org/x/text/transform"
|
||||
)
|
||||
|
||||
const backupTime = 8000
|
||||
|
||||
var (
|
||||
OpenBuffers []*Buffer
|
||||
LogBuf *Buffer
|
||||
)
|
||||
|
||||
// The BufType defines what kind of buffer this is
|
||||
type BufType struct {
|
||||
Kind int
|
||||
Readonly bool // The buffer cannot be edited
|
||||
Scratch bool // The buffer cannot be saved
|
||||
Syntax bool // Syntax highlighting is enabled
|
||||
}
|
||||
|
||||
var (
|
||||
BTDefault = BufType{0, false, false, true}
|
||||
BTHelp = BufType{1, true, true, true}
|
||||
BTLog = BufType{2, true, true, false}
|
||||
BTScratch = BufType{3, false, true, false}
|
||||
BTRaw = BufType{4, false, true, false}
|
||||
BTInfo = BufType{5, false, true, false}
|
||||
|
||||
ErrFileTooLarge = errors.New("File is too large to hash")
|
||||
)
|
||||
|
||||
type SharedBuffer struct {
|
||||
*LineArray
|
||||
// Stores the last modification time of the file the buffer is pointing to
|
||||
ModTime time.Time
|
||||
// Type of the buffer (e.g. help, raw, scratch etc..)
|
||||
Type BufType
|
||||
|
||||
isModified bool
|
||||
// Whether or not suggestions can be autocompleted must be shared because
|
||||
// it changes based on how the buffer has changed
|
||||
HasSuggestions bool
|
||||
}
|
||||
|
||||
func (b *SharedBuffer) insert(pos Loc, value []byte) {
|
||||
b.isModified = true
|
||||
b.HasSuggestions = false
|
||||
b.LineArray.insert(pos, value)
|
||||
}
|
||||
func (b *SharedBuffer) remove(start, end Loc) []byte {
|
||||
b.isModified = true
|
||||
b.HasSuggestions = false
|
||||
return b.LineArray.remove(start, end)
|
||||
}
|
||||
|
||||
// Buffer stores the main information about a currently open file including
|
||||
// the actual text (in a LineArray), the undo/redo stack (in an EventHandler)
|
||||
// all the cursors, the syntax highlighting info, the settings for the buffer
|
||||
// and some misc info about modification time and path location.
|
||||
// The syntax highlighting info must be stored with the buffer because the syntax
|
||||
// highlighter attaches information to each line of the buffer for optimization
|
||||
// purposes so it doesn't have to rehighlight everything on every update.
|
||||
type Buffer struct {
|
||||
*EventHandler
|
||||
*SharedBuffer
|
||||
|
||||
cursors []*Cursor
|
||||
curCursor int
|
||||
StartCursor Loc
|
||||
|
||||
// Path to the file on disk
|
||||
Path string
|
||||
// Absolute path to the file on disk
|
||||
AbsPath string
|
||||
// Name of the buffer on the status line
|
||||
name string
|
||||
|
||||
SyntaxDef *highlight.Def
|
||||
Highlighter *highlight.Highlighter
|
||||
|
||||
// Hash of the original buffer -- empty if fastdirty is on
|
||||
origHash [md5.Size]byte
|
||||
|
||||
// Settings customized by the user
|
||||
Settings map[string]interface{}
|
||||
|
||||
Suggestions []string
|
||||
Completions []string
|
||||
CurSuggestion int
|
||||
|
||||
Messages []*Message
|
||||
|
||||
// counts the number of edits
|
||||
// resets every backupTime edits
|
||||
lastbackup time.Time
|
||||
}
|
||||
|
||||
// NewBufferFromFile opens a new buffer using the given path
|
||||
// It will also automatically handle `~`, and line/column with filename:l:c
|
||||
// It will return an empty buffer if the path does not exist
|
||||
// and an error if the file is a directory
|
||||
func NewBufferFromFile(path string, btype BufType) (*Buffer, error) {
|
||||
var err error
|
||||
filename, cursorPos := util.GetPathAndCursorPosition(path)
|
||||
filename, err = util.ReplaceHome(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
file, err := os.Open(filename)
|
||||
fileInfo, _ := os.Stat(filename)
|
||||
|
||||
if err == nil && fileInfo.IsDir() {
|
||||
return nil, errors.New(filename + " is a directory")
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
cursorLoc, cursorerr := ParseCursorLocation(cursorPos)
|
||||
if cursorerr != nil {
|
||||
cursorLoc = Loc{-1, -1}
|
||||
}
|
||||
|
||||
var buf *Buffer
|
||||
if err != nil {
|
||||
// File does not exist -- create an empty buffer with that name
|
||||
buf = NewBufferFromString("", filename, btype)
|
||||
} else {
|
||||
buf = NewBuffer(file, util.FSize(file), filename, cursorLoc, btype)
|
||||
}
|
||||
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
// NewBufferFromString creates a new buffer containing the given string
|
||||
func NewBufferFromString(text, path string, btype BufType) *Buffer {
|
||||
return NewBuffer(strings.NewReader(text), int64(len(text)), path, Loc{-1, -1}, btype)
|
||||
}
|
||||
|
||||
// NewBuffer creates a new buffer from a given reader with a given path
|
||||
// Ensure that ReadSettings and InitGlobalSettings have been called before creating
|
||||
// a new buffer
|
||||
// Places the cursor at startcursor. If startcursor is -1, -1 places the
|
||||
// cursor at an autodetected location (based on savecursor or :LINE:COL)
|
||||
func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufType) *Buffer {
|
||||
absPath, _ := filepath.Abs(path)
|
||||
|
||||
b := new(Buffer)
|
||||
|
||||
b.Settings = config.DefaultCommonSettings()
|
||||
for k, v := range config.GlobalSettings {
|
||||
if _, ok := b.Settings[k]; ok {
|
||||
b.Settings[k] = v
|
||||
}
|
||||
}
|
||||
config.InitLocalSettings(b.Settings, path)
|
||||
|
||||
enc, err := htmlindex.Get(b.Settings["encoding"].(string))
|
||||
if err != nil {
|
||||
enc = unicode.UTF8
|
||||
b.Settings["encoding"] = "utf-8"
|
||||
}
|
||||
|
||||
reader := transform.NewReader(r, enc.NewDecoder())
|
||||
|
||||
found := false
|
||||
if len(path) > 0 {
|
||||
for _, buf := range OpenBuffers {
|
||||
if buf.AbsPath == absPath && buf.Type != BTInfo {
|
||||
found = true
|
||||
b.SharedBuffer = buf.SharedBuffer
|
||||
b.EventHandler = buf.EventHandler
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
b.Path = path
|
||||
b.AbsPath = absPath
|
||||
|
||||
if !found {
|
||||
b.SharedBuffer = new(SharedBuffer)
|
||||
b.Type = btype
|
||||
|
||||
hasBackup := b.ApplyBackup(size)
|
||||
|
||||
if !hasBackup {
|
||||
b.LineArray = NewLineArray(uint64(size), FFAuto, reader)
|
||||
}
|
||||
b.EventHandler = NewEventHandler(b.SharedBuffer, b.cursors)
|
||||
}
|
||||
|
||||
if b.Settings["readonly"].(bool) {
|
||||
b.Type.Readonly = true
|
||||
}
|
||||
|
||||
// The last time this file was modified
|
||||
b.UpdateModTime()
|
||||
|
||||
switch b.Endings {
|
||||
case FFUnix:
|
||||
b.Settings["fileformat"] = "unix"
|
||||
case FFDos:
|
||||
b.Settings["fileformat"] = "dos"
|
||||
}
|
||||
|
||||
b.UpdateRules()
|
||||
config.InitLocalSettings(b.Settings, b.Path)
|
||||
|
||||
if _, err := os.Stat(config.ConfigDir + "/buffers/"); os.IsNotExist(err) {
|
||||
os.Mkdir(config.ConfigDir+"/buffers/", os.ModePerm)
|
||||
}
|
||||
|
||||
if startcursor.X != -1 && startcursor.Y != -1 {
|
||||
b.StartCursor = startcursor
|
||||
} else {
|
||||
if b.Settings["savecursor"].(bool) || b.Settings["saveundo"].(bool) {
|
||||
err := b.Unserialize()
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
b.AddCursor(NewCursor(b, b.StartCursor))
|
||||
b.GetActiveCursor().Relocate()
|
||||
|
||||
if !b.Settings["fastdirty"].(bool) {
|
||||
if size > LargeFileThreshold {
|
||||
// If the file is larger than LargeFileThreshold fastdirty needs to be on
|
||||
b.Settings["fastdirty"] = true
|
||||
} else {
|
||||
calcHash(b, &b.origHash)
|
||||
}
|
||||
}
|
||||
|
||||
err = config.RunPluginFn("onBufferOpen", luar.New(ulua.L, b))
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
|
||||
OpenBuffers = append(OpenBuffers, b)
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
// Close removes this buffer from the list of open buffers
|
||||
func (b *Buffer) Close() {
|
||||
for i, buf := range OpenBuffers {
|
||||
if b == buf {
|
||||
b.Fini()
|
||||
copy(OpenBuffers[i:], OpenBuffers[i+1:])
|
||||
OpenBuffers[len(OpenBuffers)-1] = nil
|
||||
OpenBuffers = OpenBuffers[:len(OpenBuffers)-1]
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fini should be called when a buffer is closed and performs
|
||||
// some cleanup
|
||||
func (b *Buffer) Fini() {
|
||||
if !b.Modified() {
|
||||
b.Serialize()
|
||||
}
|
||||
b.RemoveBackup()
|
||||
}
|
||||
|
||||
// GetName returns the name that should be displayed in the statusline
|
||||
// for this buffer
|
||||
func (b *Buffer) GetName() string {
|
||||
if b.name == "" {
|
||||
if b.Path == "" {
|
||||
return "No name"
|
||||
}
|
||||
return b.Path
|
||||
}
|
||||
return b.name
|
||||
}
|
||||
|
||||
//SetName changes the name for this buffer
|
||||
func (b *Buffer) SetName(s string) {
|
||||
b.name = s
|
||||
}
|
||||
|
||||
func (b *Buffer) Insert(start Loc, text string) {
|
||||
if !b.Type.Readonly {
|
||||
b.EventHandler.cursors = b.cursors
|
||||
b.EventHandler.active = b.curCursor
|
||||
b.EventHandler.Insert(start, text)
|
||||
|
||||
go b.Backup(true)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Buffer) Remove(start, end Loc) {
|
||||
if !b.Type.Readonly {
|
||||
b.EventHandler.cursors = b.cursors
|
||||
b.EventHandler.active = b.curCursor
|
||||
b.EventHandler.Remove(start, end)
|
||||
|
||||
go b.Backup(true)
|
||||
}
|
||||
}
|
||||
|
||||
// FileType returns the buffer's filetype
|
||||
func (b *Buffer) FileType() string {
|
||||
return b.Settings["filetype"].(string)
|
||||
}
|
||||
|
||||
// ExternallyModified returns whether the file being edited has
|
||||
// been modified by some external process
|
||||
func (b *Buffer) ExternallyModified() bool {
|
||||
modTime, err := util.GetModTime(b.Path)
|
||||
if err == nil {
|
||||
return modTime != b.ModTime
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// UpdateModTime updates the modtime of this file
|
||||
func (b *Buffer) UpdateModTime() (err error) {
|
||||
b.ModTime, err = util.GetModTime(b.Path)
|
||||
return
|
||||
}
|
||||
|
||||
// ReOpen reloads the current buffer from disk
|
||||
func (b *Buffer) ReOpen() error {
|
||||
file, err := os.Open(b.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
enc, err := htmlindex.Get(b.Settings["encoding"].(string))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
reader := transform.NewReader(file, enc.NewDecoder())
|
||||
data, err := ioutil.ReadAll(reader)
|
||||
txt := string(data)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.EventHandler.ApplyDiff(txt)
|
||||
|
||||
err = b.UpdateModTime()
|
||||
b.isModified = false
|
||||
b.RelocateCursors()
|
||||
return err
|
||||
}
|
||||
|
||||
func (b *Buffer) RelocateCursors() {
|
||||
for _, c := range b.cursors {
|
||||
c.Relocate()
|
||||
}
|
||||
}
|
||||
|
||||
// RuneAt returns the rune at a given location in the buffer
|
||||
func (b *Buffer) RuneAt(loc Loc) rune {
|
||||
line := b.LineBytes(loc.Y)
|
||||
if len(line) > 0 {
|
||||
i := 0
|
||||
for len(line) > 0 {
|
||||
r, size := utf8.DecodeRune(line)
|
||||
line = line[size:]
|
||||
i++
|
||||
|
||||
if i == loc.X {
|
||||
return r
|
||||
}
|
||||
}
|
||||
}
|
||||
return '\n'
|
||||
}
|
||||
|
||||
// Modified returns if this buffer has been modified since
|
||||
// being opened
|
||||
func (b *Buffer) Modified() bool {
|
||||
if b.Type.Scratch {
|
||||
return false
|
||||
}
|
||||
|
||||
if b.Settings["fastdirty"].(bool) {
|
||||
return b.isModified
|
||||
}
|
||||
|
||||
var buff [md5.Size]byte
|
||||
|
||||
calcHash(b, &buff)
|
||||
return buff != b.origHash
|
||||
}
|
||||
|
||||
// calcHash calculates md5 hash of all lines in the buffer
|
||||
func calcHash(b *Buffer, out *[md5.Size]byte) error {
|
||||
h := md5.New()
|
||||
|
||||
size := 0
|
||||
if len(b.lines) > 0 {
|
||||
n, e := h.Write(b.lines[0].data)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
size += n
|
||||
|
||||
for _, l := range b.lines[1:] {
|
||||
n, e = h.Write([]byte{'\n'})
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
size += n
|
||||
n, e = h.Write(l.data)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
size += n
|
||||
}
|
||||
}
|
||||
|
||||
if size > LargeFileThreshold {
|
||||
return ErrFileTooLarge
|
||||
}
|
||||
|
||||
h.Sum((*out)[:0])
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateRules updates the syntax rules and filetype for this buffer
|
||||
// This is called when the colorscheme changes
|
||||
func (b *Buffer) UpdateRules() {
|
||||
if !b.Type.Syntax {
|
||||
return
|
||||
}
|
||||
syntaxFile := ""
|
||||
ft := b.Settings["filetype"].(string)
|
||||
var header *highlight.Header
|
||||
for _, f := range config.ListRuntimeFiles(config.RTSyntaxHeader) {
|
||||
data, err := f.Data()
|
||||
if err != nil {
|
||||
screen.TermMessage("Error loading syntax header file " + f.Name() + ": " + err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
header, err = highlight.MakeHeader(data)
|
||||
if err != nil {
|
||||
screen.TermMessage("Error reading syntax header file", f.Name(), err)
|
||||
continue
|
||||
}
|
||||
|
||||
if ft == "unknown" || ft == "" {
|
||||
if highlight.MatchFiletype(header.FtDetect, b.Path, b.lines[0].data) {
|
||||
syntaxFile = f.Name()
|
||||
break
|
||||
}
|
||||
} else if header.FileType == ft {
|
||||
syntaxFile = f.Name()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if syntaxFile == "" {
|
||||
// search for the syntax file in the user's custom syntax files
|
||||
for _, f := range config.ListRealRuntimeFiles(config.RTSyntax) {
|
||||
data, err := f.Data()
|
||||
if err != nil {
|
||||
screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
header, err = highlight.MakeHeaderYaml(data)
|
||||
file, err := highlight.ParseFile(data)
|
||||
if err != nil {
|
||||
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
if (ft == "unknown" || ft == "" && highlight.MatchFiletype(header.FtDetect, b.Path, b.lines[0].data)) || header.FileType == ft {
|
||||
syndef, err := highlight.ParseDef(file, header)
|
||||
if err != nil {
|
||||
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
|
||||
continue
|
||||
}
|
||||
b.SyntaxDef = syndef
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, f := range config.ListRuntimeFiles(config.RTSyntax) {
|
||||
if f.Name() == syntaxFile {
|
||||
data, err := f.Data()
|
||||
if err != nil {
|
||||
screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
file, err := highlight.ParseFile(data)
|
||||
if err != nil {
|
||||
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
syndef, err := highlight.ParseDef(file, header)
|
||||
if err != nil {
|
||||
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
|
||||
continue
|
||||
}
|
||||
b.SyntaxDef = syndef
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if b.SyntaxDef != nil && highlight.HasIncludes(b.SyntaxDef) {
|
||||
includes := highlight.GetIncludes(b.SyntaxDef)
|
||||
|
||||
var files []*highlight.File
|
||||
for _, f := range config.ListRuntimeFiles(config.RTSyntax) {
|
||||
data, err := f.Data()
|
||||
if err != nil {
|
||||
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
|
||||
continue
|
||||
}
|
||||
header, err := highlight.MakeHeaderYaml(data)
|
||||
if err != nil {
|
||||
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
for _, i := range includes {
|
||||
if header.FileType == i {
|
||||
file, err := highlight.ParseFile(data)
|
||||
if err != nil {
|
||||
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
|
||||
continue
|
||||
}
|
||||
files = append(files, file)
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(files) >= len(includes) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
highlight.ResolveIncludes(b.SyntaxDef, files)
|
||||
}
|
||||
|
||||
if b.Highlighter == nil || syntaxFile != "" {
|
||||
if b.SyntaxDef != nil {
|
||||
b.Settings["filetype"] = b.SyntaxDef.FileType
|
||||
b.Highlighter = highlight.NewHighlighter(b.SyntaxDef)
|
||||
if b.Settings["syntax"].(bool) {
|
||||
b.Highlighter.HighlightStates(b)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ClearMatches clears all of the syntax highlighting for the buffer
|
||||
func (b *Buffer) ClearMatches() {
|
||||
for i := range b.lines {
|
||||
b.SetMatch(i, nil)
|
||||
b.SetState(i, nil)
|
||||
}
|
||||
}
|
||||
|
||||
// IndentString returns this buffer's indent method (a tabstop or n spaces
|
||||
// depending on the settings)
|
||||
func (b *Buffer) IndentString(tabsize int) string {
|
||||
if b.Settings["tabstospaces"].(bool) {
|
||||
return util.Spaces(tabsize)
|
||||
}
|
||||
return "\t"
|
||||
}
|
||||
|
||||
// SetCursors resets this buffer's cursors to a new list
|
||||
func (b *Buffer) SetCursors(c []*Cursor) {
|
||||
b.cursors = c
|
||||
b.EventHandler.cursors = b.cursors
|
||||
b.EventHandler.active = b.curCursor
|
||||
}
|
||||
|
||||
// AddCursor adds a new cursor to the list
|
||||
func (b *Buffer) AddCursor(c *Cursor) {
|
||||
b.cursors = append(b.cursors, c)
|
||||
b.EventHandler.cursors = b.cursors
|
||||
b.EventHandler.active = b.curCursor
|
||||
b.UpdateCursors()
|
||||
}
|
||||
|
||||
// SetCurCursor sets the current cursor
|
||||
func (b *Buffer) SetCurCursor(n int) {
|
||||
b.curCursor = n
|
||||
}
|
||||
|
||||
// GetActiveCursor returns the main cursor in this buffer
|
||||
func (b *Buffer) GetActiveCursor() *Cursor {
|
||||
return b.cursors[b.curCursor]
|
||||
}
|
||||
|
||||
// GetCursor returns the nth cursor
|
||||
func (b *Buffer) GetCursor(n int) *Cursor {
|
||||
return b.cursors[n]
|
||||
}
|
||||
|
||||
// GetCursors returns the list of cursors in this buffer
|
||||
func (b *Buffer) GetCursors() []*Cursor {
|
||||
return b.cursors
|
||||
}
|
||||
|
||||
// NumCursors returns the number of cursors
|
||||
func (b *Buffer) NumCursors() int {
|
||||
return len(b.cursors)
|
||||
}
|
||||
|
||||
// MergeCursors merges any cursors that are at the same position
|
||||
// into one cursor
|
||||
func (b *Buffer) MergeCursors() {
|
||||
var cursors []*Cursor
|
||||
for i := 0; i < len(b.cursors); i++ {
|
||||
c1 := b.cursors[i]
|
||||
if c1 != nil {
|
||||
for j := 0; j < len(b.cursors); j++ {
|
||||
c2 := b.cursors[j]
|
||||
if c2 != nil && i != j && c1.Loc == c2.Loc {
|
||||
b.cursors[j] = nil
|
||||
}
|
||||
}
|
||||
cursors = append(cursors, c1)
|
||||
}
|
||||
}
|
||||
|
||||
b.cursors = cursors
|
||||
|
||||
for i := range b.cursors {
|
||||
b.cursors[i].Num = i
|
||||
}
|
||||
|
||||
if b.curCursor >= len(b.cursors) {
|
||||
b.curCursor = len(b.cursors) - 1
|
||||
}
|
||||
b.EventHandler.cursors = b.cursors
|
||||
b.EventHandler.active = b.curCursor
|
||||
}
|
||||
|
||||
// UpdateCursors updates all the cursors indicies
|
||||
func (b *Buffer) UpdateCursors() {
|
||||
b.EventHandler.cursors = b.cursors
|
||||
b.EventHandler.active = b.curCursor
|
||||
for i, c := range b.cursors {
|
||||
c.Num = i
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Buffer) RemoveCursor(i int) {
|
||||
copy(b.cursors[i:], b.cursors[i+1:])
|
||||
b.cursors[len(b.cursors)-1] = nil
|
||||
b.cursors = b.cursors[:len(b.cursors)-1]
|
||||
b.curCursor = util.Clamp(b.curCursor, 0, len(b.cursors)-1)
|
||||
b.UpdateCursors()
|
||||
}
|
||||
|
||||
// ClearCursors removes all extra cursors
|
||||
func (b *Buffer) ClearCursors() {
|
||||
for i := 1; i < len(b.cursors); i++ {
|
||||
b.cursors[i] = nil
|
||||
}
|
||||
b.cursors = b.cursors[:1]
|
||||
b.UpdateCursors()
|
||||
b.curCursor = 0
|
||||
b.GetActiveCursor().ResetSelection()
|
||||
}
|
||||
|
||||
// MoveLinesUp moves the range of lines up one row
|
||||
func (b *Buffer) MoveLinesUp(start int, end int) {
|
||||
if start < 1 || start >= end || end > len(b.lines) {
|
||||
return
|
||||
}
|
||||
l := string(b.LineBytes(start - 1))
|
||||
if end == len(b.lines) {
|
||||
b.Insert(
|
||||
Loc{
|
||||
utf8.RuneCount(b.lines[end-1].data),
|
||||
end - 1,
|
||||
},
|
||||
"\n"+l,
|
||||
)
|
||||
} else {
|
||||
b.Insert(
|
||||
Loc{0, end},
|
||||
l+"\n",
|
||||
)
|
||||
}
|
||||
b.Remove(
|
||||
Loc{0, start - 1},
|
||||
Loc{0, start},
|
||||
)
|
||||
}
|
||||
|
||||
// MoveLinesDown moves the range of lines down one row
|
||||
func (b *Buffer) MoveLinesDown(start int, end int) {
|
||||
if start < 0 || start >= end || end >= len(b.lines)-1 {
|
||||
return
|
||||
}
|
||||
l := string(b.LineBytes(end))
|
||||
b.Insert(
|
||||
Loc{0, start},
|
||||
l+"\n",
|
||||
)
|
||||
end++
|
||||
b.Remove(
|
||||
Loc{0, end},
|
||||
Loc{0, end + 1},
|
||||
)
|
||||
}
|
||||
|
||||
var BracePairs = [][2]rune{
|
||||
{'(', ')'},
|
||||
{'{', '}'},
|
||||
{'[', ']'},
|
||||
}
|
||||
|
||||
// FindMatchingBrace returns the location in the buffer of the matching bracket
|
||||
// It is given a brace type containing the open and closing character, (for example
|
||||
// '{' and '}') as well as the location to match from
|
||||
// TODO: maybe can be more efficient with utf8 package
|
||||
// returns the location of the matching brace
|
||||
// if the boolean returned is true then the original matching brace is one character left
|
||||
// of the starting location
|
||||
func (b *Buffer) FindMatchingBrace(braceType [2]rune, start Loc) (Loc, bool) {
|
||||
curLine := []rune(string(b.LineBytes(start.Y)))
|
||||
startChar := ' '
|
||||
if start.X >= 0 && start.X < len(curLine) {
|
||||
startChar = curLine[start.X]
|
||||
}
|
||||
leftChar := ' '
|
||||
if start.X-1 >= 0 && start.X-1 < len(curLine) {
|
||||
leftChar = curLine[start.X-1]
|
||||
}
|
||||
var i int
|
||||
if startChar == braceType[0] || leftChar == braceType[0] {
|
||||
for y := start.Y; y < b.LinesNum(); y++ {
|
||||
l := []rune(string(b.LineBytes(y)))
|
||||
xInit := 0
|
||||
if y == start.Y {
|
||||
if startChar == braceType[0] {
|
||||
xInit = start.X
|
||||
} else {
|
||||
xInit = start.X - 1
|
||||
}
|
||||
}
|
||||
for x := xInit; x < len(l); x++ {
|
||||
r := l[x]
|
||||
if r == braceType[0] {
|
||||
i++
|
||||
} else if r == braceType[1] {
|
||||
i--
|
||||
if i == 0 {
|
||||
if startChar == braceType[0] {
|
||||
return Loc{x, y}, false
|
||||
}
|
||||
return Loc{x, y}, true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if startChar == braceType[1] || leftChar == braceType[1] {
|
||||
for y := start.Y; y >= 0; y-- {
|
||||
l := []rune(string(b.lines[y].data))
|
||||
xInit := len(l) - 1
|
||||
if y == start.Y {
|
||||
if leftChar == braceType[1] {
|
||||
xInit = start.X - 1
|
||||
} else {
|
||||
xInit = start.X
|
||||
}
|
||||
}
|
||||
for x := xInit; x >= 0; x-- {
|
||||
r := l[x]
|
||||
if r == braceType[0] {
|
||||
i--
|
||||
if i == 0 {
|
||||
if leftChar == braceType[1] {
|
||||
return Loc{x, y}, true
|
||||
}
|
||||
return Loc{x, y}, false
|
||||
}
|
||||
} else if r == braceType[1] {
|
||||
i++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return start, true
|
||||
}
|
||||
|
||||
// Retab changes all tabs to spaces or vice versa
|
||||
func (b *Buffer) Retab() {
|
||||
toSpaces := b.Settings["tabstospaces"].(bool)
|
||||
tabsize := util.IntOpt(b.Settings["tabsize"])
|
||||
dirty := false
|
||||
|
||||
for i := 0; i < b.LinesNum(); i++ {
|
||||
l := b.LineBytes(i)
|
||||
|
||||
ws := util.GetLeadingWhitespace(l)
|
||||
if len(ws) != 0 {
|
||||
if toSpaces {
|
||||
ws = bytes.Replace(ws, []byte{'\t'}, bytes.Repeat([]byte{' '}, tabsize), -1)
|
||||
} else {
|
||||
ws = bytes.Replace(ws, bytes.Repeat([]byte{' '}, tabsize), []byte{'\t'}, -1)
|
||||
}
|
||||
}
|
||||
|
||||
l = bytes.TrimLeft(l, " \t")
|
||||
b.lines[i].data = append(ws, l...)
|
||||
dirty = true
|
||||
}
|
||||
|
||||
b.isModified = dirty
|
||||
}
|
||||
|
||||
// ParseCursorLocation turns a cursor location like 10:5 (LINE:COL)
|
||||
// into a loc
|
||||
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])
|
||||
startpos.Y -= 1
|
||||
if err == nil {
|
||||
if len(cursorPositions) > 1 {
|
||||
startpos.X, err = strconv.Atoi(cursorPositions[1])
|
||||
if startpos.X > 0 {
|
||||
startpos.X -= 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return startpos, err
|
||||
}
|
||||
|
||||
// Line returns the string representation of the given line number
|
||||
func (b *Buffer) Line(i int) string {
|
||||
return string(b.LineBytes(i))
|
||||
}
|
||||
|
||||
// WriteLog writes a string to the log buffer
|
||||
func WriteLog(s string) {
|
||||
LogBuf.EventHandler.Insert(LogBuf.End(), s)
|
||||
}
|
||||
|
|
@ -1,15 +1,23 @@
|
|||
package main
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/zyedidia/clipboard"
|
||||
"github.com/zyedidia/micro/internal/util"
|
||||
)
|
||||
|
||||
// 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
|
||||
|
|
@ -28,6 +36,23 @@ type Cursor struct {
|
|||
Num int
|
||||
}
|
||||
|
||||
func NewCursor(b *Buffer, l Loc) *Cursor {
|
||||
c := &Cursor{
|
||||
buf: b,
|
||||
Loc: l,
|
||||
}
|
||||
c.StoreVisualX()
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Cursor) SetBuf(b *Buffer) {
|
||||
c.buf = b
|
||||
}
|
||||
|
||||
func (c *Cursor) Buf() *Buffer {
|
||||
return c.buf
|
||||
}
|
||||
|
||||
// Goto puts the cursor at the given cursor's location and gives
|
||||
// the current cursor its selection too
|
||||
func (c *Cursor) Goto(b Cursor) {
|
||||
|
|
@ -39,6 +64,54 @@ func (c *Cursor) Goto(b Cursor) {
|
|||
// the current cursor its selection too
|
||||
func (c *Cursor) GotoLoc(l Loc) {
|
||||
c.X, c.Y = l.X, l.Y
|
||||
c.StoreVisualX()
|
||||
}
|
||||
|
||||
// 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 util.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))
|
||||
return util.GetCharPosInLine(b, visualPos, tabsize)
|
||||
}
|
||||
|
||||
// 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 util.IsWhitespace(c.RuneUnder(c.X)) {
|
||||
if c.X == utf8.RuneCount(c.buf.LineBytes(c.Y)) {
|
||||
break
|
||||
}
|
||||
c.Right()
|
||||
}
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
|
|
@ -47,7 +120,7 @@ func (c *Cursor) GotoLoc(l Loc) {
|
|||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -86,15 +159,30 @@ func (c *Cursor) DeleteSelection() {
|
|||
}
|
||||
}
|
||||
|
||||
// Deselect closes the cursor's current selection
|
||||
// Start indicates whether the cursor should be placed
|
||||
// at the start or end of the selection
|
||||
func (c *Cursor) Deselect(start bool) {
|
||||
if c.HasSelection() {
|
||||
if start {
|
||||
c.Loc = c.CurSelection[0]
|
||||
} else {
|
||||
c.Loc = c.CurSelection[1].Move(-1, c.buf)
|
||||
}
|
||||
c.ResetSelection()
|
||||
c.StoreVisualX()
|
||||
}
|
||||
}
|
||||
|
||||
// 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 +190,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 +217,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
|
||||
|
|
@ -301,7 +263,7 @@ func (c *Cursor) Left() {
|
|||
c.Up()
|
||||
c.End()
|
||||
}
|
||||
c.LastVisualX = c.GetVisualX()
|
||||
c.StoreVisualX()
|
||||
}
|
||||
|
||||
// Right moves the cursor right one cell (if possible) or
|
||||
|
|
@ -310,74 +272,13 @@ 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()
|
||||
c.Start()
|
||||
}
|
||||
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()
|
||||
c.StoreVisualX()
|
||||
}
|
||||
|
||||
// Relocate makes sure that the cursor is inside the bounds
|
||||
|
|
@ -386,13 +287,154 @@ func (c *Cursor) StoreVisualX() {
|
|||
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))
|
||||
}
|
||||
}
|
||||
|
||||
// SelectWord selects the word the cursor is currently on
|
||||
func (c *Cursor) SelectWord() {
|
||||
if len(c.buf.LineBytes(c.Y)) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if !util.IsWordChar(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 && util.IsWordChar(c.RuneUnder(backward-1)) {
|
||||
backward--
|
||||
}
|
||||
|
||||
c.SetSelectionStart(Loc{backward, c.Y})
|
||||
c.OrigSelection[0] = c.CurSelection[0]
|
||||
|
||||
lineLen := utf8.RuneCount(c.buf.LineBytes(c.Y)) - 1
|
||||
for forward < lineLen && util.IsWordChar(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 && util.IsWordChar(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
|
||||
|
||||
lineLen := utf8.RuneCount(c.buf.LineBytes(c.Y)) - 1
|
||||
for forward < lineLen && util.IsWordChar(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 util.IsWhitespace(c.RuneUnder(c.X)) {
|
||||
if c.X == utf8.RuneCount(c.buf.LineBytes(c.Y)) {
|
||||
c.Right()
|
||||
return
|
||||
}
|
||||
c.Right()
|
||||
}
|
||||
c.Right()
|
||||
for util.IsWordChar(c.RuneUnder(c.X)) {
|
||||
if c.X == utf8.RuneCount(c.buf.LineBytes(c.Y)) {
|
||||
return
|
||||
}
|
||||
c.Right()
|
||||
}
|
||||
}
|
||||
|
||||
// WordLeft moves the cursor one word to the left
|
||||
func (c *Cursor) WordLeft() {
|
||||
c.Left()
|
||||
for util.IsWhitespace(c.RuneUnder(c.X)) {
|
||||
if c.X == 0 {
|
||||
return
|
||||
}
|
||||
c.Left()
|
||||
}
|
||||
c.Left()
|
||||
for util.IsWordChar(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 := c.buf.LineBytes(c.Y)
|
||||
if len(line) == 0 || x >= utf8.RuneCount(line) {
|
||||
return '\n'
|
||||
} else if x < 0 {
|
||||
x = 0
|
||||
}
|
||||
i := 0
|
||||
for len(line) > 0 {
|
||||
r, size := utf8.DecodeRune(line)
|
||||
line = line[size:]
|
||||
|
||||
if i == x {
|
||||
return r
|
||||
}
|
||||
|
||||
i++
|
||||
}
|
||||
return '\n'
|
||||
}
|
||||
|
||||
func (c *Cursor) StoreVisualX() {
|
||||
c.LastVisualX = c.GetVisualX()
|
||||
}
|
||||
|
|
@ -1,11 +1,10 @@
|
|||
package main
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
dmp "github.com/sergi/go-diff/diffmatchpatch"
|
||||
"github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -17,6 +16,8 @@ const (
|
|||
TextEventRemove = -1
|
||||
// TextEventReplace represents a replace event
|
||||
TextEventReplace = 0
|
||||
|
||||
undoThreshold = 500 // If two events are less than n milliseconds apart, undo both of them
|
||||
)
|
||||
|
||||
// TextEvent holds data for a manipulation on some text that can be undone
|
||||
|
|
@ -30,16 +31,16 @@ type TextEvent struct {
|
|||
|
||||
// A Delta is a change to the buffer
|
||||
type Delta struct {
|
||||
Text string
|
||||
Text []byte
|
||||
Start Loc
|
||||
End Loc
|
||||
}
|
||||
|
||||
// ExecuteTextEvent runs a text event
|
||||
func ExecuteTextEvent(t *TextEvent, buf *Buffer) {
|
||||
func ExecuteTextEvent(t *TextEvent, buf *SharedBuffer) {
|
||||
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 +49,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]
|
||||
|
|
@ -59,24 +60,27 @@ func ExecuteTextEvent(t *TextEvent, buf *Buffer) {
|
|||
}
|
||||
|
||||
// UndoTextEvent undoes a text event
|
||||
func UndoTextEvent(t *TextEvent, buf *Buffer) {
|
||||
func UndoTextEvent(t *TextEvent, buf *SharedBuffer) {
|
||||
t.EventType = -t.EventType
|
||||
ExecuteTextEvent(t, buf)
|
||||
}
|
||||
|
||||
// EventHandler executes text manipulations and allows undoing and redoing
|
||||
type EventHandler struct {
|
||||
buf *Buffer
|
||||
UndoStack *Stack
|
||||
RedoStack *Stack
|
||||
buf *SharedBuffer
|
||||
cursors []*Cursor
|
||||
active int
|
||||
UndoStack *TEStack
|
||||
RedoStack *TEStack
|
||||
}
|
||||
|
||||
// NewEventHandler returns a new EventHandler
|
||||
func NewEventHandler(buf *Buffer) *EventHandler {
|
||||
func NewEventHandler(buf *SharedBuffer, cursors []*Cursor) *EventHandler {
|
||||
eh := new(EventHandler)
|
||||
eh.UndoStack = new(Stack)
|
||||
eh.RedoStack = new(Stack)
|
||||
eh.UndoStack = new(TEStack)
|
||||
eh.RedoStack = new(TEStack)
|
||||
eh.buf = buf
|
||||
eh.cursors = cursors
|
||||
return eh
|
||||
}
|
||||
|
||||
|
|
@ -86,38 +90,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.MoveLA(utf8.RuneCountInString(d.Text), eh.buf.LineArray))
|
||||
} else {
|
||||
if d.Type == dmp.DiffInsert {
|
||||
eh.Insert(loc, d.Text)
|
||||
}
|
||||
loc = loc.Move(Count(d.Text), eh.buf)
|
||||
loc = loc.MoveLA(utf8.RuneCountInString(d.Text), eh.buf.LineArray)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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.cursors[eh.active],
|
||||
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.MoveLA(utf8.RuneCount(text), eh.buf.LineArray)
|
||||
end := e.Deltas[0].End
|
||||
|
||||
for _, c := range eh.buf.cursors {
|
||||
for _, c := range eh.cursors {
|
||||
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.MoveLA(utf8.RuneCount(text), eh.buf.LineArray)
|
||||
}
|
||||
return loc
|
||||
}
|
||||
|
|
@ -133,19 +138,19 @@ 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.cursors[eh.active],
|
||||
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.cursors {
|
||||
move := func(loc Loc) Loc {
|
||||
if start.Y != end.Y && loc.GreaterThan(end) {
|
||||
loc.Y -= end.Y - start.Y
|
||||
} else if loc.Y == end.Y && loc.GreaterEqual(end) {
|
||||
loc = loc.Move(-Diff(start, end, eh.buf), eh.buf)
|
||||
loc = loc.MoveLA(-DiffLA(start, end, eh.buf.LineArray), eh.buf.LineArray)
|
||||
}
|
||||
return loc
|
||||
}
|
||||
|
|
@ -161,7 +166,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.cursors[eh.active],
|
||||
EventType: TextEventReplace,
|
||||
Deltas: deltas,
|
||||
Time: time.Now(),
|
||||
|
|
@ -178,19 +183,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") {
|
||||
// screen.TermMessage(err)
|
||||
// }
|
||||
// if val, ok := ret.(lua.LBool); ok && val == lua.LFalse {
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
|
||||
ExecuteTextEvent(t, eh.buf)
|
||||
}
|
||||
|
|
@ -236,9 +242,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 < len(eh.cursors) {
|
||||
t.C = *eh.cursors[teCursor.Num]
|
||||
eh.cursors[teCursor.Num].Goto(teCursor)
|
||||
} else {
|
||||
teCursor.Num = -1
|
||||
}
|
||||
|
|
@ -283,9 +289,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 < len(eh.cursors) {
|
||||
t.C = *eh.cursors[teCursor.Num]
|
||||
eh.cursors[teCursor.Num].Goto(teCursor)
|
||||
} else {
|
||||
teCursor.Num = -1
|
||||
}
|
||||
|
|
@ -1,13 +1,14 @@
|
|||
package main
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/zyedidia/micro/cmd/micro/highlight"
|
||||
"github.com/zyedidia/micro/pkg/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,28 @@ 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
|
||||
dlen = len(data)
|
||||
} 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 +117,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 +137,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 +175,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,51 +209,80 @@ 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])
|
||||
src := la.lines[start.Y].data[startX:endX]
|
||||
dest := make([]byte, len(src))
|
||||
copy(dest, src)
|
||||
return dest
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// LinesNum returns the number of lines in the buffer
|
||||
func (la *LineArray) LinesNum() int {
|
||||
return len(la.lines)
|
||||
}
|
||||
|
||||
// Start returns the start of the buffer
|
||||
func (la *LineArray) Start() Loc {
|
||||
return Loc{0, 0}
|
||||
}
|
||||
|
||||
// End returns the location of the last character in the buffer
|
||||
func (la *LineArray) End() Loc {
|
||||
numlines := len(la.lines)
|
||||
return Loc{utf8.RuneCount(la.lines[numlines-1].data), numlines - 1}
|
||||
}
|
||||
|
||||
// LineBytes returns line n as an array of bytes
|
||||
func (la *LineArray) LineBytes(n int) []byte {
|
||||
if n >= len(la.lines) || n < 0 {
|
||||
return []byte{}
|
||||
}
|
||||
return la.lines[n].data
|
||||
}
|
||||
|
||||
// State gets the highlight state for the given line number
|
||||
func (la *LineArray) State(lineN int) highlight.State {
|
||||
return la.lines[lineN].state
|
||||
|
|
@ -265,3 +302,11 @@ func (la *LineArray) SetMatch(lineN int, m highlight.LineMatch) {
|
|||
func (la *LineArray) Match(lineN int) highlight.LineMatch {
|
||||
return la.lines[lineN].match
|
||||
}
|
||||
|
||||
func (la *LineArray) Rehighlight(lineN int) bool {
|
||||
return la.lines[lineN].rehighlight
|
||||
}
|
||||
|
||||
func (la *LineArray) SetRehighlight(lineN int, on bool) {
|
||||
la.lines[lineN].rehighlight = on
|
||||
}
|
||||
60
internal/buffer/line_array_test.go
Normal file
60
internal/buffer/line_array_test.go
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
package buffer
|
||||
|
||||
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))
|
||||
}
|
||||
|
|
@ -1,61 +1,58 @@
|
|||
package main
|
||||
package buffer
|
||||
|
||||
// FromCharPos converts from a character position to an x, y position
|
||||
func FromCharPos(loc int, buf *Buffer) Loc {
|
||||
charNum := 0
|
||||
x, y := 0, 0
|
||||
import (
|
||||
"unicode/utf8"
|
||||
|
||||
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
|
||||
}
|
||||
"github.com/zyedidia/micro/internal/util"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
func DiffLA(a, b Loc, buf *LineArray) int {
|
||||
if a.Y == b.Y {
|
||||
if a.X > b.X {
|
||||
return a.X - b.X
|
||||
|
|
@ -71,69 +68,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 {
|
||||
func (l Loc) right(buf *LineArray) 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}
|
||||
|
|
@ -142,7 +89,7 @@ func (l Loc) right(buf *Buffer) Loc {
|
|||
}
|
||||
|
||||
// This moves the given location one character to the left
|
||||
func (l Loc) left(buf *Buffer) Loc {
|
||||
func (l Loc) left(buf *LineArray) Loc {
|
||||
if l == buf.Start() {
|
||||
return Loc{l.X - 1, l.Y}
|
||||
}
|
||||
|
|
@ -150,22 +97,41 @@ 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
|
||||
}
|
||||
|
||||
// Move moves the cursor n characters to the left or right
|
||||
// It moves the cursor left if n is negative
|
||||
func (l Loc) Move(n int, buf *Buffer) Loc {
|
||||
func (l Loc) MoveLA(n int, buf *LineArray) Loc {
|
||||
if n > 0 {
|
||||
for i := 0; i < n; i++ {
|
||||
l = l.right(buf)
|
||||
}
|
||||
return l
|
||||
}
|
||||
for i := 0; i < Abs(n); i++ {
|
||||
for i := 0; i < util.Abs(n); i++ {
|
||||
l = l.left(buf)
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
func (l Loc) Diff(a, b Loc, buf *Buffer) int {
|
||||
return DiffLA(a, b, buf.LineArray)
|
||||
}
|
||||
func (l Loc) Move(n int, buf *Buffer) Loc {
|
||||
return l.MoveLA(n, buf.LineArray)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
77
internal/buffer/message.go
Normal file
77
internal/buffer/message.go
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
package buffer
|
||||
|
||||
import (
|
||||
"github.com/zyedidia/micro/internal/config"
|
||||
"github.com/zyedidia/tcell"
|
||||
)
|
||||
|
||||
type MsgType int
|
||||
|
||||
const (
|
||||
MTInfo = iota
|
||||
MTWarning
|
||||
MTError
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
Msg string
|
||||
Start, End Loc
|
||||
Kind MsgType
|
||||
Owner string
|
||||
}
|
||||
|
||||
func NewMessage(owner string, msg string, start, end Loc, kind MsgType) *Message {
|
||||
return &Message{
|
||||
Msg: msg,
|
||||
Start: start,
|
||||
End: end,
|
||||
Kind: kind,
|
||||
Owner: owner,
|
||||
}
|
||||
}
|
||||
|
||||
func NewMessageAtLine(owner string, msg string, line int, kind MsgType) *Message {
|
||||
start := Loc{-1, line - 1}
|
||||
end := start
|
||||
return NewMessage(owner, msg, start, end, kind)
|
||||
}
|
||||
|
||||
func (m *Message) Style() tcell.Style {
|
||||
switch m.Kind {
|
||||
case MTInfo:
|
||||
if style, ok := config.Colorscheme["gutter-info"]; ok {
|
||||
return style
|
||||
}
|
||||
case MTWarning:
|
||||
if style, ok := config.Colorscheme["gutter-warning"]; ok {
|
||||
return style
|
||||
}
|
||||
case MTError:
|
||||
if style, ok := config.Colorscheme["gutter-error"]; ok {
|
||||
return style
|
||||
}
|
||||
}
|
||||
return config.DefStyle
|
||||
}
|
||||
|
||||
func (b *Buffer) AddMessage(m *Message) {
|
||||
b.Messages = append(b.Messages, m)
|
||||
}
|
||||
|
||||
func (b *Buffer) removeMsg(i int) {
|
||||
copy(b.Messages[i:], b.Messages[i+1:])
|
||||
b.Messages[len(b.Messages)-1] = nil
|
||||
b.Messages = b.Messages[:len(b.Messages)-1]
|
||||
}
|
||||
|
||||
func (b *Buffer) ClearMessages(owner string) {
|
||||
for i := len(b.Messages) - 1; i >= 0; i-- {
|
||||
if b.Messages[i].Owner == owner {
|
||||
b.removeMsg(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Buffer) ClearAllMessages() {
|
||||
b.Messages = make([]*Message, 0)
|
||||
}
|
||||
228
internal/buffer/save.go
Normal file
228
internal/buffer/save.go
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
package buffer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/zyedidia/micro/internal/config"
|
||||
"github.com/zyedidia/micro/internal/screen"
|
||||
"github.com/zyedidia/micro/internal/util"
|
||||
"golang.org/x/text/encoding"
|
||||
"golang.org/x/text/encoding/htmlindex"
|
||||
"golang.org/x/text/transform"
|
||||
)
|
||||
|
||||
// LargeFileThreshold is the number of bytes when fastdirty is forced
|
||||
// because hashing is too slow
|
||||
const LargeFileThreshold = 50000
|
||||
|
||||
// overwriteFile opens the given file for writing, truncating if one exists, and then calls
|
||||
// the supplied function with the file as io.Writer object, also making sure the file is
|
||||
// closed afterwards.
|
||||
func overwriteFile(name string, enc encoding.Encoding, fn func(io.Writer) error) (err error) {
|
||||
var file *os.File
|
||||
|
||||
if file, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if e := file.Close(); e != nil && err == nil {
|
||||
err = e
|
||||
}
|
||||
}()
|
||||
|
||||
w := transform.NewWriter(file, enc.NewEncoder())
|
||||
// w := bufio.NewWriter(file)
|
||||
|
||||
if err = fn(w); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// err = w.Flush()
|
||||
return
|
||||
}
|
||||
|
||||
// overwriteFileAsRoot executes dd as root and then calls the supplied function
|
||||
// with dd's standard input as an io.Writer object. Dd opens the given file for writing,
|
||||
// truncating it if it exists, and writes what it receives on its standard input to the file.
|
||||
func overwriteFileAsRoot(name string, enc encoding.Encoding, fn func(io.Writer) error) (err error) {
|
||||
cmd := exec.Command(config.GlobalSettings["sucmd"].(string), "dd", "status=none", "bs=4K", "of="+name)
|
||||
var stdin io.WriteCloser
|
||||
|
||||
screenb := screen.TempFini()
|
||||
|
||||
// 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()
|
||||
}
|
||||
}()
|
||||
|
||||
if stdin, err = cmd.StdinPipe(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = cmd.Start(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
e := fn(stdin)
|
||||
|
||||
if err = stdin.Close(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = cmd.Wait(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
screen.TempStart(screenb)
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// Save saves the buffer to its default path
|
||||
func (b *Buffer) Save() error {
|
||||
return b.SaveAs(b.Path)
|
||||
}
|
||||
|
||||
// SaveAs saves the buffer to a specified path (filename), creating the file if it does not exist
|
||||
func (b *Buffer) SaveAs(filename string) error {
|
||||
return b.saveToFile(filename, false)
|
||||
}
|
||||
|
||||
func (b *Buffer) SaveWithSudo() error {
|
||||
return b.SaveAsWithSudo(b.Path)
|
||||
}
|
||||
|
||||
func (b *Buffer) SaveAsWithSudo(filename string) error {
|
||||
return b.saveToFile(filename, true)
|
||||
}
|
||||
|
||||
func (b *Buffer) saveToFile(filename string, withSudo bool) error {
|
||||
var err error
|
||||
if b.Type.Readonly {
|
||||
return errors.New("Cannot save readonly buffer")
|
||||
}
|
||||
if b.Type.Scratch {
|
||||
return errors.New("Cannot save scratch buffer")
|
||||
}
|
||||
|
||||
b.UpdateRules()
|
||||
if b.Settings["rmtrailingws"].(bool) {
|
||||
for i, l := range b.lines {
|
||||
leftover := utf8.RuneCount(bytes.TrimRightFunc(l.data, unicode.IsSpace))
|
||||
|
||||
linelen := utf8.RuneCount(l.data)
|
||||
b.Remove(Loc{leftover, i}, Loc{linelen, i})
|
||||
}
|
||||
|
||||
b.RelocateCursors()
|
||||
}
|
||||
|
||||
if b.Settings["eofnewline"].(bool) {
|
||||
end := b.End()
|
||||
if b.RuneAt(Loc{end.X - 1, end.Y}) != '\n' {
|
||||
b.Insert(end, "\n")
|
||||
}
|
||||
}
|
||||
|
||||
// Update the last time this file was updated after saving
|
||||
defer func() {
|
||||
b.ModTime, _ = util.GetModTime(filename)
|
||||
err = b.Serialize()
|
||||
}()
|
||||
|
||||
// Removes any tilde and replaces with the absolute path to home
|
||||
absFilename, _ := util.ReplaceHome(filename)
|
||||
|
||||
// Get the leading path to the file | "." is returned if there's no leading path provided
|
||||
if dirname := filepath.Dir(absFilename); dirname != "." {
|
||||
// Check if the parent dirs don't exist
|
||||
if _, statErr := os.Stat(dirname); os.IsNotExist(statErr) {
|
||||
// Prompt to make sure they want to create the dirs that are missing
|
||||
if b.Settings["mkparents"].(bool) {
|
||||
// Create all leading dir(s) since they don't exist
|
||||
if mkdirallErr := os.MkdirAll(dirname, os.ModePerm); mkdirallErr != nil {
|
||||
// If there was an error creating the dirs
|
||||
return mkdirallErr
|
||||
}
|
||||
} else {
|
||||
return errors.New("Parent dirs don't exist, enable 'mkparents' for auto creation")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var fileSize int
|
||||
|
||||
enc, err := htmlindex.Get(b.Settings["encoding"].(string))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fwriter := func(file io.Writer) (e error) {
|
||||
if len(b.lines) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// end of line
|
||||
var eol []byte
|
||||
if b.Endings == FFDos {
|
||||
eol = []byte{'\r', '\n'}
|
||||
} else {
|
||||
eol = []byte{'\n'}
|
||||
}
|
||||
|
||||
// write lines
|
||||
if fileSize, e = file.Write(b.lines[0].data); e != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, l := range b.lines[1:] {
|
||||
if _, e = file.Write(eol); e != nil {
|
||||
return
|
||||
}
|
||||
if _, e = file.Write(l.data); e != nil {
|
||||
return
|
||||
}
|
||||
fileSize += len(eol) + len(l.data)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if withSudo {
|
||||
err = overwriteFileAsRoot(absFilename, enc, fwriter)
|
||||
} else {
|
||||
err = overwriteFile(absFilename, enc, fwriter)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !b.Settings["fastdirty"].(bool) {
|
||||
if fileSize > LargeFileThreshold {
|
||||
// For large files 'fastdirty' needs to be on
|
||||
b.Settings["fastdirty"] = true
|
||||
} else {
|
||||
calcHash(b, &b.origHash)
|
||||
}
|
||||
}
|
||||
|
||||
b.Path = filename
|
||||
absPath, _ := filepath.Abs(filename)
|
||||
b.AbsPath = absPath
|
||||
b.isModified = false
|
||||
return err
|
||||
}
|
||||
170
internal/buffer/search.go
Normal file
170
internal/buffer/search.go
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
package buffer
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/zyedidia/micro/internal/util"
|
||||
)
|
||||
|
||||
func (b *Buffer) findDown(r *regexp.Regexp, start, end Loc) ([2]Loc, bool) {
|
||||
start.Y = util.Clamp(start.Y, 0, b.LinesNum()-1)
|
||||
end.Y = util.Clamp(end.Y, 0, b.LinesNum()-1)
|
||||
|
||||
if start.GreaterThan(end) {
|
||||
start, end = end, start
|
||||
}
|
||||
|
||||
for i := start.Y; i <= end.Y; i++ {
|
||||
l := b.LineBytes(i)
|
||||
charpos := 0
|
||||
|
||||
if i == start.Y && start.Y == end.Y {
|
||||
nchars := utf8.RuneCount(l)
|
||||
start.X = util.Clamp(start.X, 0, nchars)
|
||||
end.X = util.Clamp(end.X, 0, nchars)
|
||||
l = util.SliceStart(l, end.X)
|
||||
l = util.SliceEnd(l, start.X)
|
||||
charpos = start.X
|
||||
} else if i == start.Y {
|
||||
nchars := utf8.RuneCount(l)
|
||||
start.X = util.Clamp(start.X, 0, nchars)
|
||||
l = util.SliceEnd(l, start.X)
|
||||
charpos = start.X
|
||||
} else if i == end.Y {
|
||||
nchars := utf8.RuneCount(l)
|
||||
end.X = util.Clamp(end.X, 0, nchars)
|
||||
l = util.SliceStart(l, end.X)
|
||||
}
|
||||
|
||||
match := r.FindIndex(l)
|
||||
|
||||
if match != nil {
|
||||
start := Loc{charpos + util.RunePos(l, match[0]), i}
|
||||
end := Loc{charpos + util.RunePos(l, match[1]), i}
|
||||
return [2]Loc{start, end}, true
|
||||
}
|
||||
}
|
||||
return [2]Loc{}, false
|
||||
}
|
||||
|
||||
func (b *Buffer) findUp(r *regexp.Regexp, start, end Loc) ([2]Loc, bool) {
|
||||
start.Y = util.Clamp(start.Y, 0, b.LinesNum()-1)
|
||||
end.Y = util.Clamp(end.Y, 0, b.LinesNum()-1)
|
||||
|
||||
if start.GreaterThan(end) {
|
||||
start, end = end, start
|
||||
}
|
||||
|
||||
for i := end.Y; i >= start.Y; i-- {
|
||||
l := b.LineBytes(i)
|
||||
charpos := 0
|
||||
|
||||
if i == start.Y && start.Y == end.Y {
|
||||
nchars := utf8.RuneCount(l)
|
||||
start.X = util.Clamp(start.X, 0, nchars)
|
||||
end.X = util.Clamp(end.X, 0, nchars)
|
||||
l = util.SliceStart(l, end.X)
|
||||
l = util.SliceEnd(l, start.X)
|
||||
charpos = start.X
|
||||
} else if i == start.Y {
|
||||
nchars := utf8.RuneCount(l)
|
||||
start.X = util.Clamp(start.X, 0, nchars)
|
||||
l = util.SliceEnd(l, start.X)
|
||||
charpos = start.X
|
||||
} else if i == end.Y {
|
||||
nchars := utf8.RuneCount(l)
|
||||
end.X = util.Clamp(end.X, 0, nchars)
|
||||
l = util.SliceStart(l, end.X)
|
||||
}
|
||||
|
||||
match := r.FindIndex(l)
|
||||
|
||||
if match != nil {
|
||||
start := Loc{charpos + util.RunePos(l, match[0]), i}
|
||||
end := Loc{charpos + util.RunePos(l, match[1]), i}
|
||||
return [2]Loc{start, end}, true
|
||||
}
|
||||
}
|
||||
return [2]Loc{}, false
|
||||
}
|
||||
|
||||
// FindNext finds the next occurrence of a given string in the buffer
|
||||
// It returns the start and end location of the match (if found) and
|
||||
// a boolean indicating if it was found
|
||||
// May also return an error if the search regex is invalid
|
||||
func (b *Buffer) FindNext(s string, start, end, from Loc, down bool, useRegex bool) ([2]Loc, bool, error) {
|
||||
if s == "" {
|
||||
return [2]Loc{}, false, nil
|
||||
}
|
||||
|
||||
var r *regexp.Regexp
|
||||
var err error
|
||||
|
||||
if !useRegex {
|
||||
s = regexp.QuoteMeta(s)
|
||||
}
|
||||
|
||||
if b.Settings["ignorecase"].(bool) {
|
||||
r, err = regexp.Compile("(?i)" + s)
|
||||
} else {
|
||||
r, err = regexp.Compile(s)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return [2]Loc{}, false, err
|
||||
}
|
||||
|
||||
var found bool
|
||||
var l [2]Loc
|
||||
if down {
|
||||
l, found = b.findDown(r, from, end)
|
||||
if !found {
|
||||
l, found = b.findDown(r, start, from)
|
||||
}
|
||||
} else {
|
||||
l, found = b.findUp(r, from, start)
|
||||
if !found {
|
||||
l, found = b.findUp(r, end, from)
|
||||
}
|
||||
}
|
||||
return l, found, nil
|
||||
}
|
||||
|
||||
// ReplaceRegex replaces all occurrences of 'search' with 'replace' in the given area
|
||||
// and returns the number of replacements made
|
||||
func (b *Buffer) ReplaceRegex(start, end Loc, search *regexp.Regexp, replace []byte) int {
|
||||
if start.GreaterThan(end) {
|
||||
start, end = end, start
|
||||
}
|
||||
|
||||
found := 0
|
||||
var deltas []Delta
|
||||
for i := start.Y; i <= end.Y; i++ {
|
||||
l := b.lines[i].data
|
||||
charpos := 0
|
||||
|
||||
if start.Y == end.Y && i == start.Y {
|
||||
l = util.SliceStart(l, end.X)
|
||||
l = util.SliceEnd(l, start.X)
|
||||
charpos = start.X
|
||||
} else if i == start.Y {
|
||||
l = util.SliceEnd(l, start.X)
|
||||
charpos = start.X
|
||||
} else if i == end.Y {
|
||||
l = util.SliceStart(l, end.X)
|
||||
}
|
||||
newText := search.ReplaceAllFunc(l, func(in []byte) []byte {
|
||||
found++
|
||||
return replace
|
||||
})
|
||||
|
||||
from := Loc{charpos, i}
|
||||
to := Loc{charpos + utf8.RuneCount(l), i}
|
||||
|
||||
deltas = append(deltas, Delta{newText, from, to})
|
||||
}
|
||||
b.MultipleReplace(deltas)
|
||||
|
||||
return found
|
||||
}
|
||||
74
internal/buffer/serialize.go
Normal file
74
internal/buffer/serialize.go
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
package buffer
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"golang.org/x/text/encoding"
|
||||
|
||||
"github.com/zyedidia/micro/internal/config"
|
||||
"github.com/zyedidia/micro/internal/util"
|
||||
)
|
||||
|
||||
// The SerializedBuffer holds the types that get serialized when a buffer is saved
|
||||
// These are used for the savecursor and saveundo options
|
||||
type SerializedBuffer struct {
|
||||
EventHandler *EventHandler
|
||||
Cursor Loc
|
||||
ModTime time.Time
|
||||
}
|
||||
|
||||
// Serialize serializes the buffer to config.ConfigDir/buffers
|
||||
func (b *Buffer) Serialize() error {
|
||||
if !b.Settings["savecursor"].(bool) && !b.Settings["saveundo"].(bool) {
|
||||
return nil
|
||||
}
|
||||
if b.Path == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
name := config.ConfigDir + "/buffers/" + util.EscapePath(b.AbsPath)
|
||||
|
||||
return overwriteFile(name, encoding.Nop, func(file io.Writer) error {
|
||||
err := gob.NewEncoder(file).Encode(SerializedBuffer{
|
||||
b.EventHandler,
|
||||
b.GetActiveCursor().Loc,
|
||||
b.ModTime,
|
||||
})
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (b *Buffer) Unserialize() error {
|
||||
// If either savecursor or saveundo is turned on, we need to load the serialized information
|
||||
// from ~/.config/micro/buffers
|
||||
if b.Path == "" {
|
||||
return nil
|
||||
}
|
||||
file, err := os.Open(config.ConfigDir + "/buffers/" + util.EscapePath(b.AbsPath))
|
||||
defer file.Close()
|
||||
if err == nil {
|
||||
var buffer SerializedBuffer
|
||||
decoder := gob.NewDecoder(file)
|
||||
err = decoder.Decode(&buffer)
|
||||
if err != nil {
|
||||
return errors.New(err.Error() + "\nYou may want to remove the files in ~/.config/micro/buffers (these files store the information for the 'saveundo' and 'savecursor' options) if this problem persists.")
|
||||
}
|
||||
if b.Settings["savecursor"].(bool) {
|
||||
b.StartCursor = buffer.Cursor
|
||||
}
|
||||
|
||||
if b.Settings["saveundo"].(bool) {
|
||||
// We should only use last time's eventhandler if the file wasn't modified by someone else in the meantime
|
||||
if b.ModTime == buffer.ModTime {
|
||||
b.EventHandler = buffer.EventHandler
|
||||
b.EventHandler.cursors = b.cursors
|
||||
b.EventHandler.buf = b.SharedBuffer
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
57
internal/buffer/settings.go
Normal file
57
internal/buffer/settings.go
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
package buffer
|
||||
|
||||
import (
|
||||
"github.com/zyedidia/micro/internal/config"
|
||||
"github.com/zyedidia/micro/internal/screen"
|
||||
)
|
||||
|
||||
func (b *Buffer) SetOptionNative(option string, nativeValue interface{}) error {
|
||||
b.Settings[option] = nativeValue
|
||||
|
||||
if option == "fastdirty" {
|
||||
if !nativeValue.(bool) {
|
||||
e := calcHash(b, &b.origHash)
|
||||
if e == ErrFileTooLarge {
|
||||
b.Settings["fastdirty"] = false
|
||||
}
|
||||
}
|
||||
} else if option == "statusline" {
|
||||
screen.Redraw()
|
||||
} else if option == "filetype" {
|
||||
b.UpdateRules()
|
||||
} else if option == "fileformat" {
|
||||
switch b.Settings["fileformat"].(string) {
|
||||
case "unix":
|
||||
b.Endings = FFUnix
|
||||
case "dos":
|
||||
b.Endings = FFDos
|
||||
}
|
||||
b.isModified = true
|
||||
} else if option == "syntax" {
|
||||
if !nativeValue.(bool) {
|
||||
b.ClearMatches()
|
||||
} else {
|
||||
b.UpdateRules()
|
||||
}
|
||||
} else if option == "encoding" {
|
||||
b.isModified = true
|
||||
} else if option == "readonly" {
|
||||
b.Type.Readonly = nativeValue.(bool)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetOption sets a given option to a value just for this buffer
|
||||
func (b *Buffer) SetOption(option, value string) error {
|
||||
if _, ok := b.Settings[option]; !ok {
|
||||
return config.ErrInvalidOption
|
||||
}
|
||||
|
||||
nativeValue, err := config.GetNativeValue(option, b.Settings[option], value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return b.SetOptionNative(option, nativeValue)
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package main
|
||||
package buffer
|
||||
|
||||
// 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
internal/buffer/stack_test.go
Normal file
35
internal/buffer/stack_test.go
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
package buffer
|
||||
|
||||
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)
|
||||
}
|
||||
45
internal/config/autosave.go
Normal file
45
internal/config/autosave.go
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var Autosave chan bool
|
||||
var autotime int
|
||||
|
||||
// lock for autosave
|
||||
var autolock sync.Mutex
|
||||
|
||||
func init() {
|
||||
Autosave = make(chan bool)
|
||||
}
|
||||
|
||||
func SetAutoTime(a int) {
|
||||
autolock.Lock()
|
||||
autotime = a
|
||||
autolock.Unlock()
|
||||
}
|
||||
|
||||
func GetAutoTime() int {
|
||||
autolock.Lock()
|
||||
a := autotime
|
||||
autolock.Unlock()
|
||||
return a
|
||||
}
|
||||
|
||||
func StartAutoSave() {
|
||||
go func() {
|
||||
for {
|
||||
if autotime < 1 {
|
||||
break
|
||||
}
|
||||
time.Sleep(time.Duration(autotime) * time.Second)
|
||||
// it's possible autotime was changed while sleeping
|
||||
if autotime < 1 {
|
||||
break
|
||||
}
|
||||
Autosave <- true
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package main
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"errors"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
|
@ -9,15 +9,15 @@ import (
|
|||
"github.com/zyedidia/tcell"
|
||||
)
|
||||
|
||||
// Colorscheme is a map from string to style -- it represents a colorscheme
|
||||
type Colorscheme map[string]tcell.Style
|
||||
// Micro's default style
|
||||
var DefStyle tcell.Style = tcell.StyleDefault
|
||||
|
||||
// The current colorscheme
|
||||
var colorscheme Colorscheme
|
||||
var Colorscheme map[string]tcell.Style
|
||||
|
||||
// GetColor takes in a syntax group and returns the colorscheme's style for that group
|
||||
func GetColor(color string) tcell.Style {
|
||||
st := defStyle
|
||||
st := DefStyle
|
||||
if color == "" {
|
||||
return st
|
||||
}
|
||||
|
|
@ -29,11 +29,11 @@ func GetColor(color string) tcell.Style {
|
|||
curGroup += "."
|
||||
}
|
||||
curGroup += g
|
||||
if style, ok := colorscheme[curGroup]; ok {
|
||||
if style, ok := Colorscheme[curGroup]; ok {
|
||||
st = style
|
||||
}
|
||||
}
|
||||
} else if style, ok := colorscheme[color]; ok {
|
||||
} else if style, ok := Colorscheme[color]; ok {
|
||||
st = style
|
||||
} else {
|
||||
st = StringToStyle(color)
|
||||
|
|
@ -48,47 +48,46 @@ func ColorschemeExists(colorschemeName string) bool {
|
|||
}
|
||||
|
||||
// InitColorscheme picks and initializes the colorscheme when micro starts
|
||||
func InitColorscheme() {
|
||||
colorscheme = make(Colorscheme)
|
||||
defStyle = tcell.StyleDefault.
|
||||
Foreground(tcell.ColorDefault).
|
||||
Background(tcell.ColorDefault)
|
||||
if screen != nil {
|
||||
// screen.SetStyle(defStyle)
|
||||
}
|
||||
func InitColorscheme() error {
|
||||
Colorscheme = make(map[string]tcell.Style)
|
||||
DefStyle = tcell.StyleDefault
|
||||
|
||||
LoadDefaultColorscheme()
|
||||
return LoadDefaultColorscheme()
|
||||
}
|
||||
|
||||
// LoadDefaultColorscheme loads the default colorscheme from $(configDir)/colorschemes
|
||||
func LoadDefaultColorscheme() {
|
||||
LoadColorscheme(globalSettings["colorscheme"].(string))
|
||||
// LoadDefaultColorscheme loads the default colorscheme from $(ConfigDir)/colorschemes
|
||||
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) (map[string]tcell.Style, error) {
|
||||
var err error
|
||||
parser := regexp.MustCompile(`color-link\s+(\S*)\s+"(.*)"`)
|
||||
|
||||
lines := strings.Split(text, "\n")
|
||||
|
||||
c := make(Colorscheme)
|
||||
c := make(map[string]tcell.Style)
|
||||
|
||||
for _, line := range lines {
|
||||
if strings.TrimSpace(line) == "" ||
|
||||
|
|
@ -106,17 +105,14 @@ func ParseColorscheme(text string) Colorscheme {
|
|||
c[link] = style
|
||||
|
||||
if link == "default" {
|
||||
defStyle = style
|
||||
}
|
||||
if screen != nil {
|
||||
// screen.SetStyle(defStyle)
|
||||
DefStyle = style
|
||||
}
|
||||
} 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
|
||||
|
|
@ -141,17 +137,17 @@ func StringToStyle(str string) tcell.Style {
|
|||
|
||||
var fgColor, bgColor tcell.Color
|
||||
if fg == "" {
|
||||
fgColor, _, _ = defStyle.Decompose()
|
||||
fgColor, _, _ = DefStyle.Decompose()
|
||||
} else {
|
||||
fgColor = StringToColor(fg)
|
||||
}
|
||||
if bg == "" {
|
||||
_, bgColor, _ = defStyle.Decompose()
|
||||
_, bgColor, _ = DefStyle.Decompose()
|
||||
} else {
|
||||
bgColor = StringToColor(bg)
|
||||
}
|
||||
|
||||
style := defStyle.Foreground(fgColor).Background(bgColor)
|
||||
style := DefStyle.Foreground(fgColor).Background(bgColor)
|
||||
if strings.Contains(str, "bold") {
|
||||
style = style.Bold(true)
|
||||
}
|
||||
62
internal/config/colorscheme_test.go
Normal file
62
internal/config/colorscheme_test.go
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
package config
|
||||
|
||||
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)
|
||||
}
|
||||
59
internal/config/config.go
Normal file
59
internal/config/config.go
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
)
|
||||
|
||||
var ConfigDir string
|
||||
|
||||
// InitConfigDir finds the configuration directory for micro according to the XDG spec.
|
||||
// If no directory is found, it creates one.
|
||||
func InitConfigDir(flagConfigDir string) error {
|
||||
var e error
|
||||
|
||||
microHome := os.Getenv("MICRO_CONFIG_HOME")
|
||||
if microHome == "" {
|
||||
// The user has not set $MICRO_CONFIG_HOME so we'll try $XDG_CONFIG_HOME
|
||||
xdgHome := os.Getenv("XDG_CONFIG_HOME")
|
||||
if xdgHome == "" {
|
||||
// The user has not set $XDG_CONFIG_HOME so we should act like it was set to ~/.config
|
||||
home, err := homedir.Dir()
|
||||
if err != nil {
|
||||
return errors.New("Error finding your home directory\nCan't load config files")
|
||||
}
|
||||
xdgHome = home + "/.config"
|
||||
}
|
||||
microHome = xdgHome + "/micro"
|
||||
}
|
||||
ConfigDir = microHome
|
||||
|
||||
if len(flagConfigDir) > 0 {
|
||||
if _, err := os.Stat(flagConfigDir); os.IsNotExist(err) {
|
||||
e = errors.New("Error: " + flagConfigDir + " does not exist. Defaulting to " + ConfigDir + ".")
|
||||
} else {
|
||||
ConfigDir = flagConfigDir
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := os.Stat(microHome); os.IsNotExist(err) {
|
||||
// If the microHome doesn't exist we should create it
|
||||
err = os.Mkdir(microHome, os.ModePerm)
|
||||
if err != nil {
|
||||
return errors.New("Error creating XDG_CONFIG_HOME directory: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := os.Stat(ConfigDir); os.IsNotExist(err) {
|
||||
// If the micro specific config directory doesn't exist we should create that too
|
||||
err = os.Mkdir(ConfigDir, os.ModePerm)
|
||||
if err != nil {
|
||||
return errors.New("Error creating configuration directory: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
7
internal/config/globals.go
Normal file
7
internal/config/globals.go
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package config
|
||||
|
||||
const (
|
||||
DoubleClickThreshold = 400 // How many milliseconds to wait before a second click is not a double click
|
||||
)
|
||||
|
||||
var Bindings map[string]string
|
||||
132
internal/config/plugin.go
Normal file
132
internal/config/plugin.go
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
ulua "github.com/zyedidia/micro/internal/lua"
|
||||
)
|
||||
|
||||
var ErrNoSuchFunction = errors.New("No such function exists")
|
||||
|
||||
// LoadAllPlugins loads all detected plugins (in runtime/plugins and ConfigDir/plugins)
|
||||
func LoadAllPlugins() error {
|
||||
var reterr error
|
||||
for _, p := range Plugins {
|
||||
err := p.Load()
|
||||
if err != nil {
|
||||
reterr = err
|
||||
}
|
||||
}
|
||||
return reterr
|
||||
}
|
||||
|
||||
// RunPluginFn runs a given function in all plugins
|
||||
// returns an error if any of the plugins had an error
|
||||
func RunPluginFn(fn string, args ...lua.LValue) error {
|
||||
var reterr error
|
||||
for _, p := range Plugins {
|
||||
if !p.IsEnabled() {
|
||||
continue
|
||||
}
|
||||
_, err := p.Call(fn, args...)
|
||||
if err != nil && err != ErrNoSuchFunction {
|
||||
reterr = errors.New("Plugin " + p.Name + ": " + err.Error())
|
||||
}
|
||||
}
|
||||
return reterr
|
||||
}
|
||||
|
||||
// RunPluginFnBool runs a function in all plugins and returns
|
||||
// false if any one of them returned false
|
||||
// also returns an error if any of the plugins had an error
|
||||
func RunPluginFnBool(fn string, args ...lua.LValue) (bool, error) {
|
||||
var reterr error
|
||||
retbool := true
|
||||
for _, p := range Plugins {
|
||||
if !p.IsEnabled() {
|
||||
continue
|
||||
}
|
||||
val, err := p.Call(fn, args...)
|
||||
if err == ErrNoSuchFunction {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
reterr = errors.New("Plugin " + p.Name + ": " + err.Error())
|
||||
continue
|
||||
}
|
||||
if v, ok := val.(lua.LBool); !ok {
|
||||
reterr = errors.New(p.Name + "." + fn + " should return a boolean")
|
||||
} else {
|
||||
retbool = retbool && bool(v)
|
||||
}
|
||||
}
|
||||
return retbool, reterr
|
||||
}
|
||||
|
||||
type Plugin struct {
|
||||
DirName string // name of plugin folder
|
||||
Name string // name of plugin
|
||||
Info *PluginInfo // json file containing info
|
||||
Srcs []RuntimeFile // lua files
|
||||
Loaded bool
|
||||
Default bool // pre-installed plugin
|
||||
}
|
||||
|
||||
func (p *Plugin) IsEnabled() bool {
|
||||
if v, ok := GlobalSettings[p.Name]; ok {
|
||||
return v.(bool) && p.Loaded
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
var Plugins []*Plugin
|
||||
|
||||
func (p *Plugin) Load() error {
|
||||
for _, f := range p.Srcs {
|
||||
if v, ok := GlobalSettings[p.Name]; ok && !v.(bool) {
|
||||
return nil
|
||||
}
|
||||
dat, err := f.Data()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = ulua.LoadFile(p.Name, f.Name(), dat)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.Loaded = true
|
||||
RegisterGlobalOption(p.Name, true)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Plugin) Call(fn string, args ...lua.LValue) (lua.LValue, error) {
|
||||
plug := ulua.L.GetGlobal(p.Name)
|
||||
luafn := ulua.L.GetField(plug, fn)
|
||||
if luafn == lua.LNil {
|
||||
return nil, ErrNoSuchFunction
|
||||
}
|
||||
err := ulua.L.CallByParam(lua.P{
|
||||
Fn: luafn,
|
||||
NRet: 1,
|
||||
Protect: true,
|
||||
}, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret := ulua.L.Get(-1)
|
||||
ulua.L.Pop(1)
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func FindPlugin(name string) *Plugin {
|
||||
var pl *Plugin
|
||||
for _, p := range Plugins {
|
||||
if p.Name == name {
|
||||
pl = p
|
||||
break
|
||||
}
|
||||
}
|
||||
return pl
|
||||
}
|
||||
66
internal/config/plugin_manager.go
Normal file
66
internal/config/plugin_manager.go
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrMissingName = errors.New("Missing or empty name field")
|
||||
ErrMissingDesc = errors.New("Missing or empty description field")
|
||||
ErrMissingSite = errors.New("Missing or empty website field")
|
||||
ErrMissingInstall = errors.New("Missing or empty install field")
|
||||
ErrMissingVstr = errors.New("Missing or empty versions field")
|
||||
ErrMissingRequire = errors.New("Missing or empty require field")
|
||||
)
|
||||
|
||||
// PluginInfo contains all the needed info about a plugin
|
||||
// The info is just strings and are not used beyond that (except
|
||||
// the Site and Install fields should be valid URLs). This means
|
||||
// that the requirements for example can be formatted however the
|
||||
// plugin maker decides, the fields will only be parsed by humans
|
||||
// Name: name of plugin
|
||||
// Desc: description of plugin
|
||||
// Site: home website of plugin
|
||||
// Install: install link for plugin (can be link to repo or zip file)
|
||||
// Vstr: version
|
||||
// Require: list of dependencies and requirements
|
||||
type PluginInfo struct {
|
||||
Name string `json:"name"`
|
||||
Desc string `json:"description"`
|
||||
Site string `json:"website"`
|
||||
Install string `json:"install"`
|
||||
Vstr string `json:"version"`
|
||||
Require []string `json:"require"`
|
||||
}
|
||||
|
||||
// NewPluginInfo parses a JSON input into a valid PluginInfo struct
|
||||
// Returns an error if there are any missing fields or any invalid fields
|
||||
// There are no optional fields in a plugin info json file
|
||||
func NewPluginInfo(data []byte) (*PluginInfo, error) {
|
||||
var info PluginInfo
|
||||
|
||||
dec := json.NewDecoder(bytes.NewReader(data))
|
||||
// dec.DisallowUnknownFields() // Force errors
|
||||
|
||||
if err := dec.Decode(&info); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// if len(info.Name) == 0 {
|
||||
// return nil, ErrMissingName
|
||||
// } else if len(info.Desc) == 0 {
|
||||
// return nil, ErrMissingDesc
|
||||
// } else if len(info.Site) == 0 {
|
||||
// return nil, ErrMissingSite
|
||||
// } else if len(info.Install) == 0 {
|
||||
// return nil, ErrMissingInstall
|
||||
// } else if len(info.Vstr) == 0 {
|
||||
// return nil, ErrMissingVstr
|
||||
// } else if len(info.Require) == 0 {
|
||||
// return nil, ErrMissingRequire
|
||||
// }
|
||||
|
||||
return &info, nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue