From 4b73239b55536244f0da61d7b477452e42371d66 Mon Sep 17 00:00:00 2001 From: prod2 Date: Fri, 30 Dec 2022 00:17:51 +0100 Subject: [PATCH] double enter, backspace and delete can merge lines --- editor.nim | 30 ++++++++++-- renderer.nim | 78 +++++++++++++++++++++++-------- terminalUtils/buffer.nim | 29 ++++++++++-- terminalUtils/escapeSequences.nim | 36 ++++---------- terminalUtils/keycodes.nim | 15 +++--- terminalUtils/terminalGetInfo.nim | 2 +- textBuffer.nim | 25 ++++++++-- 7 files changed, 146 insertions(+), 69 deletions(-) diff --git a/editor.nim b/editor.nim index 85ac8d3..2b3aa71 100644 --- a/editor.nim +++ b/editor.nim @@ -51,6 +51,13 @@ proc read*(ed: EditorState): (EditorResult, string) = var scroll: Scroll = new(Scroll) + template moveInHistory(delta: int) = + let newHistoryIndex = min(max(ed.historyIndex, 0), ed.history.high()) + if newHistoryIndex != ed.historyIndex: + ed.textBuffer = ed.history[newHistoryIndex] + scroll.reset() + ed.historyIndex = newHistoryIndex + while true: render(ed.textBuffer, ed.termBuffer, ed.prompt, scroll) let (getKeyResult, key) = getKey() @@ -61,7 +68,7 @@ proc read*(ed: EditorState): (EditorResult, string) = let control = key.JaleKeycode() case control: of jkEnter: - if ed.textBuffer.isCursorAtEnd(): + if ed.textBuffer.isLineEmpty() and ed.textBuffer.isLastLine(): editorResult = erEnter break else: @@ -82,20 +89,33 @@ proc read*(ed: EditorState): (EditorResult, string) = of jkRight: ed.textBuffer.moveCursor(1, 0) of jkDown: - ed.textBuffer.moveCursor(0, 1) + if ed.textBuffer.isLastLine(): + moveInHistory(1) + else: + ed.textBuffer.moveCursor(0, 1) of jkUp: - ed.textBuffer.moveCursor(0, -1) + if ed.textBuffer.isFistLine(): + moveInHistory(-1) + else: + ed.textBuffer.moveCursor(0, -1) of jkEnd: ed.textBuffer.moveToEnd(true) of jkHome: ed.textBuffer.moveToEnd(false) + of jkPageDown: + let termHeight = termGetHeight() + ed.textBuffer.moveCursor(0, termHeight div 2) + of jkPageUp: + let termHeight = termGetHeight() + ed.textBuffer.moveCursor(0, -(termHeight div 2)) of jkCtrlDown: ed.textBuffer.newLine() else: discard # not implemented - # return val - result = (editorResult, ed.textBuffer.getContent()) + # return val, strip final newline (due to how double enter is how you enter) + let cont = ed.textBuffer.getContent() + result = (editorResult, if cont.len() == 0: "" else: cont[0..^2]) # cleanup stdout.write("\n") diff --git a/renderer.nim b/renderer.nim index d657588..a21e16c 100644 --- a/renderer.nim +++ b/renderer.nim @@ -11,33 +11,24 @@ import terminalUtils/terminalGetInfo type Scroll* = ref object x, y: int +proc reset*(scroll: Scroll) = + scroll.x = 0 + scroll.y = 0 + proc render*(textBuffer: TextBuffer, termBuffer: Buffer, prompt: string, scroll: Scroll) = - # TODO: vertical scroll # we are free to "redraw" everything everytime # since termBuffer double buffers - # resizing the buffer if needed - let lineCount = textBuffer.getLineCount() + # GET INFO + let (x, y) = textBuffer.getCursorPos() + let lines = textBuffer.lines() + let lineCount = lines.len() let termWidth = termGetWidth() let termHeight = termGetHeight() - - let (bufWidth, bufHeight) = termBuffer.getSize() - - if bufWidth != termWidth or bufHeight != min(lineCount, termHeight): - termBuffer.resize(termWidth, min(lineCount, termHeight)) - - - termBuffer.clear() - - let (x, y) = textBuffer.getCursorPos() - - let lines = textBuffer.lines() let promptLen = prompt.len() let maxTextLen = termWidth - promptLen - if maxTextLen < 1: - termBuffer.redraw() - return # nothing will fit anyways + # SCROLL const scrollOffset = 2 # right scroll if x - scroll.x >= maxTextLen - scrollOffset: @@ -47,10 +38,55 @@ proc render*(textBuffer: TextBuffer, termBuffer: Buffer, prompt: string, scroll: scroll.x = x # left scroll on backspace - TODO IMPLEMENT - for i in 0..lines.high(): + # vertical scroll + # scroll down - when it would be out of reach + # mode 1: by moving the location up + # this one is based on content, not y position + # if this one scrolls too much + block: + # in a block so positionY can't be used below, since it can change! + let (_, positionY) = termBuffer.getPosition() + if positionY > 0 and positionY + lineCount > termHeight: + termBuffer.scroll(min(positionY + lineCount - termHeight, positionY)) + + # scroll down + # mode 2 - only if positionY is 0, so mode 1 is already done + # this one is based on the cursor + block: + let (_, positionY) = termBuffer.getPosition() + if positionY == 0 and y - scroll.y > termHeight - 1: + scroll.y = y - termHeight + 1 + + # scroll up + if y < scroll.y: + scroll.y = y + + # resizing the buffer if needed + block: + # in a block, so bufWidth and height can't be used below, since they get changed! + let (bufWidth, bufHeight) = termBuffer.getSize() + # prevent shrinking vertically to avoid uncleaned parts + let desiredBufHeight = min(max(lineCount, bufHeight), termHeight) + if bufWidth != termWidth or bufHeight != desiredBufHeight: + termBuffer.resize(termWidth, desiredBufHeight, termWidth, termHeight) + + termBuffer.clear() + + if maxTextLen < 1: + termBuffer.redraw() + return # nothing will fit anyways + + let fromY = scroll.y + # assumes that the MODE 1 scroll up has happened + # and if lineCount doesn't fit in termheight + # then it has already been scrolled to pos 0 + let toY = min(fromY+termHeight-1, lineCount-1) + for i in fromY..toY: let line = lines[i] if i == 0: termBuffer.write(prompt) + elif i == y: + termBuffer.write(">" & " ".repeat(promptLen - 1)) else: termBuffer.write(" ".repeat(promptLen)) @@ -59,7 +95,9 @@ proc render*(textBuffer: TextBuffer, termBuffer: Buffer, prompt: string, scroll: let toPos = min(scroll.x+maxTextLen-2, line.high()) if fromPos <= line.high(): termBuffer.write(line[fromPos..toPos]) - termBuffer.newLine() + if i < toY: + termBuffer.newLine() + termBuffer.setCursorPos(x + promptLen - scroll.x, y - scroll.y) termBuffer.redraw() diff --git a/terminalUtils/buffer.nim b/terminalUtils/buffer.nim index d6a236a..6d028ab 100644 --- a/terminalUtils/buffer.nim +++ b/terminalUtils/buffer.nim @@ -18,6 +18,8 @@ type width, height: int # current style we're writing with cursorAttributes: seq[uint8] + # scrolling debt + toScroll: int ForegroundColors* = enum fcBlack = 30, fcRed, fcGreen, fcYellow, fcBlue, @@ -91,6 +93,8 @@ proc write*(buf: Buffer, runes: seq[Rune]) = # if it would go out of the screen, crash if buf.bufferX + runes.len() >= buf.width: raise newException(ValueError, "Text too long, would overflow the buffer.") + if buf.bufferY >= buf.height: + raise newException(ValueError, "BufferY is outside of height.") let fromPos = buf.lineStart(buf.bufferY) + buf.bufferX let toPos = fromPos + runes.high() @@ -106,11 +110,17 @@ proc write*(buf: Buffer, text: string) = buf.write(runes) proc newLine*(buf: Buffer) = + if buf.bufferY >= buf.height - 1: + raise newException(ValueError, "Attempt to go to new line at the last line.") buf.bufferY += 1 buf.bufferX = 0 proc redraw*(buf: Buffer, force: bool = false) = - var toPrint = "" + var force = force + if buf.toScroll > 0: + force = true + var toPrint = escScroll(buf.toScroll) + buf.toScroll = 0 # go over display and buffer to find any differences for i in 0..buf.buffered.high(): @@ -148,7 +158,12 @@ proc redraw*(buf: Buffer, force: bool = false) = func getSize*(buf: Buffer): (int, int) = (buf.width, buf.height) -proc resize*(buf: Buffer, newWidth, newHeight: int) = +proc resize*(buf: Buffer, newWidth, newHeight: int, terminalWidth, terminalHeight: int) = + + # assert sizes + if newWidth + buf.positionX > terminalWidth or newHeight + buf.positionY > terminalHeight: + raise newException(ValueError, "Given height/width would go beyond terminal limits.") + # shrinking X if newWidth < buf.width: # dropping cells outside the screen @@ -182,9 +197,13 @@ proc resize*(buf: Buffer, newWidth, newHeight: int) = func getPosition*(buf: Buffer): (int, int) = (buf.positionX, buf.positionY) -proc setPosition*(buf: Buffer, x, y: int) = - buf.positionX = x - buf.positionY = y +proc scroll*(buf: Buffer, delta: int) = + # scrolls terminal and moves the Y position + if delta < 1: + raise newException(ValueError, "Only positive values are allowed for delta.") + + buf.toScroll += delta + buf.positionY -= delta proc newBuffer*(x, y, w, h: int, stdout: File): Buffer = new(result) diff --git a/terminalUtils/escapeSequences.nim b/terminalUtils/escapeSequences.nim index 2fa11d6..d01ad35 100644 --- a/terminalUtils/escapeSequences.nim +++ b/terminalUtils/escapeSequences.nim @@ -3,43 +3,23 @@ import strformat import strutils -import terminal func escSetCursorPos*(x, y: int): string = &"\e[{y+1};{x+1}H" const escGetCursorPos* = "\e[6n" -type InvalidResponseError* = object of CatchableError - -proc termGetCursorPos*(ouput: File): (int, int) = - - ouput.write(escGetCursorPos) - - var response = "" - if getch() != '\e': - raise newException(InvalidResponseError, "Unsupported terminal - can't get cursor position.") - if getch() != '[': - raise newException(InvalidResponseError, "Can't parse response in getCursorPos") - var newChar = getch() - while newChar != ';': - response &= newChar - newChar = getch() - let y = parseInt(response) - 1 - response = "" - newChar = getch() - while newChar != 'R': - response &= newChar - newChar = getch() - let x = parseInt(response) - 1 - - return (x, y) - - +type InvalidResponseError* = object of CatchableError func escAttributes*(attributes: seq[uint8]): string = let joined = attributes.join(";") &"\e[{joined}m" func escEraseCharacters*(n: int): string = - &"\e[{n}X" \ No newline at end of file + &"\e[{n}X" + +func escScroll*(n: int): string = + if n == 0: + "" + else: + &"\e[{n}S" \ No newline at end of file diff --git a/terminalUtils/keycodes.nim b/terminalUtils/keycodes.nim index 0b3d8b8..652c1ae 100644 --- a/terminalUtils/keycodes.nim +++ b/terminalUtils/keycodes.nim @@ -7,21 +7,22 @@ import unicode type JaleKeycode* = enum + jkNull = 0, jkCtrlA = 1, jkCtrlB, jkCtrlC, jkCtrlD, jkCtrlE, - jkCtrlF, jkCtrlG, # ctrl+h invalid - jkBackspace = 8, - jkTab = 9, + jkCtrlF, jkCtrlG, + jkBackspace = 8, # ctrl+h = backspace + jkTab = 9, # ctrl+i = tab jkCtrlJ = 10, jkCtrlK, jkCtrlL, - jkEnter = 13, + jkEnter = 13, # ctrl+m = enter jkCtrlN = 14, jkCtrlO, jkCtrlP, jkCtrlQ, jkCtrlR, jkCtrlS, jkCtrlT, jkCtrlU, jkCtrlV, jkCtrlW, jkCtrlX, jkCtrlY, jkCtrlZ, jkEscape = 27, jkCtrlBackslash = 28, jkCtrlRightBracket = 29, - jkAltBackspace = 127, - - jkLeft = 256, jkRight, jkUp, jkDown, + # the rest are handled by escapeSeqs + jkAltBackspace, + jkLeft, jkRight, jkUp, jkDown, jkCtrlLeft, jkCtrlRight, jkCtrlUp, jkCtrlDown, # other 4 move keys jkHome, jkEnd, jkPageUp, jkPageDown, diff --git a/terminalUtils/terminalGetInfo.nim b/terminalUtils/terminalGetInfo.nim index 886c838..6fa7838 100644 --- a/terminalUtils/terminalGetInfo.nim +++ b/terminalUtils/terminalGetInfo.nim @@ -4,7 +4,7 @@ import strutils import escapeSequences proc termGetCursorPos*(ouput: File): (int, int) = - + # TODO, must only call this at the start of the program ouput.write(escGetCursorPos) var response = "" diff --git a/textBuffer.nim b/textBuffer.nim index 75530b1..1f4b3ef 100644 --- a/textBuffer.nim +++ b/textBuffer.nim @@ -38,7 +38,11 @@ proc delete*(buf: TextBuffer) = if buf.cursorX == 0 and buf.cline().len() > 0: buf.cline() = buf.cline[1..buf.cline.high()] elif buf.cursorX > buf.cline.high(): - discard # merge two lines TODO + 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()] @@ -46,7 +50,13 @@ proc delete*(buf: TextBuffer) = proc backspace*(buf: TextBuffer) = # emulates a backspace key if buf.cursorX == 0: - discard # merge two lines TODO + 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 @@ -106,4 +116,13 @@ proc getLineCount*(buf: TextBuffer): int = buf.content.len() proc lines*(buf: TextBuffer): seq[seq[Rune]] = - buf.content \ No newline at end of file + 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 \ No newline at end of file