Skip to content

hymkor/go-multiline-ny

Repository files navigation

go-multiline-ny

Go Reference

go-multiline-ny is a REPL-oriented multi-line editor built on top of go-readline-ny, a pure Go Readline-compatible implementation. It allows vertical cursor movement across lines, updates only the affected input lines, and does not take over the full screen — making it ideal for SQL-like multi-line inputs. It is implemented in pure Go, and works on Windows and most UNIX-like systems (confirmed on Linux and FreeBSD; macOS is expected to work as well).

Key Feature
Ctrl+M or Enter Insert a new line1
Ctrl+J, Ctrl+Enter or Meta+Enter Submit all lines
Ctrl+P or Up Move cursor to previous line or last line of previous set of input lines in history
Ctrl+N or Down Move cursor to next line or first line of next set of input lines in history
Meta+P or Ctrl+Up Fetch previous set of input lines in history
Meta+N or Ctrl+Down Fetch next set of input lines in history
Ctrl+Y Paste the string in the clipboard
Ctrl+R Incremental search

Meta means either Alt+key or Esc followed by key.

image

package main

import (
    "context"
    "fmt"
    "io"
    "os"
    "regexp"
    "strings"

    "github.com/mattn/go-colorable"

    "github.com/nyaosorg/go-readline-ny"
    "github.com/nyaosorg/go-readline-ny/keys"
    "github.com/nyaosorg/go-readline-ny/simplehistory"

    "github.com/hymkor/go-multiline-ny"
    "github.com/hymkor/go-multiline-ny/completion"
)

var (
    commands = []string{"select", "insert", "delete", "update"}
    tables   = []string{"dept", "emp", "bonus", "salgrade"}
    columns  = []string{"deptno", "dname", "loc", "empno", "ename", "job", "mgr", "hiredate", "sal", "comm", "grade", "losal", "hisal"}
)

func getCompletionCandidates(fields []string) (forCompletion []string, forListing []string) {
    candidates := commands
    for _, word := range fields {
        if strings.EqualFold(word, "from") {
            candidates = append([]string{"where"}, tables...)
        } else if strings.EqualFold(word, "set") {
            candidates = append([]string{"where"}, columns...)
        } else if strings.EqualFold(word, "update") {
            candidates = append([]string{"set"}, tables...)
        } else if strings.EqualFold(word, "delete") {
            candidates = []string{"from"}
        } else if strings.EqualFold(word, "select") {
            candidates = append([]string{"from"}, columns...)
        } else if strings.EqualFold(word, "where") {
            candidates = append([]string{"and", "or"}, columns...)
        }
    }
    return candidates, candidates
}

