# simple ass modal ass line ass editor #* printed by the h command *# const ed_usage = """ ed, the only good and standard text editor on earth addresses: . current | $ last | , or % all | n nth line p print | n numbered | a append | i insert | c change | d delete w [file] write | q quit | = number | h help | o outline | empty prints next ! [file] run buffer or file | low,high! run range | v check | x eval and show a/i/c take lines until a lone . """ fn list_insert(items, index, value) -> table do let result = {} for position in 0..index do result:push(items[position]) end result:push(value) for position in index..items:len() do result:push(items[position]) end return result end fn list_delete(items, low, high) -> table do let result = {} for position in 0..items:len() do if (position < low or position > high) result:push(items[position]) end return result end fn ed_is_digit(character) -> bool do const code = character:ascii() return code >= 48 and code <= 57 end #* parse one address at index *# fn ed_addr(state, line, index) -> { value: num, next: num } do if index >= line:len() do return { value = -1, next = index } end if line[index] == "." do return { value = state.dot, next = index + 1 } end if line[index] == "$" do return { value = state.lines:len() - 1, next = index + 1 } end if ed_is_digit(line[index]) do let number = 0 let scan = index while (scan < line:len() and ed_is_digit(line[scan])) do number = number * 10 + (line[scan]:ascii() - 48) scan += 1 end return { value = number - 1, next = scan } end return { value = -1, next = index } end #* split a command line into low, high and command *# fn ed_parse(state, line) -> { low: num, high: num, command: string } do let index = 0 let low = -1 let high = -1 if line:len() > 0 and (line[0] == "," or line[0] == "%") do low = 0 high = state.lines:len() - 1 index = 1 end else do const first_addr = ed_addr(state, line, 0) if first_addr.value >= 0 do low = first_addr.value index = first_addr.next end if index < line:len() and line[index] == "," do index += 1 const second_addr = ed_addr(state, line, index) if second_addr.value >= 0 do high = second_addr.value index = second_addr.next end else do high = state.lines:len() - 1 end end end if (low < 0) low = state.dot if (high < 0) high = low const command = if (index < line:len()) line[index..] else "" return { low = low, high = high, command = command } end fn ed_print(state, low, high, numbered) do let index = low while (index <= high) do if index >= 0 and index < state.lines:len() do if numbered do # todo make the a \t once it renders right puts("#{index + 1} #{state.lines[index]}\n") end else do puts("#{state.lines[index]}\n") end end index += 1 end if (high >= 0 and high < state.lines:len()) state.dot = high end fn ed_goto(state, low) do if state.lines:len() == 0 do puts("?\n") return :nil end let target = low if (target < 0) target = 0 if (target >= state.lines:len()) target = state.lines:len() - 1 state.dot = target puts("#{state.lines[target]}\n") end fn ed_begin_input(state, position) do let clamped = position if (clamped < 0) clamped = 0 if (clamped > state.lines:len()) clamped = state.lines:len() state.input = :true state.insert_at = clamped end fn ed_delete(state, low, high) do state.lines = list_delete(state.lines, low, high) state.dirty = :true let new_dot = low if (new_dot >= state.lines:len()) new_dot = state.lines:len() - 1 if (new_dot < 0) new_dot = 0 state.dot = new_dot end fn ed_change(state, low, high) do ed_delete(state, low, high) ed_begin_input(state, low) end fn ed_join(items) -> string do let result = "" for index in 0..items:len() do result = result ~ items[index] ~ "\n" end return result end fn ed_range_text(state, low, high) -> string do let range = {} for index in low..(high + 1) do if index >= 0 and index < state.lines:len() do range:push(state.lines[index]) end end return ed_join(range) end fn ed_write(state, command) do let path = state.path if command:len() > 1 do const filename = command[1..]:trim() if (filename:len() > 0) path = expand_path(filename) end if path:len() == 0 do puts("?\n") return :nil end const contents = ed_join(state.lines) fs:store(path, contents) state.path = path state.dirty = :false puts("#{contents:len()}\n") end fn ed_quit(state) do state.active = :false puts(prompt()) end fn ed_run_file(state, line, parsed) do if line:len() > 0 and line[0] != "!" do const text = ed_range_text(state, parsed.low, parsed.high) if text:len() == 0 do puts("?\n") return :nil end env.ctx = make_ctx("", {""}) const error_msg = eval_script(text) if error_msg != :nil do puts("!: #{error_msg}\n") end return :nil end let rest = "" if parsed.command:len() > 1 do rest = parsed.command[1..]:trim() end if rest:len() == 0 do if state.lines:len() == 0 do puts("?\n") return :nil end env.ctx = make_ctx("", {""}) const error_msg = eval_script(ed_join(state.lines)) if error_msg != :nil do puts("!: #{error_msg}\n") end return :nil end const path = expand_path(rest:split(" ")[0]) if fs:size(path) < 0 do puts("?\n") return :nil end run_script_path(path, path, make_ctx(rest, rest:split(" "))) end #* check the buffer (or range) without running it *# fn ed_check(state, line, parsed) do let text = ed_join(state.lines) if line:len() > 0 and line[0] != "v" do text = ed_range_text(state, parsed.low, parsed.high) end if text:len() == 0 do puts("?\n") return :nil end match revo.compile(text) | {:ok, _} => puts("ok\n") | {:err, e} => puts("#{e}\n") end #* eval and show the value, like the repl does *# fn ed_eval_show(state, line, parsed) do let text = ed_join(state.lines) if line:len() > 0 and line[0] != "x" do text = ed_range_text(state, parsed.low, parsed.high) end if text:len() == 0 do puts("?\n") return :nil end env.ctx = make_ctx("", {""}) match revo.eval(text) | {:ok, v} => puts("#{v}\n") | {:err, e} => puts("#{e}\n") end #* list fn and global definitions with line numbers *# fn ed_outline(state) do for index in 0..state.lines:len() do const text = state.lines[index]:trim() if text:starts_with?("fn ") or text:starts_with?("global ") do puts("#{index + 1}\t#{text}\n") end end end global ed_state = { active = :false, input = :false, path = "", lines = {}, dot = 0, insert_at = 0, dirty = :false } #* open the editor, takes over the shell until quit *# fn gils_ed(context) -> :hold do ed_state.active = :true ed_state.input = :false ed_state.dirty = :false ed_state.lines = {} ed_state.path = if (context.argv:len() > 1) expand_path(context.argv[1]) else "" if ed_state.path:len() > 0 do const size = fs:size(ed_state.path) if size > 0 do const contents = fs:read_bytes(ed_state.path, 0, size) const raw_lines = contents:split("\n") for index in 0..raw_lines:len() do if index < raw_lines:len() - 1 or raw_lines[index]:len() > 0 do ed_state.lines:push(raw_lines[index]) end end puts("#{size}\n") end else do puts("#{ed_state.path}: new file\n") end end ed_state.dot = if (ed_state.lines:len() > 0) ed_state.lines:len() - 1 else 0 return :hold end fn gils_ed_feed(line) do const state = ed_state if state.input do if line == "." do state.input = :false end else do state.lines = list_insert(state.lines, state.insert_at, line) state.insert_at += 1 state.dot = state.insert_at - 1 state.dirty = :true end return :nil end if line:len() == 0 do if state.lines:len() == 0 do puts("?\n") return :nil end if (state.dot < state.lines:len() - 1) state.dot += 1 puts("#{state.lines[state.dot]}\n") return :nil end const parsed = ed_parse(state, line) const command_char = if (parsed.command:len() > 0) parsed.command[0] else "" match command_char | "p" => ed_print(state, parsed.low, parsed.high, numbered = :false) | "n" => ed_print(state, parsed.low, parsed.high, numbered = :true) | "a" => ed_begin_input(state, parsed.low + 1) | "i" => ed_begin_input(state, parsed.low) | "c" => ed_change(state, parsed.low, parsed.high) | "d" => ed_delete(state, parsed.low, parsed.high) | "w" => ed_write(state, parsed.command) | "!" => ed_run_file(state, line, parsed) | "v" => ed_check(state, line, parsed) | "x" => ed_eval_show(state, line, parsed) | "q" => ed_quit(state) | "h" => puts(ed_usage ~ "\n") | "o" => ed_outline(state) | "=" => puts("#{state.dot + 1}\n") | "" => ed_goto(state, parsed.low) | _ => puts("?\n") end fn gils_ed_active() -> bool do return ed_state.active end register("ed [path]", "gils", "the editor (type h for help)", gils_ed)