From d695d05fbdfce414fd238dfb5c6ccf149a8c7036 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D1=80=D0=B8=D0=B3=D0=BE=D1=80=D0=B8=D0=B9=20=D0=A1?= =?UTF-8?q?=D0=B0=D1=84=D1=80=D0=BE=D0=BD=D0=BE=D0=B2?= Date: Sat, 19 Sep 2026 22:31:37 +0000 Subject: [PATCH] Upload files to "internal/repl" --- internal/repl/completer.go | 305 +++++++++++++++++++++++++++++++++++++ internal/repl/pager.go | 195 ++++++++++++++++++++++++ internal/repl/table.go | 252 ++++++++++++++++++++++++++++++ 3 files changed, 752 insertions(+) create mode 100644 internal/repl/completer.go create mode 100644 internal/repl/pager.go create mode 100644 internal/repl/table.go diff --git a/internal/repl/completer.go b/internal/repl/completer.go new file mode 100644 index 0000000..d2376ea --- /dev/null +++ b/internal/repl/completer.go @@ -0,0 +1,305 @@ +/* + * 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) + } +} diff --git a/internal/repl/pager.go b/internal/repl/pager.go new file mode 100644 index 0000000..db8d801 --- /dev/null +++ b/internal/repl/pager.go @@ -0,0 +1,195 @@ +/* + * 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/pager.go +// Назначение: Пейджер для постраничного вывода длинных результатов. +// Использует внешний pager (less/more) если доступен, иначе встроенный. +// Совместимо с Linux и OpenIndiana. + +package repl + +import ( + "bufio" + "fmt" + "io" + "os" + "os/exec" + "strings" +) + +// Pager предоставляет постраничный вывод. +type Pager struct { + enabled bool + threshold int + external string + lines int +} + +// NewPager создаёт пейджер. +func NewPager(enabled bool, threshold int) *Pager { + p := &Pager{ + enabled: enabled, + threshold: threshold, + lines: 24, + } + p.external = detectPager() + return p +} + +// detectPager ищет доступный внешний pager. +// Порядок: $PAGER -> less -> more -> "" (встроенный). +func detectPager() string { + if env := os.Getenv("PAGER"); env != "" { + return env + } + if path, err := exec.LookPath("less"); err == nil { + return path + } + if path, err := exec.LookPath("more"); err == nil { + return path + } + return "" +} + +// Enabled возвращает, включён ли пейджер. +func (p *Pager) Enabled() bool { + return p != nil && p.enabled +} + +// SetEnabled включает/выключает пейджер. +func (p *Pager) SetEnabled(v bool) { + if p != nil { + p.enabled = v + } +} + +// SetThreshold устанавливает порог строк. +func (p *Pager) SetThreshold(n int) { + if p != nil && n > 0 { + p.threshold = n + } +} + +// Page выводит текст через пейджер, если он длиннее порога. +func (p *Pager) Page(text string) error { + if p == nil || !p.enabled { + fmt.Print(text) + return nil + } + + lineCount := strings.Count(text, "\n") + if lineCount < p.threshold { + fmt.Print(text) + return nil + } + + if p.external != "" { + if err := p.pageExternal(text); err == nil { + return nil + } + // fallback на встроенный + } + + return p.pageBuiltin(text) +} + +// pageExternal запускает внешний pager. +func (p *Pager) pageExternal(text string) error { + args := []string{} + base := strings.ToLower(p.external) + + if strings.Contains(base, "less") { + // -R: сохранять ANSI-цвета, -F: выйти если всё влезает, -X: не чистить экран + args = append(args, "-RFX") + } + + cmd := exec.Command(p.external, args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + stdin, err := cmd.StdinPipe() + if err != nil { + return err + } + + if err := cmd.Start(); err != nil { + return err + } + + if _, err := io.WriteString(stdin, text); err != nil { + stdin.Close() + _ = cmd.Wait() + return err + } + stdin.Close() + + return cmd.Wait() +} + +// pageBuiltin — встроенный пейджер (fallback). +// Управление: Enter — следующая строка, Space — страница, q — выход. +func (p *Pager) pageBuiltin(text string) error { + scanner := bufio.NewScanner(strings.NewReader(text)) + buf := make([]byte, 0, 64*1024) + scanner.Buffer(buf, 1024*1024) + + lines := []string{} + for scanner.Scan() { + lines = append(lines, scanner.Text()) + } + if err := scanner.Err(); err != nil { + return err + } + + pageSize := p.lines - 1 + if pageSize < 5 { + pageSize = 5 + } + + reader := bufio.NewReader(os.Stdin) + pos := 0 + + for pos < len(lines) { + end := pos + pageSize + if end > len(lines) { + end = len(lines) + } + for i := pos; i < end; i++ { + fmt.Println(lines[i]) + } + pos = end + + if pos >= len(lines) { + break + } + + fmt.Printf("\033[7m--More--\033[0m (Enter/Space/q) ") + ch, err := reader.ReadByte() + fmt.Print("\r\033[K") + if err != nil { + break + } + switch ch { + case 'q', 'Q': + return nil + case '\n', '\r': + // одна строка + if pos < len(lines) { + fmt.Println(lines[pos]) + pos++ + } + case ' ': + // страница + default: + // игнорируем + } + } + return nil +} diff --git a/internal/repl/table.go b/internal/repl/table.go new file mode 100644 index 0000000..bda3f03 --- /dev/null +++ b/internal/repl/table.go @@ -0,0 +1,252 @@ +/* + * 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/table.go +// Назначение: Табличный вывод результатов (по умолчанию для find/count/stats и т.п.). +// Работает без внешних зависимостей, совместимо с Linux и OpenIndiana. + +package repl + +import ( + "fmt" + "sort" + "strings" + "unicode/utf8" +) + +// Table — простая ASCII-таблица. +type Table struct { + headers []string + rows [][]string +} + +// NewTable создаёт таблицу с заголовками. +func NewTable(headers ...string) *Table { + return &Table{ + headers: append([]string(nil), headers...), + rows: make([][]string, 0), + } +} + +// AddRow добавляет строку. +func (t *Table) AddRow(cells ...string) { + row := make([]string, len(cells)) + copy(row, cells) + t.rows = append(t.rows, row) +} + +// Len возвращает число строк. +func (t *Table) Len() int { + return len(t.rows) +} + +// String рендерит таблицу. +func (t *Table) String() string { + if len(t.headers) == 0 && len(t.rows) == 0 { + return "" + } + + cols := len(t.headers) + for _, r := range t.rows { + if len(r) > cols { + cols = len(r) + } + } + + // Нормализуем + headers := make([]string, cols) + copy(headers, t.headers) + + rows := make([][]string, len(t.rows)) + for i, r := range t.rows { + rr := make([]string, cols) + copy(rr, r) + rows[i] = rr + } + + // Ширины + widths := make([]int, cols) + for i, h := range headers { + widths[i] = displayWidth(h) + } + for _, r := range rows { + for i, c := range r { + if w := displayWidth(c); w > widths[i] { + widths[i] = w + } + } + } + + var sb strings.Builder + + // Разделитель + sep := buildSeparator(widths) + + // Заголовок + sb.WriteString(sep) + sb.WriteString("\n") + sb.WriteString(renderRow(headers, widths)) + sb.WriteString("\n") + sb.WriteString(sep) + sb.WriteString("\n") + + // Строки + for _, r := range rows { + sb.WriteString(renderRow(r, widths)) + sb.WriteString("\n") + } + + sb.WriteString(sep) + sb.WriteString("\n") + + return sb.String() +} + +func buildSeparator(widths []int) string { + var sb strings.Builder + sb.WriteString("+") + for _, w := range widths { + sb.WriteString(strings.Repeat("-", w+2)) + sb.WriteString("+") + } + return sb.String() +} + +func renderRow(cells []string, widths []int) string { + var sb strings.Builder + sb.WriteString("|") + for i, w := range widths { + cell := "" + if i < len(cells) { + cell = cells[i] + } + sb.WriteString(" ") + sb.WriteString(padRight(cell, w)) + sb.WriteString(" |") + } + return sb.String() +} + +func padRight(s string, w int) string { + dw := displayWidth(s) + if dw >= w { + return s + } + return s + strings.Repeat(" ", w-dw) +} + +// displayWidth учитывает широкие руны (грубо). +func displayWidth(s string) int { + w := 0 + for _, r := range s { + if r == '\t' { + w += 4 + continue + } + if r < 32 { + continue + } + if isWide(r) { + w += 2 + } else { + w += 1 + } + } + _ = utf8.RuneCountInString(s) + return w +} + +// isWide — грубая эвристика для широких символов CJK. +func isWide(r rune) bool { + return (r >= 0x1100 && r <= 0x115F) || + (r >= 0x2E80 && r <= 0xA4CF) || + (r >= 0xAC00 && r <= 0xD7A3) || + (r >= 0xF900 && r <= 0xFAFF) || + (r >= 0xFE30 && r <= 0xFE4F) || + (r >= 0xFF00 && r <= 0xFF60) || + (r >= 0xFFE0 && r <= 0xFFE6) +} + +// MapToTable преобразует список map в таблицу с сортировкой ключей. +func MapToTable(rows []map[string]interface{}) *Table { + if len(rows) == 0 { + return NewTable() + } + + // Собираем все ключи + keySet := map[string]struct{}{} + for _, r := range rows { + for k := range r { + keySet[k] = struct{}{} + } + } + + keys := make([]string, 0, len(keySet)) + for k := range keySet { + keys = append(keys, k) + } + sort.Strings(keys) + + // _id всегда первым, если есть + if _, ok := keySet["_id"]; ok { + sort.SliceStable(keys, func(i, j int) bool { + if keys[i] == "_id" { + return true + } + if keys[j] == "_id" { + return false + } + return keys[i] < keys[j] + }) + } + + t := NewTable(keys...) + for _, r := range rows { + cells := make([]string, len(keys)) + for i, k := range keys { + v, ok := r[k] + if !ok { + cells[i] = "" + continue + } + cells[i] = FormatCell(v) + } + t.AddRow(cells...) + } + + return t +} + +// FormatCell форматирует значение для ячейки. +func FormatCell(v interface{}) string { + if v == nil { + return "NULL" + } + switch x := v.(type) { + case string: + return x + case bool: + if x { + return "true" + } + return "false" + case float64: + if x == float64(int64(x)) { + return fmt.Sprintf("%d", int64(x)) + } + return fmt.Sprintf("%g", x) + case int, int8, int16, int32, int64: + return fmt.Sprintf("%d", x) + case uint, uint8, uint16, uint32, uint64: + return fmt.Sprintf("%d", x) + default: + return fmt.Sprintf("%v", x) + } +}