lib/ver/text.rb in ver-2009.10.14 vs lib/ver/text.rb in ver-2009.11.28
- old
+ new
@@ -1,169 +1,205 @@
module VER
class Text < Tk::Text
+ autoload :Index, 'ver/text/index'
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
+ 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,
+ ]
+ MATCH_WORD_RIGHT = /[^a-zA-Z0-9]+[a-zA-Z0-9'"{}\[\]\n-]/
+ MATCH_WORD_LEFT = /(^|\b)\S+(\b|$)/
+
+ attr_accessor :keymap, :view, :status
+ attr_reader :filename, :encoding, :pristine, :syntax
+
+ # attributes for diverse functionality
+ attr_accessor :selection_mode, :selection_start
+
def initialize(view, options = {})
super
self.view = view
- keymap_name = VER.options.fetch(:keymap)
+ keymap_name = VER.options.keymap
self.keymap = Keymap.get(name: keymap_name, receiver: self)
- @selection_start = @highlight_thread = nil
+ apply_mode_style(keymap.mode) # for startup
+ setup_tags
+
+ self.selection_start = nil
@pristine = true
+ @syntax = nil
+ @options = Options.new(:text, VER.options)
+ @encoding = Encoding.default_internal
+ @dirty_indices = []
+
+ self.mode = keymap.mode
end
- def short_filename
- filename.sub(Dir.pwd + '/', '') if filename
+ def index(idx)
+ Index.new(self, execute('index', idx).to_s)
end
- def open_path(path)
- @filename = Pathname(path.to_s).expand_path
+ def message(*args)
+ status.message(*args)
+ end
- begin
- self.value = @filename.read
- status.message "Opened #{short_filename}"
- rescue Errno::ENOENT
- clear
- status.message "Create #{short_filename}"
- end
-
- after_open
+ def noop(*args)
+ message "Noop %p in mode %p" % [args, keymap.mode]
end
- def open_empty
- clear
- status.message "[No File]"
- after_open
+ def short_filename
+ filename.sub(Dir.pwd + '/', '') if filename
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
+ def filename=(path)
+ @filename = Pathname(path.to_s).expand_path
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]"
+ format = "%s %s %s [%s]"
- insert_y, insert_x = insert_index
- end_y, end_x = end_index
+ top, bot = yview
- percent = (100.0 / (end_y - 2)) * (insert_y - 1)
- percent = 100.0 if percent.nan?
+ if top < 0.5
+ percent = '[top]'
+ elsif bot > 99.5
+ percent = '[bot]'
+ else
+ percent = "#{bot.to_i}%"
+ end
additional = [keymap.mode]
- syntax_name = @syntax.name if @syntax
+ syntax_name = syntax.name if syntax
additional << syntax_name if syntax_name
+ additional << @encoding
values = [
short_filename,
- insert_y, insert_x,
+ index(:insert).idx,
percent,
additional.join(' | '),
]
into.value = format % values
end
- TAG_ALL_MATCHING_OPTIONS = {
- foreground: '#f00',
- background: '#00f',
- }
+ 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, '0.0', 'end')
+ tag_remove(name, from, to)
else
- options = TAG_ALL_MATCHING_OPTIONS.merge(options)
- TktNamedTag.new(self, name, options)
+ fg, bg = options.values_at(:foreground, :background)
+ tag_configure(name, foreground: fg, background: bg)
end
- start = '0.0'
- while result = search_with_length(regexp, start, 'end - 1 chars')
- pos, len, match = result
- break if !result || len == 0
+ 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
+ end
+ end
- start = "#{pos} + #{len} chars"
- tag_add name, pos, start
+ 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
+
+ while result = search(regexp, from, to, :count)
+ pos, len = result
+ break if !pos || len == 0
+
+ match = get(pos, "#{pos} + #{len} chars")
+ from = "#{pos} + #{len} chars"
+
+ yield(match, pos, from)
end
end
+ def rsearch_all(regexp, from = 'end', to = '1.0')
+ return Enumerator.new(self, :rsearch_all, regexp, from) unless block_given?
+
+ while result = rsearch(regexp, from, to, :count)
+ pos, len = result
+ break if !pos || len == 0
+
+ match = get(pos, "#{pos} + #{len} chars")
+ from = index("#{pos} - #{len} chars")
+
+ yield(match, pos, from)
+ end
+ end
+
def tag_exists?(given_path)
- list = tk_split_simplelist(tk_send_without_enc('tag', 'names', None), false, true)
- list.include?(given_path)
+ tag_names.include?(given_path)
+ rescue RuntimeError => ex
+ false
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>')
+ Tk::Event.generate(self, '<<Movement>>')
end
def refresh_selection
- return unless start = @selection_start
+ 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'
+ now = index(:insert)
+ left, right = [start, now].sort
+ tag_remove :sel, '1.0', 'end'
- case keymap.mode
+ case selection_mode
when :select_char
tag_add :sel, left, "#{right} + 1 chars"
when :select_line
- tag_add :sel, "#{left} linestart", "#{right} lineend + 1 chars"
+ tag_add :sel, "#{left} linestart", "#{right} lineend"
when :select_block
- ly, lx = left.split('.').map(&:to_i)
- ry, rx = right.split('.').map(&:to_i)
+ ly, lx = left.split
+ ry, rx = right.split
from_y, to_y = [ly, ry].sort
from_x, to_x = [lx, rx].sort
from_y.upto to_y do |y|
@@ -180,74 +216,48 @@
deleted = get(*args)
end
copy(deleted)
- tk_send_without_enc('delete', *args)
+ execute('delete', *args)
- touch!
+ touch!(*args)
end
def insert(*args)
super
- touch!
+ touch!(args.first)
end
- def setup_highlight
- return unless filename
- return unless @syntax = Syntax.from_filename(filename)
-
- @highlight_thread = create_highlight_thread
+ # 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, *rest)
+ super
+ touch!(*index(index1).upto(index(index2)).to_a)
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>')
+ Tk::Event.generate(self, '<<Focus>>')
end
def fast_tag_add(tag, *indices)
- tk_send_without_enc('tag', 'add', _get_eval_enc_str(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'])
@@ -261,114 +271,163 @@
end
else
title = "[No Name] - VER"
end
- VER.root['title'] = title
+ VER.root.wm_title = title
end
- private
+ def setup_highlight
+ return unless filename
+ return if @encoding == Encoding::BINARY
- 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)
+ if @syntax = Syntax.from_filename(filename)
+ defer{ syntax.highlight(self, value) }
end
end
- def copy_string(text)
- TkClipboard.set(text = text.to_str)
-
- copy_message text.count("\n"), text.size
+ 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 copy_array(text)
- TkClipboard.set(text, type: Array)
-
- copy_message text.size, text.reduce(0){|s,v| s + v.size }
+ def schedule_highlight(options = {})
+ return unless @syntax
+ schedule_highlight!
end
- def copy_fallback(text)
- TkClipboard.set(text)
+ private
- status.message "Copied unkown entity of class %p" % [text.class]
+ def schedule_highlight!(*args)
+ defer do
+ syntax.highlight(self, value)
+ tag_all_trailing_whitespace
+ tag_all_uris
+ end
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
+ # 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 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
+ # 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
+ Tk::Event.generate(self, '<<Modified>>')
end
def mode=(name)
keymap.mode = mode = name.to_sym
+ edit_separator
+ apply_mode_style(mode)
+ status_projection(status) if status
+ end
+ def apply_mode_style(mode)
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 syntax
return unless found = Theme.find(name)
- @syntax.theme = Theme.load(found)
- refresh_highlight
+ syntax.theme = Theme.load(found)
+ schedule_highlight
- status.message "Theme #{found} loaded"
+ 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)
+ elsif found = Syntax.find(name)
+ @syntax = Syntax.new(name, theme)
+ else
+ return false
+ end
+
+ schedule_highlight
+
+ message "Syntax #{@syntax.name} loaded"
+ end
+
+ def load_preferences
return unless @syntax
- return unless found = Syntax.find(name)
- theme = @syntax.theme
- @syntax = Syntax.new(name, theme)
- refresh_highlight
+ name = @syntax.name
+ file = VER.find_in_loadpath("preferences/#{name}.json")
+ @preferences = JSON.load(File.read(file))
+ rescue Errno::ENOENT, TypeError => ex
+ VER.error(ex)
+ end
- status.message "Syntax #{found} loaded"
+ def setup_tags
+ setup_highlight_trailing_whitespace
+ setup_highlight_links
end
- def clear_selection
- @selection_start = nil
- tag_remove :sel, '0.0', 'end'
+ def setup_highlight_trailing_whitespace
+ tag_configure 'invalid.trailing-whitespace', background: '#f00'
+ tag_all_trailing_whitespace
+ end
+
+ def setup_highlight_links
+ tag_configure 'markup.underline.link' , underline: true, foreground: '#00f'
+
+ 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
+
+ tag_all_uris
+ 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 defer
+ Tk::After.idle do
+ begin
+ yield
+ rescue Exception => ex
+ VER.error(ex)
+ end
+ end
+ end
+
+ def font(options)
+ VER.options[:font].configure options
end
end
end