From af387e1a6c85f8c50f955b0c88f3a14772e961c4 Mon Sep 17 00:00:00 2001 From: "KOSMOS, Tzushih.K" <48860861+kisaraki@users.noreply.github.com> Date: Mon, 24 Aug 2026 13:50:20 +0800 Subject: [PATCH] feat: add boundary alignment command --- crates/edit/src/bin/edit/draw_boundary.rs | 181 ++++++++++++++++++++++ crates/edit/src/bin/edit/draw_menubar.rs | 9 ++ crates/edit/src/bin/edit/main.rs | 5 + crates/edit/src/bin/edit/state.rs | 6 + i18n/edit.toml | 22 +++ 5 files changed, 223 insertions(+) create mode 100644 crates/edit/src/bin/edit/draw_boundary.rs diff --git a/crates/edit/src/bin/edit/draw_boundary.rs b/crates/edit/src/bin/edit/draw_boundary.rs new file mode 100644 index 00000000000..78c26589237 --- /dev/null +++ b/crates/edit/src/bin/edit/draw_boundary.rs @@ -0,0 +1,181 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use edit::buffer::TextBuffer; +use edit::framebuffer::IndexedColor; +use edit::helpers::*; +use edit::input::vk; +use edit::tui::*; + +use crate::localization::*; +use crate::state::*; + +pub fn draw_dialog_boundary_align(ctx: &mut Context, state: &mut State) { + // EN: Ask for a positive boundary column, defaulting to 80, before changing the document. + // 中文:修改文件前要求輸入正整數邊界欄位,預設值為 80。 + let mut apply; + let mut cancel; + + ctx.modal_begin("boundary-align", loc(LocId::BoundaryAlignDialogTitle)); + { + ctx.block_begin("boundary-align-content"); + ctx.inherit_focus(); + ctx.attr_padding(Rect::three(1, 2, 1)); + { + let contains_focus = ctx.contains_focus(); + ctx.label("boundary-align-description", loc(LocId::BoundaryAlignDescription)); + + if ctx.editline("boundary-align-column", &mut state.boundary_align_column) { + state.boundary_align_invalid = false; + } + ctx.attr_intrinsic_size(Size { width: 24, height: 1 }); + if state.boundary_align_invalid { + ctx.attr_background_rgba(ctx.indexed(IndexedColor::Red)); + ctx.attr_foreground_rgba(ctx.indexed(IndexedColor::BrightWhite)); + ctx.label("boundary-align-invalid", loc(LocId::BoundaryAlignInvalid)); + ctx.attr_foreground_rgba(ctx.indexed(IndexedColor::Red)); + } + + ctx.table_begin("boundary-align-choices"); + ctx.inherit_focus(); + ctx.attr_padding(Rect::three(1, 0, 0)); + ctx.attr_position(Position::Center); + ctx.table_set_cell_gap(Size { width: 2, height: 0 }); + { + ctx.table_next_row(); + ctx.inherit_focus(); + apply = ctx.button("ok", loc(LocId::Ok), ButtonStyle::default()); + ctx.inherit_focus(); + cancel = ctx.button("cancel", loc(LocId::Cancel), ButtonStyle::default()); + } + ctx.table_end(); + + if contains_focus && ctx.consume_shortcut(vk::RETURN) { + apply = true; + } + } + ctx.block_end(); + } + cancel |= ctx.modal_end(); + + if apply { + let column = + state.boundary_align_column.trim().parse::().ok().filter(|column| *column > 0); + if let Some(column) = column { + if let Some(doc) = state.documents.active_mut() { + let mut tb = doc.buffer.borrow_mut(); + align_buffer_to_boundary(&mut tb, column); + tb.make_cursor_visible(); + } + state.wants_boundary_align = false; + reset_boundary_input(state); + ctx.needs_rerender(); + } else { + state.boundary_align_invalid = true; + ctx.needs_rerender(); + } + } else if cancel { + state.wants_boundary_align = false; + reset_boundary_input(state); + ctx.needs_rerender(); + } +} + +/// EN: Restores the input state shared by apply and cancel paths. +/// 中文:還原套用與取消流程共用的輸入狀態。 +fn reset_boundary_input(state: &mut State) { + state.boundary_align_column.clear(); + state.boundary_align_column.push_str("80"); + state.boundary_align_invalid = false; +} + +fn align_buffer_to_boundary(tb: &mut TextBuffer, column: usize) { + let text = read_buffer_text(tb); + let aligned = align_text_to_boundary(&String::from_utf8_lossy(&text), column); + if aligned.as_bytes() == text { + return; + } + + tb.select_all(); + tb.write_raw(aligned.as_bytes()); +} + +fn read_buffer_text(tb: &TextBuffer) -> Vec { + let mut text = Vec::with_capacity(tb.text_length()); + while text.len() < tb.text_length() { + let chunk = tb.read_forward(text.len()); + if chunk.is_empty() { + break; + } + text.extend_from_slice(chunk); + } + text +} + +fn align_text_to_boundary(text: &str, column: usize) -> String { + // EN: Overflow becomes carry text joined to the next original line until end-of-file. + // 中文:超出邊界的文字會成為延續內容,與下一原始行合併並持續處理至檔尾。 + if column == 0 || text.is_empty() { + return text.to_string(); + } + + let newline = if text.contains("\r\n") { "\r\n" } else { "\n" }; + let has_final_newline = text.ends_with('\n'); + let mut output = Vec::new(); + let mut carry = None::; + + for line in text.lines() { + let mut combined = carry.take().unwrap_or_default(); + combined.push_str(line); + let mut split = false; + + while let Some(byte_offset) = combined.char_indices().nth(column).map(|(offset, _)| offset) + { + output.push(combined[..byte_offset].to_string()); + combined = combined[byte_offset..].to_string(); + split = true; + } + + if split { + carry = Some(combined); + } else { + output.push(combined); + } + } + + if let Some(carry) = carry { + output.push(carry); + } + + let mut aligned = output.join(newline); + if has_final_newline { + aligned.push_str(newline); + } + aligned +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn wraps_overflow_and_merges_it_with_the_next_line() { + assert_eq!( + align_text_to_boundary("1234567\nABC\nshort\n12345678901\nXY", 5), + "12345\n67ABC\nshort\n12345\n67890\n1XY" + ); + } + + #[test] + fn counts_unicode_characters_and_preserves_crlf_and_final_newline() { + assert_eq!( + align_text_to_boundary("一二三四五六\r\n甲乙\r\n短\r\n", 5), + "一二三四五\r\n六甲乙\r\n短\r\n" + ); + } + + #[test] + fn keeps_short_lines_unchanged() { + assert_eq!(align_text_to_boundary("one\ntwo\nthree", 80), "one\ntwo\nthree"); + } +} diff --git a/crates/edit/src/bin/edit/draw_menubar.rs b/crates/edit/src/bin/edit/draw_menubar.rs index 401614786a1..9f42ffa4d07 100644 --- a/crates/edit/src/bin/edit/draw_menubar.rs +++ b/crates/edit/src/bin/edit/draw_menubar.rs @@ -118,6 +118,15 @@ fn draw_menu_edit(ctx: &mut Context, state: &mut State) { tb.select_all(); ctx.needs_rerender(); } + drop(tb); + // EN: Boundary Alignment is the final Edit-menu command and starts at column 80. + // 中文:「邊界對齊」位於編輯選單末項,輸入欄預設為第 80 字元。 + if ctx.menubar_menu_button(loc(LocId::EditBoundaryAlign), 'B', vk::NULL) { + state.wants_boundary_align = true; + state.boundary_align_column.clear(); + state.boundary_align_column.push_str("80"); + state.boundary_align_invalid = false; + } ctx.menubar_menu_end(); } diff --git a/crates/edit/src/bin/edit/main.rs b/crates/edit/src/bin/edit/main.rs index 27ae7fab6c0..0fe770a433e 100644 --- a/crates/edit/src/bin/edit/main.rs +++ b/crates/edit/src/bin/edit/main.rs @@ -3,6 +3,7 @@ mod apperr; mod documents; +mod draw_boundary; mod draw_editor; mod draw_filepicker; mod draw_menubar; @@ -15,6 +16,7 @@ use std::path::Path; use std::time::Duration; use std::{env, process}; +use draw_boundary::*; use draw_editor::*; use draw_filepicker::*; use draw_menubar::*; @@ -365,6 +367,9 @@ fn draw(ctx: &mut Context, state: &mut State) { if state.wants_go_to_file { draw_go_to_file(ctx, state); } + if state.wants_boundary_align { + draw_dialog_boundary_align(ctx, state); + } if state.wants_about { draw_dialog_about(ctx, state); } diff --git a/crates/edit/src/bin/edit/state.rs b/crates/edit/src/bin/edit/state.rs index 13a1cefbbea..ea8091d7344 100644 --- a/crates/edit/src/bin/edit/state.rs +++ b/crates/edit/src/bin/edit/state.rs @@ -166,6 +166,9 @@ pub struct State { pub wants_statusbar_focus: bool, pub wants_indentation_picker: bool, pub wants_go_to_file: bool, + pub wants_boundary_align: bool, + pub boundary_align_column: String, + pub boundary_align_invalid: bool, pub wants_about: bool, pub wants_close: bool, pub wants_exit: bool, @@ -216,6 +219,9 @@ impl State { wants_encoding_change: StateEncodingChange::None, wants_indentation_picker: false, wants_go_to_file: false, + wants_boundary_align: false, + boundary_align_column: "80".into(), + boundary_align_invalid: false, wants_about: false, wants_close: false, wants_exit: false, diff --git a/i18n/edit.toml b/i18n/edit.toml index 11039636308..107cce6d413 100644 --- a/i18n/edit.toml +++ b/i18n/edit.toml @@ -888,6 +888,28 @@ zh-hans = "全选" zh-hant = "全選" # A menu bar item +# EN: Hard boundary alignment commands. +# 中文:固定邊界對齊命令。 +[EditBoundaryAlign] +en = "Boundary Alignment…" +zh-hans = "边界对齐…" +zh-hant = "邊界對齊…" + +[BoundaryAlignDialogTitle] +en = "Boundary Alignment" +zh-hans = "边界对齐" +zh-hant = "邊界對齊" + +[BoundaryAlignDescription] +en = "Right boundary column (positive integer):" +zh-hans = "右边界字元位置(正整数):" +zh-hant = "右邊界字元位置(正整數):" + +[BoundaryAlignInvalid] +en = "Enter a positive integer." +zh-hans = "请输入正整数。" +zh-hant = "請輸入正整數。" + [View] en = "View" ar = "عرض"