mirror of
https://github.com/kennethreitz-archive/kJS.tmbundle.git
synced 2026-06-05 23:50:19 +00:00
503 lines
18 KiB
Ruby
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
|