use Tui Screen use Pos Pos use TextBuf TextBuf use CharType char-type is-word-boundary use Theme Theme use Config Config Mode Normal Insert Select inst Eq Mode eq (l _, r _): (case (l, r)) (Normal, Normal): True (Insert, Insert): True (Select, Select): True _: False inst Str Mode chars (self _): (case self) chars() Normal: 'Normal' Insert: 'Insert' Select: 'Select' _: todo() fn short-mode-name (mode Mode): (case mode) Normal: 'NOR' Insert: 'INS' Select: 'SEL' _: todo() # all of them are `si` (screen index shit) ScreenCursorOffset screen-top I32 screen-left I32 Pane al buf Ptr (TextBuf al) cursor Pos vi I32 sel Maybe Pos mode Mode sc-off ScreenCursorOffset fn mk (al al, buf Ptr (TextBuf al)) -> Pane al: Pane { buf , cursor: Pos.mk(0, 0) , vi: 0 , sel: None , mode: Normal , sc-off: { screen-top: 0, screen-left: 0 } } # line processing # bi = byte index # vi = visual index (uses wcwidth) # si = screen index (coordinates for Tui, ignores wcwidth) (to convert to it, just convert to vi. then just iterate over chars.) # later make it configurable. fn char-replacement (c Char) -> Maybe StrView if c == '\t' return ' ' Just() else return None fn char-screen-width (c Char) -> I32: char-replacement(c) Misc.maybe(fn s: s chars() count() Mem.size-i32(), Tui.rendered-wcwidth(c)) fn vi-bi (self Ptr (TextBuf al), ln I32, vx I32) -> I32 bi = 0 as I32 vi = 0 for c in self TextBuf.line(ln) chars() vi <= vi + c char-screen-width() if vx < vi break bi <= bi + c.num-bytes Mem.size-i32() return bi # basically bi-vi, put we also need the line, so it's the same as pos. fn pos-vi (self Ptr (TextBuf al), pos Pos) -> I32 bi = 0 as I32 vi = 0 for c in self TextBuf.line(pos.line) chars() bi <= bi + c.num-bytes Mem.size-i32() if pos.bi < bi break vi <= vi + c char-screen-width() return vi fn char-at(pane Ptr (Pane al), pos Pos) line = pane&.buf TextBuf.line(pos.line) return line StrView.byte-substr(pos.bi Mem.i32-size(), line StrView.num-bytes()) chars() head() or-else('\n') fn is-in-selection (pane Ptr (Pane al), pos Pos): (case pane&.sel) Just(sel): pos Math.between(pane&.cursor, sel) _: False # maybe this should not be here? # note: also when i make a proper rendered version, i'll be able to pass negative numbers to display even more text? ScreenDims from-sx I32 to-sx I32 # ie. last possible index from-sy I32 to-sy I32 # ie. last possible index # screen size for the total pane, including pane's margin. different from total pane area fn screen-width (sd ScreenDims): sd.to-sx - sd.from-sx + 1 fn screen-height (sd ScreenDims): sd.to-sy - sd.from-sy + 1 fn update-screen-offset (pane Ptr (Pane al), sd ScreenDims) cur = pane&.cursor cur-sx = pane&.buf pos-vi(cur) sc-off = pane&.sc-off # not really width/height, sort of "offset so that the cursor arrives at right/lower-most position" sw = sd screen-width() - 1 sh = sd screen-height() - 1 margin = 5 # default margin. # idea. if screen is really small, reduce margin to 1 # if still smaller (3 cells only :]), just center the cursor. # but it's so smol it's pointless. helix does not care. csi = pane&.buf pos-vi(cur) # this takes into account screen tab len n shii csx = csi - sc-off.screen-left csy = cur.line - sc-off.screen-top nu-screen-left = sc-off.screen-left if csx < margin nu-screen-left <= csi - margin elif csx > sw - margin nu-screen-left <= csi - (sw - margin) nu-screen-left = Math.max(nu-screen-left, 0) nu-screen-top = sc-off.screen-top if csy < margin nu-screen-top <= cur.line - margin elif csy > sh - margin nu-screen-top <= cur.line - (sh - margin) nu-screen-top = Math.max(nu-screen-top, 0) pane <&.sc-off= { screen-top: nu-screen-top, screen-left: nu-screen-left } fn set-colors (screen Ptr (Screen Allocator), colors Theme.Colors) screen Tui.set-screen-colors(colors.fg, colors.bg) fn render (screen Ptr (Screen al), pane Ptr (Pane al), sd ScreenDims, cfg Ptr Config) <= Allocator al theme = cfg&.theme display-line-numbers = cfg&.display-line-numbers relative-line-numbers = cfg&.relative-line-numbers max-line-num-chars = pane&.buf TextBuf.num-lines() chars() count() Mem.size-i32() num-front-margin = 2 num-back-margin = 2 total-margin = if display-line-numbers: max-line-num-chars + num-front-margin + num-back-margin else 0 # moves the window "in position" of the cursor. # must operate on total text screen area (excluding margin) pane update-screen-offset(sd { from-sx: sd.from-sx + total-margin }) sc-off = pane&.sc-off for line-num in (sc-off.screen-top + 1) to (pane&.buf TextBuf.num-lines() Math.min(sc-off.screen-top + sd screen-height())) ybi = line-num - 1 xbi = pane&.buf vi-bi(ybi, sc-off.screen-left) line-content = pane&.buf TextBuf.line(ybi) ys = sd.from-sy + line-num - sc-off.screen-top - 1 # first draw line number if display-line-numbers if pane&.cursor.line == ybi # if cursor is on this line, do special highlight screen set-colors(theme&.line-num-hl) else screen set-colors(theme&.line-num) line-display = if not relative-line-numbers or pane&.cursor.line == ybi: line-num else Math.abs(pane&.cursor.line - ybi) screen Tui.draw-str-right(line-display, screen&.tui&.width Mem.u32-i32() - sd.from-sx - total-margin + num-back-margin, ys) # trust the science # reset em! screen set-colors(theme&.default) vx = pane&.buf pos-vi(Pos.mk(ybi, xbi)) - sc-off.screen-left assert(vx <= 0, 'vx must be <= 0, but got \(vx)') left-offset = sd.from-sx + total-margin for c in line-content StrView.byte-substr-from(xbi Mem.i32-size()) chars() append(' ') if vx > sd.to-sx break curpos = Pos.mk(ybi, xbi) is-cursor = curpos == pane&.cursor in-selection = pane is-in-selection(Pos.mk(ybi, xbi)) if is-cursor screen set-colors(theme&.cursor) elif in-selection screen set-colors(theme&.selection) else # check highlight pane&.buf TextBuf.highlight-at(curpos) if-just(fn(type)) screen set-colors(theme Theme.find-colors-for-highlight(type)) case c char-replacement() None if not (vx < 0 or vx + c char-screen-width() - 1 > sd.to-sx) screen Tui.put-char(c, left-offset + vx, ys) # ah fuck, we are modifying the underlying buffer. vx <= vx + c char-screen-width() Just(s) for c in s chars() if vx >= 0 screen Tui.put-char(c, left-offset + vx, ys) vx <= vx + c char-screen-width() screen set-colors(theme&.default) xbi <= xbi + c.num-bytes Mem.size-i32() # overlay screen set-colors(theme&.overlay) modename = pane&.mode short-mode-name() screen Tui.draw-str(' \(modename) ', sd.from-sx, sd.to-sy) # idea: make mode appear briefly in the middle/lower-middle of the screen. more dynamic! fn set-sel (self Ptr (Pane al), sel Maybe Pos) if self&.mode /= Select self <&.sel= sel fn set-mode (self Ptr (Pane al), mode Mode) <= Allocator al if mode == Normal self&.buf TextBuf.flush-insert-action() self <&.mode= mode CursorMovement NoChanges UpdateVI OverrideSelect # includes update vi inst Eq CursorMovement eq (l _, r _): (case (l, r)) (NoChanges, NoChanges): True (UpdateVI, UpdateVI): True (OverrideSelect, OverrideSelect): True _: False fn set-cursors (self Ptr (Pane al), cur Pos, sel Maybe Pos, cursor-movement-type CursorMovement) # todo: change flags to a readable enum. if cursor-movement-type /= NoChanges self <&.vi= self&.buf pos-vi(cur) self <&.cursor= cur if cursor-movement-type /= OverrideSelect self set-sel(sel) else self <&.sel= sel fn move-right (self Ptr (Pane al)) self set-cursors(TextBuf.right-pos(self&.buf, self&.cursor), None, UpdateVI) fn move-left (self Ptr (Pane al)) self set-cursors(TextBuf.left-pos(self&.buf, self&.cursor), None, UpdateVI) fn move-down (self Ptr (Pane al), amnt I32) cur = self&.cursor nu-line = Math.min(cur.line + amnt, self&.buf TextBuf.num-lines() - 1) vci = self&.vi bi = self&.buf vi-bi(nu-line, vci) self set-cursors({ line: nu-line, bi }, None, NoChanges) fn move-up (self Ptr (Pane al), amnt I32) cur = self&.cursor nu-line = Math.max(cur.line - amnt, 0) vci = self&.vi bi = self&.buf vi-bi(nu-line, vci) self set-cursors({ line: nu-line, bi }, None, NoChanges) fn is-at-line-end (pane Ptr (Pane al), pos Pos): pos.bi Mem.i32-size() == pane&.buf TextBuf.line(pos.line) StrView.num-bytes() fn cursor-is-at-line-end (pane Ptr (Pane al)): pane is-at-line-end(pane&.cursor) MoveDirection MoveFwd MoveBwd MoveTarget NextWordStart NextWordEnd fn reached-target (pane Ptr (Pane al), target MoveTarget, prev Pos): (case target) NextWordStart: is-word-boundary(pane char-at(prev), pane char-at(pane&.cursor)) and not pane char-at(pane&.cursor) char-type() == CharType.CharSpace NextWordEnd: is-word-boundary(pane char-at(prev), pane char-at(pane&.cursor)) and not pane char-at(prev) char-type() == CharType.CharSpace _: todo() # todo: maybe make it stateless. this will allow me to not accidentally end up setting stuff. fn advance-word (pane Ptr (Pane al), dir MoveDirection, target MoveTarget) <= Allocator al prev =& pane&.cursor Misc.own() fn advance() prev <&= pane&.cursor case dir MoveFwd pane move-right() MoveBwd pane move-left() # skip newlines sel = pane&.cursor was-at-line-end = pane cursor-is-at-line-end() advance() if was-at-line-end or pane reached-target(target, prev&) sel <= pane&.cursor while pane cursor-is-at-line-end() if pane&.cursor == prev& return # at the end of the file! advance() sel <= pane&.cursor # check first word boundary # if is-word-boundary(pane char-at(prev&), pane char-at(pane&.cursor)) and pane char-at(pane&.cursor) CharType.char-type() /= CharType.CharSpace # sel <= pane&.cursor while True advance() if pane cursor-is-at-line-end() or pane reached-target(target, prev&) case dir MoveFwd pane move-left() MoveBwd pane move-right() break # when moving backwards, we might get to the start of the file. this line breaks us out. if prev& == pane&.cursor break pane set-sel(Just(sel)) fn min-pos (pane Ptr (Pane al)): (case pane&.sel) Just(sel): Math.min(pane&.cursor, sel) _: pane&.cursor fn max-pos (pane Ptr (Pane al)): (case pane&.sel) Just(sel): Math.max(pane&.cursor, sel) _: pane&.cursor fn indent-at-line (pane Ptr (Pane al), line I32) -> I32: pane&.buf TextBuf.line(line) take-while(fn c: c is-whitespace()) map(fn c: c.num-bytes) sum() Mem.size-i32() # helix's 'x' cmd. # 1. if the ends are not already expanded, then expand current selection to ends of screen. # 2. if it's already expanded, select next line. fn expand(pane Ptr (Pane al)) min-pos = pane min-pos() max-pos = pane max-pos() max-pos-max = pane&.buf TextBuf.line(max-pos.line) StrView.num-bytes() Mem.size-i32() if not (min-pos.bi == 0 and max-pos.bi == max-pos-max) pane <&.sel= min-pos { bi: 0 } Just() pane <&.cursor= max-pos { bi: max-pos-max } else if max-pos.line + 1 >= pane&.buf TextBuf.num-lines() return pane <&.sel= min-pos { bi: 0 } Just() next-line = max-pos.line + 1 pane <&.cursor= { line: next-line, bi: pane&.buf TextBuf.line(next-line) StrView.num-bytes() Mem.size-i32() } fn move-to-beginning (self Ptr (Pane al)) self set-cursors(self&.cursor { bi: 0 }, None, OverrideSelect) fn move-to-beginning-indented (self Ptr (Pane al)) cur = self&.cursor indent = self indent-at-line(cur.line) self set-cursors(cur { bi: indent }, None, OverrideSelect) fn move-to-end (self Ptr (Pane al)) cur = self&.cursor self set-cursors(cur { bi: self&.buf TextBuf.line(cur.line) StrView.num-bytes() Mem.size-i32() }, None, OverrideSelect) fn line-begins-with-comment (self Ptr (Pane al), line I32) -> Maybe (Pos, Pos) indent = self indent-at-line(line) if self char-at(Pos.mk(line, indent)) == '#' i = indent + 1 while (fn c: c /= '\n' and c is-whitespace())(self char-at(Pos.mk(line, i))) i <= i + 1 return Just((Pos.mk(line, indent), Pos.mk(line, i))) else return None # modification fn add-str-at-char (self Ptr (Pane al), s StrView) <= Allocator al cur = self&.cursor self&.buf TextBuf.change(cur, cur, s, None) fn backspace (self Ptr (Pane Allocator)) prev-cur = self&.cursor self move-left() self&.buf TextBuf.change(self&.cursor, prev-cur, '', None) fn undo (self Ptr (Pane al)) <= Allocator al mcursors = self&.buf TextBuf.undo() mcursors if-just(fn(cursors)) self set-cursors(cursors.cur, cursors.sel, OverrideSelect) fn redo (self Ptr (Pane al)) <= Allocator al mcursors = self&.buf TextBuf.redo() mcursors if-just(fn(cursors)) self set-cursors(cursors.cur, cursors.sel, OverrideSelect) # selection in a textbuf format (with the later position being moved by one.) fn selection (self Ptr (Pane al)) -> (Pos, Pos) from-to = Math.minmax(self&.sel or-else(self&.cursor), self&.cursor) from = from-to fst() to = self&.buf TextBuf.right-pos(from-to snd()) return (from, to) fn replace-selection (self Ptr (Pane al), (prev-pos, next-pos) (Pos, Pos), cp StrView) <= Allocator al mcursors = self&.buf TextBuf.change(prev-pos, next-pos, cp, Just({ cur: self&.cursor, sel: self&.sel })) mcursors if-just(fn(cursors)) self set-cursors(cursors.cur, cursors.sel, OverrideSelect) # to = self&.buf TextBuf.pos-after-str(cp, pos's fst()) # if cp StrView.num-bytes() > 0 # to <= TextBuf.left-pos(self&.buf, to) # self set-cursors(to, pos's fst()) fn indent-at (self Ptr (Pane Allocator), line I32) self&.buf TextBuf.change({ line, bi: 0 }, { line, bi: 0 }, '\t', None) fn indent-selection (self Ptr (Pane al)) <= Allocator al self&.buf TextBuf.begin-changeset({ cur: self&.cursor, sel: self&.sel }) for line in self min-pos().line to (self max-pos().line) self indent-at(line) self&.buf TextBuf.end-changeset() self set-cursors(self&.cursor { bi: self&.cursor.bi + 1 }, self&.sel Misc.fmap-maybe(fn sel: sel { bi: sel.bi + 1 }), UpdateVI) fn dedent-selection (self Ptr (Pane al)) <= Allocator al self&.buf TextBuf.begin-changeset({ cur: self&.cursor, sel: self&.sel }) first-char-was-tab = self char-at(self min-pos() { bi: 0 }) == '\t' last-char-was-tab = self char-at(self max-pos() { bi: 0 }) == '\t' for line in self min-pos().line to (self max-pos().line) if self char-at({ line, bi: 0 }) == '\t' self&.buf TextBuf.change({ line, bi: 0 }, { line, bi: 1 }, '', None) self&.buf TextBuf.end-changeset() self set-cursors(self&.cursor { bi: if first-char-was-tab: self&.cursor.bi - 1 else self&.cursor.bi }, self&.sel Misc.fmap-maybe(fn sel: sel { bi: if last-char-was-tab: sel.bi - 1 else sel.bi }), UpdateVI) fn indent-at-current (self Ptr (Pane Allocator), indent I32) for _ in 1 to (indent) self indent-at(self&.cursor.line) self move-right() fn toggle-comment (self Ptr (Pane Allocator)) from-line = self min-pos().line to-line = self max-pos().line min-indent = from-line to (to-line) map(fn ln: self indent-at-line(ln)) minimum() or-fail('no lines what??') all-have-comment = from-line to (to-line) all(fn ln: self line-begins-with-comment(ln) is-just()) self&.buf TextBuf.begin-changeset({ cur: self&.cursor, sel: self&.sel }) for line in from-line to (to-line) comment-str = '#' if not all-have-comment self set-cursors(Pos.mk(line, min-indent), None, NoChanges) self add-str-at-char('# ') else comment-pos = self line-begins-with-comment(line) or-fail('(Pane.toggle-comment) impossiburu') self replace-selection(comment-pos, '') self&.buf TextBuf.end-changeset() ## search # note: currently does not search between the lines. fn search-from (pane Ptr (Pane al), pos Pos, query StrView) -> Maybe (Pos, Pos) fn search-on-line (pos Pos): pane&.buf TextBuf.line(pos.line) StrView.byte-substr-from(pos.bi Mem.i32-size()).contents Slice.find-slice(query.contents) case search-on-line(pos) Just(idx) sel-pos = pos { bi: pos.bi + idx Mem.size-i32() } cur-pos = pos { bi: pos.bi + (idx + query StrView.num-bytes()) Mem.size-i32() - 1 } return Just((sel-pos, cur-pos)) None num-lines = pane&.buf TextBuf.num-lines() for i in 1 to (num-lines) line = (pos.line + i) Math.mod(num-lines) case search-on-line({ line, bi: 0 }) Just(idx) sel-pos = { line, bi: idx Mem.size-i32() } cur-pos = { line, bi: (idx + query StrView.num-bytes()) Mem.size-i32() - 1 } return Just((sel-pos, cur-pos)) None pass return None fn search-back (pane Ptr (Pane al), pos Pos, query StrView) -> Maybe (Pos, Pos) case pane&.buf TextBuf.line(pos.line) StrView.byte-substr(0, pos.bi Mem.i32-size()).contents Slice.find-last-occurence-of-slice(query.contents) Just(idx) sel-pos = pos { bi: idx Mem.size-i32() } cur-pos = pos { bi: (idx + query StrView.num-bytes()) Mem.size-i32() - 1 } return Just((sel-pos, cur-pos)) None num-lines = pane&.buf TextBuf.num-lines() for i in 1 to (num-lines) line = (pos.line - i) Math.mod(num-lines) case pane&.buf TextBuf.line(line).contents Slice.find-last-occurence-of-slice(query.contents) Just(idx) sel-pos = { line, bi: idx Mem.size-i32() } cur-pos = { line, bi: (idx + query StrView.num-bytes()) Mem.size-i32() - 1 } return Just((sel-pos, cur-pos)) None pass return None