unicode support, half done multiline
This commit is contained in:
parent
3799102798
commit
d2001bb458
138
editor.nim
138
editor.nim
|
@ -1,32 +1,22 @@
|
|||
import unicode
|
||||
import strutils
|
||||
import strformat
|
||||
import posix
|
||||
import unicode
|
||||
|
||||
import terminalUtils/buffer
|
||||
import terminalUtils/terminalGetInfo
|
||||
import terminalUtils/keycodes
|
||||
|
||||
import textBuffer
|
||||
import renderer
|
||||
|
||||
type
|
||||
EditorState* = ref object
|
||||
# config
|
||||
prompt: string # prompt to display
|
||||
maxRowsGoal: int # if terminal resizes, how to compute max rows
|
||||
# positive: it will set max rows to maxRowsGoal, or terminal height, whichever is lower
|
||||
# negative: it will set it to terminal height - maxRowsGoal or terminal height, whichever is lower
|
||||
# 0: terminal height
|
||||
|
||||
# state reached during execution
|
||||
x: int # current cursor x position on screen
|
||||
bx: int # current x position in buffer (not same as x due to UTF-8)
|
||||
y: int # which row are we editing
|
||||
size: int # size of the row being edited in characters
|
||||
buffer: seq[string] # current text
|
||||
screenBuffer: Buffer
|
||||
history: seq[seq[string]] # past texts
|
||||
textBuffer: TextBuffer # current text
|
||||
termBuffer: Buffer
|
||||
history: seq[TextBuffer] # past texts
|
||||
historyIndex: int # where are we in history
|
||||
cols: int # number of columns in the terminal
|
||||
maxRows: int # max number of rows allowed
|
||||
|
||||
EditorResult* = enum
|
||||
erEnter, erCtrlC, erCtrlD, erError
|
||||
|
@ -38,11 +28,9 @@ var editors: seq[EditorState]
|
|||
onSignal(28):
|
||||
discard
|
||||
|
||||
proc newEditor*(prompt: string = "> ", multiline: bool = true): EditorState =
|
||||
proc newEditor*(prompt: string = "> "): EditorState =
|
||||
new(result)
|
||||
result.prompt = prompt
|
||||
result.maxRowsGoal = if multiline: 0 else: 1
|
||||
result.buffer.add("")
|
||||
editors.add(result)
|
||||
|
||||
proc destroyEditor*(oldEditor: EditorState) =
|
||||
|
@ -52,80 +40,62 @@ proc destroyEditor*(oldEditor: EditorState) =
|
|||
editors.del(i)
|
||||
return
|
||||
|
||||
proc render(ed: EditorState) =
|
||||
ed.screenBuffer.clearLine()
|
||||
ed.screenBuffer.write(ed.prompt)
|
||||
ed.screenBuffer.write(ed.buffer[ed.y])
|
||||
ed.screenBuffer.redraw()
|
||||
|
||||
template cline(ed: EditorState): var string =
|
||||
ed.buffer[ed.y]
|
||||
|
||||
proc read*(ed: EditorState): (EditorResult, string) =
|
||||
|
||||
var editorResult = erError
|
||||
let (_, cursorY) = termGetCursorPos(stdout)
|
||||
ed.screenBuffer = newBuffer(0, cursorY, termGetWidth(), termGetHeight() - cursorY, stdout)
|
||||
ed.termBuffer = newBuffer(0, cursorY, termGetWidth(), 1, stdout)
|
||||
ed.textBuffer = newTextBuffer()
|
||||
ed.history.add(ed.textBuffer)
|
||||
ed.historyIndex = ed.history.high()
|
||||
|
||||
ed.render()
|
||||
while true:
|
||||
let key = getKey()
|
||||
case key:
|
||||
of jkEnter.int:
|
||||
editorResult = erEnter
|
||||
break
|
||||
of jkBackspace.int:
|
||||
if ed.x == 0:
|
||||
discard # merge two lines TODO
|
||||
elif ed.x > ed.cline().high():
|
||||
# TODO UTF8
|
||||
ed.cline() = ed.cline[0..ed.cline().high()-1]
|
||||
dec ed.x
|
||||
dec ed.bx
|
||||
else:
|
||||
# TODO UTF8
|
||||
ed.cline() = ed.cline[0..ed.x-1] & ed.cline[ed.x+1..ed.cline().high()]
|
||||
dec ed.x
|
||||
dec ed.bx
|
||||
of jkDelete.int:
|
||||
if ed.x > ed.cline.high():
|
||||
discard # merge two lines TODO
|
||||
elif ed.x == 0:
|
||||
# TODO UTF8
|
||||
ed.cline() = ed.cline[1..ed.cline.high()]
|
||||
else:
|
||||
# TODO UTF8
|
||||
discard # TODO implement
|
||||
of 3: # ctrl+c
|
||||
editorResult = erCtrlC
|
||||
break
|
||||
of 4: # ctrl+d
|
||||
if ed.buffer.len() <= 1 and ed.cline().len() == 0:
|
||||
editorResult = erCtrlD
|
||||
break
|
||||
else:
|
||||
if key > 31 and key < 127:
|
||||
# ascii char
|
||||
let ch = key.char()
|
||||
if ed.x == 0:
|
||||
ed.cline() = $ch & ed.cline
|
||||
elif ed.x > ed.cline.high():
|
||||
ed.cline() = ed.cline & $ch
|
||||
render(ed.textBuffer, ed.termBuffer, ed.prompt)
|
||||
let (getKeyResult, key) = getKey()
|
||||
case getKeyResult:
|
||||
of gkChar:
|
||||
ed.textBuffer.insertRune(key.uint32().Rune())
|
||||
of gkControl:
|
||||
let control = key.JaleKeycode()
|
||||
case control:
|
||||
of jkEnter:
|
||||
if ed.textBuffer.isCursorAtEnd():
|
||||
editorResult = erEnter
|
||||
break
|
||||
else:
|
||||
ed.textBuffer.enter()
|
||||
of {jkBackspace, jkAltBackspace}:
|
||||
ed.textBuffer.backspace()
|
||||
of jkDelete:
|
||||
ed.textBuffer.delete()
|
||||
of jkCtrlC: # ctrl+c
|
||||
editorResult = erCtrlC
|
||||
break
|
||||
of jkCtrlD: # ctrl+d
|
||||
if ed.textBuffer.isEmpty():
|
||||
editorResult = erCtrlD
|
||||
break
|
||||
of jkLeft:
|
||||
ed.textBuffer.moveCursor(-1, 0)
|
||||
of jkRight:
|
||||
ed.textBuffer.moveCursor(1, 0)
|
||||
of jkDown:
|
||||
ed.textBuffer.moveCursor(0, 1)
|
||||
of jkUp:
|
||||
ed.textBuffer.moveCursor(0, -1)
|
||||
of jkEnd:
|
||||
ed.textBuffer.moveToEnd(true)
|
||||
of jkHome:
|
||||
ed.textBuffer.moveToEnd(false)
|
||||
of jkCtrlDown:
|
||||
ed.textBuffer.newLine()
|
||||
else:
|
||||
ed.cline() = ed.cline[0..ed.x-1] & $ch & ed.cline[ed.x..ed.cline().high()]
|
||||
inc ed.x
|
||||
inc ed.bx
|
||||
|
||||
ed.render()
|
||||
discard # not implemented
|
||||
|
||||
# return val
|
||||
result = (editorResult, ed.buffer.join("\n"))
|
||||
result = (editorResult, ed.textBuffer.getContent())
|
||||
|
||||
# cleanup
|
||||
stdout.write("\n")
|
||||
ed.history.add(ed.buffer)
|
||||
ed.buffer = @[""]
|
||||
ed.y = 0
|
||||
ed.x = 0
|
||||
ed.screenBuffer = nil
|
||||
ed.termBuffer = nil
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import editor
|
||||
|
||||
let e = newEditor(">>> ", false)
|
||||
let e = newEditor(">>> ")
|
||||
|
||||
while true:
|
||||
let (res, text) = e.read()
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
# takes a text buffer and a terminal buffer (terminalUtils/buffer)
|
||||
# when render() is called, rewrites the terminal buffer completely as a function of
|
||||
# of the text buffer
|
||||
|
||||
import textBuffer
|
||||
import terminalUtils/buffer
|
||||
import terminalUtils/terminalGetInfo
|
||||
|
||||
proc render*(textBuffer: TextBuffer, termBuffer: Buffer, prompt: string) =
|
||||
# we are free to "redraw" everything everytime
|
||||
# since termBuffer double buffers
|
||||
|
||||
# resizing the buffer if needed
|
||||
let lineCount = textBuffer.getLineCount()
|
||||
let termWidth = termGetWidth()
|
||||
let termHeight = termGetHeight() # TODO
|
||||
|
||||
let (bufWidth, bufHeight) = termBuffer.getSize()
|
||||
|
||||
|
||||
termBuffer.clearLine()
|
||||
termBuffer.write(prompt)
|
||||
termBuffer.write(textBuffer.getLine())
|
||||
let (x, y) = textBuffer.getCursorPos()
|
||||
termBuffer.setCursorPos(x + prompt.len(), y)
|
||||
termBuffer.redraw()
|
||||
|
|
@ -86,10 +86,7 @@ proc clearLine*(buf: Buffer) =
|
|||
buf.buffered[i] = Cell(text: Rune(0))
|
||||
buf.bufferX = 0
|
||||
|
||||
proc write*(buf: Buffer, text: string) =
|
||||
# convert text to characters
|
||||
let runes = text.toRunes()
|
||||
|
||||
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.")
|
||||
|
@ -102,6 +99,12 @@ proc write*(buf: Buffer, text: string) =
|
|||
|
||||
buf.bufferX += runes.len()
|
||||
|
||||
proc write*(buf: Buffer, text: string) =
|
||||
# convert text to characters
|
||||
let runes = text.toRunes()
|
||||
buf.write(runes)
|
||||
|
||||
|
||||
proc redraw*(buf: Buffer, force: bool = false) =
|
||||
var toPrint = ""
|
||||
|
||||
|
@ -142,8 +145,16 @@ func getSize*(buf: Buffer): (int, int) =
|
|||
(buf.width, buf.height)
|
||||
|
||||
proc resize*(buf: Buffer, newX, newY: int) =
|
||||
# TODO
|
||||
raise newException(Defect, "Not implemented")
|
||||
# shrinking X
|
||||
if newX < buf.width:
|
||||
# dropping cells outside the screen
|
||||
var i = 0
|
||||
|
||||
|
||||
|
||||
|
||||
buf.width = newX
|
||||
buf.height = newY
|
||||
|
||||
func getPosition*(buf: Buffer): (int, int) =
|
||||
(buf.positionX, buf.positionY)
|
||||
|
|
|
@ -3,49 +3,34 @@
|
|||
import tables
|
||||
import strutils
|
||||
import terminal
|
||||
import unicode
|
||||
|
||||
type
|
||||
JaleKeycode* = enum
|
||||
jkStart = 255 # jale keycodes exported start one above jkStart
|
||||
# arrow keys
|
||||
jkCtrlA = 1, jkCtrlB, jkCtrlC, jkCtrlD, jkCtrlE,
|
||||
jkCtrlF, jkCtrlG, # ctrl+h invalid
|
||||
jkBackspace = 8,
|
||||
jkTab = 9,
|
||||
jkCtrlJ = 10, jkCtrlK, jkCtrlL,
|
||||
jkEnter = 13,
|
||||
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,
|
||||
jkCtrlLeft, jkCtrlRight, jkCtrlUp, jkCtrlDown,
|
||||
# other 4 move keys
|
||||
jkHome, jkEnd, jkPageUp, jkPageDown,
|
||||
jkCtrlHome, jkCtrlEnd, jkCtrlPageUp, jkCtrlPageDown,
|
||||
# special keys
|
||||
jkDelete, jkBackspace, jkInsert, jkEnter,
|
||||
jkDelete, jkInsert,
|
||||
|
||||
jkFinish, # jale keycodes exported end one below jkFinish
|
||||
# non-exported jale keycodes come here:
|
||||
jkContinue # represents an unfinished escape sequence
|
||||
|
||||
var keysById*: Table[int, string]
|
||||
var keysByName*: Table[string, int]
|
||||
|
||||
block:
|
||||
# 1 - 26
|
||||
for i in countup(1, 26): # iterate through lowercase letters
|
||||
keysById[i] = "ctrl+" & $char(i + 96)
|
||||
|
||||
keysById[9] = "tab"
|
||||
# keysById[8] will never get triggered because it's an escape seq
|
||||
|
||||
keysById[28] = r"ctrl+\"
|
||||
keysById[29] = "ctrl+]"
|
||||
keysByName[r"ctrl+\"] = 28
|
||||
keysByName["ctrl+]"] = 29
|
||||
|
||||
for i in countup(1, 26):
|
||||
keysByName[keysById[i]] = i
|
||||
|
||||
# jale keycodes:
|
||||
for i in countup(int(jkStart) + 1, int(jkFinish) - 1):
|
||||
var name: string = ($JaleKeycode(i))
|
||||
name = name[2..name.high()].toLower()
|
||||
keysByName[name] = i
|
||||
keysById[i] = name
|
||||
|
||||
var escapeSeqs*: Table[int, JaleKeycode]
|
||||
|
||||
proc defEscSeq(keys: seq[int], id: JaleKeycode) =
|
||||
|
@ -58,6 +43,8 @@ proc defEscSeq(keys: seq[int], id: JaleKeycode) =
|
|||
escapeSeqs[result] = id
|
||||
|
||||
block:
|
||||
defEscSeq(@[127], jkAltBackspace)
|
||||
|
||||
when defined(windows):
|
||||
defEscSeq(@[224], jkContinue)
|
||||
|
||||
|
@ -83,8 +70,6 @@ block:
|
|||
defEscSeq(@[224, 117], jkCtrlEnd)
|
||||
|
||||
# special keys
|
||||
defEscSeq(@[8], jkBackspace)
|
||||
defEscSeq(@[13], jkEnter)
|
||||
defEscSeq(@[224, 82], jkInsert)
|
||||
defEscSeq(@[224, 83], jkDelete)
|
||||
else:
|
||||
|
@ -154,24 +139,88 @@ block:
|
|||
defEscSeq(@[27, 91, 50], jkContinue)
|
||||
defEscSeq(@[27, 91, 51, 126], jkDelete)
|
||||
defEscSeq(@[27, 91, 50, 126], jkInsert)
|
||||
defEscSeq(@[13], jkEnter)
|
||||
defEscSeq(@[127], jkBackspace)
|
||||
defEscSeq(@[8], jkBackspace)
|
||||
|
||||
type IsKeyUTF8Results = enum
|
||||
ikValid, ikUnfinished, ikOverlong, ikInvalid2,
|
||||
ikInvalid3, ikInvalid4, ikInvalidUnknown
|
||||
|
||||
proc getChar: int =
|
||||
getch().int
|
||||
func isContinuation(singleByte: uint8): bool =
|
||||
singleByte shr 6 == 0b10
|
||||
|
||||
proc getKey*: int =
|
||||
# TODO unicode handling
|
||||
func isValidUTF8(key: int): IsKeyUTF8Results =
|
||||
const L = sizeof(int)
|
||||
var bytes: array[L, uint8]
|
||||
var key = key
|
||||
var i = L - 1
|
||||
|
||||
# to not make any assumptions about how int is
|
||||
# stored in memory
|
||||
while key > 0:
|
||||
bytes[i] = (key mod 256).uint8
|
||||
key = key div 256
|
||||
dec i
|
||||
|
||||
# https://github.com/nim-lang/Nim/blob/version-1-6/lib/pure/unicode.nim#L166
|
||||
i = 0
|
||||
while i < L:
|
||||
if bytes[i] <= 127:
|
||||
inc i
|
||||
elif bytes[i] shr 5 == 0b110:
|
||||
# 2 long
|
||||
if bytes[i] < 0xc2: return ikOverlong # overlong
|
||||
elif i + 1 >= L: return ikUnfinished
|
||||
elif bytes[i+1].isContinuation: i += 2
|
||||
else: return ikInvalid2
|
||||
elif bytes[i] shr 4 == 0b1110:
|
||||
# 3 long
|
||||
if i + 2 >= L: return ikUnfinished
|
||||
elif bytes[i+1].isContinuation() and bytes[i+2].isContinuation():
|
||||
i += 3
|
||||
else: return ikInvalid3
|
||||
elif bytes[i] shr 3 == 0b11110:
|
||||
# 4 long
|
||||
if i+3 >= L: return ikUnfinished
|
||||
elif bytes[i+1].isContinuation() and
|
||||
bytes[i+2].isContinuation() and
|
||||
bytes[i+3].isContinuation():
|
||||
i += 4
|
||||
else: return ikInvalid4
|
||||
else:
|
||||
return ikInvalidUnknown
|
||||
return ikValid
|
||||
|
||||
type
|
||||
GetKeyResult* = enum
|
||||
gkControl, gkChar
|
||||
|
||||
proc getKey*: (GetKeyResult, int) =
|
||||
# TODO move away from int approach completely
|
||||
var key = 0
|
||||
var bytes: string
|
||||
while true:
|
||||
key *= 256
|
||||
key += getChar()
|
||||
let newChar = getch()
|
||||
key += newChar.int()
|
||||
bytes &= newChar
|
||||
if escapeSeqs.hasKey(key):
|
||||
if escapeSeqs[key] != jkContinue:
|
||||
key = escapeSeqs[key].int()
|
||||
break
|
||||
return (gkControl, key)
|
||||
elif key < 30 and key > 0:
|
||||
# JaleKeycode has values from 1 to 29
|
||||
# which are NOT in escapeSeqs to save typing
|
||||
return (gkControl, key)
|
||||
else:
|
||||
break
|
||||
return key
|
||||
let validity = isValidUTF8(key)
|
||||
case validity:
|
||||
of ikValid:
|
||||
# TODO: find a better alternative to runeAt
|
||||
return (gkChar, bytes.runeAt(0).int)
|
||||
of ikUnfinished:
|
||||
discard # continue looping
|
||||
else:
|
||||
# For now, in development builds, crash
|
||||
# later it might be wise to just ignore
|
||||
# bad bytes, might check what other line
|
||||
# editors do
|
||||
raise newException(ValueError, "Invalid UTF8 input, " & $validity)
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
# 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():
|
||||
discard # merge two lines TODO
|
||||
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:
|
||||
discard # merge two lines TODO
|
||||
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()
|
||||
|
||||
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
|
||||
buf.content.insert(@[], buf.cursorY)
|
||||
buf.cursorX = 0
|
||||
|
||||
proc getLineCount*(buf: TextBuffer): int =
|
||||
buf.content.len()
|
Loading…
Reference in New Issue