From e3768b1ea6473cb32cec6b8d0c7a8469b15e9978 Mon Sep 17 00:00:00 2001 From: Rafael Fourquet Date: Sat, 2 Dec 2017 10:42:06 +0100 Subject: [PATCH 1/2] REPL: indent multiple lines at once when highlighted --- base/repl/LineEdit.jl | 98 ++++++++++++++++++++++++++++++++----------- test/lineedit.jl | 35 +++++++++------- 2 files changed, 94 insertions(+), 39 deletions(-) diff --git a/base/repl/LineEdit.jl b/base/repl/LineEdit.jl index 0c00bbc7df6d4..6a73940fab50b 100644 --- a/base/repl/LineEdit.jl +++ b/base/repl/LineEdit.jl @@ -66,7 +66,7 @@ mutable struct PromptState <: ModeState terminal::AbstractTerminal p::Prompt input_buffer::IOBuffer - region_active::Bool + region_active::Symbol # :shift or :mark or :off undo_buffers::Vector{IOBuffer} undo_idx::Int ias::InputAreaState @@ -89,7 +89,7 @@ options(s::PromptState) = function setmark(s::MIState, guess_region_active::Bool=true) was_active = is_region_active(s) - guess_region_active && activate_region(s, s.key_repeats > 0) + guess_region_active && activate_region(s, s.key_repeats > 0 ? :mark : :off) mark(buffer(s)) was_active && refresh_line(s) end @@ -108,13 +108,20 @@ indexes(reg::Region) = first(reg)+1:last(reg) content(s, reg::Region = 0=>bufend(s)) = String(buffer(s).data[indexes(reg)]) -activate_region(s::PromptState, on=true) = s.region_active = on -activate_region(s::ModeState, on=true) = false -deactivate_region(s::ModeState) = activate_region(s, false) +function activate_region(s::PromptState, state::Symbol) + @assert state in (:mark, :shift, :off) + s.region_active = state +end + +activate_region(s::ModeState, state::Symbol) = false +deactivate_region(s::ModeState) = activate_region(s, :off) -is_region_active(s::PromptState) = s.region_active +is_region_active(s::PromptState) = s.region_active in (:shift, :mark) is_region_active(s::ModeState) = false +region_active(s::PromptState) = s.region_active +region_active(s::ModeState) = :off + input_string(s::PromptState) = String(take!(copy(s.input_buffer))) @@ -182,7 +189,7 @@ cancel_beep(::ModeState) = nothing for f in [:terminal, :on_enter, :add_history, :buffer, :(Base.isempty), :replace_line, :refresh_multi_line, :input_string, :update_display_buffer, :empty_undo, :push_undo, :pop_undo, :options, :cancel_beep, :beep, - :deactivate_region, :is_region_active, :activate_region] + :deactivate_region, :activate_region, :is_region_active, :region_active] @eval ($f)(s::MIState, args...) = $(f)(state(s), args...) end @@ -210,6 +217,11 @@ const COMMAND_GROUP = Dict(command=>group for (group, commands) in COMMAND_GROUP command_group(command::Symbol) = get(COMMAND_GROUP, command, :nogroup) command_group(command::Function) = command_group(Base.function_name(command)) +# return true if command should keep active a region +function preserve_active(command::Symbol) + command == :edit_indent +end + function set_action!(s::MIState, command::Symbol) # if a command is already running, don't update the current_action field, # as the caller is used as a helper function @@ -218,13 +230,14 @@ function set_action!(s::MIState, command::Symbol) ## handle activeness of the region is_shift_move(cmd) = startswith(String(cmd), "shift_") if is_shift_move(command) - if !is_shift_move(s.last_action) + if region_active(s) != :shift setmark(s, false) - activate_region(s) + activate_region(s, :shift) # NOTE: if the region was already active from a non-shift # move (e.g. ^Space^Space), the region is visibly changed end - elseif command_group(command) != :movement || is_shift_move(s.last_action) + elseif !(preserve_active(command) || + command_group(command) == :movement && region_active(s) == :mark) # if we move after a shift-move, the region is de-activated # (e.g. like emacs behavior) deactivate_region(s) @@ -625,19 +638,23 @@ function edit_splice!(s, r::Region=region(s), ins::AbstractString = "") A >= B && isempty(ins) && return String(ins) buf = buffer(s) pos = position(buf) + adjust_pos = true if A <= pos < B seek(buf, A) elseif B <= pos seek(buf, pos - B + A) + else + adjust_pos = false end if A < buf.mark < B buf.mark = A - elseif A < B <= buf.mark + elseif A < B <= buf.mark || # handles edit_yank + A == B < buf.mark # handles edit_indent buf.mark += sizeof(ins) - B + A end ret = splice!(buf.data, A+1:B, Vector{UInt8}(ins)) # position(), etc, are 0-indexed buf.size = buf.size + sizeof(ins) - B + A - seek(buf, position(buf) + sizeof(ins)) + adjust_pos && seek(buf, position(buf) + sizeof(ins)) String(ret) end @@ -1046,7 +1063,7 @@ edit_indent_right(s::MIState, n=1) = edit_indent(s, n) function edit_indent(s::MIState, num::Int) set_action!(s, :edit_indent) push_undo(s) - if edit_indent(buffer(s), num) + if edit_indent(buffer(s), num, is_region_active(s)) refresh_line(s) else pop_undo(s) @@ -1054,23 +1071,54 @@ function edit_indent(s::MIState, num::Int) end end +# return the indices in buffer(s) of the beginning of each lines +# having a non-empty intersection with region(s) +function get_lines_in_region(s)::Vector{Int} + buf = buffer(s) + b, e = region(buf) + bol = Int[beginofline(buf, b)] # begin of lines + while true + b = endofline(buf, b) + b >= e && break + # b < e ==> b+1 <= e <= buf.size + push!(bol, b += 1) + end + bol +end + +# compute the number of spaces from b till the next non-space on the right +# (which can also be "end of line" or "end of buffer") +function leadingspaces(buf::IOBuffer, b::Int)::Int + ls = findnext(_notspace, buf.data, b+1)-1 + ls == -1 && (ls = buf.size) + ls -= b + ls +end + # indent by abs(num) characters, on the right if num >= 0, on the left otherwise -function edit_indent(buf::IOBuffer, num::Int) - b = beginofline(buf) - if num >= 0 - edit_splice!(buf, b => b, ' '^num) - else - # count leading spaces on the line, which is an upper bound +# if multiline is true, indent all the lines in the region as a block. +function edit_indent(buf::IOBuffer, num::Int, multiline::Bool)::Bool + bol = multiline ? get_lines_in_region(buf) : Int[beginofline(buf)] + if num < 0 + # count leading spaces on the lines, which are an upper bound # on the number of spaces characters that can be removed - leadingspaces = findnext(_notspace, buf.data, b+1)-1 - leadingspaces == -1 && (leadingspaces = buf.size) - leadingspaces -= b - leadingspaces == 0 && return false # no space can be removed - edit_splice!(buf, b => b + min(leadingspaces, -num)) + ls_min = minimum(leadingspaces(buf, b) for b in bol) + ls_min == 0 && return false # can't left-indent, no space can be removed + num = -min(-num, ls_min) + end + for b in reverse!(bol) # reverse! to not mess-up the bol's offsets + _edit_indent(buf, b, num) end true end +# indents line starting a position b by num positions +# if num < 0, it is assumed that there are at least num white spaces +# at the beginning of line +_edit_indent(buf::IOBuffer, b::Int, num::Int) = + num >= 0 ? edit_splice!(buf, b => b, ' '^num) : + edit_splice!(buf, b => b - num) + history_prev(::EmptyHistoryProvider) = ("", false) history_next(::EmptyHistoryProvider) = ("", false) @@ -2137,7 +2185,7 @@ end run_interface(::Prompt) = nothing init_state(terminal, prompt::Prompt) = - PromptState(terminal, prompt, IOBuffer(), false, IOBuffer[], 1, InputAreaState(1, 1), + PromptState(terminal, prompt, IOBuffer(), :off, IOBuffer[], 1, InputAreaState(1, 1), #=indent(spaces)=# -1, Threads.SpinLock(), 0.0) function init_state(terminal, m::ModalInterface) diff --git a/test/lineedit.jl b/test/lineedit.jl index 7e4356a11f60b..3c42906185a13 100644 --- a/test/lineedit.jl +++ b/test/lineedit.jl @@ -774,24 +774,31 @@ end local buf = IOBuffer() write(buf, "1\n22\n333") seek(buf, 0) - @test LineEdit.edit_indent(buf, -1) == false - @test transform!(buf->LineEdit.edit_indent(buf, -1), buf) == ("1\n22\n333", 0, 0) - @test transform!(buf->LineEdit.edit_indent(buf, +1), buf) == (" 1\n22\n333", 1, 0) - @test transform!(buf->LineEdit.edit_indent(buf, +2), buf) == (" 1\n22\n333", 3, 0) - @test transform!(buf->LineEdit.edit_indent(buf, -2), buf) == (" 1\n22\n333", 1, 0) + @test LineEdit.edit_indent(buf, -1, false) == false + @test transform!(buf->LineEdit.edit_indent(buf, -1, false), buf) == ("1\n22\n333", 0, 0) + @test transform!(buf->LineEdit.edit_indent(buf, +1, false), buf) == (" 1\n22\n333", 1, 0) + @test transform!(buf->LineEdit.edit_indent(buf, +2, false), buf) == (" 1\n22\n333", 3, 0) + @test transform!(buf->LineEdit.edit_indent(buf, -2, false), buf) == (" 1\n22\n333", 1, 0) seek(buf, 0) # if the cursor is already on the left column, it stays there - @test transform!(buf->LineEdit.edit_indent(buf, -2), buf) == ("1\n22\n333", 0, 0) + @test transform!(buf->LineEdit.edit_indent(buf, -2, false), buf) == ("1\n22\n333", 0, 0) seek(buf, 3) # between the two "2" - @test transform!(buf->LineEdit.edit_indent(buf, +3), buf) == ("1\n 22\n333", 6, 0) - @test transform!(buf->LineEdit.edit_indent(buf, -9), buf) == ("1\n22\n333", 3, 0) + @test transform!(buf->LineEdit.edit_indent(buf, +3, false), buf) == ("1\n 22\n333", 6, 0) + @test transform!(buf->LineEdit.edit_indent(buf, -9, false), buf) == ("1\n22\n333", 3, 0) seekend(buf) # position 8 - @test transform!(buf->LineEdit.edit_indent(buf, +3), buf) == ("1\n22\n 333", 11, 0) - @test transform!(buf->LineEdit.edit_indent(buf, -1), buf) == ("1\n22\n 333", 10, 0) - @test transform!(buf->LineEdit.edit_indent(buf, -2), buf) == ("1\n22\n333", 8, 0) - @test transform!(buf->LineEdit.edit_indent(buf, -1), buf) == ("1\n22\n333", 8, 0) - @test transform!(buf->LineEdit.edit_indent(buf, +3), buf) == ("1\n22\n 333", 11, 0) + @test transform!(buf->LineEdit.edit_indent(buf, +3, false), buf) == ("1\n22\n 333", 11, 0) + @test transform!(buf->LineEdit.edit_indent(buf, -1, false), buf) == ("1\n22\n 333", 10, 0) + @test transform!(buf->LineEdit.edit_indent(buf, -2, false), buf) == ("1\n22\n333", 8, 0) + @test transform!(buf->LineEdit.edit_indent(buf, -1, false), buf) == ("1\n22\n333", 8, 0) + @test transform!(buf->LineEdit.edit_indent(buf, +3, false), buf) == ("1\n22\n 333", 11, 0) seek(buf, 5) # left column - @test transform!(buf->LineEdit.edit_indent(buf, -2), buf) == ("1\n22\n 333", 5, 0) + @test transform!(buf->LineEdit.edit_indent(buf, -2, false), buf) == ("1\n22\n 333", 5, 0) + # multiline tests + @test transform!(buf->LineEdit.edit_indent(buf, -2, true), buf) == ("1\n22\n 333", 5, 0) + @test transform!(buf->LineEdit.edit_indent(buf, +2, true), buf) == (" 1\n 22\n 333", 11, 0) + @test transform!(buf->LineEdit.edit_indent(buf, -1, true), buf) == (" 1\n 22\n 333", 8, 0) + Base.LineEdit.edit_exchange_point_and_mark(buf) + seek(buf, 5) + @test transform!(buf->LineEdit.edit_indent(buf, -1, true), buf) == (" 1\n22\n 333", 4, 6) end @testset "edit_transpose_lines_{up,down}!" begin From d78742313c18e1a12f38d38bedae4a609717e535 Mon Sep 17 00:00:00 2001 From: Rafael Fourquet Date: Sun, 3 Dec 2017 15:52:12 +0100 Subject: [PATCH 2/2] REPL: transpose multiple lines at once when highlighted --- base/repl/LineEdit.jl | 24 +++++++++++++----------- test/lineedit.jl | 30 ++++++++++++++++++++++-------- 2 files changed, 35 insertions(+), 19 deletions(-) diff --git a/base/repl/LineEdit.jl b/base/repl/LineEdit.jl index 6a73940fab50b..b5034dfe05499 100644 --- a/base/repl/LineEdit.jl +++ b/base/repl/LineEdit.jl @@ -219,7 +219,7 @@ command_group(command::Function) = command_group(Base.function_name(command)) # return true if command should keep active a region function preserve_active(command::Symbol) - command == :edit_indent + command ∈ [:edit_indent, :edit_transpose_lines_down!, :edit_transpose_lines_up!] end function set_action!(s::MIState, command::Symbol) @@ -953,37 +953,39 @@ function edit_transpose_words(buf::IOBuffer, mode=:emacs) end -# swap current line with line above -function edit_transpose_lines_up!(buf::IOBuffer) - b2 = beginofline(buf) +# swap all lines intersecting the region with line above +function edit_transpose_lines_up!(buf::IOBuffer, reg::Region) + b2 = beginofline(buf, first(reg)) b2 == 0 && return false b1 = beginofline(buf, b2-1) # we do in this order so that the buffer's position is maintained in current line line1 = edit_splice!(buf, b1 => b2) # delete whole previous line line1 = '\n'*line1[1:end-1] # don't include the final '\n' pos = position(buf) # save pos in case it's at the end of line - b = endofline(buf) + b = endofline(buf, last(reg) - b2 + b1) # b2-b1 is the size of the removed line1 edit_splice!(buf, b => b, line1) seek(buf, pos) true end -# swap current line with line below -function edit_transpose_lines_down!(buf::IOBuffer) - e1 = endofline(buf) +# swap all lines intersecting the region with line below +function edit_transpose_lines_down!(buf::IOBuffer, reg::Region) + e1 = endofline(buf, last(reg)) e1 == buf.size && return false e2 = endofline(buf, e1+1) line2 = edit_splice!(buf, e1 => e2) # delete whole next line line2 = line2[2:end]*'\n' # don't include leading '\n' - b = beginofline(buf) + b = beginofline(buf, first(reg)) edit_splice!(buf, b => b, line2) true end +# return the region if active, or the current position as a Region otherwise +region_if_active(s)::Region = is_region_active(s) ? region(s) : position(s)=>position(s) function edit_transpose_lines_up!(s::MIState) set_action!(s, :edit_transpose_lines_up!) - if edit_transpose_lines_up!(buffer(s)) + if edit_transpose_lines_up!(buffer(s), region_if_active(s)) refresh_line(s) else # beeping would be too noisy here @@ -993,7 +995,7 @@ end function edit_transpose_lines_down!(s::MIState) set_action!(s, :edit_transpose_lines_down!) - if edit_transpose_lines_down!(buffer(s)) + if edit_transpose_lines_down!(buffer(s), region_if_active(s)) refresh_line(s) else :ignore diff --git a/test/lineedit.jl b/test/lineedit.jl index 3c42906185a13..51e2a692025f9 100644 --- a/test/lineedit.jl +++ b/test/lineedit.jl @@ -1,7 +1,7 @@ # This file is a part of Julia. License is MIT: https://julialang.org/license using Base.LineEdit -using Base.LineEdit: edit_insert, buffer, content, setmark, getmark +using Base.LineEdit: edit_insert, buffer, content, setmark, getmark, region isdefined(Main, :TestHelpers) || @eval Main include(joinpath(dirname(@__FILE__), "TestHelpers.jl")) using Main.TestHelpers @@ -802,19 +802,33 @@ end end @testset "edit_transpose_lines_{up,down}!" begin + transpose_lines_up!(buf) = LineEdit.edit_transpose_lines_up!(buf, position(buf)=>position(buf)) + transpose_lines_up_reg!(buf) = LineEdit.edit_transpose_lines_up!(buf, region(buf)) + transpose_lines_down!(buf) = LineEdit.edit_transpose_lines_down!(buf, position(buf)=>position(buf)) + transpose_lines_down_reg!(buf) = LineEdit.edit_transpose_lines_down!(buf, region(buf)) + local buf buf = IOBuffer() + write(buf, "l1\nl2\nl3") seek(buf, 0) - @test LineEdit.edit_transpose_lines_up!(buf) == false - @test transform!(LineEdit.edit_transpose_lines_up!, buf) == ("l1\nl2\nl3", 0, 0) - @test transform!(LineEdit.edit_transpose_lines_down!, buf) == ("l2\nl1\nl3", 3, 0) - @test LineEdit.edit_transpose_lines_down!(buf) == true + @test transpose_lines_up!(buf) == false + @test transform!(transpose_lines_up!, buf) == ("l1\nl2\nl3", 0, 0) + @test transform!(transpose_lines_down!, buf) == ("l2\nl1\nl3", 3, 0) + @test transpose_lines_down!(buf) == true @test String(take!(copy(buf))) == "l2\nl3\nl1" - @test LineEdit.edit_transpose_lines_down!(buf) == false + @test transpose_lines_down!(buf) == false @test String(take!(copy(buf))) == "l2\nl3\nl1" # no change LineEdit.edit_move_right(buf) - @test transform!(LineEdit.edit_transpose_lines_up!, buf) == ("l2\nl1\nl3", 4, 0) + @test transform!(transpose_lines_up!, buf) == ("l2\nl1\nl3", 4, 0) LineEdit.edit_move_right(buf) - @test transform!(LineEdit.edit_transpose_lines_up!, buf) == ("l1\nl2\nl3", 2, 0) + @test transform!(transpose_lines_up!, buf) == ("l1\nl2\nl3", 2, 0) + + # multiline + @test transpose_lines_up_reg!(buf) == false + @test transform!(transpose_lines_down_reg!, buf) == ("l2\nl1\nl3", 5, 0) + Base.LineEdit.edit_exchange_point_and_mark(buf) + seek(buf, 1) + @test transpose_lines_up_reg!(buf) == false + @test transform!(transpose_lines_down_reg!, buf) == ("l3\nl2\nl1", 4, 8) end