# a multiline utf8 text buffer import strutils import unicode import sequtils type TextBuffer* = ref object content: seq[seq[Rune]] cursorX: int # rune index cursorY: int proc newTextBuffer*: TextBuffer = new(result) result.content = @[] result.content.add(@[]) result.cursorX = 0 result.cursorY = 0 proc getLine*(buf: TextBuffer): seq[Rune] = buf.content[buf.cursorY] proc getContent*(buf: TextBuffer): string = buf.content.join("\n") proc cline(buf: TextBuffer): var seq[Rune] = buf.content[buf.cursorY] proc insertRune*(buf: TextBuffer, rune: Rune) = buf.cline().insert(rune, buf.cursorX) buf.cursorX += 1 proc isEmpty*(buf: TextBuffer): bool = buf.content.len() == 1 and buf.content[0].len() == 0 proc delete*(buf: TextBuffer) = # emulates a delete key if buf.cursorX == 0 and buf.cline().len() > 0: buf.cline() = buf.cline[1..buf.cline.high()] elif buf.cursorX > buf.cline.high(): if buf.cursorY < buf.content.high(): # merging lines let left = buf.cline() buf.content.delete(buf.cursorY) buf.cline() = left & buf.cline() else: buf.cline() = buf.cline[0..buf.cursorX-1] & buf.cline[buf.cursorX+1..buf.cline().high()] proc backspace*(buf: TextBuffer) = # emulates a backspace key if buf.cursorX == 0: if buf.cursorY > 0: # merging lines let right = buf.cline() buf.content.delete(buf.cursorY) dec buf.cursorY buf.cursorX = buf.cline().len() buf.cline() &= right elif buf.cursorX > buf.cline().high(): buf.cline() = buf.cline[0..buf.cline().high()-1] dec buf.cursorX else: buf.cline() = buf.cline[0..buf.cursorX-2] & buf.cline[buf.cursorX..buf.cline().high()] dec buf.cursorX proc moveCursor*(buf: TextBuffer, dx, dy: int) = if dx != 0: buf.cursorX += dx if buf.cursorX < 0: buf.cursorX = 0 # TODO switching over lines elif buf.cursorX > buf.cline().len(): buf.cursorX = buf.cline().len() # TODO switching over lines if dy != 0: buf.cursorY += dy if buf.cursorY < 0: buf.cursorY = 0 elif buf.cursorY > buf.content.high(): buf.cursorY = buf.content.high() if buf.cursorX > buf.cline().len(): buf.cursorX = buf.cline().len() proc getCursorPos*(buf: TextBuffer): (int, int) = (buf.cursorX, buf.cursorY) proc isCursorAtEnd*(buf: TextBuffer): bool = return buf.cursorY == buf.content.high() and buf.cursorX > buf.cline().high() proc moveToEnd*(buf: TextBuffer, forward: bool) = # home/end keys if forward: buf.cursorX = buf.cline().len() else: buf.cursorX = 0 proc enter*(buf: TextBuffer) = # emulates an enter in text editors let pre = buf.cline[0..buf.cursorX-1] let post = buf.cline[buf.cursorX..^1] buf.cline() = pre inc buf.cursorY buf.content.insert(post, buf.cursorY) buf.cursorX = 0 proc newLine*(buf: TextBuffer) = # inserts a new empty line below the current one # and moves cursor to it inc buf.cursorY let empty: seq[Rune] = @[] buf.content.insert(empty, buf.cursorY) buf.cursorX = 0 proc getLineCount*(buf: TextBuffer): int = buf.content.len() proc lines*(buf: TextBuffer): seq[seq[Rune]] = buf.content proc isLastLine*(buf: TextBuffer): bool = buf.cursorY == buf.content.high() proc isFistLine*(buf: TextBuffer): bool = buf.cursorY == 0 proc isLineEmpty*(buf: TextBuffer): bool = buf.cline().len() == 0 proc stripFinalNewline*(buf: TextBuffer) = # nothing if only 1 line if buf.content.len() <= 1: return # nothing if last line not empty if buf.content[buf.content.high()].len() > 0: return # adjust cursor if buf.cursorY == buf.content.high(): dec buf.cursorY buf.cursorX = buf.cline().len() # pop final element buf.content.del(buf.content.high())