///|
/// Text Buffer - Core data structure for text editing
///|
/// Cursor position (1-indexed)
pub(all) struct CursorPosition {
row : Int
col : Int
}
///|
/// Create cursor position
pub fn CursorPosition::new(row? : Int = 1, col? : Int = 1) -> CursorPosition {
{ row, col }
}
///|
/// Selection range
pub(all) struct Selection {
anchor : CursorPosition
cursor : CursorPosition
}
///|
/// Check if selection is empty
pub fn Selection::is_empty(self : Selection) -> Bool {
self.anchor.row == self.cursor.row && self.anchor.col == self.cursor.col
}
///|
/// Selection mode
pub(all) enum SelectionMode {
Caret
VisualChar
VisualLine
VisualBlock
}
///|
/// Text buffer state
pub(all) struct TextBuffer {
lines : Array[String]
cursor : CursorPosition
selection : Selection
mode : SelectionMode
modified : Bool
}
///|
/// Create empty text buffer
pub fn TextBuffer::new() -> TextBuffer {
let cursor = CursorPosition::new()
TextBuffer::{
lines: [""],
cursor,
selection: { anchor: cursor, cursor },
mode: SelectionMode::Caret,
modified: false,
}
}
///|
/// Create text buffer from string (preserves \n line endings)
pub fn TextBuffer::from_string(text : String) -> TextBuffer {
let lines = text.split("\n").map(fn(sv) { sv.to_string() }).to_array()
TextBuffer::{
lines,
cursor: CursorPosition::new(),
selection: { anchor: CursorPosition::new(), cursor: CursorPosition::new() },
mode: SelectionMode::Caret,
modified: false,
}
}
///|
/// Create text buffer from file (not yet implemented)
pub fn TextBuffer::from_file(_path : String) -> TextBuffer {
TextBuffer::new()
}
///|
/// Get current cursor position
pub fn TextBuffer::cursor(self : TextBuffer) -> CursorPosition {
self.cursor
}
///|
/// Move cursor to position
pub fn TextBuffer::move_to(
self : TextBuffer,
row : Int,
col : Int,
) -> TextBuffer {
let max_row = self.lines.length()
let clamped_row = row.clamp(min=1, max=max_row)
let line = self.lines[clamped_row - 1]
let max_col = line.length() + 1
let clamped_col = col.clamp(min=1, max=max_col)
let new_cursor = CursorPosition::new(row=clamped_row, col=clamped_col)
TextBuffer::{
lines: self.lines,
cursor: new_cursor,
selection: { anchor: self.selection.anchor, cursor: new_cursor },
mode: self.mode,
modified: self.modified,
}
}
///|
/// Get current line text
pub fn TextBuffer::current_line(self : TextBuffer) -> String {
self.lines[self.cursor.row - 1]
}
///|
/// Insert character at cursor
pub fn TextBuffer::insert_char(self : TextBuffer, ch : Char) -> TextBuffer {
let line = self.lines[self.cursor.row - 1]
let before = String::view(
line,
start_offset=0,
end_offset=self.cursor.col - 1,
).to_string()
let after = String::view(line, start_offset=self.cursor.col - 1).to_string()
let new_line = before + ch.to_string() + after
let new_lines = self.lines.copy()
new_lines[self.cursor.row - 1] = new_line
let new_cursor = CursorPosition::new(
row=self.cursor.row,
col=self.cursor.col + 1,
)
TextBuffer::{
lines: new_lines,
cursor: new_cursor,
selection: { anchor: new_cursor, cursor: new_cursor },
mode: self.mode,
modified: true,
}
}
///|
/// Insert string at cursor
pub fn TextBuffer::insert_string(
self : TextBuffer,
text : String,
) -> TextBuffer {
let mut buffer = self
let chars = text.to_array()
for i = 0; i < chars.length(); i = i + 1 {
buffer = buffer.insert_char(chars[i])
}
buffer
}
///|
/// Insert newline at cursor
pub fn TextBuffer::insert_newline(self : TextBuffer) -> TextBuffer {
let line = self.lines[self.cursor.row - 1]
let before = String::view(
line,
start_offset=0,
end_offset=self.cursor.col - 1,
).to_string()
let after = String::view(line, start_offset=self.cursor.col - 1).to_string()
let new_lines = Array::new(capacity=self.lines.length() + 1)
for i = 0; i < self.cursor.row - 1; i = i + 1 {
new_lines.push(self.lines[i])
}
new_lines.push(before)
new_lines.push(after)
for i = self.cursor.row; i < self.lines.length(); i = i + 1 {
new_lines.push(self.lines[i])
}
let new_cursor = CursorPosition::new(row=self.cursor.row + 1, col=1)
TextBuffer::{
lines: new_lines,
cursor: new_cursor,
selection: { anchor: new_cursor, cursor: new_cursor },
mode: self.mode,
modified: true,
}
}
///|
/// Delete character before cursor (backspace)
pub fn TextBuffer::delete_backwards(self : TextBuffer) -> TextBuffer {
if self.cursor.row == 1 && self.cursor.col == 1 {
return self
}
if self.cursor.col == 1 {
let prev_line = self.lines[self.cursor.row - 2]
let curr_line = self.lines[self.cursor.row - 1]
let merged = prev_line + curr_line
let new_lines = self.lines.copy()
new_lines[self.cursor.row - 2] = merged
ignore(Array::remove(new_lines, self.cursor.row - 1))
let new_cursor = CursorPosition::new(
row=self.cursor.row - 1,
col=prev_line.length() + 1,
)
TextBuffer::{
lines: new_lines,
cursor: new_cursor,
selection: { anchor: new_cursor, cursor: new_cursor },
mode: self.mode,
modified: true,
}
} else {
let line = self.lines[self.cursor.row - 1]
let before = String::view(
line,
start_offset=0,
end_offset=self.cursor.col - 2,
).to_string()
let after = String::view(line, start_offset=self.cursor.col - 1).to_string()
let new_line = before + after
let new_lines = self.lines.copy()
new_lines[self.cursor.row - 1] = new_line
let new_cursor = CursorPosition::new(
row=self.cursor.row,
col=self.cursor.col - 1,
)
TextBuffer::{
lines: new_lines,
cursor: new_cursor,
selection: { anchor: new_cursor, cursor: new_cursor },
mode: self.mode,
modified: true,
}
}
}
///|
/// Delete character at cursor (delete key)
pub fn TextBuffer::delete_forwards(self : TextBuffer) -> TextBuffer {
let line = self.lines[self.cursor.row - 1]
if self.cursor.col <= line.length() {
let before = String::view(
line,
start_offset=0,
end_offset=self.cursor.col - 1,
).to_string()
let after = String::view(line, start_offset=self.cursor.col).to_string()
let new_line = before + after
let new_lines = self.lines.copy()
new_lines[self.cursor.row - 1] = new_line
TextBuffer::{
lines: new_lines,
cursor: self.cursor,
selection: self.selection,
mode: self.mode,
modified: true,
}
} else if self.cursor.row < self.lines.length() {
let curr_line = self.lines[self.cursor.row - 1]
let next_line = self.lines[self.cursor.row]
let merged = curr_line + next_line
let new_lines = self.lines.copy()
new_lines[self.cursor.row - 1] = merged
ignore(Array::remove(new_lines, self.cursor.row))
TextBuffer::{
lines: new_lines,
cursor: self.cursor,
selection: self.selection,
mode: self.mode,
modified: true,
}
} else {
self
}
}
///|
/// Delete current line
pub fn TextBuffer::delete_line(self : TextBuffer) -> TextBuffer {
if self.lines.length() == 1 {
TextBuffer::{
lines: [""],
cursor: CursorPosition::new(row=1, col=1),
selection: {
anchor: CursorPosition::new(row=1, col=1),
cursor: CursorPosition::new(row=1, col=1),
},
mode: self.mode,
modified: true,
}
} else if self.cursor.row == self.lines.length() {
let new_lines = self.lines.copy()
ignore(Array::remove(new_lines, self.cursor.row - 1))
let new_cursor = CursorPosition::new(
row=self.lines.length() - 1,
col=self.lines[self.lines.length() - 2].length() + 1,
)
TextBuffer::{
lines: new_lines,
cursor: new_cursor,
selection: { anchor: new_cursor, cursor: new_cursor },
mode: self.mode,
modified: true,
}
} else {
let new_lines = self.lines.copy()
ignore(Array::remove(new_lines, self.cursor.row - 1))
TextBuffer::{
lines: new_lines,
cursor: self.cursor,
selection: self.selection,
mode: self.mode,
modified: true,
}
}
}
///|
/// Get full text as string
pub fn TextBuffer::to_string(self : TextBuffer) -> String {
let mut result = ""
let mut first = true
for i = 0; i < self.lines.length(); i = i + 1 {
if not(first) {
result = result + "\n"
}
first = false
result = result + self.lines[i]
}
result
}
///|
/// Get line count
pub fn TextBuffer::line_count(self : TextBuffer) -> Int {
self.lines.length()
}