module VER class Text < Tk::Text include Methods MODE_CURSOR = { :insert => {insertbackground: 'red', blockcursor: false}, :control => {insertbackground: 'green', blockcursor: true}, :select_char => {insertbackground: 'yellow', blockcursor: true}, :select_line => {insertbackground: 'yellow', blockcursor: true}, :select_block => {insertbackground: 'yellow', blockcursor: true}, } attr_accessor :keymap, :view, :status, :filename def initialize(view, options = {}) super self.view = view keymap_name = VER.options.fetch(:keymap) self.keymap = Keymap.get(name: keymap_name, receiver: self) @selection_start = @highlight_thread = nil @pristine = true end def short_filename filename.sub(Dir.pwd + '/', '') if filename end def open_path(path) @filename = Pathname(path.to_s).expand_path begin self.value = @filename.read status.message "Opened #{short_filename}" rescue Errno::ENOENT clear status.message "Create #{short_filename}" end after_open end def open_empty clear status.message "[No File]" after_open end def after_open VER.opened_file(self) edit_reset if @pristine Thread.new do wait_visibility setup_highlight end else setup_highlight end focus mark_set :insert, '0.0' @pristine = false end def layout view.layout end def quit Tk.exit end def insert_index index(:insert).split('.').map(&:to_i) end def end_index index(:end).split('.').map(&:to_i) end # lines start from 1 # end is maximum lines + 1 def status_projection(into) format = "%s %d,%d %d%% [%s]" insert_y, insert_x = insert_index end_y, end_x = end_index percent = (100.0 / (end_y - 2)) * (insert_y - 1) percent = 100.0 if percent.nan? additional = [keymap.mode] syntax_name = @syntax.name if @syntax additional << syntax_name if syntax_name values = [ short_filename, insert_y, insert_x, percent, additional.join(' | '), ] into.value = format % values end TAG_ALL_MATCHING_OPTIONS = { foreground: '#f00', background: '#00f', } def tag_all_matching(name, regexp, options = {}) name = name.to_s if tag_exists?(name) tag_remove(name, '0.0', 'end') else options = TAG_ALL_MATCHING_OPTIONS.merge(options) TktNamedTag.new(self, name, options) end start = '0.0' while result = search_with_length(regexp, start, 'end - 1 chars') pos, len, match = result break if !result || len == 0 start = "#{pos} + #{len} chars" tag_add name, pos, start end end def tag_exists?(given_path) list = tk_split_simplelist(tk_send_without_enc('tag', 'names', None), false, true) list.include?(given_path) end # Wrap Tk methods to behave as we want and to generate events def mark_set(mark_name, index) super return unless mark_name == :insert Tk.event_generate(self, '<Movement>') end def refresh_selection return unless start = @selection_start now = index(:insert).split('.').map(&:to_i) left, right = [start, now].sort.map{|pos| pos.join('.') } tag_remove :sel, '0.0', 'end' case keymap.mode when :select_char tag_add :sel, left, "#{right} + 1 chars" when :select_line tag_add :sel, "#{left} linestart", "#{right} lineend + 1 chars" when :select_block ly, lx = left.split('.').map(&:to_i) ry, rx = right.split('.').map(&:to_i) from_y, to_y = [ly, ry].sort from_x, to_x = [lx, rx].sort from_y.upto to_y do |y| tag_add :sel, "#{y}.#{from_x}", "#{y}.#{to_x + 1}" end end end # fix the ruby definition of delete, Tk allows more than 2 indices def delete(*args) if args.size > 2 deleted = args.each_slice(2).map{|left, right| get(left, right) } else deleted = get(*args) end copy(deleted) tk_send_without_enc('delete', *args) touch! end def insert(*args) super touch! end def setup_highlight return unless filename return unless @syntax = Syntax.from_filename(filename) @highlight_thread = create_highlight_thread end def create_highlight_thread @highlight_thread = Thread.new{ this = Thread.current this[:pending] = 1 loop do if this[:pending] > 0 while this[:pending] > 0 this[:pending] -= 1 sleep 0.2 end refresh_highlight! else sleep 0.5 end end } end def refresh_highlight(lineno = 0) return unless @highlight_thread @highlight_thread[:pending] += 1 end def caret(keys=nil) if keys tk_call_without_enc('tk', 'caret', self, *hash_kv(keys)) self else lst = tk_split_list(tk_call_without_enc('tk', 'caret', self)) info = {} while key = lst.shift info[key[1..-1]] = lst.shift end info end end def focus super Tk.event_generate(self, '<Focus>') end def fast_tag_add(tag, *indices) tk_send_without_enc('tag', 'add', _get_eval_enc_str(tag), *indices) self end def set_window_title if filename home = Pathname(ENV['HOME']) dir, file = filename.split dir_relative_to_home = dir.relative_path_from(home) if dir_relative_to_home.to_s.start_with?('../') title = "#{file} (#{dir}) - VER" else title = "#{file} (#{dir_relative_to_home}) - VER" end else title = "[No Name] - VER" end VER.root['title'] = title end private def refresh_highlight! tag_all_matching('trailing_whitespace', /[ \t]+$/, foreground: '#000', background: '#f00') @syntax.highlight(self, value, lineno = 0) end def touch! Tk.event_generate(self, '<Modified>') end def copy(text) if text.respond_to?(:to_str) copy_string(text) elsif text.respond_to?(:to_ary) copy_array(text) else copy_fallback(text) end end def copy_string(text) TkClipboard.set(text = text.to_str) copy_message text.count("\n"), text.size end def copy_array(text) TkClipboard.set(text, type: Array) copy_message text.size, text.reduce(0){|s,v| s + v.size } end def copy_fallback(text) TkClipboard.set(text) status.message "Copied unkown entity of class %p" % [text.class] end def copy_message(lines, chars) lines_desc = lines == 1 ? 'line' : 'lines' chars_desc = chars == 1 ? 'character' : 'characters' msg = "copied %d %s of %d %s" % [lines, lines_desc, chars, chars_desc] status.message msg end def paste_continous(text) if text =~ /\n/ mark_set :insert, 'insert lineend' insert :insert, "\n" insert :insert, text.chomp else insert :insert, text end end def paste_tk_array(tk_array) chunks = Tk.send(:simplelist, tk_array) insert_y, insert_x = index(:insert).split('.').map(&:to_i) chunks.each_with_index do |chunk, idx| y = insert_y + idx insert "#{y}.#{insert_x}", chunk end end def mode=(name) keymap.mode = mode = name.to_sym cursor = MODE_CURSOR[mode] configure cursor return unless status && color = cursor[:insertbackground] style = status.style Tk::Tile::Style.configure style, fieldbackground: color end def load_theme(name) return unless @syntax return unless found = Theme.find(name) @syntax.theme = Theme.load(found) refresh_highlight status.message "Theme #{found} loaded" end def load_syntax(name) return unless @syntax return unless found = Syntax.find(name) theme = @syntax.theme @syntax = Syntax.new(name, theme) refresh_highlight status.message "Syntax #{found} loaded" end def clear_selection @selection_start = nil tag_remove :sel, '0.0', 'end' end end end