lib/ver/text.rb in ver-2009.12.14 vs lib/ver/text.rb in ver-2010.02

- old
+ new

@@ -1,114 +1,235 @@ module VER class Text < Tk::Text autoload :Index, 'ver/text/index' - include Methods + include Keymapped - 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}, + None = Object.new + MATCH_WORD_RIGHT = /[^a-zA-Z0-9]+[a-zA-Z0-9'"{}\[\]\n-]/ + MATCH_WORD_LEFT = /(^|\b)\S+(\b|$)/ + MODE_STYLES = { + :insert => {insertbackground: 'red', blockcursor: false}, + /select/ => {insertbackground: 'yellow', blockcursor: true}, + /replace/ => {insertbackground: 'orange', blockcursor: true}, } - GUESS_ENCODING_ORDER = [ - Encoding::US_ASCII, - Encoding::UTF_8, - Encoding::Shift_JIS, - Encoding::EUC_JP, - Encoding::EucJP_ms, - Encoding::Big5, - Encoding::UTF_16BE, - Encoding::UTF_16LE, - Encoding::UTF_32BE, - Encoding::UTF_32LE, - Encoding::CP949, - Encoding::Emacs_Mule, - Encoding::EUC_KR, - Encoding::EUC_TW, - Encoding::GB18030, - Encoding::GBK, - Encoding::Stateless_ISO_2022_JP, - Encoding::CP51932, - Encoding::EUC_CN, - Encoding::GB12345, - Encoding::Windows_31J, - Encoding::MacJapanese, - Encoding::UTF8_MAC, - Encoding::BINARY, - ] + attr_accessor(:buffer, :status, :project_root, :project_repo, + :undoer, :pristine, :prefix_arg, :readonly) + attr_reader(:filename, :options, :snippets, :preferences, :store_hash, + :default_theme_config, :encoding, :syntax, :name) - MATCH_WORD_RIGHT = /[^a-zA-Z0-9]+[a-zA-Z0-9'"{}\[\]\n-]/ - MATCH_WORD_LEFT = /(^|\b)\S+(\b|$)/ + def initialize(buffer, options = {}) + @project_repo = @project_root = @highlighter = nil - attr_accessor :keymap, :view, :status - attr_reader :filename, :encoding, :pristine, :syntax, :undoer + if peer = options.delete(:peer) + @tag_commands = {} + @tk_parent = buffer + @store_hash = peer.store_hash + @default_theme_config = peer.default_theme_config + Tk.execute(peer.tk_pathname, 'peer', 'create', assign_pathname, options) + self.filename = peer.filename + configure(peer.configure) + else + @default_theme_config = nil + @store_hash = Hash.new{|h,k| h[k] = {} } + super + end - # attributes for diverse functionality - attr_accessor :selection_mode, :selection_start + widget_setup(buffer) + end - def initialize(view, options = {}) + # This is a noop, it simply provides a target with a sane name. + def update_prefix_arg(widget) + numbers = [] + + major_mode.event_history.reverse_each do |event| + break unless event[:sequence] =~ /^(\d+)$/ + numbers << $1 + end + + if numbers.any? && numbers != ['0'] + self.prefix_arg = numbers.reverse.join.to_i + else + self.prefix_arg = nil + end + end + + # Same as [prefix_arg], but returns 1 if there is no argument. + # Useful for [Move] methods and the like. + # Please note that calling this method is destructive. + # It will reset the state of the prefix_arg in order to avoid persistent + # arguments. + # So use it only once while your action is running, and store the result in a + # variable if you need it more than once. + def prefix_count + count = prefix_arg || 1 + update_prefix_arg(self) + count + end + + def persisted? + return false unless filename + return false unless filename.file? + require 'digest/md5' + + on_disk = Digest::MD5.hexdigest(filename.read) + in_memory = Digest::MD5.hexdigest(value) + on_disk == in_memory + end + + def store(namespace, key, value = None) + if None == value + @store_hash[namespace][key] + else + @store_hash[namespace][key] = value + end + end + + def inspect + details = { + mode: major_mode + }.map{|key, value| "%s=%p" % [key, value ] }.join(' ') + "#<VER::Text #{details}>" + end + + def value=(string) super - self.view = view - @options = Options.new(:text, VER.options) + touch!('1.0', 'end') + end - keymap_name = @options.keymap - self.keymap = Keymap.get(name: keymap_name, receiver: self) + def peer_create(buffer) + self.class.new(buffer, peer: self) + end - apply_mode_style(keymap.mode) # for startup - setup_tags + def widget_setup(buffer) + self.buffer = buffer + @options = Options.new(:text, VER.options) @undoer = VER::Undo::Tree.new(self) - self.selection_start = nil + self.major_mode = :Fundamental + + sync_mode_style + setup_tags + @pristine = true @syntax = nil - @encoding = Encoding.default_internal - @dirty_indices = [] + self.encoding = Encoding.default_internal - self.mode = keymap.mode + event_setup end + def event_setup + bind '<<EnterMinorMode>>' do |event| + sync_mode_status + sync_mode_style(event.detail) + end + + bind '<<Modified>>' do |event| + see :insert + sync_position_status + end + + bind '<<Movement>>' do |event| + see :insert + Methods::Selection.refresh(self) + show_matching_brace + sync_position_status + end + + bind('<FocusIn>') do |event| + on_focus_in(event) + Tk.callback_break + end + + bind('<FocusOut>') do |event| + on_focus_out(event) + Tk.callback_break + end + + bind '<Destroy>' do |event| + VER.cancel_block(@highlighter) + end + end + + def on_focus_in(event) + Dir.chdir(filename.dirname.to_s) if options.auto_chdir + set_window_title + see(:insert) + Tk::Tile::Style.configure(buffer.style, border: 1, background: '#f00') + end + + def on_focus_out(event) + Tk::Tile::Style.configure(buffer.style, border: 1, background: '#fff') + end + + def pristine? + @pristine + end + def index(idx) Index.new(self, execute('index', idx).to_s) end def message(*args) - status.message(*args) + VER.message(*args) end def noop(*args) - message "Noop %p in mode %p" % [args, keymap.mode] + # message "Noop %p in mode %p" % [args, keymap.mode] end def short_filename - filename.sub(Dir.pwd + '/', '') if filename + if filename + if root = @project_root + filename.relative_path_from(root).to_s + else + filename.sub(Dir.pwd + '/', '').to_s + end + elsif name + name.to_s + end end + def name=(name) + @name = name + status.event :filename if status + end + def filename=(path) @filename = Pathname(path.to_s).expand_path + status.event :filename if status end + def syntax=(syn) + @syntax = syn + status.event :syntax if syn && status + end + + def encoding=(enc) + @encoding = enc + status.event :encoding if enc && status + end + def layout - view.layout + buffer.layout end - def status_projection(into) - format = options.statusline.dup + def sync_mode_status + status.event :mode + end - format.gsub!(/%([[:alpha:]]+)/, '#{\1()}') - format.gsub!(/%_([[:alpha:]]+)/, '#{(_ = \1()) ? " #{_}" : ""}') - format.gsub!(/%([+-]?\d+)([[:alpha:]]+)/, '#{\2(\1)}') - format = "%{#{format}}" + def sync_position_status + status.event :position, :percent + end - context = Status::Context.new(self) - line = context.instance_eval(format) + def sync_encoding_status + status.event :encoding + end - into.value = line - rescue => ex - puts ex, ex.backtrace + def sync_percent_status + status.event :percent end TAG_ALL_MATCHING_OPTIONS = { from: '1.0', to: 'end - 1 chars' } def tag_all_matching(name, regexp, options = {}) @@ -123,43 +244,65 @@ tag_configure(name, foreground: fg, background: bg) end search_all(regexp, from, to) do |match, match_from, match_to| name = yield(match, match_from, match_to) if block_given? - fast_tag_add name, match_from, match_to + tag_add name, match_from, match_to end end - def search_all(regexp, from = '1.0', to = 'end - 1 chars') - return Enumerator.new(self, :search_all, regexp, from) unless block_given? - from, to = from.to_s, to.to_s + def search_all(regexp, start = '1.0', stop = 'end - 1 chars') + unless block_given? + return Enumerator.new(self, :search_all, regexp, start, stop) + end - while result = search(regexp, from, to, :count) + while result = search(regexp, start, stop, :count) pos, len = result - break if !pos || len == 0 + return if !pos || len == 0 - match = get(pos, "#{pos} + #{len} chars") - from = "#{pos} + #{len} chars" + from = index(pos) + to = index("#{pos} + #{len} chars") + match = get(from, to) - yield(match, pos, from) + yield(match, from, to) + + start = to end end - def rsearch_all(regexp, from = 'end', to = '1.0') - return Enumerator.new(self, :rsearch_all, regexp, from) unless block_given? + def rsearch_all(regexp, start = 'end', stop = '1.0') + unless block_given? + return Enumerator.new(self, :rsearch_all, regexp, start, stop) + end - while result = rsearch(regexp, from, to, :count) + while result = rsearch(regexp, start, stop, :count) pos, len = result break if !pos || len == 0 - match = get(pos, "#{pos} + #{len} chars") - from = index("#{pos} - #{len} chars") + from = index(pos) + to = index("#{pos} + #{len} chars") + match = get(from, to) - yield(match, pos, from) + yield(match, from, to) + + start = from end end + def up_down_line(count) + insert = index(:insert) + + @udl_pos_orig = insert if @udl_pos_prev != insert + + lines = count(@udl_pos_orig, insert, :displaylines) + target = index("#@udl_pos_orig + #{lines + count} displaylines") + @udl_pos_prev = target + + @udl_pos_orig = target if target.x == @udl_pos_orig.x + target + end + def tag_exists?(given_path) tag_names.include?(given_path) rescue RuntimeError => ex false end @@ -169,15 +312,15 @@ super return unless mark_name == :insert Tk::Event.generate(self, '<<Movement>>') end - def insert(index, string) + def insert(index, string, tag = Tk::None) index = index(index) unless index.respond_to?(:to_index) - undo_record do |record| - record.insert(index, string) + Methods::Undo.record self do |record| + record.insert(index, string, tag) end end # Replaces the range of characters between index1 and index2 with the given # characters and tags. @@ -195,27 +338,19 @@ def replace(index1, index2, string) index1 = index(index1) unless index1.respond_to?(:to_index) index2 = index(index2) unless index2.respond_to?(:to_index) return if index1 == index2 - undo_record do |record| + Methods::Undo.record self do |record| record.replace(index1, index2, string) end end - def focus - super - Tk::Event.generate(self, '<<Focus>>') + def delete(*indices) + Methods::Delete.delete(self, *indices) end - def fast_tag_add(tag, *indices) - execute('tag', 'add', tag, *indices) - self - rescue RuntimeError => ex - VER.error(ex) - end - def set_window_title if filename home = Pathname(ENV['HOME']) dir, file = filename.split dir_relative_to_home = dir.relative_path_from(home) @@ -223,131 +358,165 @@ if dir_relative_to_home.to_s.start_with?('../') title = "#{file} (#{dir}) - VER" else title = "#{file} (#{dir_relative_to_home}) - VER" end + elsif name + title = "[#{name}] - VER" else title = "[No Name] - VER" end VER.root.wm_title = title end def setup_highlight - return unless filename - return if @encoding == Encoding::BINARY - - if @syntax = Syntax.from_filename(filename) - defer{ syntax.highlight(self, value) } - status_projection(status) if status - end + setup_highlight_for(Syntax.from_filename(filename)) if filename end - def schedule_line_highlight(raw_index) - return unless @syntax - index = index(raw_index) - schedule_line_highlight!(index.y - 1, index.linestart, index.lineend) - end + def setup_highlight_for(syntax) + return if encoding == Encoding::BINARY + return unless syntax - def schedule_highlight(options = {}) - return unless @syntax - schedule_highlight! + self.syntax = syntax + VER.cancel_block(@highlighter) + + interval = options.syntax_highlight_interval.to_int + @highlighter = VER.when_inactive_for(interval){ + handle_pending_syntax_highlights + } + + touch!('1.0', 'end') + + sync_mode_status end # TODO: maybe we can make this one faster when many lines are going to be # highlighted at once by bundling them. - def touch!(*args) - args.each{|arg| schedule_line_highlight(arg) } if @syntax + def touch!(*indices) + tag_add('ver.highlight.pending', *indices) if @syntax Tk::Event.generate(self, '<<Modified>>') end - private - - def schedule_highlight!(*args) - defer do - syntax.highlight(self, value) - tag_all_trailing_whitespace - tag_all_uris - end - end - - # TODO: only tag the current line. - def schedule_line_highlight!(line, from, to) - defer do - syntax.highlight(self, get(from, to), line, from, to) - tag_all_trailing_whitespace(from: from, to: to) - tag_all_uris(from: from, to: to) - end - end - - def mode=(name) - keymap.mode = mode = name.to_sym - undo_separator - apply_mode_style(mode) - status_projection(status) if status - end - - def apply_mode_style(mode) - cursor = MODE_CURSOR[mode] - return unless cursor - 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) - schedule_highlight + touch!('1.0', 'end') message "Theme #{found} loaded" end def load_syntax(name) return false unless syntax theme = syntax.theme if name.is_a?(Syntax) - @syntax = Syntax.new(name.name, theme) + self.syntax = Syntax.new(name.name, theme) elsif found = Syntax.find(name) - @syntax = Syntax.new(name, theme) + self.syntax = Syntax.new(name, theme) else return false end - schedule_highlight + message "Syntax #{syntax.name} loaded" + end - message "Syntax #{@syntax.name} loaded" + def handle_pending_syntax_highlights + ignore_tags = %w[ver.highlight.pending sel] + tag_ranges('ver.highlight.pending').each do |from, to| + (tag_names(from) - ignore_tags).each do |tag_name| + tag_from, _ = tag_prevrange(tag_name, from) + from = tag_from if tag_from && from > tag_from + end + + (tag_names(to) - ignore_tags).each do |tag_name| + _, tag_to = tag_nextrange(tag_name, to) + to = tag_to if tag_to && to < tag_to + end + + from, to = index(from).linestart, index(to).lineend + lineno = from.y - 1 + syntax.highlight(self, lineno, from, to) + tag_all_trailing_whitespace(from: from, to: to) + tag_all_uris(from: from, to: to) + tag_remove('ver.highlight.pending', from, to) + end end + def default_theme_config=(config) + @default_theme_config = config + sync_mode_style + end + + def ask(prompt, options = {}, &action) + options[:caller] ||= self + VER.minibuf.ask(prompt, options, &action) + end + def load_preferences - return unless @syntax + return unless syntax - name = @syntax.name - file = VER.find_in_loadpath("preferences/#{name}.json") - @preferences = JSON.load(File.read(file)) + name = syntax.name + return unless file = VER.find_in_loadpath("preferences/#{name}.rb") + @preferences = eval(file.read) rescue Errno::ENOENT, TypeError => ex VER.error(ex) end + def load_snippets + return unless syntax + + name = syntax.name + return unless file = VER.find_in_loadpath("snippets/#{name}.rb") + @snippets = eval(file.read) + rescue Errno::ENOENT, TypeError => ex + VER.error(ex) + end + + def sync_mode_style(given_mode = nil) + config = (default_theme_config || {}).merge(blockcursor: false) + + modes = given_mode ? [given_mode] : major_mode.minors + + modes.each do |mode| + mode = MinorMode[mode] + + MODE_STYLES.each do |pattern, style| + config.merge!(style) if pattern === mode.name + end + end + + configure(config) + return unless status && color = config[:insertbackground] + + status.style = { + background: cget(:background), + foreground: color, + } + end + + private + def setup_tags setup_highlight_trailing_whitespace setup_highlight_links + setup_highlight_pending end + def setup_highlight_pending + # tag_configure 'ver.highlight.pending', underline: true + end + def setup_highlight_trailing_whitespace - tag_configure 'invalid.trailing-whitespace', background: '#f00' - tag_all_trailing_whitespace + tag_configure 'invalid.trailing-whitespace', background: '#400' end def setup_highlight_links - tag_configure 'markup.underline.link' , underline: true, foreground: '#00f' + tag_configure 'markup.underline.link' , underline: true, foreground: '#0ff' tag_bind('markup.underline.link', '<1>') do |event| pos = index("@#{event.x},#{event.y}") uri = tag_ranges('markup.underline.link').find{|from, to| @@ -360,28 +529,26 @@ browser = ENV['BROWSER'] || ['links', '-g'] system(*browser, uri) message "%p opens the uri: %s" % [browser, uri] end end - - tag_all_uris end def tag_all_uris(given_options = {}) - tag_all_matching('markup.underline.link', /https?:\/\/[^)\]}\s'"]+/, given_options) + tag_all_matching('markup.underline.link', /https?:\/\/[^<>)\]}\s'"]+/, given_options) end def tag_all_trailing_whitespace(given_options = {}) tag_all_matching('invalid.trailing-whitespace', /[ \t]+$/, given_options) end - def defer - Tk::After.idle do - begin - yield - rescue Exception => ex - VER.error(ex) - end + def show_matching_brace(index = :insert) + tag = 'ver.highlight.brace' + tag_remove(tag, '1.0', 'end') + + if pos = Methods::Move.matching_brace_pos(self, index) + tag_configure(tag, background: '#ff0', foreground: '#00f') + tag_add(tag, 'insert', 'insert + 1 chars', pos, "#{pos} + 1 chars") end end def font(given_options = nil) if given_options