mirror of
https://github.com/zyedidia/micro.git
synced 2026-02-05 06:30:28 +09:00
Add infobar autocomplete
This commit is contained in:
@@ -18,109 +18,59 @@ import (
|
||||
"github.com/zyedidia/micro/cmd/micro/util"
|
||||
)
|
||||
|
||||
// A Command contains an action (a function to call) as well as information about how to autocomplete the command
|
||||
// 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)
|
||||
completions []Completion
|
||||
}
|
||||
|
||||
// A StrCommand is similar to a command but keeps the name of the action
|
||||
type StrCommand struct {
|
||||
action string
|
||||
completions []Completion
|
||||
action func(*BufPane, []string)
|
||||
completer buffer.Completer
|
||||
}
|
||||
|
||||
var commands map[string]Command
|
||||
|
||||
var commandActions = map[string]func(*BufPane, []string){
|
||||
"Set": (*BufPane).SetCmd,
|
||||
"SetLocal": (*BufPane).SetLocalCmd,
|
||||
"Show": (*BufPane).ShowCmd,
|
||||
"ShowKey": (*BufPane).ShowKeyCmd,
|
||||
"Run": (*BufPane).RunCmd,
|
||||
"Bind": (*BufPane).BindCmd,
|
||||
"Unbind": (*BufPane).UnbindCmd,
|
||||
"Quit": (*BufPane).QuitCmd,
|
||||
"Save": (*BufPane).SaveCmd,
|
||||
"Replace": (*BufPane).ReplaceCmd,
|
||||
"ReplaceAll": (*BufPane).ReplaceAllCmd,
|
||||
"VSplit": (*BufPane).VSplitCmd,
|
||||
"HSplit": (*BufPane).HSplitCmd,
|
||||
"Tab": (*BufPane).NewTabCmd,
|
||||
"Help": (*BufPane).HelpCmd,
|
||||
"Eval": (*BufPane).EvalCmd,
|
||||
"ToggleLog": (*BufPane).ToggleLogCmd,
|
||||
"Plugin": (*BufPane).PluginCmd,
|
||||
"Reload": (*BufPane).ReloadCmd,
|
||||
"Cd": (*BufPane).CdCmd,
|
||||
"Pwd": (*BufPane).PwdCmd,
|
||||
"Open": (*BufPane).OpenCmd,
|
||||
"TabSwitch": (*BufPane).TabSwitchCmd,
|
||||
"Term": (*BufPane).TermCmd,
|
||||
"MemUsage": (*BufPane).MemUsageCmd,
|
||||
"Retab": (*BufPane).RetabCmd,
|
||||
"Raw": (*BufPane).RawCmd,
|
||||
}
|
||||
|
||||
// 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...)
|
||||
commands = map[string]Command{
|
||||
"set": Command{(*BufPane).SetCmd, nil},
|
||||
"setlocal": Command{(*BufPane).SetLocalCmd, nil},
|
||||
"show": Command{(*BufPane).ShowCmd, nil},
|
||||
"showkey": Command{(*BufPane).ShowKeyCmd, nil},
|
||||
"run": Command{(*BufPane).RunCmd, nil},
|
||||
"bind": Command{(*BufPane).BindCmd, nil},
|
||||
"unbind": Command{(*BufPane).UnbindCmd, nil},
|
||||
"quit": Command{(*BufPane).QuitCmd, nil},
|
||||
"save": Command{(*BufPane).SaveCmd, nil},
|
||||
"replace": Command{(*BufPane).ReplaceCmd, nil},
|
||||
"replaceall": Command{(*BufPane).ReplaceAllCmd, nil},
|
||||
"vsplit": Command{(*BufPane).VSplitCmd, buffer.FileComplete},
|
||||
"hsplit": Command{(*BufPane).HSplitCmd, buffer.FileComplete},
|
||||
"tab": Command{(*BufPane).NewTabCmd, buffer.FileComplete},
|
||||
"help": Command{(*BufPane).HelpCmd, nil},
|
||||
"eval": Command{(*BufPane).EvalCmd, nil},
|
||||
"togglelog": Command{(*BufPane).ToggleLogCmd, nil},
|
||||
"plugin": Command{(*BufPane).PluginCmd, nil},
|
||||
"reload": Command{(*BufPane).ReloadCmd, nil},
|
||||
"cd": Command{(*BufPane).CdCmd, buffer.FileComplete},
|
||||
"pwd": Command{(*BufPane).PwdCmd, nil},
|
||||
"open": Command{(*BufPane).OpenCmd, buffer.FileComplete},
|
||||
"tabswitch": Command{(*BufPane).TabSwitchCmd, nil},
|
||||
"term": Command{(*BufPane).TermCmd, nil},
|
||||
"memusage": Command{(*BufPane).MemUsageCmd, nil},
|
||||
"retab": Command{(*BufPane).RetabCmd, nil},
|
||||
"raw": Command{(*BufPane).RawCmd, 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 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}},
|
||||
"unbind": {"Unbind", []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}},
|
||||
}
|
||||
}
|
||||
// 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}
|
||||
// }
|
||||
|
||||
// CommandEditAction returns a bindable function that opens a prompt with
|
||||
// the given string and executes the command when the user presses
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package action
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"bytes"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/zyedidia/micro/cmd/micro/buffer"
|
||||
"github.com/zyedidia/micro/cmd/micro/config"
|
||||
"github.com/zyedidia/micro/cmd/micro/util"
|
||||
)
|
||||
@@ -29,54 +30,22 @@ var pluginCompletions []func(string) []string
|
||||
// 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, _ = util.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) {
|
||||
func CommandComplete(b *buffer.Buffer) (string, []string) {
|
||||
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
|
||||
}
|
||||
|
||||
var suggestions []string
|
||||
for cmd := range commands {
|
||||
if strings.HasPrefix(cmd, input) {
|
||||
@@ -86,7 +55,7 @@ func CommandComplete(input string) (string, []string) {
|
||||
|
||||
var chosen string
|
||||
if len(suggestions) == 1 {
|
||||
chosen = suggestions[0]
|
||||
chosen = util.SliceEndStr(suggestions[0], c.X-argstart)
|
||||
}
|
||||
return chosen, suggestions
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
package action
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
|
||||
"github.com/zyedidia/micro/cmd/micro/display"
|
||||
"github.com/zyedidia/micro/cmd/micro/info"
|
||||
"github.com/zyedidia/micro/cmd/micro/util"
|
||||
"github.com/zyedidia/tcell"
|
||||
)
|
||||
|
||||
@@ -161,6 +165,32 @@ func (h *InfoPane) CursorDown() {
|
||||
}
|
||||
func (h *InfoPane) InsertTab() {
|
||||
// TODO: autocomplete
|
||||
b := h.Buf
|
||||
c := b.GetActiveCursor()
|
||||
l := b.LineBytes(0)
|
||||
l = util.SliceStart(l, c.X)
|
||||
|
||||
args := bytes.Split(l, []byte{' '})
|
||||
cmd := string(args[0])
|
||||
|
||||
var ins string
|
||||
var suggestions []string
|
||||
if len(args) == 1 {
|
||||
ins, suggestions = CommandComplete(b)
|
||||
} else {
|
||||
if action, ok := commands[cmd]; ok {
|
||||
if action.completer != nil {
|
||||
ins, suggestions = action.completer(b)
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Println(ins, suggestions)
|
||||
|
||||
if len(suggestions) == 1 {
|
||||
b.Insert(c.Loc, ins)
|
||||
} else if len(suggestions) > 1 {
|
||||
h.MakeSuggestions(suggestions)
|
||||
}
|
||||
}
|
||||
func (h *InfoPane) InsertNewline() {
|
||||
if !h.HasYN {
|
||||
|
||||
82
cmd/micro/buffer/autocomplete.go
Normal file
82
cmd/micro/buffer/autocomplete.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/zyedidia/micro/cmd/micro/util"
|
||||
)
|
||||
|
||||
type Completer func(*Buffer) (string, []string)
|
||||
|
||||
func (b *Buffer) GetSuggestions() {
|
||||
|
||||
}
|
||||
|
||||
func (b *Buffer) Autocomplete(c Completer) {
|
||||
|
||||
}
|
||||
|
||||
// FileComplete autocompletes filenames
|
||||
func FileComplete(b *Buffer) (string, []string) {
|
||||
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
|
||||
}
|
||||
|
||||
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(".")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
chosen = util.SliceEndStr(chosen, c.X-argstart)
|
||||
|
||||
return chosen, suggestions
|
||||
}
|
||||
@@ -147,18 +147,19 @@ func (i *InfoWindow) displayBuffer() {
|
||||
}
|
||||
}
|
||||
|
||||
var keydisplay = []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"}
|
||||
|
||||
func (i *InfoWindow) displayKeyMenu() {
|
||||
// TODO: maybe make this based on the actual keybindings
|
||||
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"}
|
||||
|
||||
log.Println("hi", len(display), i.Width)
|
||||
for y := 0; y < len(display); y++ {
|
||||
log.Println("hi", len(keydisplay), i.Width)
|
||||
for y := 0; y < len(keydisplay); y++ {
|
||||
for x := 0; x < i.Width; x++ {
|
||||
log.Println(x, i.Y-len(display)+y)
|
||||
if x < len(display[y]) {
|
||||
screen.Screen.SetContent(x, i.Y-len(display)+y, rune(display[y][x]), nil, config.DefStyle)
|
||||
log.Println(x, i.Y-len(keydisplay)+y)
|
||||
if x < len(keydisplay[y]) {
|
||||
screen.Screen.SetContent(x, i.Y-len(keydisplay)+y, rune(keydisplay[y][x]), nil, config.DefStyle)
|
||||
} else {
|
||||
screen.Screen.SetContent(x, i.Y-len(display)+y, ' ', nil, config.DefStyle)
|
||||
screen.Screen.SetContent(x, i.Y-len(keydisplay)+y, ' ', nil, config.DefStyle)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -191,4 +192,36 @@ func (i *InfoWindow) Display() {
|
||||
i.displayBuffer()
|
||||
}
|
||||
}
|
||||
|
||||
if i.HasSuggestions {
|
||||
i.HasSuggestions = false
|
||||
statusLineStyle := config.DefStyle.Reverse(true)
|
||||
if style, ok := config.Colorscheme["statusline"]; ok {
|
||||
statusLineStyle = style
|
||||
}
|
||||
keymenuOffset := 0
|
||||
if config.GetGlobalOption("keymenu").(bool) {
|
||||
keymenuOffset = len(keydisplay)
|
||||
}
|
||||
x := 0
|
||||
for _, s := range i.Suggestions {
|
||||
for _, r := range s {
|
||||
screen.Screen.SetContent(x, i.Y-keymenuOffset-1, r, nil, statusLineStyle)
|
||||
x++
|
||||
if x >= i.Width {
|
||||
return
|
||||
}
|
||||
}
|
||||
screen.Screen.SetContent(x, i.Y-keymenuOffset-1, ' ', nil, statusLineStyle)
|
||||
x++
|
||||
if x >= i.Width {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for x < i.Width {
|
||||
screen.Screen.SetContent(x, i.Y-keymenuOffset-1, ' ', nil, statusLineStyle)
|
||||
x++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@ type InfoBuf struct {
|
||||
HasError bool
|
||||
HasYN bool
|
||||
|
||||
HasSuggestions bool
|
||||
Suggestions []string
|
||||
|
||||
PromptType string
|
||||
|
||||
Msg string
|
||||
@@ -159,3 +162,8 @@ func (i *InfoBuf) Reset() {
|
||||
i.HasPrompt, i.HasMessage, i.HasError = false, false, false
|
||||
i.HasGutter = false
|
||||
}
|
||||
|
||||
func (i *InfoBuf) MakeSuggestions(s []string) {
|
||||
i.HasSuggestions = true
|
||||
i.Suggestions = s
|
||||
}
|
||||
|
||||
@@ -33,6 +33,24 @@ func SliceEnd(slc []byte, index int) []byte {
|
||||
return slc[totalSize:]
|
||||
}
|
||||
|
||||
// SliceEndStr is the same as SliceEnd but for strings
|
||||
func SliceEndStr(str string, index int) string {
|
||||
len := len(str)
|
||||
i := 0
|
||||
totalSize := 0
|
||||
for totalSize < len {
|
||||
if i >= index {
|
||||
return str[totalSize:]
|
||||
}
|
||||
|
||||
_, size := utf8.DecodeRuneInString(str[totalSize:])
|
||||
totalSize += size
|
||||
i++
|
||||
}
|
||||
|
||||
return str[totalSize:]
|
||||
}
|
||||
|
||||
// SliceStart returns a byte slice where the index is a rune index
|
||||
// Slices off the end of the slice
|
||||
func SliceStart(slc []byte, index int) []byte {
|
||||
@@ -52,6 +70,24 @@ func SliceStart(slc []byte, index int) []byte {
|
||||
return slc[:totalSize]
|
||||
}
|
||||
|
||||
// SliceStartStr is the same as SliceStart but for strings
|
||||
func SliceStartStr(str string, index int) string {
|
||||
len := len(str)
|
||||
i := 0
|
||||
totalSize := 0
|
||||
for totalSize < len {
|
||||
if i >= index {
|
||||
return str[:totalSize]
|
||||
}
|
||||
|
||||
_, size := utf8.DecodeRuneInString(str[totalSize:])
|
||||
totalSize += size
|
||||
i++
|
||||
}
|
||||
|
||||
return str[:totalSize]
|
||||
}
|
||||
|
||||
// SliceVisualEnd will take a byte slice and slice off the start
|
||||
// up to a given visual index. If the index is in the middle of a
|
||||
// rune the number of visual columns into the rune will be returned
|
||||
|
||||
Reference in New Issue
Block a user