///|
pub(all) enum Mode {
Normal
Insert
Select
Command
} derive(Eq, Debug)
///|
pub(all) enum EditorCommand {
Key(String)
Ex(String)
Quit
} derive(Eq, Debug)
///|
pub(all) struct Cursor {
mut row : Int
mut col : Int
} derive(Eq, Debug)
///|
pub(all) struct Document {
mut path : String?
mut lines : Array[String]
mut dirty : Bool
} derive(Eq, Debug)
///|
pub(all) struct Editor {
mut doc : Document
mut mode : Mode
mut cursor : Cursor
mut selection_anchor : Cursor?
mut register : String
mut register_is_line : Bool
mut status : String
mut should_quit : Bool
undo_stack : Array[String]
redo_stack : Array[String]
} derive(Eq, Debug)
///|
pub(all) struct DriverResult {
editor : Editor
log : Array[String]
} derive(Eq, Debug)
///|
fn clamp(value : Int, low : Int, high : Int) -> Int {
if value < low {
low
} else if value > high {
high
} else {
value
}
}
///|
fn line_len(line : String) -> Int {
line.to_array().length()
}
///|
fn char_string(ch : Char) -> String {
String::from_array([ch])
}
///|
fn hex_digit(value : Int) -> Char {
match value {
0 => '0'
1 => '1'
2 => '2'
3 => '3'
4 => '4'
5 => '5'
6 => '6'
7 => '7'
8 => '8'
9 => '9'
10 => 'A'
11 => 'B'
12 => 'C'
13 => 'D'
14 => 'E'
_ => 'F'
}
}
///|
fn hex_code(value : Int) -> String {
let chars : Array[Char] = []
let mut n = value
if n == 0 {
chars.push('0')
} else {
while n > 0 {
chars.insert(0, hex_digit(n % 16))
n = n / 16
}
}
while chars.length() < 4 {
chars.insert(0, '0')
}
String::from_array(chars)
}
///|
enum WordCategory {
WhitespaceChar
WordChar
PunctuationChar
} derive(Eq, Debug)
///|
fn is_ascii_punctuation_char(ch : Char) -> Bool {
let code = ch.to_int()
(code >= 33 && code <= 47) ||
(code >= 58 && code <= 64) ||
(code >= 91 && code <= 96) ||
(code >= 123 && code <= 126)
}
///|
fn is_unicode_punctuation_char(ch : Char) -> Bool {
let code = ch.to_int()
(code >= 0x2000 && code <= 0x206F) ||
(code >= 0x3000 && code <= 0x303F) ||
(code >= 0xFF01 && code <= 0xFF0F) ||
(code >= 0xFF1A && code <= 0xFF20) ||
(code >= 0xFF3B && code <= 0xFF40) ||
(code >= 0xFF5B && code <= 0xFF65)
}
///|
fn char_category(ch : Char) -> WordCategory {
if ch.is_whitespace() {
WhitespaceChar
} else if is_ascii_punctuation_char(ch) || is_unicode_punctuation_char(ch) {
PunctuationChar
} else {
WordChar
}
}
///|
fn movement_category(ch : Char, long : Bool) -> WordCategory {
if long {
if char_category(ch) == WhitespaceChar {
WhitespaceChar
} else {
WordChar
}
} else {
char_category(ch)
}
}
///|
fn is_word_char(ch : Char) -> Bool {
char_category(ch) == WordChar
}
///|
fn is_blank_char(ch : Char) -> Bool {
ch == ' ' || ch.to_int() == 9
}
///|
fn lower_char(ch : Char) -> Char {
let code = ch.to_int()
if code >= 65 && code <= 90 {
match (code + 32).to_char() {
Some(value) => value
None => ch
}
} else {
ch
}
}
///|
fn upper_char(ch : Char) -> Char {
let code = ch.to_int()
if code >= 97 && code <= 122 {
match (code - 32).to_char() {
Some(value) => value
None => ch
}
} else {
ch
}
}
///|
fn switch_case_char(ch : Char) -> Char {
let lower = lower_char(ch)
if lower != ch {
lower
} else {
upper_char(ch)
}
}
///|
fn transform_case_char(ch : Char, kind : Int) -> Char {
if kind == 1 {
lower_char(ch)
} else if kind == 2 {
upper_char(ch)
} else {
switch_case_char(ch)
}
}
///|
fn is_digit_char(ch : Char) -> Bool {
let code = ch.to_int()
code >= 48 && code <= 57
}
///|
fn digit_value(ch : Char) -> Int {
ch.to_int() - 48
}
///|
fn int_to_string(value : Int) -> String {
if value == 0 {
return "0"
}
let chars : Array[Char] = []
let mut n = value
let negative = n < 0
if negative {
n = 0 - n
}
while n > 0 {
chars.insert(0, hex_digit(n % 10))
n = n / 10
}
if negative {
chars.insert(0, '-')
}
String::from_array(chars)
}
///|
fn first_non_whitespace_col(line : String) -> Int {
let chars = line.to_array()
let mut col = 0
while col < chars.length() && is_blank_char(chars[col]) {
col += 1
}
col
}
///|
fn normalize_lines(lines : Array[String]) -> Array[String] {
if lines.length() == 0 {
[""]
} else {
lines
}
}
///|
fn split_lines(text : String) -> Array[String] {
let lines : Array[String] = []
for line in text.split("\n") {
lines.push(line.to_owned())
}
normalize_lines(lines)
}
///|
pub fn document(path? : String, text? : String = "") -> Document {
{ path, lines: split_lines(text), dirty: false }
}
///|
pub fn empty_editor() -> Editor {
{
doc: document(),
mode: Normal,
cursor: { row: 0, col: 0 },
selection_anchor: None,
register: "",
register_is_line: false,
status: "meolix ready",
should_quit: false,
undo_stack: [],
redo_stack: [],
}
}
///|
pub fn open_text(path : String, text : String) -> Editor {
{
doc: { path: Some(path), lines: split_lines(text), dirty: false },
mode: Normal,
cursor: { row: 0, col: 0 },
selection_anchor: None,
register: "",
register_is_line: false,
status: "opened \{path}",
should_quit: false,
undo_stack: [],
redo_stack: [],
}
}
///|
fn Editor::current_line(self : Editor) -> String {
self.doc.lines[self.cursor.row]
}
///|
fn cursor_copy(cursor : Cursor) -> Cursor {
{ row: cursor.row, col: cursor.col }
}
///|
fn cursor_before(a : Cursor, b : Cursor) -> Bool {
a.row < b.row || (a.row == b.row && a.col <= b.col)
}
///|
fn cursor_strict_before(a : Cursor, b : Cursor) -> Bool {
a.row < b.row || (a.row == b.row && a.col < b.col)
}
///|
fn Editor::remember(self : Editor) -> Unit {
self.undo_stack.push(self.buffer_text())
self.redo_stack.clear()
}
///|
fn Editor::sync_cursor(self : Editor) -> Unit {
self.cursor.row = clamp(self.cursor.row, 0, self.doc.lines.length() - 1)
self.cursor.col = clamp(self.cursor.col, 0, line_len(self.current_line()))
}
///|
pub fn Editor::set_status(self : Editor, value : String) -> Unit {
self.status = value
}
///|
pub fn Editor::move_left(self : Editor) -> Unit {
if self.cursor.col > 0 {
self.cursor.col -= 1
}
self.sync_cursor()
}
///|
pub fn Editor::move_right(self : Editor) -> Unit {
self.cursor.col = clamp(self.cursor.col + 1, 0, line_len(self.current_line()))
self.sync_cursor()
}
///|
pub fn Editor::move_up(self : Editor) -> Unit {
if self.cursor.row > 0 {
self.cursor.row -= 1
}
self.sync_cursor()
}
///|
pub fn Editor::move_down(self : Editor) -> Unit {
if self.cursor.row + 1 < self.doc.lines.length() {
self.cursor.row += 1
}
self.sync_cursor()
}
///|
pub fn Editor::move_line_start(self : Editor) -> Unit {
self.cursor.col = 0
}
///|
pub fn Editor::move_line_end(self : Editor) -> Unit {
self.cursor.col = line_len(self.current_line())
}
///|
pub fn Editor::move_to_column(self : Editor, column : Int) -> Unit {
self.cursor.col = clamp(column - 1, 0, line_len(self.current_line()))
self.sync_cursor()
self.set_status("column \{self.cursor.col + 1}")
}
///|
pub fn Editor::move_line_first_non_whitespace(self : Editor) -> Unit {
self.cursor.col = first_non_whitespace_col(self.current_line())
self.sync_cursor()
}
///|
pub fn Editor::character_info(self : Editor) -> String {
let chars = self.current_line().to_array()
if chars.length() == 0 {
"empty line"
} else {
let col = clamp(self.cursor.col, 0, chars.length() - 1)
let ch = chars[col]
"'\{char_string(ch)}' U+\{hex_code(ch.to_int())} decimal \{ch.to_int()}"
}
}
///|
pub fn Editor::move_file_start(self : Editor) -> Unit {
self.cursor.row = 0
self.cursor.col = 0
}
///|
pub fn Editor::move_file_end(self : Editor) -> Unit {
self.cursor.row = self.doc.lines.length() - 1
self.cursor.col = 0
self.sync_cursor()
}
///|
fn Editor::category_at(
self : Editor,
row : Int,
col : Int,
long : Bool,
) -> WordCategory {
if row < 0 || row >= self.doc.lines.length() {
WhitespaceChar
} else {
let chars = self.doc.lines[row].to_array()
if col < 0 || col >= chars.length() {
WhitespaceChar
} else {
movement_category(chars[col], long)
}
}
}
///|
fn Editor::find_next_word_start(self : Editor, long : Bool) -> Cursor {
let start = self.category_at(self.cursor.row, self.cursor.col, long)
let mut row = self.cursor.row
let mut col = self.cursor.col + 1
let mut skipping_start_segment = true
while row < self.doc.lines.length() {
let chars = self.doc.lines[row].to_array()
if skipping_start_segment {
while col < chars.length() && movement_category(chars[col], long) == start {
col += 1
}
skipping_start_segment = false
}
while col < chars.length() &&
movement_category(chars[col], long) == WhitespaceChar {
col += 1
}
if col < chars.length() {
return { row, col }
}
row += 1
col = 0
}
{
row: self.doc.lines.length() - 1,
col: line_len(self.doc.lines[self.doc.lines.length() - 1]),
}
}
///|
fn Editor::find_prev_char(self : Editor, row : Int, col : Int) -> Cursor? {
let mut row = row
let mut col = col
while row >= 0 {
let len = line_len(self.doc.lines[row])
if col > len {
col = len
}
if col > 0 {
return Some({ row, col: col - 1 })
}
row -= 1
if row >= 0 {
col = line_len(self.doc.lines[row])
}
}
None
}
///|
fn Editor::segment_start_at(
self : Editor,
row : Int,
col : Int,
long : Bool,
) -> Cursor {
let cat = self.category_at(row, col, long)
let mut start = col
let chars = self.doc.lines[row].to_array()
while start > 0 && movement_category(chars[start - 1], long) == cat {
start -= 1
}
{ row, col: start }
}
///|
fn Editor::segment_end_at(
self : Editor,
row : Int,
col : Int,
long : Bool,
) -> Cursor {
let cat = self.category_at(row, col, long)
let chars = self.doc.lines[row].to_array()
let mut finish = col
while finish + 1 < chars.length() &&
movement_category(chars[finish + 1], long) == cat {
finish += 1
}
{ row, col: finish }
}
///|
fn Editor::find_prev_word_start(self : Editor, long : Bool) -> Cursor {
let chars = self.current_line().to_array()
if self.cursor.col < chars.length() {
let cat = movement_category(chars[self.cursor.col], long)
if cat != WhitespaceChar &&
self.cursor.col > 0 &&
movement_category(chars[self.cursor.col - 1], long) == cat {
return self.segment_start_at(self.cursor.row, self.cursor.col, long)
}
}
let mut pos = self.find_prev_char(self.cursor.row, self.cursor.col)
while pos is Some(cursor) {
let cat = self.category_at(cursor.row, cursor.col, long)
if cat != WhitespaceChar {
return self.segment_start_at(cursor.row, cursor.col, long)
}
pos = self.find_prev_char(cursor.row, cursor.col)
}
{ row: 0, col: 0 }
}
///|
fn Editor::find_next_word_end(self : Editor, long : Bool) -> Cursor {
let chars = self.current_line().to_array()
if self.cursor.col < chars.length() {
let cat = movement_category(chars[self.cursor.col], long)
if cat != WhitespaceChar &&
self.cursor.col + 1 < chars.length() &&
movement_category(chars[self.cursor.col + 1], long) == cat {
return self.segment_end_at(self.cursor.row, self.cursor.col, long)
}
}
let start = self.find_next_word_start(long)
self.segment_end_at(start.row, start.col, long)
}
///|
pub fn Editor::move_next_word(self : Editor) -> Unit {
let target = self.find_next_word_start(false)
self.cursor.row = target.row
self.cursor.col = target.col
self.sync_cursor()
}
///|
pub fn Editor::move_prev_word(self : Editor) -> Unit {
let target = self.find_prev_word_start(false)
self.cursor.row = target.row
self.cursor.col = target.col
self.sync_cursor()
}
///|
pub fn Editor::move_next_word_end(self : Editor) -> Unit {
let target = self.find_next_word_end(false)
self.cursor.row = target.row
self.cursor.col = target.col
self.sync_cursor()
}
///|
pub fn Editor::move_next_long_word(self : Editor) -> Unit {
let target = self.find_next_word_start(true)
self.cursor.row = target.row
self.cursor.col = target.col
self.sync_cursor()
}
///|
pub fn Editor::move_prev_long_word(self : Editor) -> Unit {
let target = self.find_prev_word_start(true)
self.cursor.row = target.row
self.cursor.col = target.col
self.sync_cursor()
}
///|
pub fn Editor::move_next_long_word_end(self : Editor) -> Unit {
let target = self.find_next_word_end(true)
self.cursor.row = target.row
self.cursor.col = target.col
self.sync_cursor()
}
///|
pub fn Editor::move_paragraph_next(self : Editor) -> Unit {
let mut row = self.cursor.row + 1
while row < self.doc.lines.length() && self.doc.lines[row].trim() != "" {
row += 1
}
while row < self.doc.lines.length() && self.doc.lines[row].trim() == "" {
row += 1
}
self.cursor.row = clamp(row, 0, self.doc.lines.length() - 1)
self.cursor.col = clamp(self.cursor.col, 0, line_len(self.current_line()))
}
///|
pub fn Editor::move_paragraph_prev(self : Editor) -> Unit {
let mut row = self.cursor.row - 1
while row >= 0 && self.doc.lines[row].trim() != "" {
row -= 1
}
while row >= 0 && self.doc.lines[row].trim() == "" {
row -= 1
}
self.cursor.row = clamp(row + 1, 0, self.doc.lines.length() - 1)
self.cursor.col = clamp(self.cursor.col, 0, line_len(self.current_line()))
}
///|
fn Editor::replace_current_line(self : Editor, value : String) -> Unit {
self.doc.lines[self.cursor.row] = value
self.doc.dirty = true
self.sync_cursor()
}
///|
pub fn Editor::insert_text(self : Editor, text : String) -> Unit {
for part_index, part in text.split("\n").iter2() {
if part_index > 0 {
self.split_line()
}
for ch in part {
self.insert_char(ch)
}
}
}
///|
pub fn Editor::insert_completion(self : Editor, text : String) -> Unit {
self.remember()
let chars = self.current_line().to_array()
let col = clamp(self.cursor.col, 0, chars.length())
let mut start = col
while start > 0 && is_word_char(chars[start - 1]) {
start -= 1
}
for _ in start.. ignore
}
for ch in text {
chars.insert(start, ch)
start += 1
}
self.replace_current_line(String::from_array(chars))
self.cursor.col = start
self.doc.dirty = true
}
///|
pub fn Editor::replace_range(
self : Editor,
start_row : Int,
start_col : Int,
end_row : Int,
end_col : Int,
text : String,
) -> Unit {
self.remember()
let row1 = clamp(start_row, 0, self.doc.lines.length() - 1)
let row2 = clamp(end_row, row1, self.doc.lines.length() - 1)
let line1 = self.doc.lines[row1].to_array()
let line2 = self.doc.lines[row2].to_array()
let col1 = clamp(start_col, 0, line1.length())
let col2 = clamp(end_col, 0, line2.length())
let before = String::from_array(line1[:col1].to_owned())
let after = String::from_array(line2[col2:].to_owned())
let parts = split_lines(text)
for _ in row1..<(row2 + 1) {
self.doc.lines.remove(row1) |> ignore
}
if parts.length() == 0 {
self.doc.lines.insert(row1, before + after)
self.cursor.row = row1
self.cursor.col = line_len(before)
} else if parts.length() == 1 {
self.doc.lines.insert(row1, before + parts[0] + after)
self.cursor.row = row1
self.cursor.col = line_len(before + parts[0])
} else {
self.doc.lines.insert(row1, before + parts[0])
for index in 1..<(parts.length() - 1) {
self.doc.lines.insert(row1 + index, parts[index])
}
self.doc.lines.insert(
row1 + parts.length() - 1,
parts[parts.length() - 1] + after,
)
self.cursor.row = row1 + parts.length() - 1
self.cursor.col = line_len(parts[parts.length() - 1])
}
if self.doc.lines.length() == 0 {
self.doc.lines.push("")
}
self.selection_anchor = None
self.mode = Normal
self.doc.dirty = true
self.sync_cursor()
self.set_status("applied edit")
}
///|
pub fn Editor::insert_char(self : Editor, ch : Char) -> Unit {
self.remember()
let chars = self.current_line().to_array()
let col = clamp(self.cursor.col, 0, chars.length())
chars.insert(col, ch)
self.replace_current_line(String::from_array(chars))
self.cursor.col = col + 1
}
///|
pub fn Editor::split_line(self : Editor) -> Unit {
self.remember()
let chars = self.current_line().to_array()
let col = clamp(self.cursor.col, 0, chars.length())
let left = chars[0:col].to_owned()
let right = chars[col:].to_owned()
self.doc.lines[self.cursor.row] = String::from_array(left)
self.doc.lines.insert(self.cursor.row + 1, String::from_array(right))
self.cursor.row += 1
self.cursor.col = 0
self.doc.dirty = true
}
///|
pub fn Editor::delete_char(self : Editor) -> Unit {
let chars = self.current_line().to_array()
if chars.length() == 0 {
if self.doc.lines.length() > 1 {
self.remember()
self.register = self.doc.lines.remove(self.cursor.row)
self.register_is_line = true
self.cursor.row = clamp(self.cursor.row, 0, self.doc.lines.length() - 1)
self.cursor.col = 0
self.doc.dirty = true
}
return
}
if self.cursor.col >= chars.length() {
if self.cursor.row + 1 < self.doc.lines.length() {
self.remember()
let next = self.doc.lines.remove(self.cursor.row + 1)
self.doc.lines[self.cursor.row] = self.current_line() + next
self.doc.dirty = true
self.sync_cursor()
}
return
}
self.remember()
let col = self.cursor.col
let removed = chars.remove(col)
self.register = String::from_array([removed])
self.register_is_line = false
self.replace_current_line(String::from_array(chars))
self.cursor.col = clamp(col, 0, line_len(self.current_line()))
}
///|
pub fn Editor::delete_line(self : Editor) -> Unit {
self.remember()
self.register = self.doc.lines.remove(self.cursor.row)
self.register_is_line = true
if self.doc.lines.length() == 0 {
self.doc.lines.push("")
}
self.cursor.row = clamp(self.cursor.row, 0, self.doc.lines.length() - 1)
self.cursor.col = 0
self.doc.dirty = true
self.set_status("deleted line")
}
///|
pub fn Editor::yank_line(self : Editor) -> Unit {
self.register = self.current_line()
self.register_is_line = true
self.set_status("yanked line")
}
///|
pub fn Editor::paste_after(self : Editor) -> Unit {
guard self.register != "" else {
self.set_status("register is empty")
return
}
self.remember()
if self.register_is_line {
self.doc.lines.insert(self.cursor.row + 1, self.register)
self.cursor.row += 1
self.cursor.col = 0
} else if self.register.contains("\n") {
for line in self.register.split("\n") {
self.doc.lines.insert(self.cursor.row + 1, line.to_owned())
self.cursor.row += 1
}
self.cursor.col = 0
} else {
self.cursor.col = clamp(
self.cursor.col + 1,
0,
line_len(self.current_line()),
)
self.insert_text(self.register)
}
self.doc.dirty = true
self.set_status("pasted")
}
///|
pub fn Editor::paste_before(self : Editor) -> Unit {
guard self.register != "" else {
self.set_status("register is empty")
return
}
self.remember()
if self.register_is_line {
self.doc.lines.insert(self.cursor.row, self.register)
self.cursor.col = 0
} else if self.register.contains("\n") {
for line in self.register.split("\n") {
self.doc.lines.insert(self.cursor.row, line.to_owned())
self.cursor.row += 1
}
self.cursor.col = 0
} else {
self.insert_text(self.register)
}
self.doc.dirty = true
self.set_status("pasted before")
}
///|
pub fn Editor::open_below(self : Editor) -> Unit {
self.remember()
self.doc.lines.insert(self.cursor.row + 1, "")
self.cursor.row += 1
self.cursor.col = 0
self.mode = Insert
self.doc.dirty = true
}
///|
pub fn Editor::open_above(self : Editor) -> Unit {
self.remember()
self.doc.lines.insert(self.cursor.row, "")
self.cursor.col = 0
self.mode = Insert
self.doc.dirty = true
}
///|
pub fn Editor::add_newline_below(self : Editor) -> Unit {
self.remember()
self.doc.lines.insert(self.cursor.row + 1, "")
self.doc.dirty = true
self.set_status("added line below")
}
///|
pub fn Editor::add_newline_above(self : Editor) -> Unit {
self.remember()
self.doc.lines.insert(self.cursor.row, "")
self.doc.dirty = true
self.set_status("added line above")
}
///|
pub fn Editor::backspace(self : Editor) -> Unit {
if self.cursor.col > 0 {
self.remember()
let chars = self.current_line().to_array()
chars.remove(self.cursor.col - 1) |> ignore
self.cursor.col -= 1
self.replace_current_line(String::from_array(chars))
} else if self.cursor.row > 0 {
self.remember()
let previous_len = line_len(self.doc.lines[self.cursor.row - 1])
let line = self.doc.lines.remove(self.cursor.row)
self.cursor.row -= 1
self.cursor.col = previous_len
self.doc.lines[self.cursor.row] = self.doc.lines[self.cursor.row] + line
self.doc.dirty = true
}
}
///|
pub fn Editor::delete_word_backward(self : Editor) -> Unit {
if self.cursor.col == 0 {
self.backspace()
return
}
self.remember()
let chars = self.current_line().to_array()
let end = clamp(self.cursor.col, 0, chars.length())
let mut start = end - 1
while start >= 0 && !is_word_char(chars[start]) {
start -= 1
}
while start > 0 && is_word_char(chars[start - 1]) {
start -= 1
}
let from = clamp(start, 0, end)
for _ in from.. ignore
}
self.cursor.col = from
self.replace_current_line(String::from_array(chars))
self.set_status("deleted word backward")
}
///|
pub fn Editor::delete_word_forward(self : Editor) -> Unit {
let chars = self.current_line().to_array()
if self.cursor.col >= chars.length() {
self.delete_char()
return
}
self.remember()
let start = clamp(self.cursor.col, 0, chars.length())
let mut end = start
while end < chars.length() && !is_word_char(chars[end]) {
end += 1
}
while end < chars.length() && is_word_char(chars[end]) {
end += 1
}
for _ in start.. ignore
}
self.replace_current_line(String::from_array(chars))
self.cursor.col = start
self.set_status("deleted word forward")
}
///|
pub fn Editor::delete_to_line_start(self : Editor) -> Unit {
if self.cursor.col == 0 {
return
}
self.remember()
let chars = self.current_line().to_array()
let end = clamp(self.cursor.col, 0, chars.length())
for _ in 0.. ignore
}
self.cursor.col = 0
self.replace_current_line(String::from_array(chars))
self.set_status("deleted to line start")
}
///|
pub fn Editor::delete_to_line_end(self : Editor) -> Unit {
let chars = self.current_line().to_array()
let start = clamp(self.cursor.col, 0, chars.length())
if start == chars.length() {
return
}
self.remember()
while chars.length() > start {
chars.remove(start) |> ignore
}
self.replace_current_line(String::from_array(chars))
self.cursor.col = start
self.set_status("deleted to line end")
}
///|
pub fn Editor::undo(self : Editor) -> Unit {
if self.undo_stack.length() == 0 {
self.set_status("nothing to undo")
return
}
self.redo_stack.push(self.buffer_text())
let previous = self.undo_stack.remove(self.undo_stack.length() - 1)
self.doc.lines = split_lines(previous)
self.doc.dirty = true
self.sync_cursor()
self.set_status("undo")
}
///|
pub fn Editor::redo(self : Editor) -> Unit {
if self.redo_stack.length() == 0 {
self.set_status("nothing to redo")
return
}
self.undo_stack.push(self.buffer_text())
let next = self.redo_stack.remove(self.redo_stack.length() - 1)
self.doc.lines = split_lines(next)
self.doc.dirty = true
self.sync_cursor()
self.set_status("redo")
}
///|
pub fn Editor::enter_insert(self : Editor) -> Unit {
self.mode = Insert
self.selection_anchor = None
self.set_status("-- INSERT --")
}
///|
pub fn Editor::enter_normal(self : Editor) -> Unit {
self.mode = Normal
self.selection_anchor = None
self.sync_cursor()
self.set_status("-- NORMAL --")
}
///|
pub fn Editor::enter_select(self : Editor) -> Unit {
self.mode = Select
self.selection_anchor = Some({ row: self.cursor.row, col: self.cursor.col })
self.set_status("-- SELECT --")
}
///|
pub fn Editor::insert_at_line_start(self : Editor) -> Unit {
self.move_line_first_non_whitespace()
self.enter_insert()
}
///|
pub fn Editor::insert_at_line_end(self : Editor) -> Unit {
self.move_line_end()
self.enter_insert()
}
///|
pub fn Editor::selection_bounds(self : Editor) -> (Cursor, Cursor)? {
match self.selection_anchor {
None => None
Some(anchor) =>
if cursor_before(anchor, self.cursor) {
Some((cursor_copy(anchor), cursor_copy(self.cursor)))
} else {
Some((cursor_copy(self.cursor), cursor_copy(anchor)))
}
}
}
///|
pub fn Editor::select_line(self : Editor) -> Unit {
let (start_row, target_row) = match self.selection_bounds() {
Some((start, finish)) if self.mode == Select =>
(start.row, clamp(finish.row + 1, 0, self.doc.lines.length() - 1))
_ => (self.cursor.row, self.cursor.row)
}
self.mode = Select
self.selection_anchor = Some({ row: start_row, col: 0 })
self.cursor.row = target_row
self.cursor.col = clamp(
line_len(self.current_line()) - 1,
0,
line_len(self.current_line()),
)
self.set_status("selected line")
}
///|
pub fn Editor::extend_selection_to_line_bounds(self : Editor) -> Unit {
let (start_row, finish_row) = match self.selection_bounds() {
None => (self.cursor.row, self.cursor.row)
Some((start, finish)) => (start.row, finish.row)
}
self.mode = Select
self.selection_anchor = Some({ row: start_row, col: 0 })
self.cursor.row = finish_row
self.cursor.col = clamp(
line_len(self.doc.lines[finish_row]) - 1,
0,
line_len(self.doc.lines[finish_row]),
)
self.set_status("extended to line bounds")
}
///|
pub fn Editor::select_all(self : Editor) -> Unit {
self.mode = Select
self.selection_anchor = Some({ row: 0, col: 0 })
self.cursor.row = self.doc.lines.length() - 1
let last_len = line_len(self.current_line())
self.cursor.col = clamp(last_len - 1, 0, last_len)
self.set_status("selected file")
}
///|
pub fn Editor::collapse_selection(self : Editor) -> Unit {
self.selection_anchor = None
self.mode = Normal
self.set_status("collapsed selection")
}
///|
pub fn Editor::flip_selection(self : Editor) -> Unit {
match self.selection_anchor {
None => self.set_status("no selection to flip")
Some(anchor) => {
self.selection_anchor = Some({
row: self.cursor.row,
col: self.cursor.col,
})
self.cursor = anchor
self.mode = Select
self.set_status("flipped selection")
}
}
}
///|
pub fn Editor::is_selected(self : Editor, row : Int, col : Int) -> Bool {
match self.selection_bounds() {
None => false
Some((start, finish)) =>
if row < start.row || row > finish.row {
false
} else if start.row == finish.row {
col >= start.col && col <= finish.col
} else if row == start.row {
col >= start.col
} else if row == finish.row {
col <= finish.col
} else {
true
}
}
}
///|
fn Editor::selection_is_full_lines(self : Editor) -> Bool {
match self.selection_bounds() {
None => false
Some((start, finish)) =>
start.col == 0 &&
finish.col >=
clamp(
line_len(self.doc.lines[finish.row]) - 1,
0,
line_len(self.doc.lines[finish.row]),
)
}
}
///|
fn Editor::target_line_range(self : Editor) -> (Int, Int) {
match self.selection_bounds() {
None => (self.cursor.row, self.cursor.row)
Some((start, finish)) => (start.row, finish.row)
}
}
///|
pub fn Editor::indent_selection(self : Editor) -> Unit {
self.remember()
let (start_row, finish_row) = self.target_line_range()
for row in start_row..<(finish_row + 1) {
self.doc.lines[row] = " " + self.doc.lines[row]
}
if self.cursor.row >= start_row && self.cursor.row <= finish_row {
self.cursor.col += 2
}
match self.selection_anchor {
None => ()
Some(anchor) =>
if anchor.row >= start_row && anchor.row <= finish_row {
self.selection_anchor = Some({ row: anchor.row, col: anchor.col + 2 })
}
}
self.doc.dirty = true
self.set_status("indented")
}
///|
fn leading_indent_width(line : String, spaces : Int) -> Int {
let chars = line.to_array()
if chars.length() == 0 {
0
} else if chars[0].to_int() == 9 {
1
} else {
let mut count = 0
while count < chars.length() && count < spaces && chars[count] == ' ' {
count += 1
}
count
}
}
///|
pub fn Editor::unindent_selection(self : Editor) -> Unit {
self.remember()
let (start_row, finish_row) = self.target_line_range()
for row in start_row..<(finish_row + 1) {
let chars = self.doc.lines[row].to_array()
let remove = leading_indent_width(self.doc.lines[row], 2)
for _ in 0.. ignore
}
self.doc.lines[row] = String::from_array(chars)
if row == self.cursor.row {
self.cursor.col = clamp(
self.cursor.col - remove,
0,
line_len(self.current_line()),
)
}
match self.selection_anchor {
None => ()
Some(anchor) =>
if row == anchor.row {
self.selection_anchor = Some({
row: anchor.row,
col: clamp(anchor.col - remove, 0, line_len(self.doc.lines[row])),
})
}
}
}
self.doc.dirty = true
self.set_status("unindented")
}
///|
pub fn Editor::trim_selection(self : Editor) -> Unit {
match self.selection_bounds() {
None => self.set_status("no selection to trim")
Some((start, finish)) => {
let mut trimmed_start : Cursor? = None
for row in start.row..<(finish.row + 1) {
let chars = self.doc.lines[row].to_array()
let from = if row == start.row {
clamp(start.col, 0, chars.length())
} else {
0
}
let to = if row == finish.row {
clamp(finish.col + 1, from, chars.length())
} else {
chars.length()
}
for col in from..= start.row {
let chars = self.doc.lines[row].to_array()
let from = if row == start.row {
clamp(start.col, 0, chars.length())
} else {
0
}
let mut col = if row == finish.row {
clamp(finish.col + 1, from, chars.length()) - 1
} else {
chars.length() - 1
}
while col >= from {
if !chars[col].is_whitespace() && trimmed_finish is None {
trimmed_finish = Some({ row, col })
}
col -= 1
}
row -= 1
}
match (trimmed_start, trimmed_finish) {
(Some(new_start), Some(new_finish)) => {
self.selection_anchor = Some(new_start)
self.cursor = new_finish
self.mode = Select
self.set_status("trimmed selection")
}
_ => self.set_status("selection is whitespace")
}
}
}
}
///|
pub fn Editor::change_number_under_cursor(self : Editor, delta : Int) -> Unit {
let chars = self.current_line().to_array()
if chars.length() == 0 {
self.set_status("no number on line")
return
}
let mut at = clamp(self.cursor.col, 0, chars.length() - 1)
if !is_digit_char(chars[at]) {
if chars[at] == '-' &&
at + 1 < chars.length() &&
is_digit_char(chars[at + 1]) {
at += 1
} else {
while at < chars.length() && !is_digit_char(chars[at]) {
at += 1
}
if at >= chars.length() {
self.set_status("no number on line")
return
}
}
}
let mut start = at
while start > 0 && is_digit_char(chars[start - 1]) {
start -= 1
}
let sign_start = if start > 0 && chars[start - 1] == '-' {
start - 1
} else {
start
}
let mut finish = at + 1
while finish < chars.length() && is_digit_char(chars[finish]) {
finish += 1
}
let mut value = 0
for col in start.. ignore
}
let mut insert_at = sign_start
for ch in replacement {
chars.insert(insert_at, ch)
insert_at += 1
}
self.replace_current_line(String::from_array(chars))
self.cursor.col = clamp(insert_at - 1, 0, line_len(self.current_line()))
self.set_status("number \{value + delta}")
}
///|
fn find_substring(line : String, query : String) -> Int? {
find_substring_from(line, query, 0)
}
///|
fn find_substring_from(line : String, query : String, from : Int) -> Int? {
let haystack = line.to_array()
let needle = query.to_array()
if needle.length() == 0 || needle.length() > haystack.length() {
return None
}
let first = clamp(from, 0, haystack.length())
if first + needle.length() > haystack.length() {
return None
}
for start in first..<(haystack.length() - needle.length() + 1) {
let mut matched = true
for offset in 0.. Int? {
let haystack = line.to_array()
let needle = query.to_array()
if needle.length() == 0 || needle.length() > haystack.length() {
return None
}
if before < 0 {
return None
}
let max_end = clamp(before, 0, haystack.length() - 1)
if max_end + 1 < needle.length() {
return None
}
let last_start = clamp(
max_end - needle.length() + 1,
0,
haystack.length() - needle.length(),
)
let mut found : Int? = None
for start in 0..<(last_start + 1) {
let mut matched = true
for offset in 0.. String {
match self.selection_bounds() {
None => {
let chars = self.current_line().to_array()
if chars.length() == 0 {
""
} else {
let col = clamp(self.cursor.col, 0, chars.length() - 1)
String::from_array([chars[col]])
}
}
Some((start, finish)) =>
if self.selection_is_full_lines() {
let parts : Array[String] = []
for row in start.row..<(finish.row + 1) {
parts.push(self.doc.lines[row])
}
parts.join("\n")
} else if start.row == finish.row {
let chars = self.doc.lines[start.row].to_array()
let start_col = clamp(start.col, 0, chars.length())
let end_col = clamp(finish.col + 1, start_col, chars.length())
String::from_array(chars[start_col:end_col].to_owned())
} else {
let parts : Array[String] = []
let first_chars = self.doc.lines[start.row].to_array()
let first_col = clamp(start.col, 0, first_chars.length())
parts.push(String::from_array(first_chars[first_col:].to_owned()))
for row in (start.row + 1).. String {
match self.selection_bounds() {
Some(_) => self.selected_text().trim().to_owned()
None => {
let chars = self.current_line().to_array()
if chars.length() == 0 {
return ""
}
let col = clamp(self.cursor.col, 0, chars.length() - 1)
if chars[col].is_whitespace() {
""
} else if is_word_char(chars[col]) {
let mut start = col
while start > 0 && is_word_char(chars[start - 1]) {
start -= 1
}
let mut finish = col + 1
while finish < chars.length() && is_word_char(chars[finish]) {
finish += 1
}
String::from_array(chars[start:finish].to_owned())
} else {
char_string(chars[col])
}
}
}
}
///|
pub fn Editor::search_word_under_cursor(self : Editor) -> String? {
let query = self.word_under_cursor()
if query == "" {
self.set_status("no word under cursor")
None
} else {
self.search_forward(query) |> ignore
Some(query)
}
}
///|
pub fn Editor::yank_selection(self : Editor) -> Unit {
let text = self.selected_text()
if text == "" {
self.set_status("nothing selected")
} else {
self.register = text
self.register_is_line = self.selection_is_full_lines()
self.set_status("yanked selection")
}
}
///|
fn Editor::replace_selection_text(
self : Editor,
text : String,
linewise : Bool,
) -> Unit {
if linewise {
self.delete_selection()
self.register = text
self.register_is_line = true
let lines = split_lines(text)
for line in lines {
self.doc.lines.insert(self.cursor.row, line)
self.cursor.row += 1
}
self.cursor.row = clamp(self.cursor.row - 1, 0, self.doc.lines.length() - 1)
self.cursor.col = 0
} else {
self.delete_selection()
self.register = text
self.register_is_line = false
self.insert_text(text)
}
self.mode = Normal
self.selection_anchor = None
self.doc.dirty = true
}
///|
pub fn Editor::change_selection(self : Editor) -> Unit {
match self.selection_bounds() {
Some((start, finish)) if self.selection_is_full_lines() => {
self.register = self.selected_text()
self.register_is_line = true
self.remember()
for _ in start.row..<(finish.row + 1) {
self.doc.lines.remove(start.row) |> ignore
}
self.doc.lines.insert(start.row, "")
self.cursor.row = clamp(start.row, 0, self.doc.lines.length() - 1)
self.cursor.col = 0
self.selection_anchor = None
self.mode = Insert
self.doc.dirty = true
self.set_status("-- INSERT change --")
}
_ => {
self.delete_selection()
self.enter_insert()
self.set_status("-- INSERT change --")
}
}
}
///|
pub fn Editor::delete_selection_without_yank(self : Editor) -> Unit {
let saved = self.register
let saved_line = self.register_is_line
self.delete_selection()
self.register = saved
self.register_is_line = saved_line
self.set_status("deleted selection")
}
///|
pub fn Editor::change_selection_without_yank(self : Editor) -> Unit {
let saved = self.register
let saved_line = self.register_is_line
self.change_selection()
self.register = saved
self.register_is_line = saved_line
}
///|
pub fn Editor::replace_with_yanked(self : Editor) -> Unit {
guard self.register != "" else {
self.set_status("register is empty")
return
}
let saved = self.register
let saved_line = self.register_is_line
self.replace_selection_text(saved, saved_line)
self.register = saved
self.register_is_line = saved_line
self.set_status("replaced with yanked text")
}
///|
pub fn Editor::replace_selection_with_char(self : Editor, ch : Char) -> Unit {
self.remember()
match self.selection_bounds() {
None => {
let chars = self.current_line().to_array()
if chars.length() > 0 {
let col = clamp(self.cursor.col, 0, chars.length() - 1)
chars[col] = ch
self.replace_current_line(String::from_array(chars))
}
}
Some((start, finish)) => {
for row in start.row..<(finish.row + 1) {
let chars = self.doc.lines[row].to_array()
let from = if row == start.row {
clamp(start.col, 0, chars.length())
} else {
0
}
let to = if row == finish.row {
clamp(finish.col + 1, from, chars.length())
} else {
chars.length()
}
for col in from.. Unit {
self.remember()
match self.selection_bounds() {
None => {
let chars = self.current_line().to_array()
if chars.length() > 0 {
let col = clamp(self.cursor.col, 0, chars.length() - 1)
chars[col] = transform_case_char(chars[col], kind)
self.replace_current_line(String::from_array(chars))
}
}
Some((start, finish)) =>
for row in start.row..<(finish.row + 1) {
let chars = self.doc.lines[row].to_array()
let from = if row == start.row {
clamp(start.col, 0, chars.length())
} else {
0
}
let to = if row == finish.row {
clamp(finish.col + 1, from, chars.length())
} else {
chars.length()
}
for col in from.. Unit {
self.transform_selection_case(0, "switched case")
}
///|
pub fn Editor::lower_selection(self : Editor) -> Unit {
self.transform_selection_case(1, "lowercase")
}
///|
pub fn Editor::upper_selection(self : Editor) -> Unit {
self.transform_selection_case(2, "uppercase")
}
///|
fn join_two_lines(left : String, right : String) -> String {
let l = left.trim().to_owned()
let r = right.trim().to_owned()
if l == "" {
r
} else if r == "" {
l
} else {
l + " " + r
}
}
///|
pub fn Editor::join_lines(self : Editor) -> Unit {
if self.doc.lines.length() <= 1 {
self.set_status("nothing to join")
return
}
self.remember()
let (start_row, finish_row) = match self.selection_bounds() {
None =>
(
self.cursor.row,
clamp(self.cursor.row + 1, 0, self.doc.lines.length() - 1),
)
Some((start, finish)) => (start.row, finish.row)
}
if start_row == finish_row {
if start_row + 1 < self.doc.lines.length() {
let next = self.doc.lines.remove(start_row + 1)
self.doc.lines[start_row] = join_two_lines(
self.doc.lines[start_row],
next,
)
}
} else {
for _ in start_row.. Unit {
self.remember()
let (start_row, finish_row) = match self.selection_bounds() {
None => (self.cursor.row, self.cursor.row)
Some((start, finish)) => (start.row, finish.row)
}
for row in start_row..<(finish_row + 1) {
let chars = self.doc.lines[row].to_array()
let col = first_non_whitespace_col(self.doc.lines[row])
if col + 1 < chars.length() && chars[col] == '/' && chars[col + 1] == '/' {
chars.remove(col) |> ignore
chars.remove(col) |> ignore
if col < chars.length() && chars[col] == ' ' {
chars.remove(col) |> ignore
}
} else {
chars.insert(col, '/')
chars.insert(col + 1, '/')
chars.insert(col + 2, ' ')
}
self.doc.lines[row] = String::from_array(chars)
}
self.doc.dirty = true
self.set_status("toggled line comment")
}
///|
fn starts_with_chars(chars : Array[Char], prefix : Array[Char]) -> Bool {
if chars.length() < prefix.length() {
false
} else {
let mut ok = true
for index in 0.. Bool {
if chars.length() < suffix.length() {
false
} else {
let offset = chars.length() - suffix.length()
let mut ok = true
for index in 0.. Int {
let chars = line.to_array()
let mut index = chars.length()
while index > 0 && chars[index - 1].is_whitespace() {
index -= 1
}
index
}
///|
fn toggle_block_comment_text(text : String) -> String {
let chars = text.to_array()
if starts_with_chars(chars, ['/', '*', ' ']) &&
ends_with_chars(chars, [' ', '*', '/']) {
String::from_array(chars[3:chars.length() - 3].to_owned())
} else if starts_with_chars(chars, ['/', '*']) &&
ends_with_chars(chars, ['*', '/']) {
String::from_array(chars[2:chars.length() - 2].to_owned())
} else {
"/* " + text + " */"
}
}
///|
pub fn Editor::toggle_block_comment(self : Editor) -> Unit {
match self.selection_bounds() {
Some((start, finish)) => {
let replacement = toggle_block_comment_text(self.selected_text())
self.replace_range(
start.row,
start.col,
finish.row,
finish.col + 1,
replacement,
)
self.set_status("toggled block comment")
}
None => {
let row = self.cursor.row
let line = self.doc.lines[row]
let start = first_non_whitespace_col(line)
let finish = last_non_whitespace_exclusive(line)
let chars = line.to_array()
let text = String::from_array(chars[start:finish].to_owned())
self.replace_range(
row,
start,
row,
finish,
toggle_block_comment_text(text),
)
self.set_status("toggled block comment")
}
}
}
///|
pub fn Editor::delete_selection(self : Editor) -> Unit {
match self.selection_bounds() {
None => self.delete_char()
Some((start, finish)) => {
self.register = self.selected_text()
self.register_is_line = self.selection_is_full_lines()
self.remember()
if self.register_is_line {
for _ in start.row..<(finish.row + 1) {
self.doc.lines.remove(start.row) |> ignore
}
if self.doc.lines.length() == 0 {
self.doc.lines.push("")
}
} else if start.row == finish.row {
let chars = self.doc.lines[start.row].to_array()
let start_col = clamp(start.col, 0, chars.length())
let end_col = clamp(finish.col + 1, start_col, chars.length())
for _ in start_col.. ignore
}
self.doc.lines[start.row] = String::from_array(chars)
} else {
let first_chars = self.doc.lines[start.row].to_array()
let last_chars = self.doc.lines[finish.row].to_array()
let start_col = clamp(start.col, 0, first_chars.length())
let end_col = clamp(finish.col + 1, 0, last_chars.length())
let merged = String::from_array(first_chars[:start_col].to_owned()) +
String::from_array(last_chars[end_col:].to_owned())
self.doc.lines[start.row] = merged
for _ in start.row.. ignore
}
}
if self.doc.lines.length() == 0 {
self.doc.lines.push("")
}
self.cursor.row = clamp(start.row, 0, self.doc.lines.length() - 1)
self.cursor.col = clamp(start.col, 0, line_len(self.current_line()))
self.selection_anchor = None
self.mode = Normal
self.doc.dirty = true
self.set_status("deleted selection")
}
}
}
///|
fn bracket_match(ch : Char) -> (Char, Int)? {
match ch {
'(' => Some((')', 1))
'[' => Some((']', 1))
'{' => Some(('}', 1))
')' => Some(('(', -1))
']' => Some(('[', -1))
'}' => Some(('{', -1))
_ => None
}
}
///|
fn surround_pair(ch : Char) -> (Char, Char) {
match ch {
'(' | ')' => ('(', ')')
'[' | ']' => ('[', ']')
'{' | '}' => ('{', '}')
'<' | '>' => ('<', '>')
_ => (ch, ch)
}
}
///|
fn find_surround_pair_on_line(
line : String,
cursor : Int,
marker : Char,
) -> (Int, Int)? {
let chars = line.to_array()
if chars.length() < 2 {
return None
}
let (open, close) = surround_pair(marker)
let at = clamp(cursor, 0, chars.length() - 1)
let mut open_index : Int? = None
let mut col = at
while col >= 0 {
if chars[col] == open {
open_index = Some(col)
break
}
col -= 1
}
match open_index {
None => None
Some(left) => {
let mut right = left + 1
while right < chars.length() {
if chars[right] == close {
return Some((left, right))
}
right += 1
}
None
}
}
}
///|
pub fn Editor::surround_selection(self : Editor, marker : Char) -> Unit {
let (open, close) = surround_pair(marker)
self.remember()
match self.selection_bounds() {
None => {
let chars = self.current_line().to_array()
if chars.length() == 0 {
chars.insert(0, open)
chars.insert(1, close)
self.replace_current_line(String::from_array(chars))
self.cursor.col = 1
} else {
let col = clamp(self.cursor.col, 0, chars.length() - 1)
chars.insert(col + 1, close)
chars.insert(col, open)
self.replace_current_line(String::from_array(chars))
self.cursor.col = col + 2
}
}
Some((start, finish)) =>
if start.row == finish.row {
let chars = self.doc.lines[start.row].to_array()
let start_col = clamp(start.col, 0, chars.length())
let finish_col = clamp(finish.col + 1, start_col, chars.length())
chars.insert(finish_col, close)
chars.insert(start_col, open)
self.doc.lines[start.row] = String::from_array(chars)
self.cursor.row = start.row
self.cursor.col = finish_col + 1
} else {
let finish_chars = self.doc.lines[finish.row].to_array()
let finish_col = clamp(finish.col + 1, 0, finish_chars.length())
finish_chars.insert(finish_col, close)
self.doc.lines[finish.row] = String::from_array(finish_chars)
let start_chars = self.doc.lines[start.row].to_array()
let start_col = clamp(start.col, 0, start_chars.length())
start_chars.insert(start_col, open)
self.doc.lines[start.row] = String::from_array(start_chars)
self.cursor = { row: finish.row, col: finish_col }
}
}
self.selection_anchor = None
self.mode = Normal
self.doc.dirty = true
self.set_status("surrounded")
}
///|
pub fn Editor::delete_surround(self : Editor, marker : Char) -> Unit {
match
find_surround_pair_on_line(self.current_line(), self.cursor.col, marker) {
None => self.set_status("surround not found")
Some((left, right)) => {
self.remember()
let chars = self.current_line().to_array()
chars.remove(right) |> ignore
chars.remove(left) |> ignore
self.replace_current_line(String::from_array(chars))
self.cursor.col = clamp(left, 0, line_len(self.current_line()))
self.set_status("deleted surround")
}
}
}
///|
pub fn Editor::replace_surround(
self : Editor,
old : Char,
replacement : Char,
) -> Unit {
match find_surround_pair_on_line(self.current_line(), self.cursor.col, old) {
None => self.set_status("surround not found")
Some((left, right)) => {
let (open, close) = surround_pair(replacement)
self.remember()
let chars = self.current_line().to_array()
chars[left] = open
chars[right] = close
self.replace_current_line(String::from_array(chars))
self.cursor.col = clamp(right, 0, line_len(self.current_line()))
self.set_status("replaced surround")
}
}
}
///|
fn Editor::select_range(
self : Editor,
start : Cursor,
finish : Cursor,
status : String,
) -> Unit {
self.mode = Select
self.selection_anchor = Some({
row: clamp(start.row, 0, self.doc.lines.length() - 1),
col: clamp(start.col, 0, line_len(self.doc.lines[start.row])),
})
self.cursor = {
row: clamp(finish.row, 0, self.doc.lines.length() - 1),
col: clamp(finish.col, 0, line_len(self.doc.lines[finish.row])),
}
self.set_status(status)
}
///|
fn Editor::next_position(self : Editor, cursor : Cursor) -> Cursor {
let len = line_len(self.doc.lines[cursor.row])
if cursor.col + 1 < len {
{ row: cursor.row, col: cursor.col + 1 }
} else if cursor.row + 1 < self.doc.lines.length() {
{ row: cursor.row + 1, col: 0 }
} else {
cursor
}
}
///|
fn Editor::prev_position(self : Editor, cursor : Cursor) -> Cursor {
if cursor.col > 0 {
{ row: cursor.row, col: cursor.col - 1 }
} else if cursor.row > 0 {
let row = cursor.row - 1
{
row,
col: clamp(
line_len(self.doc.lines[row]) - 1,
0,
line_len(self.doc.lines[row]),
),
}
} else {
cursor
}
}
///|
fn Editor::select_word_textobject(self : Editor, long : Bool) -> Bool {
let chars = self.current_line().to_array()
if chars.length() == 0 {
self.set_status("textobject not found")
return false
}
let col = clamp(self.cursor.col, 0, chars.length() - 1)
let category = movement_category(chars[col], long)
if category == WhitespaceChar {
self.set_status("textobject not found")
return false
}
let mut start = col
while start > 0 && movement_category(chars[start - 1], long) == category {
start -= 1
}
let mut finish = col
while finish + 1 < chars.length() &&
movement_category(chars[finish + 1], long) == category {
finish += 1
}
self.select_range(
{ row: self.cursor.row, col: start },
{ row: self.cursor.row, col: finish },
"selected textobject",
)
true
}
///|
fn Editor::select_paragraph_textobject(self : Editor) -> Bool {
let blank = self.current_line().trim() == ""
let mut start = self.cursor.row
while start > 0 && (self.doc.lines[start - 1].trim() == "") == blank {
start -= 1
}
let mut finish = self.cursor.row
while finish + 1 < self.doc.lines.length() &&
(self.doc.lines[finish + 1].trim() == "") == blank {
finish += 1
}
self.select_range(
{ row: start, col: 0 },
{
row: finish,
col: clamp(
line_len(self.doc.lines[finish]) - 1,
0,
line_len(self.doc.lines[finish]),
),
},
"selected textobject",
)
true
}
///|
fn Editor::matching_close_from(
self : Editor,
start : Cursor,
open : Char,
close : Char,
) -> Cursor? {
let mut depth = 1
let mut row = start.row
let mut col = start.col + 1
while row < self.doc.lines.length() {
let chars = self.doc.lines[row].to_array()
while col < chars.length() {
if chars[col] == open {
depth += 1
} else if chars[col] == close {
depth -= 1
if depth == 0 {
return Some({ row, col })
}
}
col += 1
}
row += 1
col = 0
}
None
}
///|
fn Editor::surrounding_pair_textobject(
self : Editor,
marker : Char,
) -> (Cursor, Cursor)? {
let (open, close) = surround_pair(marker)
if open == close {
let chars = self.current_line().to_array()
if chars.length() < 2 {
return None
}
let at = clamp(self.cursor.col, 0, chars.length() - 1)
let mut left : Int? = None
let mut col = at
while col >= 0 {
if chars[col] == open {
left = Some(col)
break
}
col -= 1
}
match left {
None => None
Some(left_col) => {
let mut right = left_col + 1
while right < chars.length() {
if chars[right] == close {
return Some(
(
{ row: self.cursor.row, col: left_col },
{ row: self.cursor.row, col: right },
),
)
}
right += 1
}
None
}
}
} else {
let target = cursor_copy(self.cursor)
let mut best : (Cursor, Cursor)? = None
for row in 0..<(target.row + 1) {
let chars = self.doc.lines[row].to_array()
let max_col = if row == target.row {
clamp(target.col, 0, chars.length() - 1)
} else {
chars.length() - 1
}
if max_col >= 0 {
for col in 0..<(max_col + 1) {
if chars[col] == open {
let start = { row, col }
match self.matching_close_from(start, open, close) {
Some(finish) =>
if cursor_before(target, finish) {
best = Some((start, finish))
}
None => ()
}
}
}
}
}
best
}
}
///|
pub fn Editor::select_textobject(
self : Editor,
marker : Char,
inside : Bool,
) -> Unit {
match marker {
'w' => ignore(self.select_word_textobject(false))
'W' => ignore(self.select_word_textobject(true))
'p' => ignore(self.select_paragraph_textobject())
_ =>
match self.surrounding_pair_textobject(marker) {
None => self.set_status("textobject not found")
Some((open, close)) => {
let start = if inside { self.next_position(open) } else { open }
let finish = if inside { self.prev_position(close) } else { close }
if cursor_strict_before(finish, start) {
self.set_status("textobject is empty")
} else {
self.select_range(start, finish, "selected textobject")
}
}
}
}
}
///|
pub fn Editor::match_bracket(self : Editor) -> Bool {
let chars = self.current_line().to_array()
if chars.length() == 0 {
self.set_status("no bracket under cursor")
return false
}
let origin_col = clamp(self.cursor.col, 0, chars.length() - 1)
let origin = chars[origin_col]
match bracket_match(origin) {
None => {
self.set_status("no bracket under cursor")
false
}
Some((target, direction)) => {
let mut depth = 1
if direction > 0 {
let mut row = self.cursor.row
let mut col = origin_col + 1
while row < self.doc.lines.length() {
let scan = self.doc.lines[row].to_array()
while col < scan.length() {
if scan[col] == origin {
depth += 1
} else if scan[col] == target {
depth -= 1
if depth == 0 {
self.cursor.row = row
self.cursor.col = col
self.set_status("matched bracket")
return true
}
}
col += 1
}
row += 1
col = 0
}
} else {
let mut row = self.cursor.row
let mut col = origin_col - 1
while row >= 0 {
let scan = self.doc.lines[row].to_array()
if col >= scan.length() {
col = scan.length() - 1
}
while col >= 0 {
if scan[col] == origin {
depth += 1
} else if scan[col] == target {
depth -= 1
if depth == 0 {
self.cursor.row = row
self.cursor.col = col
self.set_status("matched bracket")
return true
}
}
col -= 1
}
row -= 1
if row >= 0 {
col = line_len(self.doc.lines[row]) - 1
}
}
}
self.set_status("matching bracket not found")
false
}
}
}
///|
pub fn Editor::find_char_forward(self : Editor, ch : Char, till : Bool) -> Bool {
let mut row = self.cursor.row
let mut col = self.cursor.col + 1
while row < self.doc.lines.length() {
let chars = self.doc.lines[row].to_array()
while col < chars.length() {
if chars[col] == ch {
self.cursor.row = row
self.cursor.col = if till {
clamp(col - 1, 0, chars.length())
} else {
col
}
self.set_status("found char")
return true
}
col += 1
}
row += 1
col = 0
}
self.set_status("char not found")
false
}
///|
pub fn Editor::find_char_backward(
self : Editor,
ch : Char,
till : Bool,
) -> Bool {
let mut row = self.cursor.row
let mut col = self.cursor.col - 1
while row >= 0 {
let chars = self.doc.lines[row].to_array()
if col >= chars.length() {
col = chars.length() - 1
}
while col >= 0 {
if chars[col] == ch {
self.cursor.row = row
self.cursor.col = if till {
clamp(col + 1, 0, chars.length())
} else {
col
}
self.set_status("found char")
return true
}
col -= 1
}
row -= 1
if row >= 0 {
col = line_len(self.doc.lines[row]) - 1
}
}
self.set_status("char not found")
false
}
///|
pub fn Editor::search_forward(self : Editor, query : String) -> Bool {
guard query != "" else { return false }
for row in self.cursor.row.. Bool {
guard query != "" else { return false }
let mut row = self.cursor.row
while row >= 0 {
let before = if row == self.cursor.row {
match self.selection_bounds() {
Some((start, finish)) if start.row == finish.row && start.row == row =>
start.col - 1
_ => self.cursor.col - 1
}
} else {
line_len(self.doc.lines[row])
}
if find_substring_before(self.doc.lines[row], query, before) is Some(col) {
self.cursor.row = row
self.cursor.col = col + line_len(query) - 1
self.mode = Select
self.selection_anchor = Some({ row, col })
self.set_status("found: \{query}")
return true
}
row -= 1
}
row = self.doc.lines.length() - 1
while row >= self.cursor.row {
if find_substring_before(
self.doc.lines[row],
query,
line_len(self.doc.lines[row]),
)
is Some(col) {
self.cursor.row = row
self.cursor.col = col + line_len(query) - 1
self.mode = Select
self.selection_anchor = Some({ row, col })
self.set_status("found: \{query}")
return true
}
row -= 1
}
self.set_status("not found: \{query}")
false
}
///|
pub fn Editor::buffer_text(self : Editor) -> String {
self.doc.lines.join("\n")
}
///|
fn mode_text(mode : Mode) -> String {
match mode {
Normal => "NOR"
Insert => "INS"
Select => "SEL"
Command => "CMD"
}
}
///|
fn path_text(path : String?) -> String {
match path {
None => "[scratch]"
Some(value) => value
}
}
///|
pub fn Editor::render(self : Editor, height? : Int = 18) -> String {
let out = StringBuilder::new()
out.write_string("meolix - terminal Helix-like editor in MoonBit\n")
let max_rows = clamp(height, 1, self.doc.lines.length())
for row in 0.." } else { " " }
let number = (row + 1).to_string().pad_start(4, ' ')
out.write_string("\{marker} \{number} \u{2502} ")
out.write_string(self.doc.lines[row])
out.write_string("\n")
}
let dirty = if self.doc.dirty { "[+]" } else { "[ ]" }
out.write_string(
"\{mode_text(self.mode)} \{dirty} \{path_text(self.doc.path)}:\{self.cursor.row + 1}:\{self.cursor.col + 1} \{self.status}\n",
)
out.to_string()
}
///|
pub fn Editor::apply_key(self : Editor, key : String) -> Unit {
match self.mode {
Insert =>
match key {
"esc" => self.enter_normal()
"enter" => self.split_line()
"backspace" => self.backspace()
_ => self.insert_text(key)
}
_ =>
match key {
"h" => self.move_left()
"j" => self.move_down()
"k" => self.move_up()
"l" => self.move_right()
"w" => self.move_next_word()
"b" => self.move_prev_word()
"e" => self.move_next_word_end()
"W" => self.move_next_long_word()
"B" => self.move_prev_long_word()
"E" => self.move_next_long_word_end()
"gg" => self.move_file_start()
"G" => self.set_status("G expects a line number; use :go N or ge")
"ge" => self.move_file_end()
"gh" => self.move_line_start()
"gl" => self.move_line_end()
"gs" => self.move_line_first_non_whitespace()
"g|" => self.move_to_column(1)
"ga" =>
self.set_status("goto alternate file is only available in the TUI")
"i" => self.enter_insert()
"a" => {
self.move_right()
self.enter_insert()
}
"I" => self.insert_at_line_start()
"A" => self.insert_at_line_end()
"v" =>
if self.mode == Select {
self.enter_normal()
} else {
self.enter_select()
}
"esc" => self.enter_normal()
"x" => self.select_line()
"X" => self.extend_selection_to_line_bounds()
"%" => self.select_all()
";" => self.collapse_selection()
"_" => self.trim_selection()
"d" => self.delete_selection()
"alt-d" => self.delete_selection_without_yank()
"c" => self.change_selection()
"alt-c" => self.change_selection_without_yank()
"y" => {
self.yank_selection()
self.enter_normal()
}
"dd" => self.delete_line()
"yy" => self.yank_line()
"p" => self.paste_after()
"P" => self.paste_before()
"R" => self.replace_with_yanked()
"J" => self.join_lines()
">" => self.indent_selection()
"<" => self.unindent_selection()
"*" => self.search_word_under_cursor() |> ignore
"ctrl-a" => self.change_number_under_cursor(1)
"ctrl-x" => self.change_number_under_cursor(-1)
"~" => self.switch_selection_case()
"`" => self.lower_selection()
"o" => self.open_below()
"O" => self.open_above()
"u" => self.undo()
"U" => self.redo()
_ if key.has_prefix("ms") => {
let chars = key.to_array()
if chars.length() >= 3 {
self.surround_selection(chars[2])
} else {
self.set_status("surround expects a delimiter")
}
}
_ if key.has_prefix("md") => {
let chars = key.to_array()
if chars.length() >= 3 {
self.delete_surround(chars[2])
} else {
self.set_status("delete surround expects a delimiter")
}
}
_ if key.has_prefix("mr") => {
let chars = key.to_array()
if chars.length() >= 4 {
self.replace_surround(chars[2], chars[3])
} else {
self.set_status("replace surround expects two delimiters")
}
}
_ if key.has_prefix("ma") => {
let chars = key.to_array()
if chars.length() >= 3 {
self.select_textobject(chars[2], false)
} else {
self.set_status("around textobject expects an object key")
}
}
_ if key.has_prefix("mi") => {
let chars = key.to_array()
if chars.length() >= 3 {
self.select_textobject(chars[2], true)
} else {
self.set_status("inside textobject expects an object key")
}
}
_ => self.set_status("unknown key: \{key}")
}
}
}
///|
pub fn Editor::apply_command(self : Editor, command : String) -> Unit {
let command = command.trim().to_owned()
match command {
"" => ()
"q" | "quit" =>
if self.doc.dirty {
self.set_status("buffer is dirty; use :q! to quit")
} else {
self.should_quit = true
}
"q!" | "quit!" => self.should_quit = true
"w" | "write" => self.set_status("write requested")
"wq" => {
self.set_status("write requested")
self.should_quit = true
}
"mode normal" => self.enter_normal()
"mode insert" => self.enter_insert()
"mode select" => self.enter_select()
_ if command.has_prefix("insert ") => {
self.enter_insert()
self.insert_text(command[7:].to_owned())
}
_ if command.has_prefix("append ") => {
self.move_line_end()
self.enter_insert()
self.insert_text(command[7:].to_owned())
}
_ if command.has_prefix("go ") => {
let target = command[3:].to_owned()
let line = @string.parse_int(target) catch { _ => 1 }
self.cursor.row = clamp(line - 1, 0, self.doc.lines.length() - 1)
self.sync_cursor()
}
_ => self.set_status("unknown command: :\{command}")
}
}
///|
pub fn Editor::apply_editor_command(
self : Editor,
command : EditorCommand,
) -> Unit {
match command {
Key(key) => self.apply_key(key)
Ex(ex) => self.apply_command(ex)
Quit => self.should_quit = true
}
}
///|
pub async fn load(path : String) -> Editor {
if @fs.exists(path) {
try @fs.kind(path) catch {
_ => {
let data = @fs.read_file(path)
open_text(path, data.text())
}
} noraise {
Directory => {
let editor = empty_editor()
editor.set_status("\{path} is a directory")
editor
}
_ => {
let data = @fs.read_file(path)
open_text(path, data.text())
}
}
} else {
{
doc: { path: Some(path), lines: [""], dirty: false },
mode: Normal,
cursor: { row: 0, col: 0 },
selection_anchor: None,
register: "",
register_is_line: false,
status: "new file \{path}",
should_quit: false,
undo_stack: [],
redo_stack: [],
}
}
}
///|
pub async fn Editor::save(self : Editor) -> Unit {
match self.doc.path {
None => self.set_status("no path; use a file path when starting meolix")
Some(path) => {
@fs.write_file(path, self.buffer_text(), create_mode=CreateOrTruncate)
self.doc.dirty = false
self.set_status("wrote \{path}")
}
}
}
///|
pub async fn Editor::save_as(self : Editor, path : String) -> Unit {
@fs.write_file(path, self.buffer_text(), create_mode=CreateOrTruncate)
self.doc.path = Some(path)
self.doc.dirty = false
self.set_status("wrote \{path}")
}
///|
async fn command_producer(
queue : @aqueue.Queue[EditorCommand],
commands : Array[EditorCommand],
) -> Unit {
for command in commands {
queue.put(command)
@async.pause()
}
queue.put(Quit)
}
///|
async fn command_consumer(
editor : Editor,
queue : @aqueue.Queue[EditorCommand],
log : Array[String],
save_on_write~ : Bool,
) -> Editor {
let mut running = true
while running {
let command = queue.get()
match command {
Ex("w") | Ex("write") if save_on_write => editor.save()
Ex("wq") if save_on_write => {
editor.save()
editor.should_quit = true
}
_ => editor.apply_editor_command(command)
}
log.push(editor.render(height=8))
if editor.should_quit {
running = false
}
}
editor
}
///|
pub async fn run_commands(
editor : Editor,
commands : Array[EditorCommand],
save_on_write? : Bool = true,
) -> DriverResult {
let log : Array[String] = []
let result = @async.with_task_group() <| group => {
let queue = @aqueue.Queue(kind=Unbounded)
group.spawn_bg() <| () => { command_producer(queue, commands) }
command_consumer(editor, queue, log, save_on_write~)
}
{ editor: result, log }
}
///|
pub fn parse_script_line(line : String) -> EditorCommand? {
let trimmed = line.trim().to_owned()
match trimmed {
"" => None
_ if trimmed.has_prefix("#") => None
_ if trimmed.has_prefix(":") => Some(Ex(trimmed[1:].to_owned()))
_ if trimmed.has_prefix("key ") => Some(Key(trimmed[4:].to_owned()))
_ if trimmed.has_prefix("text ") => Some(Key(trimmed[5:].to_owned()))
_ if trimmed == "quit" => Some(Quit)
_ => Some(Key(trimmed))
}
}
///|
pub fn parse_script(text : String) -> Array[EditorCommand] {
let commands : Array[EditorCommand] = []
for line in text.split("\n") {
if parse_script_line(line.to_owned()) is Some(command) {
commands.push(command)
}
}
commands
}