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