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

- old
+ new

@@ -1,562 +1,90 @@ module VER + # Common funcionality for Tk::Text widgets, mostly with shortcuts and helpers + # for VER. class Text < Tk::Text - autoload :Index, 'ver/text/index' - include Keymapped + autoload :Index, 'ver/text/index' + autoload :Mark, 'ver/text/mark' + autoload :Position, 'ver/text/position' + autoload :Range, 'ver/text/range' + autoload :Selection, 'ver/text/selection' + autoload :Tag, 'ver/text/tag' + autoload :Insert, 'ver/text/insert' + autoload :End, 'ver/text/end' - 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}, - } + attr_reader :lock - 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) - - def initialize(buffer, options = {}) - @project_repo = @project_root = @highlighter = nil - - 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 - - widget_setup(buffer) + def execute(*args, &block) + super unless lock end - # 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 + def execute_only(*args, &block) + super unless lock 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 + def focus(*args) + super unless lock 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 + def place_forget(*args) + super unless lock 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}>" + "#<VER::Text %p>" % [tk_pathname] end - def value=(string) - super - touch!('1.0', 'end') - end - - def peer_create(buffer) - self.class.new(buffer, peer: self) - end - - def widget_setup(buffer) - self.buffer = buffer - @options = Options.new(:text, VER.options) - - @undoer = VER::Undo::Tree.new(self) - - self.major_mode = :Fundamental - - sync_mode_style - setup_tags - - @pristine = true - @syntax = nil - self.encoding = Encoding.default_internal - - 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) - VER.message(*args) + def index(position) + Index.new(self, super.to_s) end - def noop(*args) - # message "Noop %p in mode %p" % [args, keymap.mode] + def insert=(position) + at_insert.index = position end - def short_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 - buffer.layout - end - - def sync_mode_status - status.event :mode - end - - def sync_position_status - status.event :position, :percent - end - - def sync_encoding_status - status.event :encoding - end - - 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 = {}) - name = name.to_s - options = TAG_ALL_MATCHING_OPTIONS.merge(options) - from, to = options.values_at(:from, :to) - - if tag_exists?(name) - tag_remove(name, from, to) + def tag(name, *indices) + if indices.empty? + Tag.new(self, name) else - fg, bg = options.values_at(:foreground, :background) - tag_configure(name, foreground: fg, background: bg) + tag = Tag.new(self, name) + tag.add(*indices) + tag end - - search_all(regexp, from, to) do |match, match_from, match_to| - name = yield(match, match_from, match_to) if block_given? - tag_add name, match_from, match_to - end end - 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, start, stop, :count) - pos, len = result - return if !pos || len == 0 - - from = index(pos) - to = index("#{pos} + #{len} chars") - match = get(from, to) - - yield(match, from, to) - - start = to - end + def tags(index = Tk::None) + tag_names(index).map{|name| Tag.new(self, name) } end - 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, start, stop, :count) - pos, len = result - break if !pos || len == 0 - - from = index(pos) - to = index("#{pos} + #{len} chars") - match = get(from, to) - - yield(match, from, to) - - start = from - end + def tag_ranges(tag_name) + super(tag_name).map{|first, last| range(first, last) } 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 + def range(first, last) + Range.new(self, Index.new(self, first), Index.new(self, last)) end - def tag_exists?(given_path) - tag_names.include?(given_path) - rescue RuntimeError => ex - false + def mark(name, index = nil, gravity = nil) + mark = Mark.new(self, name, index) + mark.gravity = gravity if gravity + mark 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>>') + def marks + mark_names.map{|name| Mark.new(self, name) } end - def insert(index, string, tag = Tk::None) - index = index(index) unless index.respond_to?(:to_index) - - Methods::Undo.record self do |record| - record.insert(index, string, tag) - end + def mark_next(index) + name = super + Mark.new(self, name) if name end - # Replaces the range of characters between index1 and index2 with the given - # characters and tags. - # See the section on [insert] for an explanation of the handling of the - # tag_list arguments, and the section on [delete] for an explanation - # of the handling of the indices. - # If index2 corresponds to an index earlier in the text than index1, an - # error will be generated. - # The deletion and insertion are arranged so that no unnecessary scrolling - # of the window or movement of insertion cursor occurs. - # In addition the undo/redo stack are correctly modified, if undo operations - # are active in the text widget. - # - # replace index1 index2 chars ?tagList chars tagList ...? - 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 - - Methods::Undo.record self do |record| - record.replace(index1, index2, string) - end - end - - def delete(*indices) - Methods::Delete.delete(self, *indices) - 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 - elsif name - title = "[#{name}] - VER" - else - title = "[No Name] - VER" - end - - VER.root.wm_title = title - end - - def setup_highlight - setup_highlight_for(Syntax.from_filename(filename)) if filename - end - - def setup_highlight_for(syntax) - return if encoding == Encoding::BINARY - return unless syntax - - 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!(*indices) - tag_add('ver.highlight.pending', *indices) if @syntax - Tk::Event.generate(self, '<<Modified>>') - end - - def load_theme(name) - return unless syntax - return unless found = Theme.find(name) - - syntax.theme = Theme.load(found) - 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) - self.syntax = Syntax.new(name.name, theme) - elsif found = Syntax.find(name) - self.syntax = Syntax.new(name, theme) - else - return false - end - - message "Syntax #{syntax.name} loaded" - end - - 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 - - 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: '#400' - end - - def setup_highlight_links - 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| - if index(from) <= pos && index(to) >= pos - break get(from, to) - end - } - - if uri - browser = ENV['BROWSER'] || ['links', '-g'] - system(*browser, uri) - message "%p opens the uri: %s" % [browser, uri] - end - end - end - - def tag_all_uris(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 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 - options.font.configure(given_options) - else - options.font - end + def mark_previous(index) + name = super + Mark.new(self, name) if name end end end