Files
futriix/internal/repl/completer.go
T

306 lines
7.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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)
}
}