"====================================================================== " " 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 == "\" if self.select >= 0 call self.visual_delete() else call self.backspace(1) endif elseif char == "\" if self.select >= 0 call self.visual_delete() else call self.delete(1) endif elseif char == "\" || char == "\" 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 == "\" || char == "\" 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 == "\" call self.history_prev() let self.select = -1 elseif char == "\" call self.history_next() let self.select = -1 elseif char == "\" if self.select < 0 let self.select = self.cursor endif call self.seek(-1, 1) elseif char == "\" if self.select < 0 let self.select = self.cursor endif call self.seek(1, 1) elseif char == "\" if self.select < 0 let self.select = self.cursor endif call self.seek(0, 0) elseif char == "\" if self.select < 0 let self.select = self.cursor endif call self.seek(0, 2) elseif char == "\" if self.select >= 0 call self.visual_delete() else call self.delete(1) endif elseif char == "\" 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 == "\" || char == "\" call self.move(0) let self.select = -1 elseif char == "\" || char == "\" call self.move(self.size) let self.select = -1 elseif char == "\" if self.select >= 0 let text = self.visual_text() if text != '' let @* = text endif endif elseif char == "\" 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 == "\" 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 == "\" if self.select >= 0 let text = self.visual_text() if text != '' let @0 = text endif endif elseif char == "\" if self.select >= 0 let text = self.visual_text() if text != '' let @0 = text call self.visual_delete() endif endif elseif char == "\" 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 = "\" endtry if type(code) == v:t_number && code == 0 try exec 'sleep 15m' continue catch /^Vim:Interrupt$/ let code = "\" endtry endif let ch = (type(code) == v:t_number)? nr2char(code) : code if ch == "" continue elseif ch == "\" break elseif ch == "\" 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