module VER module Methods module Selection module_function def enter(text, old_mode, new_mode) unless old_mode.name =~ /^select/ text.store(self, :start, text.index(:insert)) end text.store(self, :mode, new_mode) text.store(self, :refresh, true) Undo.separator(text) refresh(text) end def leave(text, old_mode, new_mode) return if new_mode.name =~ /^select/ text.store(self, :mode, new_mode) text.store(self, :refresh, false) Undo.separator(text) clear(text) end def select_mode(text) if mode = text.store(self, :mode) mode.to_sym end end def refresh(text) return unless text.store(self, :refresh) return unless start = text.store(self, :start) text.tag_remove(:sel, 1.0, :end) case select_mode(text) when :select_char ; refresh_char(text, start) when :select_line ; refresh_line(text, start) when :select_block ; refresh_block(text, start) end end # Convert all characters within the selection to upper-case using # String#upcase. # Usually only works for alphabetic ASCII characters. def upper_case(text) Undo.record text do |record| each_selected_line text do |y, fx, tx| from, to = "#{y}.#{fx}", "#{y}.#{tx}" record.replace(from, to, text.get(from, to).upcase) end end refresh(text) end # Convert all characters within the selection to lower-case using # String#downcase. # Usually only works for alphabetic ASCII characters. def lower_case(text) Undo.record text do |record| each_line text do |y, fx, tx| from, to = "#{y}.#{fx}", "#{y}.#{tx}" record.replace(from, to, text.get(from, to).downcase) end end refresh(text) end # Toggle case within the selection. # This only works for alphabetic ASCII characters, no other encodings. def toggle_case(text) Undo.record text do |record| each_line text do |y, fx, tx| from, to = "#{y}.#{fx}", "#{y}.#{tx}" record.replace(from, to, text.get(from, to).tr('a-zA-Z', 'A-Za-z')) end end refresh(text) end def wrap(text) queue = [] chunks = [] each_line text do |y, fx, tx| queue << y chunks << text.get("#{y}.0", "#{y}.0 lineend") end lines = Control.wrap_lines_of(chunks.join(' ')).join("\n") from, to = queue.first, queue.last text.replace("#{from}.0", "#{to}.0 lineend", lines) finish(text) end # Delete selection without copying it. def delete(text) case select_mode(text) when :select_char, :select_block queue = text.tag_ranges(:sel).flatten Delete.delete(text, *queue) text.mark_set(:insert, queue.first) when :select_line from, to = text.tag_ranges(:sel).flatten to = "#{to} + 1 lines linestart" Delete.delete(text, from, to) text.mark_set(:insert, from) else Kernel.raise "Not in select mode?" end finish(text) end # Copy selection and delete it. def kill(text) case select_mode(text) when :select_char, :select_block queue = text.tag_ranges(:sel).flatten Delete.kill(text, *queue) text.mark_set(:insert, queue.first) when :select_line from, to = text.tag_ranges(:sel).flatten Clipboard.copy(text, text.get(from, to)) to = "#{to} + 1 lines linestart" Delete.delete(text, from, to) text.mark_set(:insert, from) else Kernel.raise "Not in select mode?" end finish(text) end def indent(text) indent_size = text.options.shiftwidth indent = ' ' * indent_size Undo.record text do |record| each_line text do |y, fx, tx| tx = fx + indent_size next if text.get("#{y}.#{fx}", "#{y}.#{tx}").empty? record.insert("#{y}.#{fx}", indent) end end refresh(text) end def unindent(text) indent_size = text.options.shiftwidth indent = ' ' * indent_size queue = [] each_line text do |y, fx, tx| tx = fx + indent_size left, right = "#{y}.#{fx}", "#{y}.#{tx}" next unless text.get(left, right) == indent queue << left << right end text.delete(*queue) refresh(text) end def evaluate(text) text.tag_ranges(:sel).each do |from, to| code = text.get(from, to) Control.stdout_capture_evaluate(code) do |res,out| text.insert("#{to} lineend", "\n%s%p" % [out, res] ) end end finish(text) end def copy(text) chunks = text.tag_ranges(:sel).map{|sel| text.get(*sel) } Clipboard.copy(text, chunks.size == 1 ? chunks.first : chunks) finish(text) end def pipe(text) paths = ENV['PATH'].split(':').map{|path| Pathname(path).expand_path } text.ask 'Pipe command: ' do |answer, action| case action when :complete current = answer.split.last paths.map{|path| (path/"*#{current}*").glob.select{|file| begin file = File.readlink(file) if File.symlink?(file) stat = File.stat(file) stat.file? && stat.executable? rescue Errno::ENOENT end } }.flatten.compact when :attempt pipe_execute(text, answer) finish(text) :abort end end end def comment(text) comment = "#{text.options.comment_line} " indent = nil lines = [] each_line text do |y, fx, tx| lines << y next if indent == 0 # can't get lower line = text.get("#{y}.#{fx}", "#{y}.#{tx}") next unless start = line =~ /\S/ indent ||= start indent = start if start < indent end indent ||= 0 Undo.record text do |record| lines.each do |y| record.insert("#{y}.#{indent}", comment) end end refresh(text) end def uncomment(text) comment = "#{text.options.comment_line} " regex = /#{Regexp.escape(comment)}/ Undo.record text do |record| each_line text do |y, fx, tx| from, to = "#{y}.#{fx}", "#{y}.#{tx}" line = text.get(from, to) if line.sub!(regex, '') record.replace(from, to, line) end end end refresh(text) end # Replace every character in the selection with the character entered. def replace_char(text, char = text.event.unicode) replace_with(text, char, full = true) text.minor_mode(:select_replace_char, select_mode(text)) end def replace_string(text) text.ask 'Replace selection with: ', do |answer, action| case action when :attempt if answer.size > 0 replace_with(text, answer, full = false) VER.message "replaced #{answer.size} chars" :abort else VER.warn "replacement required" end end end end def replace_with_clipboard(text) string = text.clipboard_get ranges = text.tag_ranges(:sel) from, to = ranges.first.first, ranges.last.last text.replace(from, to, string) finish(text) text.mark_set :insert, from end # TODO: find better name for +full+ def replace_with(text, string, full) origin = text.index(:insert) Undo.record text do |record| if full each_line text do |y, fx, tx| diff = tx - fx record.replace("#{y}.#{fx}", "#{y}.#{tx}", string * diff) end else string_size = string.size each_line text do |y, fx, tx| record.replace("#{y}.#{fx}", "#{y}.#{tx}", string) end end end text.mark_set(:insert, origin) end def finish(text, mode = nil) text.minor_mode(select_mode(text), :control) end def clear(text) text.store(self, :start, nil) text.tag_remove(:sel, '1.0', 'end') end # For every chunk selected, this yields the corresponding coordinates as # [from_y, from_x, to_y, to_x]. # It takes into account the current selection mode. # In many cases from_y and to_y are identical, but don't rely on this. # # @see each_selected_line def each(text) return Enumerator.new(self, :each, text) unless block_given? text.tag_ranges(:sel).each do |sel| (fy, fx), (ty, tx) = sel.map{|pos| pos.split('.').map(&:to_i) } case select_mode(text) when :select_char if fy == ty yield fy, fx, ty, tx elsif (ty - fy) == 1 efy, efx = text.index("#{fy}.#{fx} lineend").split sty, stx = text.index("#{ty}.#{tx} linestart").split yield fy, fx, efy, efx yield sty, stx, ty, tx else efy, efx = text.index("#{fy}.#{fx} lineend").split yield fy, fx, efy, efx ((fy + 1)...ty).each do |y| sy, sx = text.index("#{y}.0 linestart").split ey, ex = text.index("#{y}.0 lineend").split yield sy, sx, ey, ex end sty, stx = text.index("#{ty}.#{tx} linestart").split yield sty, stx, ty, tx end when :select_line fy.upto(ty) do |y| sy, sx = text.index("#{y}.0 linestart").split ey, ex = text.index("#{y}.0 lineend").split yield sy, sx, ey, ex end when :select_block yield fy, fx, ty, tx else Kernel.raise "Not in select mode?" end end end # Abstraction for [each] that yields one y coordinate per # # line. # You usually want to use this if you work with selections. def each_line(text) each text do |fy, fx, ty, tx| fy.upto(ty) do |y| yield y, fx, tx end end end def pipe_execute(text, *cmd) require 'open3' Open3.popen3(*cmd) do |si, sose, thread| queue = [] text.tag_ranges(:sel).each do |from, to| si.write(text.get(from, to)) queue << from << to end si.close output = sose.read return if queue.empty? Undo.record text do |record| record.delete(*queue) record.insert(queue.first, output.chomp) end end end # FIXME: yes, i know i'm calling `tag add` for every line, which makes # things slower, but it seems like there is a bug in the text widget. # So we aggregate the information into a single eval. def refresh_block(text, start) ly, lx, ry, rx = if text.compare('insert', '>', start) [*text.index('insert').split, *start.split] else [*start.split, *text.index('insert').split] end from_y, to_y = [ly, ry].sort from_x, to_x = [lx, rx].sort code = [%(set win "#{text.tk_pathname}")] from_y.upto to_y do |y| code << "$win tag add sel #{y}.#{from_x} #{y}.#{to_x + 1}" end Tk.execute_only(Tk::TclString.new(code.join("\n"))) end def refresh_char(text, start) if text.compare('insert', '>', start) text.tag_add(:sel, start, "insert + 1 chars") else text.tag_add(:sel, "insert", "#{start} + 1 chars") end end def refresh_line(text, start) if text.compare('insert', '>', start) text.tag_add(:sel, "#{start} linestart", 'insert lineend') else text.tag_add(:sel, 'insert linestart', "#{start} lineend") end end end end end