use Time use StrBuilder StrBuilder use Tui Color Color8 Tui Screen use Str2 is-alphanumeric use Mem cast use StrView use Misc fmap-maybe fn char-len (c Char) if c == '\t' return 2 else return 1 CharType CharSpace CharWord CharPunctuation inst Eq CharType eq (l, r) case (l, r) (CharSpace, CharSpace) return True (CharWord, CharWord) return True (CharPunctuation, CharPunctuation) return True _ return False fn char-type (c Char) if c is-whitespace() return CharSpace elif c is-alphanumeric() or c == '-' or c == '_' return CharWord else return CharPunctuation Theme bg Color text Color cursor-bg Color cursor-text Color selection-bg Color selection-text Color selection-cursor-bg Color selection-cursor-text Color line-num-hi-bg Color line-num-hi-text Color dark-theme =& Theme { bg: Term.Color8(Term.Black8) , text: Term.Color8(Term.White8) , cursor-bg: Term.Color8(Term.White8) , cursor-text: Term.Color8(Term.Black8) , selection-bg: Term.Color16(Term.BrightWhite16) , selection-text: Term.Color16(Term.Black16) , selection-cursor-bg: Term.Color16(Term.BrightBlack16) , selection-cursor-text: Term.Color8(Term.Black8) , line-num-hi-bg: Term.Color8(Term.White8) , line-num-hi-text: Term.Color8(Term.Black8) } light-theme =& Theme { bg: Term.Color16(Term.BrightWhite16) , text: Term.Color8(Term.Black8) , cursor-bg: Term.Color8(Term.Black8) , cursor-text: Term.Color16(Term.BrightWhite16) , selection-bg: Term.Color16(Term.White16) , selection-text: Term.Color16(Term.BrightWhite16) , selection-cursor-bg: Term.Color16(Term.BrightBlack16) , selection-cursor-text: Term.Color16(Term.BrightWhite16) , line-num-hi-bg: Term.Color8(Term.Black8) , line-num-hi-text: Term.Color16(Term.BrightWhite16) } Cursor x I32 y I32 inst Eq Cursor eq (l, r): l.x == r.x and l.y == r.y inst Ord Cursor cmp (l, r) case l.y cmp (r.y) EQ return l.x cmp (r.x) els return els # cute! so that I don't get confused Visual Visual I32 fn from-visual(Visual(x)): x Mode al Normal Insert Select Cmd (StrBuilder al) SearchBox (StrBuilder al) inst Eq Mode eq (l, r) case (l, r) (Normal, Normal) return True (Insert, Insert) return True (Select, Select) return True (Cmd(_), Cmd(_)) panic('todo: implement more stuff. technically, not reachable yet.') (SearchBox(_), SearchBox(_)) panic('todo: implement more stuff. technically, not reachable yet.') _ return False inst Str Mode print-str (self) case self Normal Str.print-str('Normal') Insert Str.print-str('Insert') Select Str.print-str('Select') Cmd(s) ss = StrBuilder.as-str(&s) Str.print-str('Cmd \(ss)') SearchBox(s) ss = StrBuilder.as-str(&s) Str.print-str('Search \(ss)') chars (self) c = @undefined case self Normal c <= 'Normal' Str.StrCase1() Insert c <= 'Insert' Str.StrCase1() Select c <= 'Select' Str.StrCase1() Cmd(s) ss = StrBuilder.as-str(&s) c <= 'Cmd(\(ss))' Str.StrCase2() SearchBox(s) ss = StrBuilder.as-str(&s) c <= 'Search(\(ss))' Str.StrCase2() return c chars() Primitive PrimInsert Cursor StrView PrimDelete Cursor StrView Pos cur Cursor sel Maybe Cursor Action prim Primitive selected Bool before-pos Maybe Pos ChangeSet = Slice Action Actions al list List al (Either ChangeSet Action) cur Size changeset Maybe (List al Action) Editor al al al running Bool cursor Cursor screen-top I32 screen-left I32 # change it to Vec2 (or just Cursor)? goal-x Visual # when moving up-down, try to move it to this. it'll get reset with any horizontal movement. current-file List al (List al U8) filename Maybe ConstStr mode (Mode al) anim Maybe Cnile.Timespec selected Maybe Cursor theme Ptr Theme search-term Maybe (StrBuilder al) msg Maybe StrView clipboard Maybe StrView actions Actions al insert-start Maybe Cursor delete-acc Maybe (StrBuilder al) fn get-row (ed Ptr (Editor al), y I32) -> Ptr (List al U8): (&ed&.current-file) List.get-ptr(y i32-size()) fn cursor-row (ed Ptr (Editor al)) -> Ptr (List al U8): (&ed&.current-file) List.get-ptr(ed&.cursor.y i32-size()) fn indent-on-row (ed Ptr (Editor al), y I32): ed get-row(y) map(Char.from-u8) take-while(is-whitespace) fn num-rows (ed Ptr (Editor al)): List.size(&ed&.current-file) fn insert-row (ed Ptr (Editor al), y I32) <= Allocator al List.insert(&ed&.current-file, y i32-size(), List.mk(ed&.al)) return ed get-row(y) fn remove-row(ed Ptr (Editor Allocator), y I32) next-row = ed get-row(y) next-row List.free() (&ed&.current-file) List.remove(y i32-size()) fn is-row-empty(ed Ptr (Editor Allocator), y I32): ed get-row(y) map(Char.from-u8) all(is-whitespace) fn char-at (ed Ptr (Editor al), cur Cursor) -> Maybe Char: ed get-row(cur.y) List.try-get(cur.x i32-size()) Misc.fmap-maybe(Char.from-u8) fn char-at-cursor (ed Ptr (Editor al)) -> Maybe Char: ed cursor-row() List.try-get(ed&.cursor.x i32-size()) Misc.fmap-maybe(Char.from-u8) fn x-to-visual-x (ed Ptr (Editor al), x I32, y I32) -> Visual: ed get-row(y) take(x i32-size()) map(Char.from-u8) map(char-len) sum() Visual() fn visual-x-to-x (ed Ptr (Editor al), Visual(vx), y I32) -> I32 sum-x = 0 chars = ed get-row(y) for x in 0 to (chars&.count size-i32() - 1) # we first convert size to i32 to handle the case when count = 0. sum-x <= sum-x + chars[x i32-size()] Char.from-u8() char-len() if vx < sum-x return x return chars&.count size-i32() # SELECT compatible selection remove fn set-selection(ed Ptr (Editor al), sel Maybe Cursor) if ed&.mode /= Select or not ed&.selected is-just() ed <&.selected= sel fn reset-msg (ed Ptr (Editor Allocator)) case ed&.msg None pass Just(olmsg) ed&.al Alloc.free(olmsg.contents) ed <&.msg= None fn send-msg (ed Ptr (Editor Allocator), s Str) ed reset-msg() s = s StrBuilder.mk-dyn-str(ed&.al) ed <&.msg= Just(s) Dims cols I32 rows I32 fn move-to (ed Ptr (Editor al), x I32, y I32, dim Dims) cur = ed&.cursor should-change-goal-x = cur.x /= x y = y Math.clamp(0, ed&.current-file.count size-i32() - 1) funny-space = 5 # I DONT KNOW THE PROPER NAME if y < ed&.screen-top + funny-space ed <&.screen-top= Math.max(0, y - funny-space) elif y > ed&.screen-top + dim.rows - funny-space - 1 # onscreen-y = Math.min(ed&.current-file.count size-i32() - dim.rows, y + funny-space - dim.rows + 1) onscreen-y = y + funny-space - dim.rows + 1 ed <&.screen-top= onscreen-y else pass # todo or actually nothing? row = ed get-row(y) if should-change-goal-x x = x Math.clamp(0, row List.size() size-i32()) ed <&.goal-x= ed x-to-visual-x(x, y) vx = ed&.goal-x x = ed visual-x-to-x(vx, y) funny-space = 5 # I DONT KNOW THE PROPER NAME if x < ed&.screen-left + funny-space ed <&.screen-left= Math.max(0, x - funny-space) elif x > ed&.screen-left + dim.cols - funny-space - 1 onscreen-x = x + funny-space - dim.cols + 1 ed <&.screen-left= onscreen-x else pass # todo or actually nothing? ed <&.cursor= { x, y } ed set-selection(None) fn move-to-col (ed Ptr (Editor al), x I32, dim Dims) ed move-to(x, ed&.cursor.y, dim) fn move-to-row (ed Ptr (Editor al), y I32, dim Dims) ed move-to(ed&.cursor.x, y, dim) fn move-left (ed Ptr (Editor al), dim Dims) -> Bool cur = ed&.cursor if ed&.cursor.x == 0 # jump to previous line then. if ed&.cursor.y > 0 ed move-to-row(ed&.cursor.y - 1, dim) ed move-to-col(ed cursor-row()&.count size-i32(), dim) else ed move-to-col(ed&.cursor.x - 1, dim) return ed&.cursor /= cur fn move-right (ed Ptr (Editor al), dims Dims) -> Bool cur = ed&.cursor row-len = ed cursor-row()&.count size-i32() if ed&.cursor.x == row-len # jump to next line then. if ed&.cursor.y < ed&.current-file.count size-i32() - 1 ed move-to(0, ed&.cursor.y + 1, dims) else ed move-to-col(ed&.cursor.x + 1, dims) return ed&.cursor /= cur fn move-to-start-of-line-indented(ed Ptr (Editor al), dims Dims) off = ed indent-on-row(ed&.cursor.y) count() size-i32() ed move-to-col(off, dims) fn move-to-end-of-line (ed Ptr (Editor al), dims Dims) currow = ed cursor-row() ed move-to-col(currow List.size() size-i32(), dims) fn cursor-is-at-line-start (ed Ptr (Editor al)): ed&.cursor.x == 0 fn is-at-line-end (ed Ptr (Editor al), col I32, row I32): col == ed get-row(row) List.size() size-i32() fn cursor-is-at-line-end (ed Ptr (Editor al)): ed&.cursor.x == ed cursor-row() List.size() size-i32() fn set-clipboard (ed Ptr (Editor Allocator), clip Maybe StrView) case ed&.clipboard None pass Just(cp) ed&.al Alloc.free(cp.contents) ed <&.clipboard= clip fn copy-selection (ed Ptr (Editor Allocator)) -> Maybe StrView case ed&.selected None case ed char-at-cursor() None return None Just(c) sb =& StrBuilder.mk(ed&.al) sb StrBuilder.write-char(c) return sb StrBuilder.as-str() Just() Just(sel) sb =& StrBuilder.mk(ed&.al) from = Math.min(sel, ed&.cursor) to = Math.max(sel, ed&.cursor) if from.y == to.y currow = ed get-row(from.y) sb StrBuilder.write-slice(currow& List.to-slice() subslice(from.x i32-size(), to.x i32-size() + 1)) if ed is-at-line-end(to.x, to.y) sb StrBuilder.write('\n') else assert(to.y > from.y, 'THATS WHAT WERE CHECKIN YO') currow = ed get-row(from.y) sb StrBuilder.write-slice(currow& List.to-slice() subslice(from.x i32-size(), currow List.size())) sb StrBuilder.write('\n') for row in ed&.current-file List.to-slice() subslice(from.y i32-size() + 1, to.y i32-size()) sb StrBuilder.write-slice(row List.to-slice()) sb StrBuilder.write('\n') lastrow = ed get-row(to.y) sb StrBuilder.write-slice(lastrow& List.to-slice() subslice(0, to.x i32-size() + 1)) if ed is-at-line-end(to.x, to.y) sb StrBuilder.write('\n') s = sb StrBuilder.as-str() return Just(s) fn copy-selection-to-clipboard (ed Ptr (Editor Allocator)) ed set-clipboard(ed copy-selection()) # also moves the cursor to the end of selection. PastePos PasteBefore PasteAfter ## UNDO/REDO fn apply-primitive(ed Ptr (Editor Allocator), prim Primitive, dims Dims) case prim PrimInsert(cur, text) x = cur.x y = cur.y if y i32-size() >= ed num-rows() List.add(&ed&.current-file, List.mk(ed&.al)) for b in text.contents c = b Char.from-u8() if c == '\n' cur-line = ed get-row(y) nurow =& List.mk(ed&.al) nurow List.add-all(cur-line& List.to-slice() Slice.from(x i32-size())) cur-line List.trim(x i32-size()) (&ed&.current-file) List.insert(y i32-size() + 1, nurow&) y <= y + 1 x <= 0 else ed get-row(y) List.insert(x i32-size(), b) x <= x + 1 ed <&.goal-x= ed x-to-visual-x(x, y) ed move-to(x, y, dims) PrimDelete(cur, text) for _ in 0 to (text.contents.count size-i32() - 1) row = ed get-row(cur.y) if cur.x i32-size() >= row&.count if cur.y + 1 < ed num-rows() size-i32() next-row = ed get-row(cur.y + 1) row List.add-all(next-row) ed remove-row(cur.y + 1) else row List.remove(cur.x i32-size()) ed <&.goal-x= ed x-to-visual-x(cur.x, cur.y) ed move-to(cur.x, cur.y, dims) fn invert-primitive(prim Primitive) -> Primitive case prim PrimInsert(cur, s) return PrimDelete(cur, s) PrimDelete(cur, s) return PrimInsert(cur, s) fn free-primitive(prim Primitive, al Allocator) case prim PrimInsert(cur, s) al Alloc.free(s.contents) PrimDelete(cur, s) al Alloc.free(s.contents) fn free-action(action Either ChangeSet Action, al Allocator) case action Right(action) action.prim free-primitive(al) Left(changeset) changeset for-each(fn action: action.prim free-primitive(al)) al Alloc.free(changeset) fn start-changeset(ed Ptr (Editor Allocator)) assert(not ed&.actions.changeset is-just(), 'unfinished changeset SOMEHOW!') ed <&.actions.changeset= Just(List.mk(ed&.al)) fn end-changeset(ed Ptr (Editor Allocator)) changeset = ed&.actions.changeset or-fail('nonexistent changeset') assert(ed&.actions.cur == ed&.actions.list.count, 'no new changes shouldve been added!') ed <&.actions.changeset= None # in case there were no changes, don't make a new changeset. if changeset.count == 0 List.free(&changeset) return changeset = changeset List.to-slice() List.add(&ed&.actions.list, changeset Left()) ed <&.actions.cur= ed&.actions.cur + 1 fn push-action (actions Ptr (Actions Allocator), action Action, al Allocator) List.to-slice(actions&.list) Slice.from(actions&.cur) for-each(fn action: action free-action(al)) (&actions&.list) List.trim(actions&.cur) case actions Ptr({ changeset: Just(changeset) }) List.add(&changeset, action) _ List.add(&actions&.list, action Right()) actions <&.cur= actions&.cur + 1 # TODO/NOTE: slopped up. teh ai replaced my code, which handled all these cool cases and slowly readded handling for these cases # and now it looks stupid. i have to redo it, because it's unreadable. fn flush-insert (ed Ptr (Editor Allocator)) case ed&.insert-start None return Just(start) start = start # NOTE(case-refs) ed <&.insert-start= None del-count = 0 case ed&.delete-acc None pass Just(acc) del-text = StrBuilder.as-str(&acc) ed <&.delete-acc= None if del-text.contents.count > 0 del-count <= del-text.contents.count size-i32() rev-sb =& StrBuilder.mk(ed&.al) for i in 0 to (del-count - 1) rev-sb StrBuilder.write-char(del-text.contents[(del-count - 1 - i) i32-size()] Char.from-u8()) ed&.al Alloc.free(del-text.contents) rev-text = rev-sb StrBuilder.as-str() del-pos = { x: start.x - del-count, y: start.y } (&ed&.actions) push-action({ prim: PrimDelete(del-pos, rev-text), selected: False, before-pos: None }, ed&.al) else ed&.al Alloc.free(del-text.contents) adj-start = { x: start.x - del-count, y: start.y } if adj-start == ed&.cursor return sb =& StrBuilder.mk(ed&.al) if adj-start.y == ed&.cursor.y row = ed get-row(adj-start.y) sb StrBuilder.write-slice(row& List.to-slice() subslice(adj-start.x i32-size(), ed&.cursor.x i32-size())) else first-row = ed get-row(adj-start.y) sb StrBuilder.write-slice(first-row& List.to-slice() Slice.from(adj-start.x i32-size())) sb StrBuilder.write('\n') for (row, y) in ed&.current-file List.to-slice() subslice(adj-start.y i32-size() + 1, ed&.cursor.y i32-size()) zip(Iter.from(adj-start.y + 1)) sb StrBuilder.write-slice(row List.to-slice()) sb StrBuilder.write('\n') last-row = ed get-row(ed&.cursor.y) sb StrBuilder.write-slice(last-row& List.to-slice() subslice(0, ed&.cursor.x i32-size())) text = sb StrBuilder.as-str() if text.contents.count == 0 ed&.al Alloc.free(text.contents) return (&ed&.actions) push-action({ prim: PrimInsert(adj-start, text), selected: False, before-pos: None }, ed&.al) fn set-mode-normal(ed Ptr (Editor Allocator)) ed flush-insert() ed <&.mode= Normal fn enter-insert-mode(ed Ptr (Editor Allocator)) ed <&.mode= Insert ed <&.insert-start= Just(ed&.cursor) ed <&.delete-acc= None fn do-action(ed Ptr (Editor Allocator), action Action, dims Dims) ed flush-insert() (&ed&.actions) push-action(action, ed&.al) ed apply-primitive(action.prim, dims) fn undo(ed Ptr (Editor Allocator), dims Dims) if ed&.actions.cur == 0 return ed <&.actions.cur= ed&.actions.cur - 1 action = ed&.actions.list[ed&.actions.cur] last-action = @undefined case action Right(action) ed apply-primitive(action.prim invert-primitive(), dims) last-action <= action Left(changeset) for action in changeset Slice.reversed() ed apply-primitive(action.prim invert-primitive(), dims) last-action <= changeset[0] case last-action.before-pos None pass Just(bp) ed move-to(bp.cur.x, bp.cur.y, dims) ed <&.selected= bp.sel fn redo(ed Ptr (Editor Allocator), dims Dims) if ed&.actions.cur >= ed&.actions.list.count return action = ed&.actions.list[ed&.actions.cur] ed <&.actions.cur= ed&.actions.cur + 1 last-action = @undefined case action Right(action) ed apply-primitive(action.prim, dims) last-action <= action Left(changeset) for action in changeset ed apply-primitive(action.prim, dims) last-action <= changeset[changeset.count - 1] case last-action.prim PrimInsert(cur, _) ed move-left(dims) if last-action.selected ed <&.selected= Just(cur) _ pass fn paste-clipboard-at-cursor (ed Ptr (Editor al), pastepos PastePos, dims Dims) <= Allocator al if not ed&.clipboard is-just() return cp = ed&.clipboard or-fail('expect clipboard') before-cursor = ed&.cursor before-sel = ed&.selected before-pos = { cur: before-cursor, sel: before-sel } does-it-end-in-newline = cp.contents[cp.contents.count - 1] Char.from-u8() == '\n' if does-it-end-in-newline from-y = (case pastepos) PasteAfter: ed&.selected Misc.maybe(fn s: Math.max(s, ed&.cursor), ed&.cursor).y + 1 _: ed&.selected Misc.maybe(fn s: Math.min(s, ed&.cursor), ed&.cursor).y prim = PrimInsert({ x: 0, y: from-y }, cp StrView.clone(ed&.al)) ed do-action({ prim, selected: True, before-pos: Just(before-pos) }, dims) last-pasted-y = ed&.cursor.y - 1 ed move-to(ed get-row(last-pasted-y)&.count size-i32(), last-pasted-y, dims) ed <&.selected= Just({ x: 0, y: from-y }) ed set-mode-normal() else from-x = @undefined from-y = @undefined case pastepos PasteAfter from-cur = ed&.selected Misc.maybe(fn s: Math.max(s, ed&.cursor), ed&.cursor) from-x <= (from-cur.x + 1) i32-size() from-y <= from-cur.y if ed is-at-line-end(from-cur.x, from-cur.y) from-y <= from-y + 1 from-x <= 0 PasteBefore from-cur = ed&.selected Misc.maybe(fn s: Math.min(s, ed&.cursor), ed&.cursor) from-x <= from-cur.x i32-size() from-y <= from-cur.y prim = PrimInsert({ x: from-x size-i32(), y: from-y }, cp StrView.clone(ed&.al)) ed do-action({ prim, selected: True, before-pos: Just(before-pos) }, dims) ed move-left(dims) ed <&.selected= Just({ x: from-x size-i32(), y: from-y }) ed set-mode-normal() fn delete-selected (ed Ptr (Editor al), dims Dims) <= Allocator al before-cursor = ed&.cursor before-sel = ed&.selected before-pos = { cur: before-cursor, sel: before-sel } cfrom = (case ed&.selected) Just(sel): Math.min(sel, ed&.cursor) _: ed&.cursor del = @undefined case ed copy-selection() None sb =& StrBuilder.mk(ed&.al) sb StrBuilder.write-char('\n') del <= sb StrBuilder.as-str() ed set-clipboard(None) Just(text) if ed&.selected is-just() ed set-mode-normal() ed <&.selected= None del <= text ed set-clipboard(Just(text StrView.clone(ed&.al))) ed do-action({ prim: PrimDelete(cfrom, del), selected: True, before-pos: Just(before-pos) }, dims) # NOTE: also indents from current row! fn insert-empty-row-at (ed Ptr (Editor al), y I32, al al) <= Allocator al nurow =& List.mk(al) rowws = ed indent-on-row(ed&.cursor.y) offchars = rowws count() size-i32() nurow List.add-all(rowws map(Char.char-u8)) (&ed&.current-file) List.insert(y i32-size(), nurow&) fn is-in-selection (ed Ptr (Editor al), c Cursor): (case ed&.selected) Just(sel): c Math.between(ed&.cursor, sel) _: False # TODO: maybe I should just return the two cursor positions and set pos in the calling code? # TODO: line should center when we move the cursor outside of the screen. fn find-next-occurence (ed Ptr (Editor al), needle Slice U8) -> Maybe Cursor <= Allocator al from = Math.max(ed&.cursor, ed&.selected or-else({ x: 0, y: 0 })) curline = ed get-row(from.y)& List.to-slice() case curline subslice(from.x i32-size() + 1, curline.count) Slice.find-slice(needle) Just(idx) return Just({ x: from.x + 1 + idx size-i32(), y: from.y }) None for (row, from-y) in ed&.current-file List.to-slice() Slice.from(from.y i32-size() + 1) zip(Iter.from(from.y + 1)) case row List.to-slice() Slice.find-slice(needle) None pass Just(idx) return Just({ x: idx size-i32(), y: from-y }) ed send-msg('Wrapped!') # here, we wrapped. for (row, from-y) in ed&.current-file List.to-slice() Slice.subslice(0, from.y i32-size()) zip(Iter.from(0)) case row List.to-slice() Slice.find-slice(needle) None pass Just(idx) return Just({ x: idx size-i32(), y: from-y }) # at last, check the line BEFORE the cursor. since the cursor may sit on the pattern, just scan the whole line, since we know that the last subslice has no matches.. case curline Slice.find-slice(needle) None pass Just(idx) return Just({ x: idx size-i32(), y: from.y }) ed send-msg('NOT FOUND!!!') return None fn find-prev-occurence (ed Ptr (Editor al), needle Slice U8) -> Maybe Cursor <= Allocator al from = ed&.selected Misc.maybe(fn s: Math.min(ed&.cursor, s), ed&.cursor) curline = ed get-row(from.y)& List.to-slice() case curline subslice(0, from.x i32-size()) Slice.find-last-occurence-of-slice(needle) Just(idx) return Just({ x: idx size-i32(), y: from.y }) None from-y = from.y - 1 while from-y >= 0 row = ed get-row(from-y) case row& List.to-slice() Slice.find-last-occurence-of-slice(needle) None pass Just(idx) return Just({ x: idx size-i32(), y: from-y }) from-y <= from-y - 1 ed send-msg('Wrapped!') # here, we wrapped. from-y = ed num-rows() size-i32() - 1 while from-y > from.y row = ed get-row(from-y) case row& List.to-slice() Slice.find-last-occurence-of-slice(needle) None pass Just(idx) return Just({ x: idx size-i32(), y: from-y }) from-y <= from-y - 1 case curline Slice.find-last-occurence-of-slice(needle) None pass Just(idx) return Just({ x: idx size-i32(), y: from.y }) ed send-msg('NOT FOUND!!!') return None fn indent-selection (ed Ptr (Editor Allocator), dims Dims) from-y = ed&.selected Misc.maybe(fn s: Math.min(s.y, ed&.cursor.y), ed&.cursor.y) to-y = ed&.selected Misc.maybe(fn s: Math.max(s.y, ed&.cursor.y), ed&.cursor.y) saved-cursor = ed&.cursor saved-selected = ed&.selected before-pos = { cur: saved-cursor, sel: saved-selected } ed start-changeset() for rowid in from-y to (to-y) if not ed is-row-empty(rowid) ed do-action({ prim: PrimInsert({ x: 0, y: rowid }, '\t' StrBuilder.mk-dyn-str(ed&.al)), selected: False, before-pos: Just(before-pos) }, dims) ed end-changeset() ed <&.cursor= saved-cursor { x: saved-cursor.x + 1 } ed <&.selected= saved-selected Misc.fmap-maybe(fn s: s { x: s.x + 1 }) fn dedent-selection (ed Ptr (Editor Allocator), dims Dims) from-y = ed&.selected Misc.maybe(fn s: Math.min(s.y, ed&.cursor.y), ed&.cursor.y) to-y = ed&.selected Misc.maybe(fn s: Math.max(s.y, ed&.cursor.y), ed&.cursor.y) saved-cursor = ed&.cursor saved-selected = ed&.selected before-pos = { cur: saved-cursor, sel: saved-selected } ed start-changeset() for rowid in from-y to (to-y) if ed char-at({ x: 0, y: rowid }) == Just('\t') ed do-action({ prim: PrimDelete({ x: 0, y: rowid }, '\t' StrBuilder.mk-dyn-str(ed&.al)), selected: False, before-pos: Just(before-pos) }, dims) if saved-cursor.y == rowid saved-cursor <= saved-cursor { x: if saved-cursor.x > 0: saved-cursor.x - 1 else 0 } case saved-selected None pass Just(ss) if ss.y == rowid saved-selected <= Just(ss { x: if ss.x > 0: ss.x - 1 else 0 }) ed end-changeset() ed <&.cursor= saved-cursor ed <&.selected= saved-selected fn toggle-comment-on-selection (ed Ptr (Editor Allocator), dims Dims) comment-char = '#' from-y = ed&.selected Misc.maybe(fn s: Math.min(s.y, ed&.cursor.y), ed&.cursor.y) to-y = ed&.selected Misc.maybe(fn s: Math.max(s.y, ed&.cursor.y), ed&.cursor.y) saved-cursor = ed&.cursor saved-selected = ed&.selected before-pos = { cur: saved-cursor, sel: saved-selected } ed start-changeset() remove-comments = True for rowid in from-y to (to-y) case ed get-row(rowid) map(Char.from-u8) drop-while(is-whitespace) head() None pass # empty row. Just(c) remove-comments <= remove-comments and c == comment-char new-cursor = @undefined new-selected = None for rowid in from-y to (to-y) if not ed is-row-empty(rowid) if remove-comments row = ed get-row(rowid) first-char-idx = row map(Char.from-u8) take-while(is-whitespace) count() # asume first char is `comment-char` # also possibly remove one space s = @undefined if List.try-get(row, first-char-idx + 1) fmap-maybe(Char.from-u8) == Just(' ') s <= '\(comment-char) ' StrBuilder.mk-dyn-str(ed&.al) else s <= '\(comment-char)' StrBuilder.mk-dyn-str(ed&.al) ed do-action({ prim: PrimDelete({ x: first-char-idx size-i32(), y: rowid }, s), selected: False, before-pos: Just(before-pos) }, dims) move-len = s count() size-i32() if rowid == saved-cursor.y if saved-cursor.x >= first-char-idx size-i32() + move-len new-cursor <= saved-cursor { x: saved-cursor.x - move-len } elif saved-cursor.x >= first-char-idx size-i32() new-cursor <= saved-cursor { x: first-char-idx size-i32() } else new-cursor <= saved-cursor case saved-selected None pass Just(ss) if ss.y == rowid if ss.x >= (first-char-idx size-i32() + move-len) new-selected <= Just(ss { x: ss.x - move-len }) elif ss.x >= first-char-idx size-i32() new-selected <= Just(ss { x: first-char-idx size-i32() }) else new-selected <= Just(ss) else first-char-idx = ed get-row(rowid) map(Char.from-u8) take-while(is-whitespace) count() s = '\(comment-char) ' StrBuilder.mk-dyn-str(ed&.al) ed do-action({ prim: PrimInsert({ x: first-char-idx size-i32(), y: rowid }, s), selected: False, before-pos: Just(before-pos) }, dims) move-len = s count() size-i32() if rowid == saved-cursor.y if saved-cursor.x >= first-char-idx size-i32() new-cursor <= saved-cursor { x: saved-cursor.x + move-len } else new-cursor <= saved-cursor case saved-selected None pass Just(ss) if ss.y == rowid if ss.x >= first-char-idx size-i32() new-selected <= Just(ss { x: ss.x + move-len }) else new-selected <= Just(ss) ed end-changeset() ed <&.cursor= new-cursor ed <&.selected= new-selected al = Alloc.idc() fn load-file (filename, al) file =& filename File.read-contents(al) lines() # append('' StrView.from-const-str()) # add a single empty line just like all the other editors...? map(fn line: line.contents to-list(al)) to-list(al) if file null() file List.add(List.mk(al)) return file& fn save-file (cf, filename) sb =& StrBuilder.mk(al) for row in cf row List.to-slice() StrView.from-bytes() for-each(fn c: sb StrBuilder.write-char(c)) sb StrBuilder.write-char('\n') File.write-contents(filename, sb StrBuilder.as-str()) sb StrBuilder.free() fn free-file (cf) cf List.addresses() for-each(List.free) List.free(&cf) # offset of the left line number bar from the text file. pad = 2 fn left-offset (ed Ptr (Editor al)) -> { max-digits I32, total-offset I32 } max-digits = ed&.current-file.count Str.count-digits() Math.max(3) return { max-digits, total-offset: pad + max-digits + pad } fn screen-dims (ed Ptr (Editor al), tui Ptr Tui) -> Dims: { cols: tui&.width u32-i32() - ed left-offset().total-offset, rows: tui&.height u32-i32() } fn render-editor (screen Ptr (Screen Allocator), ed Ptr (Editor al)) # first, render screen w = screen&.tui&.width u32-i32() h = screen&.tui&.height u32-i32() from-y = ed&.screen-top from-x = ed&.screen-left x = 0 y = 0 assert(ed&.current-file.count > 0, 'number of lines in file is somehow 0 ???') offstuff = ed left-offset() max-digits = offstuff.max-digits left-off = offstuff.total-offset for (row, cy) in ed&.current-file zip(from(0)) drop(from-y i32-size()) row-num = cy + 1 for xx in 0 to (pad - 1) screen Tui.put-char(' ', xx, y) screen Tui.draw-str-right(row-num, screen&.tui&.width u32-i32() - max-digits - pad, y) for xx in 0 to (pad - 1) screen Tui.put-char(' ', xx + pad + max-digits, y) if ed&.cursor.y == row-num - 1 for xxx in 0 to (max-digits - 1) cp = screen Tui.get-cell-ptr(xxx + pad, y) or-fail('screen index somehow landed outside of screen') cp <&.fg= ed&.theme&.line-num-hi-text cp <&.bg= ed&.theme&.line-num-hi-bg for (char, cx) in row zip(from(0)) drop(from-x i32-size()) char = char Char.from-u8() in-selection = ed is-in-selection({ x: cx, y: cy }) if in-selection screen Tui.set-screen-colors(ed&.theme&.selection-text, ed&.theme&.selection-bg) if char == '\t' screen Tui.put-char(' ', left-off + x, y) x <= x + 1 screen Tui.put-char(' ', left-off + x, y) else screen Tui.put-char(char, left-off + x, y) if in-selection screen Tui.set-screen-colors(ed&.theme&.text, ed&.theme&.bg) x <= x + 1 if x >= w break if x < w and ed is-in-selection({ x: row.count size-i32(), y: cy }) cell = screen Tui.get-cell-ptr(left-off + x, y) or-fail('x < w condition should assert that the cell exists.') cell <&.fg= ed&.theme&.selection-text cell <&.bg= ed&.theme&.selection-bg y <= y + 1 x <= 0 if y >= h break # render selected cursor case ed&.selected None pass Just(sel) vx = ed x-to-visual-x(sel.x, sel.y) from-visual() len = ed get-row(sel.y) List.try-get(sel.x i32-size()) Misc.maybe(fn c: c Char.from-u8() char-len(), 1) for x in 0 to (len - 1) case screen Tui.get-cell-ptr(left-off - ed&.screen-left + vx + x, sel.y - from-y) None pass Just(cursor-cell) cursor-cell <&.fg= ed&.theme&.selection-cursor-text cursor-cell <&.bg= ed&.theme&.selection-cursor-bg # render cursor cur = ed&.cursor vx = ed x-to-visual-x(cur.x, cur.y) from-visual() len = ed cursor-row() List.try-get(cur.x i32-size()) Misc.maybe(fn c: c Char.from-u8() char-len(), 1) for x in 0 to (len - 1) cursor-cell = screen Tui.get-cell-ptr(left-off - ed&.screen-left + vx + x, ed&.cursor.y - from-y) or-fail('cursor out of screen') cursor-cell <&.fg= ed&.theme&.cursor-text cursor-cell <&.bg= ed&.theme&.cursor-bg # example anim. fn update-animation (Ptr(anim) Ptr (Maybe Cnile.Timespec), screen Ptr (Screen Allocator)) case anim None return Just(start-time) diff = Time.now() Time.diff(start-time) limit = Time.duration-from-ns(50000000) # limit = Time.duration-from-s(2) if diff > limit anim <= None return x-fill = screen&.tui&.width u32-i64() / 2 y-fill = screen&.tui&.height u32-i64() / 2 x-progress = ((Time.duration-to-ns(diff) * x-fill) / Time.duration-to-ns(limit)) i64-i32() y-progress = ((Time.duration-to-ns(diff) * y-fill) / Time.duration-to-ns(limit)) i64-i32() center-x = screen&.tui&.width / 2 + 1 center-y = screen&.tui&.height / 2 + 1 screen Tui.set-screen-bg(Term.ColorDefault) x-off = x-fill i64-i32() - x-progress y-off = y-fill i64-i32() - y-progress h = (screen&.tui&.height u32-i32() - 1) w = (screen&.tui&.width u32-i32() - 1) for screen-y in 0 to (h) for screen-x in 0 to (w) if screen-x <= x-off or screen-x >= w - x-off or screen-y <= y-off or screen-y >= h - y-off screen Tui.put-char(' ', screen-x, screen-y) fn run-cmd(ed Ptr (Editor al), s Str, dim Dims, al al) <= Allocator al s = s drop-while(is-whitespace) cmd = s take-while(is-not-whitespace) rest = s drop-while(is-not-whitespace) drop-while(is-whitespace) if cmd Str.eq ('w') filename = rest take-while(fn c: not is-whitespace(c)) alfname = @undefined if filename null() case ed&.filename None return Just(fname) alfname <= fname else case ed&.filename None pass Just(oldfname) al Alloc.free({ ptr: oldfname cast(), count: 0 }) alfname <= filename StrBuilder.mk-const-str(al) ed&.current-file save-file(alfname) ed <&.filename= Just(alfname) elif cmd Str.eq ('q') ed <&.running= False elif cmd Str.eq ('o') filename = rest take-while(fn c: not is-whitespace(c)) alfname = filename StrBuilder.mk-const-str(al) og = ed&.current-file ed <&.current-file= load-file(alfname, al) case ed&.filename None pass Just(olefname) al Alloc.free({ ptr: olefname cast(), count: 0 }) ed <&.filename= Just(alfname) # reset mouse pointer ed <&.cursor= { x: 0, y: 0 } ed <&.goal-x= 0 Visual() ed <&.screen-top= 0 og free-file() elif cmd Str.eq ('theme') theme-name = rest take-while(is-not-whitespace) if theme-name Str.eq ('dark') ed <&.theme= dark-theme elif theme-name Str.eq ('light') ed <&.theme= light-theme else # TODO: message about not being able to find a theme pass else case cmd parse-int() Just(unline) newline = (unline - 1) i64-i32() Math.clamp(0, ed&.current-file.count size-i32() - 1) ed move-to-row(newline, dim) None return # NOTE: when we add unions, we can make it take in a list of functions # fn start-option-box () -> OptionBox # pass # fn end-option-box (box OptionBox) # pass # alternatively, when it's not stdout, force stdout and return the result to the original thing if not Term.is-terminal() panic('not running in a terminal!') args = Args.get() file = @undefined mfilename = None if args.count == 0 file <= List.mk(al) (&file) List.add(List.mk(al)) else file <= args[0] load-file(al) mfilename <= Just(args[0] StrBuilder.mk-const-str(al)) ed =& Editor { cursor: { x: 0, y: 0 } , running: True , goal-x: 0 Visual() , screen-top: 0 , screen-left: 0 , current-file: file , filename: mfilename , mode: Normal , anim: Just(Time.now()) , selected: None , theme: dark-theme , search-term: None , al: al , msg: None , clipboard: None , actions: { list: List.mk(al), cur: 0, changeset: None } , insert-start: None , delete-acc: None } tui =& Tui.mk() al = Alloc.idc() screen =& tui Tui.mk-screen(al) counter = 0 as U32 while ed&.running events = 0 for ev in Iter.from-function(fn: Tui.read-event(tui)) # like Helix, reset the message for new input. be sure to do it once per a stream of events. if events == 0 ed reset-msg() events <= events + 1 tui Tui.update-dimensions() case ev Tui.Key(k) case k Tui.Char(c) case &ed&.mode Ptr(Insert) ed cursor-row() List.insert(ed&.cursor.x i32-size(), Char.ascii-u8(c)) ed move-to-col(ed&.cursor.x + 1, ed screen-dims(tui)) Ptr(Cmd(sb)) StrBuilder.write-char(&sb, Char.ascii-char(c)) Ptr(SearchBox(sb)) StrBuilder.write-char(&sb, Char.ascii-char(c)) Ptr(mode) if mode == Normal or mode == Select code = c cast() Mem.u8-i32() if c == 'q' ed <&.running= False if c == 'u' ed undo(ed screen-dims(tui)) if c == 'U' ed redo(ed screen-dims(tui)) if c == 'k' ed move-to-row(ed&.cursor.y - 1, ed screen-dims(tui)) if c == 'j' ed move-to-row(ed&.cursor.y + 1, ed screen-dims(tui)) if c == 'h' ed move-left(ed screen-dims(tui)) if c == 'l' ed move-right(ed screen-dims(tui)) if c == 'i' ed enter-insert-mode() if c == 'g' pass if c == 'v' if mode == Select ed set-mode-normal() else ed <&.mode= Select ed set-selection(Just(ed&.cursor)) if c == 'd' ed delete-selected(ed screen-dims(tui)) ed set-mode-normal() if c == 'c' ed delete-selected(ed screen-dims(tui)) ed enter-insert-mode() if c == 'y' ed copy-selection-to-clipboard() ed set-mode-normal() if c == 'p' ed paste-clipboard-at-cursor(PasteAfter, ed screen-dims(tui)) if c == 'P' ed paste-clipboard-at-cursor(PasteBefore, ed screen-dims(tui)) if c == '>' ed indent-selection(ed screen-dims(tui)) if c == '<' ed dedent-selection(ed screen-dims(tui)) if c == 'I' ed move-to-start-of-line-indented(ed screen-dims(tui)) ed enter-insert-mode() if c == 'A' ed move-to-end-of-line(ed screen-dims(tui)) ed enter-insert-mode() if c == 'o' ed insert-empty-row-at(ed&.cursor.y + 1, al) currow = ed get-row(ed&.cursor.y + 1) ed move-to(currow List.size() size-i32(), ed&.cursor.y + 1, ed screen-dims(tui)) ed enter-insert-mode() if c == 'O' ed insert-empty-row-at(ed&.cursor.y, al) ed move-to-end-of-line(ed screen-dims(tui)) ed enter-insert-mode() if c == ':' ed <&.mode= StrBuilder.mk(al) Cmd() if c == '/' ed <&.mode= StrBuilder.mk(al) SearchBox() if c == 'n' # TODO: if SELECT, multiple cursors. case ed&.search-term None pass Just(st) case ed find-next-occurence(StrBuilder.as-char-slice(&st)) None pass Just(cur) ed move-to(cur.x + StrBuilder.count(&st) size-i32() - 1, cur.y, ed screen-dims(tui)) # currently, no multiple cursors, so just jump. ed <&.selected= Just(cur) if c == 'N' case ed&.search-term None pass Just(st) case ed find-prev-occurence(StrBuilder.as-char-slice(&st)) None pass Just(cur) ed move-to(cur.x + StrBuilder.count(&st) size-i32() - 1, cur.y, ed screen-dims(tui)) ed <&.selected= Just(cur) # big selector! if c == 'x' # we should also support helix's multiselection. # the way helix does it is it checks if the selection is at the beginning and cursor at the end. # if so, we move to next row. # select start of line and move to front. if ed&.selected Misc.maybe(fn s: s.x, ed&.cursor.x) == 0 and ed cursor-is-at-line-end() dims = ed screen-dims(tui) sel = ed&.selected or-else(ed&.cursor) # it's gonna get unselected, so make sure to back up that selection. ed move-to-row(ed&.cursor.y + 1, dims) ed move-to-end-of-line(dims) ed <&.selected= Just(sel) else # expand selection and move cursor to end. ed move-to-end-of-line(ed screen-dims(tui)) ed <&.selected= Just({ x: 0, y: ed&.selected Misc.maybe(fn s: s.x, ed&.cursor.y) }) # word jumping!~ # TODO: lots of duplication, but how would we generalize this behavior? if c == 'w' # fwd to word start dims = ed screen-dims(tui) first-pos = ed&.cursor ed move-right(dims) if ed char-at-cursor() fmap-maybe(char-type) /= ed char-at(first-pos) fmap-maybe(char-type) first-pos <= ed&.cursor while ed cursor-is-at-line-end() if not ed move-right(dims) break first-pos <= ed&.cursor cur-char-type = ed char-at-cursor() Misc.fmap-maybe(char-type) while ed char-at-cursor() Misc.maybe(fn c: c char-type() Just() == cur-char-type, False) if not ed move-right(dims) break while ed char-at-cursor() Misc.maybe(fn c: c char-type() == CharSpace, False) if not ed move-right(dims) break if first-pos /= ed&.cursor ed move-left(dims) # matching helix, stop BEFORE word (useful for removing whole words) ed set-selection(Just(first-pos)) if c == 'e' # fwd to word end dims = ed screen-dims(tui) first-pos = ed&.cursor ed move-right(dims) if ed char-at-cursor() fmap-maybe(char-type) /= ed char-at(first-pos) fmap-maybe(char-type) first-pos <= ed&.cursor while ed cursor-is-at-line-end() if not ed move-right(dims) break first-pos <= ed&.cursor while ed char-at-cursor() Misc.maybe(fn c: c char-type() == CharSpace, False) if not ed move-right(dims) break cur-char-type = ed char-at-cursor() Misc.fmap-maybe(char-type) while ed char-at-cursor() Misc.maybe(fn c: c char-type() Just() == cur-char-type, False) if not ed move-right(dims) break if first-pos /= ed&.cursor ed move-left(dims) # matching helix, stop BEFORE word (useful for removing whole words) ed set-selection(Just(first-pos)) if c == 'b' # bwd to word start dims = ed screen-dims(tui) first-pos = ed&.cursor ed move-left(dims) if ed char-at-cursor() fmap-maybe(char-type) /= ed char-at(first-pos) fmap-maybe(char-type) first-pos <= ed&.cursor while ed cursor-is-at-line-end() if not ed move-left(dims) break first-pos <= ed&.cursor while ed char-at-cursor() Misc.maybe(fn c: c char-type() == CharSpace, False) if not ed move-left(dims) break cur-char-type = ed char-at-cursor() Misc.fmap-maybe(char-type) while ed char-at-cursor() Misc.maybe(fn c: c char-type() Just() == cur-char-type, False) if not ed move-left(dims) break if first-pos /= ed&.cursor if ed char-at-cursor() Misc.maybe(fn c: c char-type() Just() /= cur-char-type, True) ed move-right(dims) ed set-selection(Just(first-pos)) else panic('new mode? \(mode)') Tui.Ctrl(c) # the helix version keeps the cursor in place, but still offsets screen at top/bottom borders. # I emulate it by setting screen-top to the desired value first then moving. if c == 'u' ed <&.screen-top= Math.max(0, ed&.screen-top - 15) ed move-to-row(ed&.cursor.y - 15, ed screen-dims(tui)) if c == 'd' ed <&.screen-top= Math.max(0, ed&.screen-top + 15) ed move-to-row(ed&.cursor.y + 15, ed screen-dims(tui)) if c == 'c' ed toggle-comment-on-selection(ed screen-dims(tui)) Tui.Escape case ed&.mode Normal # ed <&.running= False pass Insert ed set-mode-normal() Cmd(sb) StrBuilder.free(&sb) ed set-mode-normal() SearchBox(sb) StrBuilder.free(&sb) ed set-mode-normal() Select ed set-mode-normal() Tui.Backspace case &ed&.mode Ptr(Normal) pass Ptr(Select) pass Ptr(Insert) # SMELL: whole block. just weird. if not (ed&.cursor.x == 0 and ed&.cursor.y == 0) dims = ed screen-dims(tui) if ed&.cursor.x == 0 before-merge-cursor = ed&.cursor ed set-mode-normal() ed move-left(dims) sb =& StrBuilder.mk(ed&.al) sb StrBuilder.write-char('\n') ed do-action({ prim: PrimDelete(ed&.cursor, sb StrBuilder.as-str()), selected: False, before-pos: None }, dims) ed enter-insert-mode() else ed move-left(dims) case ed copy-selection() None pass Just(text) del-acc-count = (case ed&.delete-acc) Just(acc): StrBuilder.count(&acc) size-i32() _: 0 case ed&.insert-start None pass Just(start) if ed&.cursor.x < start.x - del-acc-count and ed&.cursor.y == start.y if not ed&.delete-acc is-just() ed <&.delete-acc= Just(StrBuilder.mk(ed&.al)) case &ed&.delete-acc Ptr(Just(acc)) (&acc) StrBuilder.write-char(text.contents[0] Char.from-u8()) _ pass ed apply-primitive(PrimDelete(ed&.cursor, text), dims) ed&.al Alloc.free(text.contents) Ptr(Cmd(cmd)) if StrBuilder.count(&cmd) > 0 StrBuilder.pop(&cmd) Ptr(SearchBox(st)) if StrBuilder.count(&st) > 0 StrBuilder.pop(&st) Tui.Enter case ed&.mode Normal pass Select pass Insert cx = ed&.cursor.x # insert empty line below nurow =& List.mk(al) rowws = ed indent-on-row(ed&.cursor.y) offchars = rowws count() size-i32() nurow List.add-all(rowws map(Char.char-u8)) cur-row = ed cursor-row() nurow List.add-all(cur-row& List.to-slice() Slice.subslice(cx i32-size(), cur-row&.count)) cur-row List.trim(cx i32-size()) (&ed&.current-file) List.insert(ed&.cursor.y i32-size() + 1, nurow&) ed move-to(offchars, ed&.cursor.y + 1, ed screen-dims(tui)) Cmd(sb) s = StrBuilder.as-str(&sb) ed run-cmd(s, ed screen-dims(tui), al) StrBuilder.free(&sb) ed set-mode-normal() SearchBox(sb) case ed&.search-term Just(prev-st) StrBuilder.free(&prev-st) None pass # don't actually set the search box if we didn't input anything. if StrBuilder.count(&sb) > 0 ed <&.search-term= Just(sb) case ed find-next-occurence(StrBuilder.as-char-slice(&sb)) None pass Just(cur) ed move-to(cur.x + StrBuilder.count(&sb) size-i32() - 1, cur.y, ed screen-dims(tui)) ed <&.selected= Just(cur) else ed <&.search-term= None ed set-mode-normal() _ pass Tui.Mouse(mouse) case mouse.button Tui.MouseLeft if mouse.pressed linecnt = ed&.current-file.count size-i32() cy = (ed&.screen-top + mouse.y) Math.clamp(0, linecnt - 1) vx = (mouse.x - ed left-offset().total-offset) Visual() cx = ed visual-x-to-x(vx, cy) ed <&.cursor.x= cx ed <&.goal-x= vx ed move-to-row(cy, ed screen-dims(tui)) _ pass _ unreachable() screen Tui.resize-screen-if-needed() # fyi: when just sleeping and not rendering without events, we reduce CPU usage to 1% compared to 5-8% on idle with continuous rendering. # add it in the future. if tui Tui.should-redraw() or ed&.anim is-just() screen Tui.clear-screen() screen Tui.set-screen-fg(ed&.theme&.text) screen Tui.set-screen-bg(ed&.theme&.bg) # screen Tui.set-screen-fg(Term.Color16(Term.Black16)) # screen Tui.set-screen-bg(Term.Color16(Term.White16)) screen Tui.fill-default() screen render-editor(ed) debug = True if debug screen Tui.draw-str-right('mode: \(ed&.mode)', 0, 0) screen Tui.draw-str-right('fps: \(tui&.actual-fps)', 0, 1) charcode = List.try-get(&ed&.current-file, ed&.cursor.y i32-size()) and-maybe(fn l: List.try-get(&l, ed&.cursor.x i32-size())) fmap-maybe(fn(b): (Char.from-u8(b), b)) screen Tui.draw-str-right('CURRENT CHAR CODE: \(charcode)', 0, 2) screen Tui.draw-str-right('pos: \(ed&.cursor.x),\(ed&.cursor.y)', 0, 3) rowcount = ed get-row(ed&.cursor.y)&.count screen Tui.draw-str-right('row char count: \(rowcount)', 0, 4) gx = ed&.goal-x from-visual() screen Tui.draw-str-right('goal x: \(gx)', 0, 5) dim = ed screen-dims(tui) screen Tui.draw-str-right('sl: \(ed&.screen-left) | \(dim.cols)', 0, 6) # screen Tui.draw-str-right('anim: \(ed&.anim)', 0, 6) screen Tui.draw-str-right('search term: \(ed&.search-term)', 0, 7) screen Tui.draw-str-right('msg: \(ed&.msg)', 0, 8) cpsize = ed&.clipboard Misc.fmap-maybe(fn s: s.contents.count) screen Tui.draw-str-right('clipboard: \(cpsize)', 0, 9) screen Tui.draw-str-right('actions: \(ed&.actions.cur)/\(ed&.actions.list.count)', 0, 10) update-animation(&ed&.anim, screen) screen Tui.render-screen() Tui.sync(tui) screen Tui.free-screen() tui Tui.deinit()