Skip to content

Commit

Permalink
REPL: tab/backspace aligns to 4 (#22939)
Browse files Browse the repository at this point in the history
* REPL: tab/backspace aligns to 4

* when pressing tab, compute the number of spaces to insert
  so that the cursor is aligned to a multiple of 4 chars
* when pressing backspace, delete up to 4 spaces so that
  the cursor is aligned to a multiple of 4 chars

* multispaces defaults to false, and de-activate in search-mode

* tab: jump to end of line; backspace: try to keep right side aligned

* cosmetic changes
  • Loading branch information
rfourquet authored and KristofferC committed Aug 16, 2017
1 parent 271f210 commit 67f3fc0
Show file tree
Hide file tree
Showing 2 changed files with 165 additions and 36 deletions.
121 changes: 85 additions & 36 deletions base/repl/LineEdit.jl
Original file line number Diff line number Diff line change
Expand Up @@ -478,22 +478,49 @@ function edit_insert(buf::IOBuffer, c)
end
end

function edit_backspace(s::PromptState)
if edit_backspace(s.input_buffer)
refresh_line(s)
else
beep(terminal(s))
end
# align: delete up to 4 spaces to align to a multiple of 4 chars
# adjust: also delete spaces on the right of the cursor to try to keep aligned what is
# on the right
edit_backspace(s::PromptState, align::Bool=false, adjust=align) =
edit_backspace(s.input_buffer, align) ? refresh_line(s) : beep(terminal(s))

const _newline = UInt8('\n')
const _space = UInt8(' ')

_notspace(c) = c != _space

beginofline(buf, pos=position(buf)) = findprev(buf.data, _newline, pos)

function endofline(buf, pos=position(buf))
eol = findnext(buf.data[pos+1:buf.size], _newline, 1)
eol == 0 ? buf.size : pos + eol - 1
end
function edit_backspace(buf::IOBuffer)
if position(buf) > 0
oldpos = position(buf)
char_move_left(buf)
splice_buffer!(buf, position(buf):oldpos-1)
return true
else
return false

function edit_backspace(buf::IOBuffer, align::Bool=false, adjust::Bool=align)
!align && adjust &&
throw(DomainError((align, adjust),
"if `adjust` is `true`, `align` must be `true`"))
oldpos = position(buf)
oldpos == 0 && return false
c = char_move_left(buf)
newpos = position(buf)
if align && c == ' ' # maybe delete multiple spaces
beg = beginofline(buf, newpos)
align = strwidth(String(buf.data[1+beg:newpos])) % 4
nonspace = findprev(_notspace, buf.data, newpos)
if newpos - align >= nonspace
newpos -= align
seek(buf, newpos)
if adjust
spaces = findnext(_notspace, buf.data[newpos+2:buf.size], 1)
oldpos = spaces == 0 ? buf.size :
buf.data[newpos+1+spaces] == _newline ? newpos+spaces :
newpos + min(spaces, 4)
end
end
end
splice_buffer!(buf, newpos:oldpos-1)
return true
end

edit_delete(s) = edit_delete(buffer(s)) ? refresh_line(s) : beep(terminal(s))
Expand Down Expand Up @@ -1337,30 +1364,52 @@ function bracketed_paste(s)
return replace(input, '\t', " "^tabwidth)
end

function tab_should_complete(s)
# Yes, we are ignoring the possiblity
# the we could be in the middle of a multi-byte
# sequence, here but that's ok, since any
# whitespace we're interested in is only one byte
buf = buffer(s)
pos = position(buf)
pos == 0 && return true
c = buf.data[pos]
c != _newline && c != UInt8('\t') &&
# hack to allow path completion in cmds
# after a space, e.g., `cd <tab>`, while still
# allowing multiple indent levels
(c != _space || pos <= 3 || buf.data[pos-1] != _space)
end

# jump_spaces: if cursor is on a ' ', move it to the first non-' ' char on the right
# if `delete_trailing`, ignore trailing ' ' by deleting them
function edit_tab(s, jump_spaces=false, delete_trailing=jump_spaces)
tab_should_complete(s) ?
complete_line(s) :
edit_tab(buffer(s), jump_spaces, delete_trailing)
refresh_line(s)
end

function edit_tab(buf::IOBuffer, jump_spaces=false, delete_trailing=jump_spaces)
i = position(buf)
if jump_spaces && i < buf.size && buf.data[i+1] == _space
spaces = findnext(_notspace, buf.data[i+1:buf.size], 1)
if delete_trailing && (spaces == 0 || buf.data[i+spaces] == _newline)
splice_buffer!(buf, i:(spaces == 0 ? buf.size-1 : i+spaces-2))
else
jump = spaces == 0 ? buf.size : i+spaces-1
return seek(buf, jump)
end
end
# align to multiples of 4:
align = 4 - strwidth(String(buf.data[1+beginofline(buf, i):i])) % 4
return edit_insert(buf, ' '^align)
end


const default_keymap =
AnyDict(
# Tab
'\t' => (s,o...)->begin
buf = buffer(s)
# Yes, we are ignoring the possiblity
# the we could be in the middle of a multi-byte
# sequence, here but that's ok, since any
# whitespace we're interested in is only one byte
i = position(buf)
if i != 0
c = buf.data[i]
if c == UInt8('\n') || c == UInt8('\t') ||
# hack to allow path completion in cmds
# after a space, e.g., `cd <tab>`, while still
# allowing multiple indent levels
(c == UInt8(' ') && i > 3 && buf.data[i-1] == UInt8(' '))
edit_insert(s, " "^4)
return
end
end
complete_line(s)
refresh_line(s)
end,
'\t' => (s,o...)->edit_tab(s, true),
# Enter
'\r' => (s,o...)->begin
if on_enter(s) || (eof(buffer(s)) && s.key_repeats > 1)
Expand All @@ -1372,7 +1421,7 @@ AnyDict(
end,
'\n' => KeyAlias('\r'),
# Backspace/^H
'\b' => (s,o...)->edit_backspace(s),
'\b' => (s,o...)->edit_backspace(s, true),
127 => KeyAlias('\b'),
# Meta Backspace
"\e\b" => (s,o...)->edit_delete_prev_word(s),
Expand Down
80 changes: 80 additions & 0 deletions test/lineedit.jl
Original file line number Diff line number Diff line change
Expand Up @@ -427,3 +427,83 @@ end
"\r\e[2C julia = :fun\n" *
"\r\e[2Cend\r\e[5C"
end

@testset "tab/backspace alignment feature" begin
term = TestHelpers.FakeTerminal(IOBuffer(), IOBuffer(), IOBuffer())
s = LineEdit.init_state(term, ModalInterface([Prompt("test> ")]))
function bufferdata(s)
buf = LineEdit.buffer(s)
String(buf.data[1:buf.size])
end
move_left(s, n) = for x = 1:n
LineEdit.edit_move_left(s)
end

bufpos(s::Base.LineEdit.MIState) = position(LineEdit.buffer(s))

LineEdit.edit_insert(s, "for x=1:10\n")
LineEdit.edit_tab(s)
@test bufferdata(s) == "for x=1:10\n "
LineEdit.edit_backspace(s, true, false)
@test bufferdata(s) == "for x=1:10\n"
LineEdit.edit_insert(s, " ")
@test bufpos(s) == 13
LineEdit.edit_tab(s)
@test bufferdata(s) == "for x=1:10\n "
LineEdit.edit_insert(s, " ")
LineEdit.edit_backspace(s, true, false)
@test bufferdata(s) == "for x=1:10\n "
LineEdit.edit_insert(s, "éé=3 ")
LineEdit.edit_tab(s)
@test bufferdata(s) == "for x=1:10\n éé=3 "
LineEdit.edit_backspace(s, true, false)
@test bufferdata(s) == "for x=1:10\n éé=3"
LineEdit.edit_insert(s, "\n 1∉x ")
LineEdit.edit_tab(s)
@test bufferdata(s) == "for x=1:10\n éé=3\n 1∉x "
LineEdit.edit_backspace(s, false, false)
@test bufferdata(s) == "for x=1:10\n éé=3\n 1∉x "
LineEdit.edit_backspace(s, true, false)
@test bufferdata(s) == "for x=1:10\n éé=3\n 1∉x "
LineEdit.edit_move_word_left(s)
LineEdit.edit_tab(s)
@test bufferdata(s) == "for x=1:10\n éé=3\n 1∉x "
LineEdit.move_line_start(s)
@test bufpos(s) == 22
LineEdit.edit_tab(s, true)
@test bufferdata(s) == "for x=1:10\n éé=3\n 1∉x "
@test bufpos(s) == 30
LineEdit.edit_move_left(s)
@test bufpos(s) == 29
LineEdit.edit_backspace(s, true, true)
@test bufferdata(s) == "for x=1:10\n éé=3\n 1∉x "
@test bufpos(s) == 26
LineEdit.edit_tab(s, false) # same as edit_tab(s, true) here
@test bufpos(s) == 30
move_left(s, 6)
@test bufpos(s) == 24
LineEdit.edit_backspace(s, true, true)
@test bufferdata(s) == "for x=1:10\n éé=3\n 1∉x "
@test bufpos(s) == 22
LineEdit.edit_kill_line(s)
LineEdit.edit_insert(s, ' '^10)
move_left(s, 7)
@test bufferdata(s) == "for x=1:10\n éé=3\n "
@test bufpos(s) == 25
LineEdit.edit_tab(s, true, false)
@test bufpos(s) == 32
move_left(s, 7)
LineEdit.edit_tab(s, true, true)
@test bufpos(s) == 26
@test bufferdata(s) == "for x=1:10\n éé=3\n "
# test again the same, when there is a next line
LineEdit.edit_insert(s, " \nend")
move_left(s, 11)
@test bufpos(s) == 25
LineEdit.edit_tab(s, true, false)
@test bufpos(s) == 32
move_left(s, 7)
LineEdit.edit_tab(s, true, true)
@test bufpos(s) == 26
@test bufferdata(s) == "for x=1:10\n éé=3\n \nend"
end

0 comments on commit 67f3fc0

Please sign in to comment.