func main() {
    ctx := context.Background()
    fmt.Println("C-m or Enter      : Insert a linefeed")
    fmt.Println("C-p or UP         : Move to the previous line.")
    fmt.Println("C-n or DOWN       : Move to the next line")
    fmt.Println("C-j or Meta+Enter : Submit")
    fmt.Println("C-c               : Abort.")
    fmt.Println("C-D with no chars : Quit.")
    fmt.Println("C-UP   or Meta-P  : Move to the previous history entry")
    fmt.Println("C-DOWN or Meta-N  : Move to the next history entry")

    var ed multiline.Editor
    ed.SetPrompt(func(w io.Writer, lnum int) (int, error) {
        return fmt.Fprintf(w, "[%d] ", lnum+1)
    })
    ed.SetPredictColor(readline.PredictColorBlueItalic)

    ed.Highlight = []readline.Highlight{
        // Words -> dark green
        {Pattern: regexp.MustCompile(`(?i)(SELECT|INSERT|FROM|WHERE|AS)`), Sequence: "\x1B[33;49;22m"},
        // Double quotation -> light magenta
        {Pattern: regexp.MustCompile(`(?m)"([^"\n]*\\")*[^"\n]*$|"([^"\n]*\\")*[^"\n]*"`), Sequence: "\x1B[32;49;1m"},
        // Single quotation -> light red
        {Pattern: regexp.MustCompile(`(?m)'([^'\n]*\\')*[^'\n]*$|'([^'\n]*\\')*[^'\n]*'`), Sequence: "\x1B[31;49;1m"},
        // Number literal -> light blue
        {Pattern: regexp.MustCompile(`[0-9]+`), Sequence: "\x1B[34;49;1m"},
        // Comment -> dark gray
        {Pattern: regexp.MustCompile(`(?s)/\*.*?\*/`), Sequence: "\x1B[30;49;1m"},
        // Multiline string literal -> dark red
        {Pattern: regexp.MustCompile("(?s)```.*?```"), Sequence: "\x1B[31;49;22m"},
    }
    ed.ResetColor = "\x1B[0m"
    ed.DefaultColor = "\x1B[37;49;1m"

    // To enable escape sequence on Windows.
    // (On other operating systems, it can be omitted)
    ed.SetWriter(colorable.NewColorableStdout())

    // enable history (optional)
    history := simplehistory.New()
    ed.SetHistory(history)
    ed.SetHistoryCycling(true)

    // enable completion (optional)
    ed.BindKey(keys.CtrlI, &completion.CmdCompletionOrList{
        // Characters listed here are excluded from completion.
        Delimiter: "&|><;",
        // Enclose candidates with these characters when they contain spaces
        Enclosure: `"'`,
        // String to append when only one candidate remains
        Postfix: " ",
        // Function for listing candidates
        Candidates: getCompletionCandidates,
    })

    for {
        lines, err := ed.Read(ctx)
        if err != nil {
            fmt.Fprintln(os.Stderr, err.Error())
            return
        }
        L := strings.Join(lines, "\n")
        fmt.Println("-----")
        fmt.Println(L)
        fmt.Println("-----")
        history.Add(L)
    }
}
package main

import (
    "context"
    "fmt"
    "io"
    "os"
    "strings"

    "github.com/atotto/clipboard"
    "github.com/mattn/go-colorable"

    "github.com/nyaosorg/go-readline-ny/simplehistory"

    "github.com/hymkor/go-multiline-ny"
)

type OSClipboard struct{}

func (OSClipboard) Read() (string, error) {
    return clipboard.ReadAll()
}

func (OSClipboard) Write(s string) error {
    return clipboard.WriteAll(s)
}

func main() {
    ctx := context.Background()
    fmt.Println("C-m or Enter      : Submit when lines end with `;`")
    fmt.Println("                    Otherwise insert a linefeed.")
    fmt.Println("C-j               : Submit always")
    fmt.Println("C-c               : Abort.")
    fmt.Println("C-D with no chars : Quit.")

    var ed multiline.Editor
    ed.SetPrompt(func(w io.Writer, lnum int) (int, error) {
        return fmt.Fprintf(w, "[%d] ", lnum+1)
    })

    ed.SubmitOnEnterWhen(func(lines []string, _ int) bool {
        return strings.HasSuffix(strings.TrimSpace(lines[len(lines)-1]), ";")
    })

    // To enable escape sequence on Windows.
    // (On other operating systems, it can be omitted)
    ed.SetWriter(colorable.NewColorableStdout())

    // Use the clipboard of the operating system.
    ed.LineEditor.Clipboard = OSClipboard{}

    history := simplehistory.New()
    ed.SetHistory(history)
    ed.SetHistoryCycling(true)

    for {
        lines, err := ed.Read(ctx)
        if err != nil {
            fmt.Fprintln(os.Stderr, err.Error())
            return
        }
        L := strings.Join(lines, "\n")
        fmt.Println("-----")
        fmt.Println(L)
        fmt.Println("-----")
        history.Add(L)
    }
}

Changelog

Acknowledgements

Author

Footnotes

  1. The submit condition can be customized.

About

A REPL-oriented multi-line Readline, without full-screen UI.

Topics

Resources

License

Stars

Watchers

Forks

Contributors