Files
2009-04-21 23:59:43 -04:00

503 lines
18 KiB
Ruby

#!/usr/bin/env ruby
require File.dirname(__FILE__) + "/../current_word"
require ENV['TM_SUPPORT_PATH'] + '/lib/ui'
require ENV['TM_SUPPORT_PATH'] + '/lib/escape'
require ENV['TM_SUPPORT_PATH'] + '/lib/osx/plist'
require 'rubygems'
require 'json'
require 'shellwords'
module TextMate
class Complete
IMAGES_FOLDER_NAME = 'icons'
IMAGES = {
"C" => "/Applications/TextMate.app/Contents/Resources/Bundle Item Icons/Commands.png",
"D" => "/Applications/TextMate.app/Contents/Resources/Bundle Item Icons/Drag Commands.png",
"L" => "/Applications/TextMate.app/Contents/Resources/Bundle Item Icons/Languages.png",
"M" => "/Applications/TextMate.app/Contents/Resources/Bundle Item Icons/Macros.png",
"P" => "/Applications/TextMate.app/Contents/Resources/Bundle Item Icons/Preferences.png",
"S" => "/Applications/TextMate.app/Contents/Resources/Bundle Item Icons/Snippets.png",
"T" => "/Applications/TextMate.app/Contents/Resources/Bundle Item Icons/Templates.png",
"Doc" => "/Applications/TextMate.app/Contents/Resources/Bundle Item Icons/Template Files.png",
}
def initialize
end
# 0-config completion command using environment variables for everything
def complete!
return choices unless choices
TextMate::UI.complete(choices, {:images => images, :extra_chars => extra_chars})
end
def tip!
# If there is no current_word, then check the next current_word
# If there really is no current_word, then show a menu of choices
chars = "a-zA-Z0-9" # Hard-coded into D2
chars += Regexp.escape(extra_chars) if extra_chars
current_word ||= Word.current_word chars, :both
result = nil
menu_choices = nil
choices = nil
choice = 0
[current_word, current_method_name, current_collection_name].each do |initial_filter|
next unless initial_filter and not initial_filter.empty?
# p initial_filter
choices = self.choices.select { |c| (c['match'] || c['display']) =~ /^#{Regexp.quote(initial_filter)}/ }
# p choices
break if choices and not choices.empty?
end
choices ||= self.choices
menu_choices = choices.map { |c| c['display'] }
choice = TextMate::UI.menu(menu_choices) if menu_choices and menu_choices.length > 1
if choice
result = choices[choice]
end
result = {'tool_tip' => 'No information'} unless result
TextMate::UI.tool_tip( result['tool_tip'], {:format => result['tool_tip_format'] || :text })
end
def choices
@choices ||= data['suggestions']
end
def choices= choice_array
@choices = array_to_suggestions(choice_array)
end
def images
@images = data['images'] || IMAGES
data['images']||{}.each_pair do |name,path|
@images[name] = path
next if File.exists? @images[name]
@images[name] = ENV['TM_BUNDLE_SUPPORT'] + "/#{IMAGES_FOLDER_NAME}/" + path
next if File.exists? @images[name]
@images[name] = ENV['TM_SUPPORT_PATH'] + "/#{IMAGES_FOLDER_NAME}/" + path
next if File.exists? @images[name]
end
@images
end
def tool_tip_prefix
@tool_tip_prefix ||= data['tool_tip_prefix']
end
def extra_chars
ENV['TM_COMPLETIONS_EXTRACHARS'] || data['extra_chars']
end
def chars
"a-zA-Z0-9"
end
private
def data(raw_data=nil)
fix_legacy
raw_data ||= read_data
return {} unless raw_data and not raw_data.empty?
@data = parse_data raw_data
return @data
end
def read_data
raw_data = read_file
raw_data ||= read_string
raw_data
end
def read_file
paths = [ENV['TM_COMPLETIONS_FILE']] if ENV['TM_COMPLETIONS_FILE']
paths ||= Shellwords.shellwords( ENV['TM_COMPLETIONS_FILES'] ) if ENV.has_key?('TM_COMPLETIONS_FILES')
return nil unless paths
paths.map do |path|
next unless path and not path.empty?
path = ENV['TM_BUNDLE_SUPPORT'] + '/' + path unless File.exists? path
next unless File.exists? path
{ :data => File.read(path),
:format => path.scan(/\.([^\.]+)$/).last.last
}
end
end
def read_string
[{:data => ENV['TM_COMPLETIONS'],
:format => ENV['TM_COMPLETIONS_SPLIT']
}]
end
attr_accessor :filepath
def parse_data(raw_datas)
return @parsed if @parsed
parsed = {"suggestions"=>[]}
raw_datas.each do |raw_data|
suggestions = parsed['suggestions']
case raw_data[:format]
when 'plist'
par = parse_plist(raw_data)
when 'json'
par = parse_json(raw_data)
when "txt"
raw_data[:format] = "\n"
par = parse_string(raw_data)
when nil
raw_data[:format] = ","
par = parse_string(raw_data)
else
par = parse_string(raw_data)
end
if par['tool_tip_prefix']
par['suggestions'] = par['suggestions'].map do |suggestion|
suggestion['tool_tip'] = par['tool_tip_prefix'] + suggestion['tool_tip']
suggestion
end
end
parsed.merge! par
parsed['suggestions'] = suggestions + parsed['suggestions']
end
@parsed = parsed
end
def parse_string(raw_data)
return {} unless raw_data and raw_data[:data]
return raw_data[:data] unless raw_data[:data].respond_to? :to_str
raw_data[:data] = raw_data[:data].to_str
data = {}
data['suggestions'] = array_to_suggestions(raw_data[:data].split(raw_data[:format]))
data
end
def parse_plist(raw_data)
OSX::PropertyList.load(raw_data[:data])
end
def parse_json(raw_data)
JSON.parse(raw_data[:data])
end
def array_to_suggestions(suggestions)
suggestions.delete('')
suggestions.map! do |c|
{'display' => c}
end
suggestions
end
def current_method_name
# Regex for finding a method or function name that's close to your caret position using Word.current_word
# TODO: Allow completion prefs to define their own Complete.tip! method_name
characters = "a-zA-Z0-9" # Hard-coded into D2
characters += Regexp.escape(extra_chars) if extra_chars
regex = %r/
(?> [^\(\)]+ | \) (?> [^\(\)]+ | \) (?> [^\(\)]+ | \) (?> [^\(\)]+ | \) (?> [^\(\)]+ | \) (?> [^\(\)]* ) \( | )+ \( | )+ \( | )+ \( | )+ \( | )+
(?: \(([#{characters}]+) )?/ix
Word.current_word(regex,:left)
end
def current_collection_name
characters = "a-zA-Z0-9" # Hard-coded into D2
characters += Regexp.escape(extra_chars) if extra_chars
regex = %r/
(?> [^\[\]]+ | \] (?> [^\[\]]+ | \] (?> [^\[\]]+ | \] (?> [^\[\]]+ | \] (?> [^\[\]]+ | \] (?> [^\[\]]* ) \[ | )+ \[ | )+ \[ | )+ \[ | )+ \[ | )+
(?: \[([#{characters}]+) )?/ix
Word.current_word(regex,:left)
end
def fix_legacy
ENV['TM_COMPLETIONS_SPLIT'] ||= ENV['TM_COMPLETIONS_split']
end
end
end
if __FILE__ == $0
`open "txmt://open?url=file://$TM_FILEPATH"` #For testing purposes, make this document the topmost so that the complete popup works
ENV['WEB_PREVIEW_RUBY']='NO-RUN'
require "test/unit"
# require "complete"
class TestComplete < Test::Unit::TestCase
def setup
@string_raw = 'ad(),adipisicing,aliqua,aliquip,amet,anim,aute,cillum,commodo,consectetur,consequat,culpa,cupidatat,deserunt,do,dolor,dolore,Duis,ea,eiusmod,elit,enim,esse,est,et,eu,ex,Excepteur,exercitation,fugiat,id,in,incididunt,ipsum,irure,labore,laboris,laborum,Lorem,magna,minim,mollit,nisi,non,nostrud,nulla,occaecat,officia,pariatur,proident,qui,quis,reprehenderit,sed,sint,sit,sunt,tempor,ullamco,Ut,ut,velit,veniam,voluptate,'
@plist_raw = <<-'PLIST'
{ suggestions = (
{ display = moo; image = Drag; insert = "(${1:one}, ${2:one}, ${3:three}${4:, ${5:five}, ${6:six}})"; tool_tip = "moo(one, two, four[, five])\n This method does something or other maybe.\n Insert longer description of it here."; },
{ display = foo; image = Macro; insert = "(${1:one}, \"${2:one}\", ${3:three}${4:, ${5:five}, ${6:six}})"; tool_tip = "foo(one, two)\n This method does something or other maybe.\n Insert longer description of it here."; },
{ display = bar; image = Command; insert = "(${1:one}, ${2:one}, \"${3:three}\"${4:, \"${5:five}\", ${6:six}})"; tool_tip = "bar(one, two[, three])\n This method does something or other maybe.\n Insert longer description of it here."; }
);
extra_chars = '.';
images = {
Command = "Commands.png";
Drag = "Drag Commands.png";
Language = "Languages.png";
Macro = "Macros.png";
Preference = "Preferences.png";
Snippet = "Snippets.png";
Template = "Template Files.png";
Templates = "Templates.png";
};
}
PLIST
@json_raw = <<-'JSON'
{
"extra_chars": "-_$.",
"suggestions": [
{ "display": ".moo", "image": "", "insert": "(${1:one}, ${2:one}, ${3:three}${4:, ${5:five}, ${6:six}})", "tool_tip": "moo(one, two, four[, five])\n This method does something or other maybe.\n Insert longer description of it here." },
{ "display": "foo", "image": "", "insert": "(${1:one}, \"${2:one}\", ${3:three}${4:, ${5:five}, ${6:six}})", "tool_tip": "foo(one, two)\n This method does something or other maybe.\n Insert longer description of it here." },
{ "display": "bar", "image": "", "insert": "(${1:one}, ${2:one}, \"${3:three}\"${4:, \"${5:five}\", ${6:six}})", "tool_tip": "bar(one, two[, three])\n This method does something or other maybe.\n Insert longer description of it here." }
],
"images": {
"String" : "String.png",
"RegExp" : "RegExp.png",
"Number" : "Number.png",
"Array" : "Array.png",
"Function": "Function.png",
"Object" : "Object.png",
"Node" : "Node.png",
"NodeList": "NodeList.png"
}
}
JSON
end
def test_basic_complete
ENV['TM_COMPLETIONS'] = @string_raw
assert_equal ENV['TM_COMPLETIONS'].split(','), TextMate::Complete.new.choices.map{|c| c['display']}
assert_equal TextMate::Complete::IMAGES, TextMate::Complete.new.images
TextMate::Complete.new.complete!
end
#
def test_should_support_plist
ENV['TM_COMPLETIONS_SPLIT']='plist'
ENV['TM_COMPLETIONS'] = @plist_raw
TextMate::Complete.new.complete!
end
#
def test_should_support_json
ENV.delete 'TM_COMPLETIONS'
assert_nil(ENV['TM_COMPLETIONS'])
ENV.delete 'TM_COMPLETIONS_SPLIT'
assert_nil(ENV['TM_COMPLETIONS_SPLIT'])
ENV['TM_COMPLETIONS_SPLIT']='json'
ENV['TM_COMPLETIONS'] = @json_raw
fred = TextMate::Complete.new
assert_equal(3, fred.choices.length)
end
#
def test_should_be_able_to_modify_the_choices
ENV['TM_COMPLETIONS'] = @string_raw
fred = TextMate::Complete.new
assert_not_nil fred.choices
assert_equal ENV['TM_COMPLETIONS'].split(','), fred.choices.map{|c| c['display']}
fred.choices.reject!{|choice| choice['display'] !~ /^a/ }
assert_equal ENV['TM_COMPLETIONS'].split(',').grep(/^a/), fred.choices.map{|c| c['display']}
fred.choices=%w[fred is not my name]
assert_equal %w[fred is not my name], fred.choices.map{|c| c['display']}
end
#
def test_should_parse_files_based_on_extension_plist
ENV['TM_COMPLETIONS_FILE'] = '/tmp/completions_test.plist'
File.open(ENV['TM_COMPLETIONS_FILE'],'w'){|file| file.write @plist_raw }
assert File.exists?(ENV['TM_COMPLETIONS_FILE'])
fred = TextMate::Complete.new
assert_equal(['moo', 'foo', 'bar'], fred.choices.map{|c| c['display']})
end
#
def test_should_parse_files_based_on_extension_txt
ENV.delete 'TM_COMPLETIONS'
assert_nil(ENV['TM_COMPLETIONS'])
ENV.delete 'TM_COMPLETIONS_SPLIT'
assert_nil(ENV['TM_COMPLETIONS_SPLIT'])
ENV['TM_COMPLETIONS_FILE'] = '/tmp/completions_test.txt'
File.open(ENV['TM_COMPLETIONS_FILE'],'w'){|file| file.write @string_raw.gsub(',',"\n") }
assert File.exists?(ENV['TM_COMPLETIONS_FILE'])
fred = TextMate::Complete.new
assert_equal(@string_raw.split(','), fred.choices.map{|c| c['display']})
end
#
def test_should_parse_multiple_files
ENV.delete 'TM_COMPLETIONS'
assert_nil(ENV['TM_COMPLETIONS'])
ENV.delete 'TM_COMPLETIONS_SPLIT'
assert_nil(ENV['TM_COMPLETIONS_SPLIT'])
ENV.delete 'TM_COMPLETIONS_FILE'
assert_nil(ENV['TM_COMPLETIONS_FILE'])
ENV['TM_COMPLETIONS_FILES'] = "'/tmp/completions_test.txt' '/tmp/completions_test1.txt' '/tmp/completions_test2.txt'"
require 'shellwords'
Shellwords.shellwords( ENV['TM_COMPLETIONS_FILES'] ).each_with_index do |filepath,i|
File.open(filepath,'w'){|file| file.write @string_raw.gsub(',',"#{i}\n") }
assert File.exists?(filepath)
end
fred = TextMate::Complete.new
assert_equal(@string_raw.split(',').uniq.length * 3, fred.choices.map{|c| c['display']}.length )
end
#
def test_should_override_split_with_extension
ENV['TM_COMPLETIONS_SPLIT'] = ','
ENV['TM_COMPLETIONS_FILE'] = '/tmp/completions_test.plist'
File.open(ENV['TM_COMPLETIONS_FILE'],'w'){|file| file.write @plist_raw }
assert File.exists?(ENV['TM_COMPLETIONS_FILE'])
fred = TextMate::Complete.new
assert_equal(['moo', 'foo', 'bar'], fred.choices.map{|c| c['display']})
end
#
def test_should_get_extra_chars_from_var
ENV['TM_COMPLETIONS_SPLIT']=','
ENV['TM_COMPLETIONS'] = @string_raw
ENV['TM_COMPLETIONS_EXTRACHARS'] = '.'
fred = TextMate::Complete.new
assert_equal('.', fred.extra_chars)
end
#
def test_should_get_extra_chars_from_plist
ENV['TM_COMPLETIONS_SPLIT']='plist'
ENV['TM_COMPLETIONS'] = @plist_raw
assert_nil(ENV['TM_COMPLETIONS_EXTRACHARS'])
fred = TextMate::Complete.new
assert_equal('.', fred.extra_chars)
end
# TODO: should_fix_image_paths
=begin
def test_should_fix_image_paths
ENV['TM_COMPLETIONS_SPLIT'] = 'plist'
ENV['TM_COMPLETIONS'] = @plist_raw
ENV['TM_BUNDLE_SUPPORT'] = '/tmp'
ENV['TM_SUPPORT_PATH'] = '/tmp'
images = OSX::PropertyList.load(@plist_raw)['images']
FileUtils.mkdir_p "#{ENV['TM_SUPPORT_PATH']}/#{TextMate::Complete::IMAGES_FOLDER_NAME}"
images.each_pair do |name,path|
File.open("#{ENV['TM_SUPPORT_PATH']}/#{TextMate::Complete::IMAGES_FOLDER_NAME}/#{path}", 'w'){ |file| file.write('') }
end
TextMate::Complete.new.images.each_pair do |name,path|
assert File.exists?(path)
end
end
=end
def test_should_apply_prefix
ENV.delete 'TM_COMPLETIONS'
assert_nil(ENV['TM_COMPLETIONS'])
ENV.delete 'TM_COMPLETIONS_SPLIT'
assert_nil(ENV['TM_COMPLETIONS_SPLIT'])
@json_raw = <<-'JSON'
{
"extra_chars": "-_$.",
"tool_tip_prefix":"prefix",
"suggestions": [
{ "display": ".moo", "image": "", "insert": "(${1:one}, ${2:one}, ${3:three}${4:, ${5:five}, ${6:six}})", "tool_tip": "moo(one, two, four[, five])\n This method does something or other maybe.\n Insert longer description of it here." },
{ "display": "foo", "image": "", "insert": "(${1:one}, \"${2:one}\", ${3:three}${4:, ${5:five}, ${6:six}})", "tool_tip": "foo(one, two)\n This method does something or other maybe.\n Insert longer description of it here." },
{ "display": "bar", "image": "", "insert": "(${1:one}, ${2:one}, \"${3:three}\"${4:, \"${5:five}\", ${6:six}})", "tool_tip": "bar(one, two[, three])\n This method does something or other maybe.\n Insert longer description of it here." }
],
"images": {
"String" : "String.png",
"RegExp" : "RegExp.png",
"Number" : "Number.png",
"Array" : "Array.png",
"Function": "Function.png",
"Object" : "Object.png",
"Node" : "Node.png",
"NodeList": "NodeList.png"
}
}
JSON
ENV['TM_COMPLETIONS_SPLIT']='json'
ENV['TM_COMPLETIONS'] = @json_raw
fred = TextMate::Complete.new
assert_equal(3, fred.choices.length)
assert fred.choices.first['tool_tip'].match(/^prefix/)
end
#
def test_should_show_tooltip_without_inserting_anything
# This method passes if it shows a tooltip when selecting a menu-item
# and DOESN'T insert anything or cause the document think anything has changed
ENV.delete 'TM_COMPLETIONS'
assert_nil(ENV['TM_COMPLETIONS'])
ENV.delete 'TM_COMPLETIONS_SPLIT'
assert_nil(ENV['TM_COMPLETIONS_SPLIT'])
ENV['TM_COMPLETIONS_SPLIT']='json'
ENV['TM_COMPLETIONS'] = @json_raw
fred = TextMate::Complete.new
assert_equal(3, fred.choices.length)
TextMate::Complete.new.tip!
end
#
def test_tip_should_look_for_the_current_word_and_then_try_the_closest_function_name
ENV.delete 'TM_COMPLETIONS'
assert_nil(ENV['TM_COMPLETIONS'])
ENV.delete 'TM_COMPLETIONS_SPLIT'
assert_nil(ENV['TM_COMPLETIONS_SPLIT'])
ENV['TM_COMPLETIONS_SPLIT']='json'
ENV['TM_COMPLETIONS'] = @json_raw
fred = TextMate::Complete.new
assert_equal(3, fred.choices.length)
TextMate::Complete.new.tip!
#
# This test passes if, when run, you see the tooltip for closest function
# Be sure to more your caret around and try a few times
# Showing a menu is a fail
#
# foo( bar( ), '.moo' )
# ^ Caret here should give the tip for 'foo'
# ^ Caret here should give the tip for 'bar'
# ^ Caret here should give the tip for 'bar'
# ^ Caret here should give the tip for '.moo'
# foo( bar(one,two,foo), '.moo' )
# foo['bar']
end
#
end
end#if