readline

    readline library for MoonBit

    libedit
    readline
    repl
    terminal
    Download zip
    Version
    0.1.2
    License
    Apache-2.0
    Last updated
    last month
    Downloads
    268

    Dependencies

    #Kaida-Amethyst/readline

    面向 MoonBit native 后端的行编辑库。项目 API 仍在早期阶段;当前提供由私有 native owner 管理的 LineEditor 和 History,支持异步行读取、prompt、Ctrl-C 恢复、 application name、Emacs/Vi 编辑模式、editrc 配置以及 History 文件持久化。复制公开 handle 只会共享同一个 owner;最后一个引用消失时自动释放底层资源,不需要调用 close()。应用还可以为 editor 注册同步 Tab 补全。

    ///|
    test {
    let config = @readline.LineEditorConfig::new(
    application_name="example-repl",
    editing_mode=@readline.Emacs,
    editrc=@readline.Disabled,
    )
    let history = @readline.History::new(capacity=100)
    history.add("help")
    let editor = @readline.LineEditor::open(config~, history~)
    inspect(editor.history().length(), content="1")
    }

    #异步 REPL

    read_line() 返回 Some(line) 时由应用决定是否加入 History;None 只表示 EOF。 等待输入期间当前 task 会挂起,其他 async task 可以继续运行。Ctrl-C 会抛出可恢复的 Interrupted,当前未提交输入会被丢弃,同一个 editor 可以继续读取。

    ///|
    async fn repl_loop(
    editor : @readline.LineEditor,
    history : @readline.History,
    ) -> Unit {
    for ;; {
    let input = editor.read_line(prompt=">> ") catch {
    @readline.Interrupted => continue
    error => raise error
    }
    match input {
    None => break
    Some(line) => {
    history.add(line)
    println("\{line} (\{history.length()})")
    }
    }
    }
    }

    ///|
    async fn run_repl_example() -> Unit {
    let history = @readline.History::new()
    let editor = @readline.LineEditor::open(history~)
    repl_loop(editor, history)
    }

    #同步 Tab 补全

    completer 在用户按 Tab 时同步收到 CompletionContext,并返回字符串候选。下面的候选 只负责匹配当前词;读取循环仍然决定每条命令的实际行为。

    ///|
    fn complete_command(context : @readline.CompletionContext) -> Array[String] {
    ["help", "history", "quit"].filter(command => {
    command.has_prefix(context.current_word)
    })
    }

    ///|
    async fn completion_example() -> Unit {
    let editor = @readline.LineEditor::open()
    editor.set_completer(complete_command)
    editor.read_line(prompt=">> ") |> ignore
    editor.clear_completer()
    }

    before_cursor 和 after_cursor 分别是光标两侧的完整文本;current_word 是光标前最大 的连续非空白后缀,也包含在 before_cursor 末尾。每个候选表示对 current_word 的 完整替换,库不会替应用筛选候选,也不会自动追加空格。

    零候选不会修改输入;单候选会直接替换当前词。多个候选会先插入 Unicode 安全的最长 公共前缀;没有更多公共前缀时,再按一次 Tab 会依照 completer 返回顺序列出候选并 重绘当前行。空字符串会被忽略,重复候选只保留第一次出现的项。

    completer 的函数类型不带 raise,异常不能穿过 callback 边界。callback 运行在当前 read_line() 内,应保持同步且尽快返回;在 callback 内重入 read_line()、 set_completer() 或 clear_completer() 会抛出 Busy。注册新 completer 会替换旧 callback;调用 clear_completer() 后,后续 Tab 不再调用旧 callback。

    #中断、取消与错误

    终端配置的 interrupt character(通常是 Ctrl-C)产生 Interrupted;应用可以捕获它并 继续下一轮 REPL。调用者或父 task group 发起的 task cancellation 则保持 async runtime 的取消语义,不会转换成 Interrupted。两条路径都会先恢复终端和内部读取状态。

    同一进程同一时刻只允许一个活跃读取;同一个或另一个 editor 的冲突读取会立即抛出 Busy,不会排队。一般输入失败使用 ReadFailed(code);若终端等关键状态无法完整恢复, 则抛出 CleanupFailed(code),此后不应继续使用该 editor。

    #History 持久化

    History 文件的加载和保存由应用显式控制;读取一行不会自动写入内存或文件。

    ///|
    fn persist_history() -> Unit raise @readline.ReadlineError {
    let history = @readline.History::new(capacity=1000)
    history.add("help")
    history.save("session.history")

    let restored = @readline.History::load("session.history", capacity=1000)
    println(restored.length())
    }

    当前预编译分发只支持 macOS ARM64 native。预编译库默认缓存到:

    $MOON_HOME/cache/lib/Kaida-Amethyst/readline.mbt/...

    项目自身使用 Apache-2.0;随包分发的第三方源码及二进制保留上游许可,详见 THIRD_PARTY_NOTICES.md。

    ReadlineError

    pub(all) suberror ReadlineError {
    EmbeddedNul
    UnsupportedLocale
    InitializationFailed
    ConfigurationFailed
    InvalidHistoryCapacity(Int)
    HistoryUpdateFailed
    HistoryLoadFailed(String, Int?)
    HistorySaveFailed(String, Int?)
    InvalidUtf8
    ReadFailed(Int)
    CleanupFailed(Int)
    Busy
    Interrupted
    } derive(
    Debug
    )

    描述构造或配置行编辑器时可能发生的错误。

    CompletionContext

    pub(all) struct CompletionContext {
    before_cursor : String
    after_cursor : String
    current_word : String
    } derive(
    Debug
    )

    描述一次同步补全发生时的行缓冲区和当前词。

    EditingMode

    pub(all) enum EditingMode {
    Emacs
    Vi
    } derive(
    Debug
    )

    选择行编辑器使用的编辑命令集。

    Editrc

    pub(all) enum Editrc {
    Disabled
    Default
    File(String)
    } derive(
    Debug
    )

    决定是否读取 editrc 命令,以及从何处读取。

    History

    pub struct History {
    // private fields
    }

    表示一个有界的命令历史记录集合。

    History 是可复制的共享 handle。不同副本共享记录和容量,不会复制底层资源。

    Construction:

    请使用 History::new 构造。

    Lifecycle:

    最后一个引用消失后,底层资源会被自动释放;调用者不需要也不能显式关闭。

    History::add

    fn History::add(self : History, line : StringView) -> Unit raise ReadlineError

    原样添加一条记录。

    空字符串和重复记录都会被保留;此操作不会 trim 或去重。超过容量时会自动丢弃最旧的 记录。

    Errors:

    • line 包含 NUL 时抛出 EmbeddedNul。
    • 无法更新 History 时抛出 HistoryUpdateFailed。
    • 当前 History 正被行读取使用时抛出 Busy。

    History::clear

    fn History::clear(self : History) -> Unit raise ReadlineError

    清空当前 History 中的全部记录。

    Errors:

    当前 History 正被行读取使用时抛出 Busy。

    History::length

    fn History::length(self : History) -> Int raise ReadlineError

    返回当前保存的记录条数。

    Errors:

    • 无法查询底层 History 状态时抛出 HistoryUpdateFailed。
    • 当前 History 正被行读取使用时抛出 Busy。

    History::load

    fn History::load(path : StringView, capacity? : Int) -> History raise ReadlineError

    创建一个新的 History,并从文件加载记录。

    加载成功后,超过容量的旧记录不会被保留。加载失败时不会返回半初始化对象,也不会 修改任何已有 History。

    Parameters:

    • path:要读取的 History 文件路径。
    • capacity:最多保留的记录条数,必须大于零,默认为 1000。

    Errors:

    • capacity 不大于零时抛出 InvalidHistoryCapacity。
    • path 包含 NUL 时抛出 EmbeddedNul。
    • 无法创建或配置底层资源时抛出 InitializationFailed 或 HistoryUpdateFailed。
    • 无法读取或解析文件时抛出 HistoryLoadFailed;错误中包含路径,以及底层能够可靠 提供时的 OS error code。

    Side effects:

    此操作会读取 path 指向的文件。

    History::new

    fn History::new(capacity? : Int) -> History raise ReadlineError

    创建一个空的、有界的 History。

    Parameters:

    • capacity:最多保留的记录条数,必须大于零,默认为 1000。

    Errors:

    • capacity 不大于零时抛出 InvalidHistoryCapacity。
    • 无法创建底层资源时抛出 InitializationFailed。
    • 无法设置容量时抛出 HistoryUpdateFailed。

    Lifecycle:

    返回值的所有副本共享同一个私有 owner,并由 finalizer 自动释放底层资源。

    Examples:

    test {
    let history = History::new(capacity=100)
    inspect(history.length(), content="0")
    }

    History::save

    fn History::save(self : History, path : StringView) -> Unit raise ReadlineError

    将当前全部记录保存到文件,并覆盖目标文件原有内容。

    保存失败不会改变内存中的 History,但目标文件可能已经被创建或截断。

    Parameters:

    • path:要覆盖写入的文件路径。

    Errors:

    • path 包含 NUL 时抛出 EmbeddedNul。
    • 无法写入文件时抛出 HistorySaveFailed;错误中包含路径,以及底层能够可靠提供时的 OS error code。
    • 当前 History 正被行读取使用时抛出 Busy。

    Side effects:

    此操作会创建或覆盖 path 指向的文件。

    LineEditor

    pub struct LineEditor {
    // private fields
    }

    表示一个可长期持有的行编辑上下文。

    同一个 LineEditor 的不同副本共享私有 owner 和编辑状态,不会创建额外的 native 编辑器。

    Construction:

    请使用 LineEditor::open 构造,也可以传入可重复使用的 LineEditorConfig 和 History。

    Lifecycle:

    最后一个引用消失后,native 编辑器会被自动释放。调用者不需要也不能显式关闭 LineEditor,任一别名也不会使其他别名失效。

    LineEditor::clear_completer

    fn LineEditor::clear_completer(self : LineEditor) -> Unit raise ReadlineError

    清除当前 editor 注册的同步 completer。

    清除后,后续 Tab 不再调用先前的 callback。已经开始的读取不会在运行中切换 completer。

    Errors:

    当前进程正在读取,或者从 completion callback 中重入时抛出 Busy。

    Lifecycle:

    LineEditor 的所有别名共享清除结果;不需要单独释放 callback 注册。

    LineEditor::history

    fn LineEditor::history(self : LineEditor) -> History

    返回当前连接的 History 共享 handle。

    返回值与构造时传入或默认创建的 History 共享同一个 owner。修改任一 handle 都会影响 行编辑器后续使用的历史记录。

    LineEditor::open

    fn LineEditor::open(config? : LineEditorConfig, history? : History) -> LineEditor raise ReadlineError

    基于进程的标准输入输出创建并完整配置一个行编辑器。

    配置按以下顺序应用:建立默认配置、读取所选 editrc、应用显式编辑模式、连接 History。 默认 editrc 不存在时会被忽略;显式指定的文件不存在时会报告错误。

    Parameters:

    • config:不可变的构造选项。省略时,应用名称为 readline.mbt,存在默认 editrc 时读取该文件,并且不使用显式编辑模式覆盖配置。
    • history:要连接的 History。省略时创建容量为 1000 的空 History。

    Errors:

    • 应用名称或显式 editrc 路径包含 NUL 时抛出 EmbeddedNul。
    • 无法创建线程局部的 UTF-8 locale 时抛出 UnsupportedLocale。
    • 无法创建底层编辑器时抛出 InitializationFailed。
    • 无法应用 editrc、显式编辑模式或 History 连接时抛出 ConfigurationFailed。
    • 无法创建默认 History 时抛出相应的 History 构造错误。
    • 当前进程已有活跃读取时抛出 Busy。

    Lifecycle:

    返回值的所有副本共享同一个私有 owner 和已连接的 History。editor owner 在底层编辑器 存活期间强引用 History owner,并在释放底层编辑器后才释放该引用。两种资源都不提供 公开的关闭操作。

    Side effects:

    启用 editrc 时,构造过程会打开相应文件;此操作不会修改进程全局 locale。

    Thread safety:

    当前不保证可以跨线程使用。终端读取的并发约束见 LineEditor::read_line。

    Examples:

    test {
    let config = LineEditorConfig::new(editrc=Disabled)
    let history = History::new(capacity=100)
    let editor = LineEditor::open(config~, history~)
    inspect(editor.history().length(), content="0")
    }

    LineEditor::read_line

    async fn LineEditor::read_line(self : LineEditor, prompt? : StringView) -> String?

    异步读取并编辑一行文本。

    返回 Some(line) 表示用户提交了一行,返回 None 只表示正常 EOF。直接回车返回 Some(""),不会与 EOF 混淆。返回文本由 MoonBit 独立持有,不包含末尾换行符。 此方法不会自动将返回值加入 History。

    Parameters:

    • prompt:本次读取期间显示的提示符,默认为空字符串,不会成为长期配置。

    Errors:

    • prompt 包含 NUL,或底层行数据包含 NUL 时抛出 EmbeddedNul。
    • completer 返回的任一候选包含 NUL 时抛出 EmbeddedNul;当前输入会被丢弃,editor 与原 completer 均可继续使用。
    • 底层行数据不是合法 UTF-8 时抛出 InvalidUtf8。
    • 同一进程已有活跃读取,或所连接的 History 正在使用时抛出 Busy。
    • 用户按下 Ctrl-C 中断本次输入时抛出 Interrupted;未提交输入会被丢弃,editor 可以继续读取。
    • 发生读取错误时抛出 ReadFailed,并携带 C 边界立即保存的 OS error code;零表示 没有可靠错误码。
    • 无法完整恢复终端状态时抛出 CleanupFailed;当前 editor 此后不可继续使用。
    • 调用 task 被取消时先恢复读取状态,再继续传播 async runtime 的原取消,不会转换成 Interrupted 或 ReadFailed。

    Lifecycle:

    prompt 及底层返回缓冲区只在本次调用期间使用。方法返回或抛错前会复制结果,并恢复 editor、History、locale、完整终端状态和进程级读取 guard。

    Side effects:

    此方法等待标准输入时会挂起当前 task,并暂时接管标准终端的行编辑状态。

    Thread safety:

    多个 LineEditor 可以共存和顺序读取,但整个进程同一时刻只允许一个活跃读取;冲突 不等待,直接抛出 Busy。

    Examples:

    let editor = LineEditor::open()
    match editor.read_line(prompt=">> ") {
    Some(line) => println(line)
    None => println("EOF")
    }

    LineEditor::set_completer

    fn LineEditor::set_completer(self : LineEditor, completer : (CompletionContext) -> Array[String]) -> Unit raise ReadlineError

    注册或替换当前 editor 的同步 completer。

    completer 在用户按 Tab 时同步执行。参数中的三个字符串均由 MoonBit 独立持有;返回 的每个字符串表示对 current_word 的完整替换。空字符串会被忽略,重复候选保留第一 项;空数组表示没有候选。单候选直接替换,多候选先扩展最长公共前缀,再次按 Tab 时 按返回顺序列出候选。库不会用非前缀的公共部分缩短当前输入。

    Parameters:

    • completer:不抛异常的同步 callback;库不会按当前词筛选返回值或追加空格。

    Errors:

    当前进程正在读取,或者从 completion callback 中重入时抛出 Busy。

    Lifecycle:

    LineEditor 的所有别名共享同一个 completer。callback 只由 MoonBit 状态持有;读取 开始时取得的快照会强持有到本次 read_line() 结束。

    Thread safety:

    此方法不提供跨线程同步;活跃读取期间不会等待,而是抛出 Busy。

    LineEditorConfig

    pub struct LineEditorConfig {
    // private fields
    }

    保存用于构造 LineEditor 的不可变选项。

    配置中只包含由 MoonBit 持有的值,因此可以安全复制,也可以重复用于创建彼此独立的 编辑器。

    Construction:

    请使用 LineEditorConfig::new 构造;所有字段均有意保持私有。

    LineEditorConfig::new

    fn LineEditorConfig::new(application_name? : StringView, editing_mode? : EditingMode, editrc? : Editrc) -> LineEditorConfig

    创建用于构造行编辑器的不可变选项。

    Parameters:

    • application_name:标识当前应用,并供 editrc 条件命令匹配。
    • editing_mode:在读取 editrc 后显式选择 Emacs 或 Vi 命令集;省略时由默认配置或 editrc 决定。
    • editrc:控制是否以及如何读取 editrc;默认按照 EDITRC 和 HOME 的标准规则 查找。

    Examples:

    test {
    let config = LineEditorConfig::new(
    application_name="example-repl",
    editing_mode=Vi,
    editrc=Disabled,
    )
    config |> ignore
    }