# simple ass modal ass line ass editor fn list_insert(items, index, value) 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) 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) do const code = character:ascii() return code >= 48 and code <= 57 end # parse one address at index fn ed_addr(state, line, index) do if index >= line:len() return { value = -1, next = index } if line[index] == "." return { value = state.dot, next = index + 1 } if line[index] == "$" return { value = state.lines:len() - 1, next = index + 1 } 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) 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 do low = state.dot end if high < 0 do high = low end 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 puts("#{index + 1}\t#{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()) do state.dot = high end end fn ed_goto(state, low) do if state.lines:len() == 0 do puts("?\n") return :nil end let target = low if target < 0 do target = 0 end if target >= state.lines:len() do target = state.lines:len() - 1 end state.dot = target puts("#{state.lines[target]}\n") end fn ed_begin_input(state, position) do let clamped = position if clamped < 0 do clamped = 0 end if clamped > state.lines:len() do clamped = state.lines:len() end 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() do new_dot = state.lines:len() - 1 end if new_dot < 0 do new_dot = 0 end 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) do let result = "" for index in 0..items:len() do result = result ~ items[index] ~ "\n" end return result 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 do path = abspath(filename) end 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 global ed_state = { active = :false, input = :false, path = "", lines = {}, dot = 0, insert_at = 0, dirty = :false } fn gils_ed(context) do ed_state.active = :true ed_state.input = :false ed_state.dirty = :false ed_state.lines = {} ed_state.path = if context.argv:len() > 1 abspath(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 do state.dot += 1 end 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, :false) | "n" => ed_print(state, parsed.low, parsed.high, :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) | "q" => ed_quit(state) | "=" => puts("#{state.dot + 1}\n") | "" => ed_goto(state, parsed.low) | _ => puts("?\n") end fn gils_ed_active() ed_state.active register("ed [path]", "gils", "the standard editor (a i d p w q)", gils_ed)