306 lines
7.8 KiB
Go
306 lines
7.8 KiB
Go
/*
|
||
* Copyright 2026 Safronov Grigorii
|
||
*
|
||
* Licensed under the CDDL, Version 1.0 (the "License");
|
||
* you may not use this file except in compliance with the License.
|
||
*
|
||
* You may obtain a copy of the License at
|
||
* https://opensource.org/licenses/CDDL-1.0
|
||
*/
|
||
|
||
// Файл: internal/repl/completer.go
|
||
// Назначение: Автодополнение команд, имён коллекций, полей и операторов.
|
||
// Поддерживает как собственный API, так и интеграцию с chzyer/readline.
|
||
|
||
package repl
|
||
|
||
import (
|
||
"sort"
|
||
"strings"
|
||
|
||
"github.com/chzyer/readline"
|
||
)
|
||
|
||
// Completer предоставляет автодополнение.
|
||
type Completer struct {
|
||
repl *Repl
|
||
}
|
||
|
||
// NewCompleter создаёт автодополнение.
|
||
func NewCompleter(r *Repl) *Completer {
|
||
return &Completer{repl: r}
|
||
}
|
||
|
||
// Complete возвращает список подсказок для заданного префикса.
|
||
func (c *Completer) Complete(prefix string) []string {
|
||
if c == nil || c.repl == nil {
|
||
return nil
|
||
}
|
||
|
||
prefix = strings.TrimSpace(prefix)
|
||
if prefix == "" {
|
||
return nil
|
||
}
|
||
|
||
parts := strings.Fields(prefix)
|
||
trailingSpace := strings.HasSuffix(prefix, " ")
|
||
|
||
if len(parts) == 0 || (len(parts) == 1 && !trailingSpace) {
|
||
return c.completeCommand(prefix)
|
||
}
|
||
|
||
cmdName := ""
|
||
for name := range c.repl.commands {
|
||
if strings.HasPrefix(prefix, name) {
|
||
if len(name) > len(cmdName) {
|
||
cmdName = name
|
||
}
|
||
}
|
||
}
|
||
|
||
if cmdName == "" {
|
||
if len(parts) > 0 {
|
||
cmdName = parts[0]
|
||
}
|
||
}
|
||
|
||
argsPrefix := strings.TrimPrefix(prefix, cmdName)
|
||
argsPrefix = strings.TrimSpace(argsPrefix)
|
||
|
||
argParts := strings.Fields(argsPrefix)
|
||
lastArg := ""
|
||
if len(argParts) > 0 && !trailingSpace {
|
||
lastArg = argParts[len(argParts)-1]
|
||
}
|
||
|
||
return c.completeArgs(cmdName, lastArg)
|
||
}
|
||
|
||
func (c *Completer) completeCommand(prefix string) []string {
|
||
var out []string
|
||
seen := map[string]struct{}{}
|
||
for name := range c.repl.commands {
|
||
if strings.HasPrefix(name, prefix) {
|
||
if _, ok := seen[name]; !ok {
|
||
seen[name] = struct{}{}
|
||
out = append(out, name)
|
||
}
|
||
}
|
||
}
|
||
|
||
extra := []string{
|
||
"help", "clear", "quit", "exit", "pager",
|
||
}
|
||
for _, e := range extra {
|
||
if strings.HasPrefix(e, prefix) {
|
||
if _, ok := seen[e]; !ok {
|
||
seen[e] = struct{}{}
|
||
out = append(out, e)
|
||
}
|
||
}
|
||
}
|
||
|
||
sort.Strings(out)
|
||
return out
|
||
}
|
||
|
||
func (c *Completer) completeArgs(cmdName, lastArg string) []string {
|
||
switch cmdName {
|
||
case "use", "drop database", "create slice", "export", "import":
|
||
return c.completeDatabases(lastArg)
|
||
case "create collection", "drop collection", "show collections",
|
||
"show indexes", "show triggers", "show deleted", "count",
|
||
"stats timestamps", "compress collection":
|
||
return c.completeCollections(lastArg)
|
||
case "insert", "find", "update", "delete", "permanent delete", "restore",
|
||
"findbyindex", "findbytime", "create index", "drop index",
|
||
"add required", "add unique", "add min", "add max", "add enum",
|
||
"create trigger", "drop trigger", "enable trigger", "disable trigger",
|
||
"show timestamps", "doc compression":
|
||
return c.completeCollectionThenField(cmdName, lastArg)
|
||
case "migration":
|
||
return c.completeMigration(lastArg)
|
||
default:
|
||
return c.completeOperators(lastArg)
|
||
}
|
||
}
|
||
|
||
func (c *Completer) completeDatabases(prefix string) []string {
|
||
if c.repl.store == nil {
|
||
return nil
|
||
}
|
||
var out []string
|
||
for _, db := range c.repl.store.ListDatabases() {
|
||
if strings.HasPrefix(db, prefix) {
|
||
out = append(out, db)
|
||
}
|
||
}
|
||
sort.Strings(out)
|
||
return out
|
||
}
|
||
|
||
func (c *Completer) completeCollections(prefix string) []string {
|
||
if c.repl.store == nil || c.repl.currentDB == "" {
|
||
return nil
|
||
}
|
||
db, err := c.repl.store.GetDatabase(c.repl.currentDB)
|
||
if err != nil {
|
||
return nil
|
||
}
|
||
var out []string
|
||
for _, coll := range db.ListCollections() {
|
||
if strings.HasPrefix(coll, prefix) {
|
||
out = append(out, coll)
|
||
}
|
||
}
|
||
sort.Strings(out)
|
||
return out
|
||
}
|
||
|
||
func (c *Completer) completeCollectionThenField(cmdName, lastArg string) []string {
|
||
if c.repl.store == nil || c.repl.currentDB == "" {
|
||
return c.completeCollections(lastArg)
|
||
}
|
||
|
||
db, err := c.repl.store.GetDatabase(c.repl.currentDB)
|
||
if err != nil {
|
||
return nil
|
||
}
|
||
colls := db.ListCollections()
|
||
for _, coll := range colls {
|
||
if coll == lastArg {
|
||
return c.completeFields(coll, "")
|
||
}
|
||
}
|
||
|
||
if cols := c.completeCollections(lastArg); len(cols) > 0 {
|
||
return cols
|
||
}
|
||
|
||
if len(colls) > 0 {
|
||
return c.completeFields(colls[0], lastArg)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (c *Completer) completeFields(collName, prefix string) []string {
|
||
if c.repl.store == nil || c.repl.currentDB == "" {
|
||
return nil
|
||
}
|
||
db, err := c.repl.store.GetDatabase(c.repl.currentDB)
|
||
if err != nil {
|
||
return nil
|
||
}
|
||
coll, err := db.GetCollection(collName)
|
||
if err != nil {
|
||
return nil
|
||
}
|
||
|
||
fieldSet := map[string]struct{}{}
|
||
for _, doc := range coll.GetAllDocuments() {
|
||
for k := range doc.GetFields() {
|
||
fieldSet[k] = struct{}{}
|
||
}
|
||
}
|
||
|
||
var out []string
|
||
for k := range fieldSet {
|
||
if strings.HasPrefix(k, prefix) {
|
||
out = append(out, k)
|
||
}
|
||
}
|
||
sort.Strings(out)
|
||
return out
|
||
}
|
||
|
||
func (c *Completer) completeOperators(prefix string) []string {
|
||
ops := []string{
|
||
"BEFORE_INSERT", "AFTER_INSERT",
|
||
"BEFORE_UPDATE", "AFTER_UPDATE",
|
||
"BEFORE_DELETE", "AFTER_DELETE",
|
||
"abort", "skip", "modify", "log", "notify",
|
||
"unique", "required",
|
||
"start", "status", "list", "pause", "resume", "cancel", "stats", "config", "queue", "validate",
|
||
"--description", "--set", "--inc", "--currentDate", "--condition",
|
||
}
|
||
var out []string
|
||
for _, op := range ops {
|
||
if strings.HasPrefix(op, prefix) {
|
||
out = append(out, op)
|
||
}
|
||
}
|
||
sort.Strings(out)
|
||
return out
|
||
}
|
||
|
||
func (c *Completer) completeMigration(prefix string) []string {
|
||
subs := []string{"start", "status", "list", "pause", "resume", "cancel", "stats", "config", "queue", "validate"}
|
||
var out []string
|
||
for _, s := range subs {
|
||
if strings.HasPrefix(s, prefix) {
|
||
out = append(out, s)
|
||
}
|
||
}
|
||
sort.Strings(out)
|
||
return out
|
||
}
|
||
|
||
// CommonPrefix возвращает общий префикс для набора строк.
|
||
func CommonPrefix(items []string) string {
|
||
if len(items) == 0 {
|
||
return ""
|
||
}
|
||
prefix := items[0]
|
||
for _, s := range items[1:] {
|
||
for !strings.HasPrefix(s, prefix) {
|
||
if len(prefix) == 0 {
|
||
return ""
|
||
}
|
||
prefix = prefix[:len(prefix)-1]
|
||
}
|
||
}
|
||
return prefix
|
||
}
|
||
|
||
// ReadlineCompleter строит статическое дерево команд для readline.
|
||
// Динамические имена коллекций/полей дополняются через DynamicCompleter
|
||
// (см. ReadlineDynamicFunc ниже), чтобы не пересобирать дерево на каждый ввод.
|
||
func (c *Completer) ReadlineCompleter() *readline.PrefixCompleter {
|
||
items := []readline.PrefixCompleterInterface{}
|
||
|
||
// Сортируем имена команд для стабильного вывода.
|
||
names := make([]string, 0, len(c.repl.commands))
|
||
for name := range c.repl.commands {
|
||
names = append(names, name)
|
||
}
|
||
sort.Strings(names)
|
||
|
||
for _, name := range names {
|
||
items = append(items, readline.PcItem(name))
|
||
}
|
||
|
||
// Системные команды.
|
||
items = append(items, readline.PcItem("help"))
|
||
items = append(items, readline.PcItem("clear"))
|
||
items = append(items, readline.PcItem("quit"))
|
||
items = append(items, readline.PcItem("exit"))
|
||
items = append(items, readline.PcItem("pager",
|
||
readline.PcItem("on"),
|
||
readline.PcItem("off"),
|
||
))
|
||
|
||
return readline.NewPrefixCompleter(items...)
|
||
}
|
||
|
||
// ReadlineDynamicFunc возвращает функцию для readline.Config.DynamicComplete.
|
||
// Она вызывается на каждый Tab и позволяет дополнять коллекции и поля.
|
||
func (c *Completer) ReadlineDynamicFunc() func(string, int) []string {
|
||
return func(line string, pos int) []string {
|
||
prefix := ""
|
||
if pos <= len(line) {
|
||
prefix = line[:pos]
|
||
}
|
||
return c.Complete(prefix)
|
||
}
|
||
}
|