2024-02-19 17:02:19 +01:00

915 lines
24 KiB
VimL

"======================================================================
"
" readline.vim -
"
" Created by skywind on 2021/02/20
" Last Modified: 2021/11/30 00:04
"
"======================================================================
" vim: set ts=4 sw=4 tw=78 noet :
"----------------------------------------------------------------------
" readline class
"----------------------------------------------------------------------
let s:readline = {}
let s:readline.cursor = 0 " cursur position in character
let s:readline.code = [] " buffer character in int list
let s:readline.wide = [] " char display width
let s:readline.size = 0 " buffer size in character
let s:readline.text = '' " text buffer
let s:readline.dirty = 0 " dirty
let s:readline.select = -1 " visual selection start pos
let s:readline.history = [] " history text
let s:readline.index = 0 " history pointer, 0 for current
let s:readline.timer = -1 " cursor blink timer
"----------------------------------------------------------------------
" move pos
"----------------------------------------------------------------------
function! s:readline.move(pos) abort
let pos = a:pos
let pos = (pos < 0)? 0 : pos
let pos = (pos > self.size)? self.size : pos
let self.cursor = pos
let self.timer = -1
return pos
endfunc
"----------------------------------------------------------------------
" change position, mode: 0/start, 1/current, 2/eol
"----------------------------------------------------------------------
function! s:readline.seek(pos, mode) abort
if a:mode == 0
call self.move(a:pos)
elseif a:mode == 1
call self.move(self.cursor + a:pos)
else
call self.move(self.size + a:pos)
endif
endfunc
"----------------------------------------------------------------------
" set text
"----------------------------------------------------------------------
function! s:readline.set(text)
let code = str2list(a:text)
let wide = []
for cc in code
let ch = nr2char(cc)
let wide += [strdisplaywidth(ch)]
endfor
let self.code = code
let self.wide = wide
let self.size = len(code)
let self.dirty = 1
call self.move(self.cursor)
endfunc
"----------------------------------------------------------------------
" internal: update text parts
"----------------------------------------------------------------------
function! s:readline.update() abort
let self.text = list2str(self.code)
let self.dirty = 0
return self.text
endfunc
"----------------------------------------------------------------------
" slice
"----------------------------------------------------------------------
let s:has_nvim = has('nvim')? 1 : 0
function! s:list_slice(code, start, endup)
let start = a:start
let endup = a:endup
if s:has_nvim == 0
return slice(a:code, a:start, a:endup)
else
if start == endup
return []
else
return a:code[start:endup-1]
endif
endif
endfunc
"----------------------------------------------------------------------
" extract text: -1/0/1 for text before/on/after cursor
"----------------------------------------------------------------------
function! s:readline.extract(locate)
let cc = self.cursor
if a:locate < 0
let p = s:list_slice(self.code, 0, cc)
elseif a:locate == 0
let p = s:list_slice(self.code, cc, cc + 1)
else
let p = s:list_slice(self.code, cc + 1, len(self.code))
endif
return list2str(p)
endfunc
"----------------------------------------------------------------------
" insert text in current cursor position
"----------------------------------------------------------------------
function! s:readline.insert(text) abort
let code = str2list(a:text)
let wide = []
let cursor = self.cursor
for cc in code
let ch = nr2char(cc)
let ww = strwidth(ch)
let wide += [ww]
endfor
call extend(self.code, code, cursor)
call extend(self.wide, wide, cursor)
let self.size = len(self.code)
let self.cursor += len(code)
let self.timer = -1
let self.dirty = 1
endfunc
"----------------------------------------------------------------------
" internal function: delete n characters on and after cursor
"----------------------------------------------------------------------
function! s:readline.delete(size) abort
let cursor = self.cursor
let avail = self.size - cursor
if avail > 0
let size = a:size
let size = (size > avail)? avail : size
let cursor = self.cursor
call remove(self.code, cursor, cursor + size - 1)
call remove(self.wide, cursor, cursor + size - 1)
let self.size = len(self.code)
let self.timer = -1
let self.dirty = 1
endif
endfunc
"----------------------------------------------------------------------
" backspace
"----------------------------------------------------------------------
function! s:readline.backspace(size) abort
let avail = self.cursor
let size = a:size
let size = (size > avail)? avail : size
if size > 0
let self.cursor -= size
call self.delete(size)
let self.timer = -1
let self.dirty = 1
endif
endfunc
"----------------------------------------------------------------------
" replace
"----------------------------------------------------------------------
function! s:readline.replace(text) abort
let length = strchars(a:text)
if length > 0
call self.delete(length)
call self.insert(a:text)
let self.dirty = 1
endif
endfunc
"----------------------------------------------------------------------
" get selection range [start, end)
"----------------------------------------------------------------------
function! s:readline.visual_range() abort
if self.select < 0
return [-1, -1]
elseif self.select <= self.cursor
return [self.select, self.cursor]
else
return [self.cursor, self.select]
endif
endfunc
"----------------------------------------------------------------------
" get selection text
"----------------------------------------------------------------------
function! s:readline.visual_text() abort
if self.select < 0
return ''
else
let [start, end] = self.visual_range()
let code = s:list_slice(self.code, start, end)
return list2str(code)
endif
endfunc
"----------------------------------------------------------------------
" delete selection
"----------------------------------------------------------------------
function! s:readline.visual_delete() abort
if self.select >= 0
let cursor = self.cursor
let length = self.cursor - self.select
if length > 0
call self.backspace(length)
let self.select = -1
elseif length < 0
call self.delete(-length)
let self.select = -1
endif
endif
endfunc
"----------------------------------------------------------------------
" replace selection
"----------------------------------------------------------------------
function! s:readline.visual_replace(text) abort
if self.select >= 0
call self.visual_delete()
call self.insert(a:text)
endif
endfunc
"----------------------------------------------------------------------
" check is eol
"----------------------------------------------------------------------
function! s:readline.is_eol()
return self.cursor >= self.size
endfunc
"----------------------------------------------------------------------
" cursor blink, returns 0 for not blink, 1 for blink (invisible)
"----------------------------------------------------------------------
function! s:readline.blink(millisec)
let delay_wait = 500
let delay_on = 300
let delay_off = 300
if self.timer < 0
let self.timer = a:millisec
return 0
endif
let offset = a:millisec - self.timer
if offset < delay_wait
return 0
else
let size = max([delay_on + delay_off, 1])
return ((offset % size) < delay_on)? 0 : 1
endif
endfunc
"----------------------------------------------------------------------
" read code (what == 0) or wide (what != 0)
"----------------------------------------------------------------------
function! s:readline.read_data(pos, width, what)
let x = a:pos
let w = a:width
let size = self.size
if x < 0
let w += x
let x = 0
endif
if x + w > size
let w = size - x
endif
if x >= size || w <= 0
return []
endif
let data = (a:what == 0)? self.code : self.wide
return s:list_slice(data, x, x + w)
endfunc
"----------------------------------------------------------------------
" calculate available view port size, give length in display-width,
" returns how many characters can fit in length.
"----------------------------------------------------------------------
function! s:readline.avail(pos, length)
let length = a:length
let size = self.size
let wide = self.wide
let pos = a:pos
let sum = 0
if length == 0
return 0
elseif length > 0
while 1
let char_width = (pos >= 0 && pos < size)? wide[pos] : 1
" echo 'pos=' . pos . ' char_width=' . char_width
let sum += char_width
if sum > length
break
endif
let pos += 1
endwhile
return pos - a:pos
else
let length = -length
while 1
let char_width = (pos >= 0 && pos < size)? wide[pos] : 1
let sum += char_width
if sum > length
break
endif
let pos -= 1
endwhile
return a:pos - pos
endif
endfunc
"----------------------------------------------------------------------
" return display width
"----------------------------------------------------------------------
function! s:readline.width(start, endup) abort
let wide = self.wide
let acc = 0
let pos = a:start
let end = a:endup
while pos < end
let acc += wide[pos]
let pos += 1
endwhile
return acc
endfunc
"----------------------------------------------------------------------
" display: returns a list of text string with attributes
" eg. the readline buffer is "Hello, World !!" and cursor is on "W"
" the returns value should be:
" [(0, "Hello, "), (1, "W"), (0, "orld !!")]
" avail attributes: 0/normal-text, 1/cursor, 2/visual, 3/visual+cursor
"----------------------------------------------------------------------
function! s:readline.display() abort
let size = self.size
let cursor = self.cursor
let codes = self.code
let display = []
if (self.select < 0) || (self.select == cursor)
" content before cursor
if cursor > 0
let code = s:list_slice(codes, 0, cursor)
let display += [[0, list2str(code)]]
endif
" content on cursor
let code = (cursor < size)? codes[cursor] : char2nr(' ')
let display += [[1, list2str([code])]]
" content after cursor
if cursor + 1 < size
let code = s:list_slice(codes, cursor + 1, size)
let display += [[0, list2str(code)]]
endif
else
let vis_start = (cursor < self.select)? cursor : self.select
let vis_endup = (cursor > self.select)? cursor : self.select
" content befor visual selection
if vis_start > 0
let code = s:list_slice(codes, 0, vis_start)
let display += [[0, list2str(code)]]
endif
" content in visual selection
if cursor < self.select
let code = [codes[cursor]]
let display += [[3, list2str(code)]]
let code = s:list_slice(codes, cursor + 1, vis_endup)
let display += [[2, list2str(code)]]
if vis_endup < size
let code = s:list_slice(codes, vis_endup, size)
let display += [[0, list2str(code)]]
endif
else
" visual selection
let code = s:list_slice(codes, vis_start, vis_endup)
let display += [[2, list2str(code)]]
" content on cursor
let code = (cursor < size)? codes[cursor] : char2nr(' ')
let display += [[1, list2str([code])]]
" content after cursor
if cursor + 1 < size
let code = s:list_slice(codes, cursor + 1, size)
let display += [[0, list2str(code)]]
endif
endif
endif
return display
endfunc
"----------------------------------------------------------------------
" filter display list with a window
"----------------------------------------------------------------------
function! s:readline.window(display, start, endup) abort
let start = a:start
let endup = a:endup
let display = []
if start < 0
let avail = endup - start
let avail = min([avail, -start])
if avail > 0
let display += [[0, repeat(' ', avail)]]
endif
let start += avail
endif
if start >= endup
return display
endif
let pos = 0
for item in a:display
let attribute = item[0]
let text = item[1]
let chars = strchars(text)
let open = pos
let close = open + chars
if close > start && open < endup
let open = max([open, start])
let open = min([open, endup])
let close = max([close, start])
let close = min([close, endup])
if open < close
if open == pos && close == open + chars
let display += [[attribute, text]]
else
let text = strcharpart(text, open - pos, close - open)
let display += [[attribute, text]]
endif
endif
endif
let pos += chars
endfor
if pos < endup
let display += [[0, repeat(' ', endup - pos)]]
endif
return display
endfunc
"----------------------------------------------------------------------
" returns new window pos to fit in
"----------------------------------------------------------------------
function! s:readline.slide(window_pos, display_width)
let window_pos = a:window_pos
let display_width = a:display_width
let cursor = self.cursor
if display_width < 1
return cursor
elseif cursor < window_pos
return cursor
endif
let window_pos = (window_pos < 0)? 0 : window_pos
let wides = self.read_data(window_pos, cursor - window_pos, 1)
if s:has_nvim == 0
let width = reduce(wides, { acc, val -> acc + val }, 0) + 1
else
let width = 1
for w in wides
let width += w
endfor
endif
if width <= display_width
return window_pos
else
let avail = self.avail(cursor, -display_width)
let pos = cursor - avail + 1
return max([pos, 0])
endif
return window_pos
endfunc
"----------------------------------------------------------------------
" render a window
"----------------------------------------------------------------------
function! s:readline.render(pos, display_width)
let nchars = self.avail(a:pos, a:display_width)
let display = self.display()
let display = self.window(display, a:pos, a:pos + nchars)
let total = 0
for [attr, text] in display
let total += strwidth(text)
endfor
if total < a:display_width
let attr = 0
if self.cursor == a:pos + nchars
let attr = 1
if self.select >= 0
let attr = (self.cursor < self.select)? 3 : 1
endif
else
if self.select > a:pos + nchars
let attr = (self.cursor < a:pos + nchars)? 2 : 0
endif
endif
let display += [[attr, repeat(' ', a:display_width - total)]]
endif
return display
endfunc
"----------------------------------------------------------------------
" calculate mouse click position
"----------------------------------------------------------------------
function! s:readline.mouse_click(winpos, offset)
let index = self.avail(a:winpos, a:offset) + a:winpos
return (index > self.size)? self.size : index
endfunc
"----------------------------------------------------------------------
" save history in current position
"----------------------------------------------------------------------
function! s:readline.history_save() abort
let size = len(self.history)
if size > 0
let self.index = (self.index < 0)? 0 : self.index
let self.index = (self.index >= size)? (size - 1) : self.index
if self.dirty
call self.update()
endif
let self.history[self.index] = self.text
endif
endfunc
"----------------------------------------------------------------------
" previous history
"----------------------------------------------------------------------
function! s:readline.history_prev() abort
let size = len(self.history)
if size > 0
call self.history_save()
let self.index = (self.index < size - 1)? (self.index + 1) : 0
call self.set(self.history[self.index])
call self.update()
endif
endfunc
"----------------------------------------------------------------------
" next history
"----------------------------------------------------------------------
function! s:readline.history_next() abort
let size = len(self.history)
if size > 0
call self.history_save()
let self.index = (self.index <= 0)? (size - 1) : (self.index - 1)
call self.set(self.history[self.index])
call self.update()
endif
endfunc
"----------------------------------------------------------------------
" init history
"----------------------------------------------------------------------
function! s:readline.history_init(history) abort
if len(a:history) == 0
let self.history = []
let self.index = 0
else
let history = deepcopy(a:history) + ['']
call reverse(history)
let self.history = history
let self.index = 0
endif
endfunc
"----------------------------------------------------------------------
" feed character
"----------------------------------------------------------------------
function! s:readline.feed(char) abort
let char = a:char
let code = str2list(char)
let head = len(code)? code[0] : 0
if head < 0x20 || head == 0x80
if char == "\<BS>"
if self.select >= 0
call self.visual_delete()
else
call self.backspace(1)
endif
elseif char == "\<DELETE>"
if self.select >= 0
call self.visual_delete()
else
call self.delete(1)
endif
elseif char == "\<LEFT>" || char == "\<c-b>"
if self.select >= 0
call self.move(min([self.select, self.cursor]))
let self.select = -1
else
call self.seek(-1, 1)
endif
elseif char == "\<RIGHT>" || char == "\<c-f>"
if self.select >= 0
call self.move(max([self.select, self.cursor]))
let self.select = -1
else
call self.seek(1, 1)
endif
elseif char == "\<UP>"
call self.history_prev()
let self.select = -1
elseif char == "\<DOWN>"
call self.history_next()
let self.select = -1
elseif char == "\<S-Left>"
if self.select < 0
let self.select = self.cursor
endif
call self.seek(-1, 1)
elseif char == "\<S-Right>"
if self.select < 0
let self.select = self.cursor
endif
call self.seek(1, 1)
elseif char == "\<S-Home>"
if self.select < 0
let self.select = self.cursor
endif
call self.seek(0, 0)
elseif char == "\<S-End>"
if self.select < 0
let self.select = self.cursor
endif
call self.seek(0, 2)
elseif char == "\<c-d>"
if self.select >= 0
call self.visual_delete()
else
call self.delete(1)
endif
elseif char == "\<c-k>"
if self.select >= 0
call self.visual_delete()
else
if self.size > self.cursor
call self.delete(self.size - self.cursor)
endif
endif
elseif char == "\<home>" || char == "\<c-a>"
call self.move(0)
let self.select = -1
elseif char == "\<end>" || char == "\<c-e>"
call self.move(self.size)
let self.select = -1
elseif char == "\<C-Insert>"
if self.select >= 0
let text = self.visual_text()
if text != ''
let @* = text
endif
endif
elseif char == "\<S-Insert>"
let text = split(@*, "\n", 1)[0]
let text = substitute(text, '[\r\n\t]', ' ', 'g')
if text != ''
if self.select >= 0
call self.visual_delete()
endif
call self.insert(text)
endif
elseif char == "\<c-w>"
if self.select < 0
let head = self.extract(-1)
let word = matchstr(head, '\S\+\s*$')
if word != ''
call self.backspace(strchars(word))
endif
else
call self.visual_delete()
endif
elseif char == "\<c-c>"
if self.select >= 0
let text = self.visual_text()
if text != ''
let @0 = text
endif
endif
elseif char == "\<c-x>"
if self.select >= 0
let text = self.visual_text()
if text != ''
let @0 = text
call self.visual_delete()
endif
endif
elseif char == "\<c-v>"
let text = split(@0, "\n", 1)[0]
let text = substitute(text, '[\r\n\t]', ' ', 'g')
if text != ''
if self.select >= 0
call self.visual_delete()
endif
call self.insert(text)
endif
else
return -1
endif
return 0
else
if self.select >= 0
call self.visual_delete()
endif
call self.insert(char)
endif
return 0
endfunc
"----------------------------------------------------------------------
" display parts
"----------------------------------------------------------------------
function! s:readline.echo(blink, ...)
if a:0 < 2
let display = self.render(0, self.size * 4)
else
let display = self.render(a:1, a:2)
endif
for [attr, text] in display
if attr == 0
echohl Normal
elseif attr == 1
if a:blink == 0
echohl Cursor
else
echohl Normal
endif
elseif attr == 2
echohl Visual
elseif attr == 3
if a:blink == 0
echohl Cursor
else
echohl Visual
endif
endif
echon text
endfor
endfunc
"----------------------------------------------------------------------
" constructor
"----------------------------------------------------------------------
function! quickui#readline#new()
let obj = deepcopy(s:readline)
return obj
endfunc
"----------------------------------------------------------------------
" test suit
"----------------------------------------------------------------------
function! quickui#readline#test()
let v:errors = []
let obj = quickui#readline#new()
call obj.set('0123456789')
call assert_equal('0123456789', obj.update(), 'test set')
call obj.insert('ABC')
call assert_equal('ABC0123456789', obj.update(), 'test insert')
call obj.delete(3)
call assert_equal('ABC3456789', obj.update(), 'test delete')
call obj.backspace(2)
call assert_equal('A3456789', obj.update(), 'test backspace')
call obj.delete(1000)
call assert_equal('A', obj.update(), 'test kill right')
call obj.insert('BCD')
call assert_equal('ABCD', obj.update(), 'test append')
call obj.delete(1000)
call assert_equal('ABCD', obj.update(), 'test append')
call obj.backspace(1000)
call assert_equal('', obj.update(), 'test append')
call obj.insert('0123456789')
call assert_equal('0123456789', obj.update(), 'test reinit')
call obj.move(3)
call obj.replace('abcd')
call assert_equal('012abcd789', obj.update(), 'test replace')
let obj.select = obj.cursor
call obj.seek(-2, 1)
call obj.visual_delete()
call assert_equal('012ab789', obj.update(), 'test visual delete')
let obj.select = obj.cursor
call obj.seek(2, 1)
echo obj.display()
call assert_equal('78', obj.visual_text(), 'test visual selection')
call obj.visual_delete()
call assert_equal('012ab9', obj.update(), 'test visual delete2')
call obj.seek(-2, 1)
if len(v:errors)
for error in v:errors
echoerr error
endfor
endif
call obj.move(1)
let obj.select = 4
echo obj.display()
return obj.update()
endfunc
" echo quickui#readline#test()
"----------------------------------------------------------------------
" cli test
"----------------------------------------------------------------------
function! quickui#readline#cli(prompt)
let rl = quickui#readline#new()
let rl.history = ['', 'abcd', '12345']
let index = 0
let accept = ''
let pos = 0
while 1
noautocmd redraw
echohl Question
echon a:prompt
let ts = float2nr(reltimefloat(reltime()) * 1000)
if 0
call rl.echo(rl.blink(ts))
else
let size = 10
let pos = rl.slide(pos, size)
echohl Title
echon "<"
call rl.echo(rl.blink(ts), pos, size)
echohl Title
echon ">"
echon " size=" . size
echon " cursor=" . rl.cursor
echon " pos=". pos
echon " blink=". rl.blink(ts)
echon " avail=". rl.avail(pos, size)
endif
" echon rl.display()
try
let code = getchar()
catch /^Vim:Interrupt$/
let code = "\<c-c>"
endtry
if type(code) == v:t_number && code == 0
try
exec 'sleep 15m'
continue
catch /^Vim:Interrupt$/
let code = "\<c-c>"
endtry
endif
let ch = (type(code) == v:t_number)? nr2char(code) : code
if ch == ""
continue
elseif ch == "\<ESC>"
break
elseif ch == "\<cr>"
let accept = rl.update()
break
else
call rl.feed(ch)
endif
endwhile
echohl None
noautocmd redraw
echo ""
return accept
endfunc
"----------------------------------------------------------------------
" testing suit
"----------------------------------------------------------------------
if 0
let suit = 0
if suit == 0
call quickui#readline#test()
elseif suit == 1
let rl = quickui#readline#new()
call rl.insert('abad')
echo rl.mouse_click(0, 5)
elseif suit == 2
echo quickui#readline#cli(">>> ")
elseif suit == 3
let rl = quickui#readline#new()
let size = 10
echo "avail=" . rl.avail(0, size)
call rl.insert("hello")
echo "cursor=" . rl.cursor
echo "avail=" . rl.avail(0, size)
endif
endif