double enter, backspace and delete can merge lines
This commit is contained in:
parent
4323e5872c
commit
4b73239b55
30
editor.nim
30
editor.nim
|
@ -51,6 +51,13 @@ proc read*(ed: EditorState): (EditorResult, string) =
|
||||||
|
|
||||||
var scroll: Scroll = new(Scroll)
|
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:
|
while true:
|
||||||
render(ed.textBuffer, ed.termBuffer, ed.prompt, scroll)
|
render(ed.textBuffer, ed.termBuffer, ed.prompt, scroll)
|
||||||
let (getKeyResult, key) = getKey()
|
let (getKeyResult, key) = getKey()
|
||||||
|
@ -61,7 +68,7 @@ proc read*(ed: EditorState): (EditorResult, string) =
|
||||||
let control = key.JaleKeycode()
|
let control = key.JaleKeycode()
|
||||||
case control:
|
case control:
|
||||||
of jkEnter:
|
of jkEnter:
|
||||||
if ed.textBuffer.isCursorAtEnd():
|
if ed.textBuffer.isLineEmpty() and ed.textBuffer.isLastLine():
|
||||||
editorResult = erEnter
|
editorResult = erEnter
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
|
@ -82,20 +89,33 @@ proc read*(ed: EditorState): (EditorResult, string) =
|
||||||
of jkRight:
|
of jkRight:
|
||||||
ed.textBuffer.moveCursor(1, 0)
|
ed.textBuffer.moveCursor(1, 0)
|
||||||
of jkDown:
|
of jkDown:
|
||||||
ed.textBuffer.moveCursor(0, 1)
|
if ed.textBuffer.isLastLine():
|
||||||
|
moveInHistory(1)
|
||||||
|
else:
|
||||||
|
ed.textBuffer.moveCursor(0, 1)
|
||||||
of jkUp:
|
of jkUp:
|
||||||
ed.textBuffer.moveCursor(0, -1)
|
if ed.textBuffer.isFistLine():
|
||||||
|
moveInHistory(-1)
|
||||||
|
else:
|
||||||
|
ed.textBuffer.moveCursor(0, -1)
|
||||||
of jkEnd:
|
of jkEnd:
|
||||||
ed.textBuffer.moveToEnd(true)
|
ed.textBuffer.moveToEnd(true)
|
||||||
of jkHome:
|
of jkHome:
|
||||||
ed.textBuffer.moveToEnd(false)
|
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:
|
of jkCtrlDown:
|
||||||
ed.textBuffer.newLine()
|
ed.textBuffer.newLine()
|
||||||
else:
|
else:
|
||||||
discard # not implemented
|
discard # not implemented
|
||||||
|
|
||||||
# return val
|
# return val, strip final newline (due to how double enter is how you enter)
|
||||||
result = (editorResult, ed.textBuffer.getContent())
|
let cont = ed.textBuffer.getContent()
|
||||||
|
result = (editorResult, if cont.len() == 0: "" else: cont[0..^2])
|
||||||
|
|
||||||
# cleanup
|
# cleanup
|
||||||
stdout.write("\n")
|
stdout.write("\n")
|
||||||
|
|
78
renderer.nim
78
renderer.nim
|
@ -11,33 +11,24 @@ import terminalUtils/terminalGetInfo
|
||||||
type Scroll* = ref object
|
type Scroll* = ref object
|
||||||
x, y: int
|
x, y: int
|
||||||
|
|
||||||
|
proc reset*(scroll: Scroll) =
|
||||||
|
scroll.x = 0
|
||||||
|
scroll.y = 0
|
||||||
|
|
||||||
proc render*(textBuffer: TextBuffer, termBuffer: Buffer, prompt: string, scroll: Scroll) =
|
proc render*(textBuffer: TextBuffer, termBuffer: Buffer, prompt: string, scroll: Scroll) =
|
||||||
# TODO: vertical scroll
|
|
||||||
# we are free to "redraw" everything everytime
|
# we are free to "redraw" everything everytime
|
||||||
# since termBuffer double buffers
|
# since termBuffer double buffers
|
||||||
|
|
||||||
# resizing the buffer if needed
|
# GET INFO
|
||||||
let lineCount = textBuffer.getLineCount()
|
let (x, y) = textBuffer.getCursorPos()
|
||||||
|
let lines = textBuffer.lines()
|
||||||
|
let lineCount = lines.len()
|
||||||
let termWidth = termGetWidth()
|
let termWidth = termGetWidth()
|
||||||
let termHeight = termGetHeight()
|
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 promptLen = prompt.len()
|
||||||
let maxTextLen = termWidth - promptLen
|
let maxTextLen = termWidth - promptLen
|
||||||
if maxTextLen < 1:
|
|
||||||
termBuffer.redraw()
|
|
||||||
return # nothing will fit anyways
|
|
||||||
|
|
||||||
|
# SCROLL
|
||||||
const scrollOffset = 2
|
const scrollOffset = 2
|
||||||
# right scroll
|
# right scroll
|
||||||
if x - scroll.x >= maxTextLen - scrollOffset:
|
if x - scroll.x >= maxTextLen - scrollOffset:
|
||||||
|
@ -47,10 +38,55 @@ proc render*(textBuffer: TextBuffer, termBuffer: Buffer, prompt: string, scroll:
|
||||||
scroll.x = x
|
scroll.x = x
|
||||||
# left scroll on backspace - TODO IMPLEMENT
|
# 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]
|
let line = lines[i]
|
||||||
if i == 0:
|
if i == 0:
|
||||||
termBuffer.write(prompt)
|
termBuffer.write(prompt)
|
||||||
|
elif i == y:
|
||||||
|
termBuffer.write(">" & " ".repeat(promptLen - 1))
|
||||||
else:
|
else:
|
||||||
termBuffer.write(" ".repeat(promptLen))
|
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())
|
let toPos = min(scroll.x+maxTextLen-2, line.high())
|
||||||
if fromPos <= line.high():
|
if fromPos <= line.high():
|
||||||
termBuffer.write(line[fromPos..toPos])
|
termBuffer.write(line[fromPos..toPos])
|
||||||
termBuffer.newLine()
|
if i < toY:
|
||||||
|
termBuffer.newLine()
|
||||||
|
|
||||||
termBuffer.setCursorPos(x + promptLen - scroll.x, y - scroll.y)
|
termBuffer.setCursorPos(x + promptLen - scroll.x, y - scroll.y)
|
||||||
termBuffer.redraw()
|
termBuffer.redraw()
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,8 @@ type
|
||||||
width, height: int
|
width, height: int
|
||||||
# current style we're writing with
|
# current style we're writing with
|
||||||
cursorAttributes: seq[uint8]
|
cursorAttributes: seq[uint8]
|
||||||
|
# scrolling debt
|
||||||
|
toScroll: int
|
||||||
|
|
||||||
ForegroundColors* = enum
|
ForegroundColors* = enum
|
||||||
fcBlack = 30, fcRed, fcGreen, fcYellow, fcBlue,
|
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 it would go out of the screen, crash
|
||||||
if buf.bufferX + runes.len() >= buf.width:
|
if buf.bufferX + runes.len() >= buf.width:
|
||||||
raise newException(ValueError, "Text too long, would overflow the buffer.")
|
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 fromPos = buf.lineStart(buf.bufferY) + buf.bufferX
|
||||||
let toPos = fromPos + runes.high()
|
let toPos = fromPos + runes.high()
|
||||||
|
@ -106,11 +110,17 @@ proc write*(buf: Buffer, text: string) =
|
||||||
buf.write(runes)
|
buf.write(runes)
|
||||||
|
|
||||||
proc newLine*(buf: Buffer) =
|
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.bufferY += 1
|
||||||
buf.bufferX = 0
|
buf.bufferX = 0
|
||||||
|
|
||||||
proc redraw*(buf: Buffer, force: bool = false) =
|
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
|
# go over display and buffer to find any differences
|
||||||
for i in 0..buf.buffered.high():
|
for i in 0..buf.buffered.high():
|
||||||
|
@ -148,7 +158,12 @@ proc redraw*(buf: Buffer, force: bool = false) =
|
||||||
func getSize*(buf: Buffer): (int, int) =
|
func getSize*(buf: Buffer): (int, int) =
|
||||||
(buf.width, buf.height)
|
(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
|
# shrinking X
|
||||||
if newWidth < buf.width:
|
if newWidth < buf.width:
|
||||||
# dropping cells outside the screen
|
# dropping cells outside the screen
|
||||||
|
@ -182,9 +197,13 @@ proc resize*(buf: Buffer, newWidth, newHeight: int) =
|
||||||
func getPosition*(buf: Buffer): (int, int) =
|
func getPosition*(buf: Buffer): (int, int) =
|
||||||
(buf.positionX, buf.positionY)
|
(buf.positionX, buf.positionY)
|
||||||
|
|
||||||
proc setPosition*(buf: Buffer, x, y: int) =
|
proc scroll*(buf: Buffer, delta: int) =
|
||||||
buf.positionX = x
|
# scrolls terminal and moves the Y position
|
||||||
buf.positionY = y
|
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 =
|
proc newBuffer*(x, y, w, h: int, stdout: File): Buffer =
|
||||||
new(result)
|
new(result)
|
||||||
|
|
|
@ -3,43 +3,23 @@
|
||||||
|
|
||||||
import strformat
|
import strformat
|
||||||
import strutils
|
import strutils
|
||||||
import terminal
|
|
||||||
|
|
||||||
func escSetCursorPos*(x, y: int): string =
|
func escSetCursorPos*(x, y: int): string =
|
||||||
&"\e[{y+1};{x+1}H"
|
&"\e[{y+1};{x+1}H"
|
||||||
|
|
||||||
const escGetCursorPos* = "\e[6n"
|
const escGetCursorPos* = "\e[6n"
|
||||||
|
|
||||||
type InvalidResponseError* = object of CatchableError
|
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)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
func escAttributes*(attributes: seq[uint8]): string =
|
func escAttributes*(attributes: seq[uint8]): string =
|
||||||
let joined = attributes.join(";")
|
let joined = attributes.join(";")
|
||||||
&"\e[{joined}m"
|
&"\e[{joined}m"
|
||||||
|
|
||||||
func escEraseCharacters*(n: int): string =
|
func escEraseCharacters*(n: int): string =
|
||||||
&"\e[{n}X"
|
&"\e[{n}X"
|
||||||
|
|
||||||
|
func escScroll*(n: int): string =
|
||||||
|
if n == 0:
|
||||||
|
""
|
||||||
|
else:
|
||||||
|
&"\e[{n}S"
|
|
@ -7,21 +7,22 @@ import unicode
|
||||||
|
|
||||||
type
|
type
|
||||||
JaleKeycode* = enum
|
JaleKeycode* = enum
|
||||||
|
jkNull = 0,
|
||||||
jkCtrlA = 1, jkCtrlB, jkCtrlC, jkCtrlD, jkCtrlE,
|
jkCtrlA = 1, jkCtrlB, jkCtrlC, jkCtrlD, jkCtrlE,
|
||||||
jkCtrlF, jkCtrlG, # ctrl+h invalid
|
jkCtrlF, jkCtrlG,
|
||||||
jkBackspace = 8,
|
jkBackspace = 8, # ctrl+h = backspace
|
||||||
jkTab = 9,
|
jkTab = 9, # ctrl+i = tab
|
||||||
jkCtrlJ = 10, jkCtrlK, jkCtrlL,
|
jkCtrlJ = 10, jkCtrlK, jkCtrlL,
|
||||||
jkEnter = 13,
|
jkEnter = 13, # ctrl+m = enter
|
||||||
jkCtrlN = 14, jkCtrlO, jkCtrlP, jkCtrlQ,
|
jkCtrlN = 14, jkCtrlO, jkCtrlP, jkCtrlQ,
|
||||||
jkCtrlR, jkCtrlS, jkCtrlT, jkCtrlU, jkCtrlV,
|
jkCtrlR, jkCtrlS, jkCtrlT, jkCtrlU, jkCtrlV,
|
||||||
jkCtrlW, jkCtrlX, jkCtrlY, jkCtrlZ,
|
jkCtrlW, jkCtrlX, jkCtrlY, jkCtrlZ,
|
||||||
jkEscape = 27,
|
jkEscape = 27,
|
||||||
jkCtrlBackslash = 28,
|
jkCtrlBackslash = 28,
|
||||||
jkCtrlRightBracket = 29,
|
jkCtrlRightBracket = 29,
|
||||||
jkAltBackspace = 127,
|
# the rest are handled by escapeSeqs
|
||||||
|
jkAltBackspace,
|
||||||
jkLeft = 256, jkRight, jkUp, jkDown,
|
jkLeft, jkRight, jkUp, jkDown,
|
||||||
jkCtrlLeft, jkCtrlRight, jkCtrlUp, jkCtrlDown,
|
jkCtrlLeft, jkCtrlRight, jkCtrlUp, jkCtrlDown,
|
||||||
# other 4 move keys
|
# other 4 move keys
|
||||||
jkHome, jkEnd, jkPageUp, jkPageDown,
|
jkHome, jkEnd, jkPageUp, jkPageDown,
|
||||||
|
|
|
@ -4,7 +4,7 @@ import strutils
|
||||||
import escapeSequences
|
import escapeSequences
|
||||||
|
|
||||||
proc termGetCursorPos*(ouput: File): (int, int) =
|
proc termGetCursorPos*(ouput: File): (int, int) =
|
||||||
|
# TODO, must only call this at the start of the program
|
||||||
ouput.write(escGetCursorPos)
|
ouput.write(escGetCursorPos)
|
||||||
|
|
||||||
var response = ""
|
var response = ""
|
||||||
|
|
|
@ -38,7 +38,11 @@ proc delete*(buf: TextBuffer) =
|
||||||
if buf.cursorX == 0 and buf.cline().len() > 0:
|
if buf.cursorX == 0 and buf.cline().len() > 0:
|
||||||
buf.cline() = buf.cline[1..buf.cline.high()]
|
buf.cline() = buf.cline[1..buf.cline.high()]
|
||||||
elif buf.cursorX > 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:
|
else:
|
||||||
buf.cline() = buf.cline[0..buf.cursorX-1] & buf.cline[buf.cursorX+1..buf.cline().high()]
|
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) =
|
proc backspace*(buf: TextBuffer) =
|
||||||
# emulates a backspace key
|
# emulates a backspace key
|
||||||
if buf.cursorX == 0:
|
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():
|
elif buf.cursorX > buf.cline().high():
|
||||||
buf.cline() = buf.cline[0..buf.cline().high()-1]
|
buf.cline() = buf.cline[0..buf.cline().high()-1]
|
||||||
dec buf.cursorX
|
dec buf.cursorX
|
||||||
|
@ -106,4 +116,13 @@ proc getLineCount*(buf: TextBuffer): int =
|
||||||
buf.content.len()
|
buf.content.len()
|
||||||
|
|
||||||
proc lines*(buf: TextBuffer): seq[seq[Rune]] =
|
proc lines*(buf: TextBuffer): seq[seq[Rune]] =
|
||||||
buf.content
|
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
|
Loading…
Reference in New Issue