Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 181 additions & 0 deletions crates/edit/src/bin/edit/draw_boundary.rs
Original file line number Diff line number Diff line change
@@ -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::<usize>().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<u8> {
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::<String>;

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");
}
}
9 changes: 9 additions & 0 deletions crates/edit/src/bin/edit/draw_menubar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down
5 changes: 5 additions & 0 deletions crates/edit/src/bin/edit/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

mod apperr;
mod documents;
mod draw_boundary;
mod draw_editor;
mod draw_filepicker;
mod draw_menubar;
Expand All @@ -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::*;
Expand Down Expand Up @@ -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);
}
Expand Down
6 changes: 6 additions & 0 deletions crates/edit/src/bin/edit/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
22 changes: 22 additions & 0 deletions i18n/edit.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "عرض"
Expand Down