use super::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EditorKind {
Plain,
Json,
}
#[derive(Debug, Clone, PartialEq)]
pub enum EditorSubmit {
PromptCreate { name: String },
PromptEdit { id: String },
ProviderFormApplyJson,
ProviderFormApplyOpenClawModels,
ProviderFormApplyCodexAuth,
ProviderFormApplyCodexConfigToml,
ProviderAdd,
ProviderEdit { id: String },
McpAdd,
McpEdit { id: String },
ConfigCommonSnippet { app_type: AppType },
OpenClawWorkspaceFile { filename: String },
OpenClawDailyMemoryFile { filename: String },
ConfigOpenClawEnv,
ConfigOpenClawTools,
ConfigOpenClawAgents,
ConfigWebDavSettings,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EditorMode {
Edit,
}
#[derive(Debug, Clone)]
pub struct EditorState {
pub title: String,
pub kind: EditorKind,
pub submit: EditorSubmit,
pub mode: EditorMode,
pub lines: Vec<String>,
pub scroll: usize,
pub cursor_row: usize,
pub cursor_col: usize,
pub initial_text: String,
}
impl EditorState {
pub fn new(
title: impl Into<String>,
kind: EditorKind,
submit: EditorSubmit,
initial: impl Into<String>,
) -> Self {
let initial_text = initial.into();
let mut lines = initial_text
.lines()
.map(|s| s.to_string())
.collect::<Vec<_>>();
if lines.is_empty() {
lines.push(String::new());
}
Self {
title: title.into(),
kind,
submit,
mode: EditorMode::Edit,
lines,
scroll: 0,
cursor_row: 0,
cursor_col: 0,
initial_text,
}
}
pub fn is_dirty(&self) -> bool {
self.text().trim_end() != self.initial_text.trim_end()
}
pub fn text(&self) -> String {
self.lines.join("\n")
}
pub(crate) fn replace_text(&mut self, updated: impl Into<String>) {
let updated = updated.into();
let mut lines = updated.lines().map(|s| s.to_string()).collect::<Vec<_>>();
if lines.is_empty() {
lines.push(String::new());
}
self.lines = lines;
self.cursor_row = self.cursor_row.min(self.lines.len().saturating_sub(1));
self.cursor_col = self.cursor_col.min(self.line_len_chars(self.cursor_row));
self.scroll = self.scroll.min(self.cursor_row);
}
pub(crate) fn line_len_chars(&self, row: usize) -> usize {
self.lines.get(row).map(|s| s.chars().count()).unwrap_or(0)
}
pub(crate) fn wrap_line_segments(line: &str, width: u16) -> Vec<String> {
let width = width as usize;
if width == 0 {
return vec![String::new()];
}
let mut segments = Vec::new();
let mut current = String::new();
let mut current_width = 0usize;
for ch in line.chars() {
let ch_width = UnicodeWidthChar::width(ch).unwrap_or(0).max(1);
if current_width.saturating_add(ch_width) > width && !current.is_empty() {
segments.push(current);
current = String::new();
current_width = 0;
}
current.push(ch);
current_width = current_width.saturating_add(ch_width);
}
if segments.is_empty() {
segments.push(current);
} else {
segments.push(current);
}
segments
}
pub(crate) fn wrapped_line_height(line: &str, width: u16) -> usize {
Self::wrap_line_segments(line, width).len().max(1)
}
pub(crate) fn wrapped_cursor_subline_and_x(
line: &str,
width: u16,
cursor_col: usize,
) -> (usize, u16) {
let width = width as usize;
if width == 0 {
return (0, 0);
}
let mut subline = 0usize;
let mut current_width = 0usize;
let mut col = 0usize;
for ch in line.chars() {
if col >= cursor_col {
break;
}
let ch_width = UnicodeWidthChar::width(ch).unwrap_or(0).max(1);
if current_width.saturating_add(ch_width) > width && current_width > 0 {
subline = subline.saturating_add(1);
current_width = 0;
}
current_width = current_width.saturating_add(ch_width);
col = col.saturating_add(1);
}
let x = current_width.min(width.saturating_sub(1)) as u16;
(subline, x)
}
pub(crate) fn cursor_visual_offset_from_scroll(&self, width: u16) -> (usize, u16) {
if self.lines.is_empty() {
return (0, 0);
}
let cursor_row = self.cursor_row.min(self.lines.len().saturating_sub(1));
let scroll = self
.scroll
.min(self.lines.len().saturating_sub(1))
.min(cursor_row);
let mut y = 0usize;
for row in scroll..cursor_row {
y = y.saturating_add(Self::wrapped_line_height(&self.lines[row], width));
}
let cursor_col = self.cursor_col.min(self.line_len_chars(cursor_row));
let (subline, x) =
Self::wrapped_cursor_subline_and_x(&self.lines[cursor_row], width, cursor_col);
(y.saturating_add(subline), x)
}
pub(crate) fn ensure_cursor_visible(&mut self, viewport: Size) {
if self.lines.is_empty() {
self.lines.push(String::new());
}
self.cursor_row = self.cursor_row.min(self.lines.len() - 1);
self.cursor_col = self.cursor_col.min(self.line_len_chars(self.cursor_row));
if !self.lines.is_empty() {
self.scroll = self.scroll.min(self.lines.len() - 1);
} else {
self.scroll = 0;
}
if self.cursor_row < self.scroll {
self.scroll = self.cursor_row;
}
let height = viewport.height as usize;
if height == 0 {
return;
}
let width = viewport.width.max(1);
let (mut cursor_y, _) = self.cursor_visual_offset_from_scroll(width);
while cursor_y >= height && self.scroll < self.cursor_row {
let removed = Self::wrapped_line_height(&self.lines[self.scroll], width);
cursor_y = cursor_y.saturating_sub(removed);
self.scroll = self.scroll.saturating_add(1);
}
}
pub(crate) fn byte_index(line: &str, col: usize) -> usize {
line.char_indices()
.nth(col)
.map(|(i, _)| i)
.unwrap_or(line.len())
}
fn apply_current_line_command(&mut self, command: TextEditCommand) -> bool {
if self.lines.is_empty() {
self.lines.push(String::new());
}
self.cursor_row = self.cursor_row.min(self.lines.len() - 1);
let mut input = TextInput {
value: self.lines[self.cursor_row].clone(),
cursor: self.cursor_col,
};
let changed = input.apply_command(command, TextInputPolicy::default());
self.lines[self.cursor_row] = input.value;
self.cursor_col = input.cursor;
changed
}
pub(crate) fn apply_text_command(&mut self, command: TextEditCommand) -> bool {
match command {
TextEditCommand::MoveLeft => self.move_left(),
TextEditCommand::MoveRight => self.move_right(),
TextEditCommand::MoveLineStart
| TextEditCommand::MoveLineEnd
| TextEditCommand::DeleteToLineStart
| TextEditCommand::DeleteToLineEnd
| TextEditCommand::Insert(_) => self.apply_current_line_command(command),
TextEditCommand::MoveWordLeft => self.move_word_left(),
TextEditCommand::MoveWordRight => self.move_word_right(),
TextEditCommand::DeleteBackward => self.backspace(),
TextEditCommand::DeleteForward => self.delete(),
TextEditCommand::DeleteWordBackward => self.delete_word_backward(),
}
}
pub(crate) fn move_left(&mut self) -> bool {
if self.lines.is_empty() {
self.lines.push(String::new());
}
self.cursor_row = self.cursor_row.min(self.lines.len() - 1);
if self.cursor_col > 0 {
self.cursor_col -= 1;
return true;
}
if self.cursor_row > 0 {
self.cursor_row -= 1;
self.cursor_col = self.line_len_chars(self.cursor_row);
return true;
}
false
}
pub(crate) fn move_right(&mut self) -> bool {
if self.lines.is_empty() {
self.lines.push(String::new());
}
self.cursor_row = self.cursor_row.min(self.lines.len() - 1);
let line_len = self.line_len_chars(self.cursor_row);
if self.cursor_col < line_len {
self.cursor_col += 1;
return true;
}
if self.cursor_row + 1 < self.lines.len() {
self.cursor_row += 1;
self.cursor_col = 0;
return true;
}
false
}
fn move_word_left(&mut self) -> bool {
if self.lines.is_empty() {
self.lines.push(String::new());
}
self.cursor_row = self.cursor_row.min(self.lines.len() - 1);
let before = (self.cursor_row, self.cursor_col);
loop {
if self.cursor_col > 0 {
let line = &self.lines[self.cursor_row];
self.cursor_col =
super::super::text_edit::previous_word_boundary(line, self.cursor_col);
break;
}
if self.cursor_row == 0 {
break;
}
self.cursor_row -= 1;
self.cursor_col = self.line_len_chars(self.cursor_row);
}
(self.cursor_row, self.cursor_col) != before
}
fn move_word_right(&mut self) -> bool {
if self.lines.is_empty() {
self.lines.push(String::new());
}
self.cursor_row = self.cursor_row.min(self.lines.len() - 1);
let before = (self.cursor_row, self.cursor_col);
loop {
let line_len = self.line_len_chars(self.cursor_row);
if self.cursor_col < line_len {
let line = &self.lines[self.cursor_row];
self.cursor_col =
super::super::text_edit::next_word_boundary(line, self.cursor_col);
break;
}
if self.cursor_row + 1 >= self.lines.len() {
break;
}
self.cursor_row += 1;
self.cursor_col = 0;
}
(self.cursor_row, self.cursor_col) != before
}
pub(crate) fn insert_char(&mut self, c: char) {
if self.lines.is_empty() {
self.lines.push(String::new());
}
self.cursor_row = self.cursor_row.min(self.lines.len() - 1);
let line = &mut self.lines[self.cursor_row];
let idx = Self::byte_index(line, self.cursor_col);
line.insert(idx, c);
self.cursor_col += 1;
}
pub(crate) fn insert_str(&mut self, s: &str) {
for c in s.chars() {
self.insert_char(c);
}
}
pub(crate) fn newline(&mut self) {
if self.lines.is_empty() {
self.lines.push(String::new());
}
self.cursor_row = self.cursor_row.min(self.lines.len() - 1);
let line = &mut self.lines[self.cursor_row];
let idx = Self::byte_index(line, self.cursor_col);
let rest = line.split_off(idx);
let next_row = self.cursor_row + 1;
self.lines.insert(next_row, rest);
self.cursor_row = next_row;
self.cursor_col = 0;
}
pub(crate) fn backspace(&mut self) -> bool {
if self.lines.is_empty() {
self.lines.push(String::new());
}
self.cursor_row = self.cursor_row.min(self.lines.len() - 1);
if self.cursor_col > 0 {
let line = &mut self.lines[self.cursor_row];
let start = Self::byte_index(line, self.cursor_col.saturating_sub(1));
let end = Self::byte_index(line, self.cursor_col);
if start < end && end <= line.len() {
line.replace_range(start..end, "");
self.cursor_col -= 1;
return true;
}
return false;
}
if self.cursor_row == 0 {
return false;
}
let current = self.lines.remove(self.cursor_row);
self.cursor_row -= 1;
let prev = &mut self.lines[self.cursor_row];
self.cursor_col = prev.chars().count();
prev.push_str(¤t);
true
}
pub(crate) fn delete(&mut self) -> bool {
if self.lines.is_empty() {
self.lines.push(String::new());
}
self.cursor_row = self.cursor_row.min(self.lines.len() - 1);
let line_len = self.line_len_chars(self.cursor_row);
if self.cursor_col < line_len {
let line = &mut self.lines[self.cursor_row];
let start = Self::byte_index(line, self.cursor_col);
let end = Self::byte_index(line, self.cursor_col + 1);
if start < end && end <= line.len() {
line.replace_range(start..end, "");
return true;
}
return false;
}
if self.cursor_row + 1 >= self.lines.len() {
return false;
}
let next = self.lines.remove(self.cursor_row + 1);
self.lines[self.cursor_row].push_str(&next);
true
}
fn delete_word_backward(&mut self) -> bool {
if self.lines.is_empty() {
self.lines.push(String::new());
}
self.cursor_row = self.cursor_row.min(self.lines.len() - 1);
if self.cursor_col > 0 {
return self.apply_current_line_command(TextEditCommand::DeleteWordBackward);
}
if self.cursor_row == 0 {
return false;
}
self.backspace();
self.apply_current_line_command(TextEditCommand::DeleteWordBackward)
}
}