Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

REPL: tab/backspace aligns to 4 #22939

Merged
merged 4 commits into from
Aug 16, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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