///|
fn normalize_line_ending(line : String) -> String {
if line.has_suffix("\r\n") {
line[:line.length() - 2].to_owned()
} else if line.has_suffix("\n") {
line[:line.length() - 1].to_owned()
} else {
line
}
}
///|
fn decode_line(bytes : Bytes) -> String raise ReadlineError {
let line = @utf8.decode(bytes) catch { _ => raise InvalidUtf8 }
normalize_line_ending(line)
}
///|
fn EditorOwner::raise_session_operation(
self : EditorOwner,
status : Int,
) -> Unit raise ReadlineError {
match status {
0 => ()
1 => raise Busy
2 => raise EmbeddedNul
5 => raise CleanupFailed(self.session_errno())
_ => raise ReadFailed(self.session_errno())
}
}
///|
priv struct StdinInputBuffer {
mut remaining : Bytes?
}
///|
let stdin_input_buffer : StdinInputBuffer = { remaining: None }
///|
fn StdinInputBuffer::take(self : StdinInputBuffer) -> Bytes? {
let remaining = self.remaining
self.remaining = None
remaining
}
///|
fn StdinInputBuffer::save(self : StdinInputBuffer, remaining : Bytes) -> Unit {
if remaining.length() > 0 {
self.remaining = Some(remaining)
}
}
///|
async fn EditorOwner::drive_session(
self : EditorOwner,
generation : Int64,
completer : ((CompletionContext) -> Array[String])?,
) -> Int {
for ;; {
match self.session_drive(generation) {
0 => {
let input = match stdin_input_buffer.take() {
Some(bytes) => Some(bytes)
None =>
@stdio.stdin.read_some(max_len=256) catch {
error if @async.is_being_cancelled() => raise error
@os_error.OSError(error_code, ..) => raise ReadFailed(error_code)
_ => raise ReadFailed(0)
}
}
let operation = match input {
Some(bytes) => self.session_feed(generation, bytes, false)
None => self.session_feed(generation, Bytes::new(0), true)
}
self.raise_session_operation(operation)
}
1 => continue
2..=5 as status => return status
7 => {
guard completer is Some(completer) else { raise ReadFailed(0) }
let context = self.session_completion_context(generation)
let response = make_session_completion_response(completer, context)
self.session_apply_completion(generation, response)
}
_ => raise ReadFailed(self.session_errno())
}
}
}
///|
fn EditorOwner::finish_session(
self : EditorOwner,
generation : Int64,
drive_status : Int,
) -> String? raise {
let error_code = self.session_errno()
let operation_status = self.session_operation_status()
let bytes = self.session_finish(generation)
guard self.session_operation_status() == 0 else {
raise CleanupFailed(self.session_errno())
}
match drive_status {
2 => Some(decode_line(bytes))
3 => None
4 => raise Interrupted
5 if operation_status == 2 => raise EmbeddedNul
_ => raise ReadFailed(error_code)
}
}
///|
fn EditorOwner::finish_failed_session(
self : EditorOwner,
generation : Int64,
) -> Unit raise ReadlineError {
self.session_finish(generation) |> ignore
guard self.session_operation_status() == 0 else {
raise CleanupFailed(self.session_errno())
}
}
///|
/// 异步读取并编辑一行文本。
///
/// 返回 `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:**
///
/// ```mbt nocheck
/// let editor = LineEditor::open()
/// match editor.read_line(prompt=">> ") {
/// Some(line) => println(line)
/// None => println("EOF")
/// }
/// ```
pub async fn LineEditor::read_line(
self : LineEditor,
prompt? : StringView = "",
) -> String? {
self.ensure_usable()
let prompt = @utf8.encode(prompt)
let completer = self.completion.get()
let generation = self.owner.session_begin(prompt, completer is Some(_))
guard generation != 0L else {
self.owner.raise_session_operation(self.owner.session_operation_status())
raise ReadFailed(self.owner.session_errno())
}
let active = Ref(true)
defer (if active.val { self.owner.session_cancel(generation) |> ignore })
let drive_status = self.owner.drive_session(generation, completer) catch {
error if @async.is_being_cancelled() => {
@async.protect_from_cancel(() => {
self.owner.session_cancel(generation) |> ignore
})
active.val = false
raise error
}
error => {
self.owner.finish_failed_session(generation)
active.val = false
raise error
}
}
stdin_input_buffer.save(self.owner.session_take_remaining_input(generation))
let result = self.owner.finish_session(generation, drive_status)
active.val = false
result
}