Emacs config utilizing prelude as a base
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

467 lines
14 KiB

  1. #!/usr/bin/env ruby
  2. # -*- coding: utf-8 -*-
  3. # textmate_import.rb --- import textmate snippets
  4. #
  5. # Copyright (C) 2009 Rob Christie, 2010 João Távora
  6. #
  7. # This is a quick script to generate YASnippets from TextMate Snippets.
  8. #
  9. # I based the script off of a python script of a similar nature by
  10. # Jeff Wheeler: http://nokrev.com
  11. # http://code.nokrev.com/?p=snippet-copier.git;a=blob_plain;f=snippet_copier.py
  12. #
  13. # Use textmate_import.rb --help to get usage information.
  14. require 'rubygems'
  15. require 'plist'
  16. require 'trollop'
  17. require 'fileutils'
  18. require 'shellwords' # String#shellescape
  19. require 'ruby-debug' if $DEBUG
  20. opts = Trollop::options do
  21. opt :bundle_dir, "TextMate bundle directory", :short => '-d', :type => :string
  22. opt :output_dir, "Output directory", :short => '-o', :type => :string
  23. opt :glob, "Specific snippet file (or glob) inside <bundle_dir>", :short => '-g', :default => '*.{tmSnippet,tmCommand,plist,tmMacro}'
  24. opt :pretty, 'Pretty prints multiple snippets when printing to standard out', :short => '-p'
  25. opt :quiet, "Be quiet", :short => '-q'
  26. opt :plist_file, "Use a specific plist file to derive menu information from", :type => :string
  27. end
  28. Trollop::die :bundle_dir, "must be provided" unless opts.bundle_dir
  29. Trollop::die :bundle_dir, "must exist" unless File.directory? opts.bundle_dir
  30. Trollop::die :output_dir, "must be provided" unless opts.output_dir
  31. Trollop::die :output_dir, "must exist" unless File.directory? opts.output_dir
  32. Trollop::die :plist_file, "must exist" if opts.plist_file && File.directory?(opts.plist_file)
  33. # Represents and is capable of outputting the representation of a
  34. # TextMate menu in terms of `yas/define-menu'
  35. #
  36. class TmSubmenu
  37. @@excluded_items = [];
  38. def self.excluded_items; @@excluded_items; end
  39. attr_reader :items, :name
  40. def initialize(name, hash)
  41. @items = hash["items"]
  42. @name = name
  43. end
  44. def to_lisp(allsubmenus,
  45. deleteditems,
  46. indent = 0,
  47. thingy = ["(", ")"])
  48. first = true;
  49. string = ""
  50. separator_useless = true;
  51. items.each do |uuid|
  52. if deleteditems && deleteditems.index(uuid)
  53. $stderr.puts "#{uuid} has been deleted!"
  54. next
  55. end
  56. string += "\n"
  57. string += " " * indent
  58. string += (first ? thingy[0] : (" " * thingy[0].length))
  59. submenu = allsubmenus[uuid]
  60. snippet = TmSnippet::snippets_by_uid[uuid]
  61. unimplemented = TmSnippet::unknown_substitutions["content"][uuid]
  62. if submenu
  63. str = "(yas/submenu "
  64. string += str + "\"" + submenu.name + "\""
  65. string += submenu.to_lisp(allsubmenus, deleteditems,
  66. indent + str.length + thingy[0].length)
  67. elsif snippet and not unimplemented
  68. string += ";; " + snippet.name + "\n"
  69. string += " " * (indent + thingy[0].length)
  70. string += "(yas/item \"" + uuid + "\")"
  71. separator_useless = false;
  72. elsif snippet and unimplemented
  73. string += ";; Ignoring " + snippet.name + "\n"
  74. string += " " * (indent + thingy[0].length)
  75. string += "(yas/ignore-item \"" + uuid + "\")"
  76. separator_useless = true;
  77. elsif (uuid =~ /---------------------/)
  78. string += "(yas/separator)" unless separator_useless
  79. end
  80. first = false;
  81. end
  82. string += ")"
  83. string += thingy[1]
  84. return string
  85. end
  86. def self.main_menu_to_lisp (parsed_plist, modename)
  87. mainmenu = parsed_plist["mainMenu"]
  88. deleted = parsed_plist["deleted"]
  89. root = TmSubmenu.new("__main_menu__", mainmenu)
  90. all = {}
  91. mainmenu["submenus"].each_pair do |k,v|
  92. all[k] = TmSubmenu.new(v["name"], v)
  93. end
  94. excluded = (mainmenu["excludedItems"] || []) + TmSubmenu::excluded_items
  95. closing = "\n '("
  96. closing+= excluded.collect do |uuid|
  97. "\"" + uuid + "\""
  98. end.join( "\n ") + "))"
  99. str = "(yas/define-menu "
  100. return str + "'#{modename}" + root.to_lisp(all,
  101. deleted,
  102. str.length,
  103. ["'(" , closing])
  104. end
  105. end
  106. # Represents a textmate snippet
  107. #
  108. # - @file is the .tmsnippet/.plist file path relative to cwd
  109. #
  110. # - optional @info is a Plist.parsed info.plist found in the bundle dir
  111. #
  112. # - @@snippets_by_uid is where one can find all the snippets parsed so
  113. # far.
  114. #
  115. #
  116. class SkipSnippet < RuntimeError; end
  117. class TmSnippet
  118. @@known_substitutions = {
  119. "content" => {
  120. "${TM_RAILS_TEMPLATE_START_RUBY_EXPR}" => "<%= ",
  121. "${TM_RAILS_TEMPLATE_END_RUBY_EXPR}" => " %>",
  122. "${TM_RAILS_TEMPLATE_START_RUBY_INLINE}" => "<% ",
  123. "${TM_RAILS_TEMPLATE_END_RUBY_INLINE}" => " -%>",
  124. "${TM_RAILS_TEMPLATE_END_RUBY_BLOCK}" => "end" ,
  125. "${0:$TM_SELECTED_TEXT}" => "${0:`yas/selected-text`}",
  126. /\$\{(\d+)\}/ => "$\\1",
  127. "${1:$TM_SELECTED_TEXT}" => "${1:`yas/selected-text`}",
  128. "${2:$TM_SELECTED_TEXT}" => "${2:`yas/selected-text`}",
  129. '$TM_SELECTED_TEXT' => "`yas/selected-text`",
  130. %r'\$\{TM_SELECTED_TEXT:([^\}]*)\}' => "`(or (yas/selected-text) \"\\1\")`",
  131. %r'`[^`]+\n[^`]`' => Proc.new {|uuid, match| "(yas/multi-line-unknown " + uuid + ")"}},
  132. "condition" => {
  133. /^source\..*$/ => "" },
  134. "binding" => {},
  135. "type" => {}
  136. }
  137. def self.extra_substitutions; @@extra_substitutions; end
  138. @@extra_substitutions = {
  139. "content" => {},
  140. "condition" => {},
  141. "binding" => {},
  142. "type" => {}
  143. }
  144. def self.unknown_substitutions; @@unknown_substitutions; end
  145. @@unknown_substitutions = {
  146. "content" => {},
  147. "condition" => {},
  148. "binding" => {},
  149. "type" => {}
  150. }
  151. @@snippets_by_uid={}
  152. def self.snippets_by_uid; @@snippets_by_uid; end
  153. def initialize(file,info=nil)
  154. @file = file
  155. @info = info
  156. @snippet = TmSnippet::read_plist(file)
  157. @@snippets_by_uid[self.uuid] = self;
  158. raise SkipSnippet.new "not a snippet/command/macro." unless (@snippet["scope"] || @snippet["command"])
  159. raise SkipSnippet.new "looks like preferences."if @file =~ /Preferences\//
  160. raise RuntimeError.new("Cannot convert this snippet #{file}!") unless @snippet;
  161. end
  162. def name
  163. @snippet["name"]
  164. end
  165. def uuid
  166. @snippet["uuid"]
  167. end
  168. def key
  169. @snippet["tabTrigger"]
  170. end
  171. def condition
  172. yas_directive "condition"
  173. end
  174. def type
  175. override = yas_directive "type"
  176. if override
  177. return override
  178. else
  179. return "# type: command\n" if @file =~ /(Commands\/|Macros\/)/
  180. end
  181. end
  182. def binding
  183. yas_directive "binding"
  184. end
  185. def content
  186. known = @@known_substitutions["content"]
  187. extra = @@extra_substitutions["content"]
  188. if direct = extra[uuid]
  189. return direct
  190. else
  191. ct = @snippet["content"]
  192. if ct
  193. known.each_pair do |k,v|
  194. if v.respond_to? :call
  195. ct.gsub!(k) {|match| v.call(uuid, match)}
  196. else
  197. ct.gsub!(k,v)
  198. end
  199. end
  200. extra.each_pair do |k,v|
  201. ct.gsub!(k,v)
  202. end
  203. # the remaining stuff is an unknown substitution
  204. #
  205. [ %r'\$\{ [^/\}\{:]* / [^/]* / [^/]* / [^\}]*\}'x ,
  206. %r'\$\{[^\d][^}]+\}',
  207. %r'`[^`]+`',
  208. %r'\$TM_[\w_]+',
  209. %r'\(yas/multi-line-unknown [^\)]*\)'
  210. ].each do |reg|
  211. ct.scan(reg) do |match|
  212. @@unknown_substitutions["content"][match] = self
  213. end
  214. end
  215. return ct
  216. else
  217. @@unknown_substitutions["content"][uuid] = self
  218. TmSubmenu::excluded_items.push(uuid)
  219. return "(yas/unimplemented)"
  220. end
  221. end
  222. end
  223. def to_yas
  224. doc = "# -*- mode: snippet -*-\n"
  225. doc << (self.type || "")
  226. doc << "# uuid: #{self.uuid}\n"
  227. doc << "# key: #{self.key}\n" if self.key
  228. doc << "# contributor: Translated from textmate snippet by PROGRAM_NAME\n"
  229. doc << "# name: #{self.name}\n"
  230. doc << (self.binding || "")
  231. doc << (self.condition || "")
  232. doc << "# --\n"
  233. doc << (self.content || "(yas/unimplemented)")
  234. doc
  235. end
  236. def self.canonicalize(filename)
  237. invalid_char = /[^ a-z_0-9.+=~(){}\/'`&#,-]/i
  238. filename.
  239. gsub(invalid_char, ''). # remove invalid characters
  240. gsub(/ {2,}/,' '). # squeeze repeated spaces into a single one
  241. rstrip # remove trailing whitespaces
  242. end
  243. def yas_file()
  244. File.join(TmSnippet::canonicalize(@file[0, @file.length-File.extname(@file).length]) + ".yasnippet")
  245. end
  246. def self.read_plist(xml_or_binary)
  247. begin
  248. parsed = Plist::parse_xml(xml_or_binary)
  249. return parsed if parsed
  250. raise ArgumentError.new "Probably in binary format and parse_xml is very quiet..."
  251. rescue StandardError => e
  252. if (system "plutil -convert xml1 #{xml_or_binary.shellescape} -o /tmp/textmate_import.tmpxml")
  253. return Plist::parse_xml("/tmp/textmate_import.tmpxml")
  254. else
  255. raise RuntimeError.new "plutil failed miserably, check if you have it..."
  256. end
  257. end
  258. end
  259. private
  260. @@yas_to_tm_directives = {"condition" => "scope", "binding" => "keyEquivalent", "key" => "tabTrigger"}
  261. def yas_directive(yas_directive)
  262. #
  263. # Merge "known" hardcoded substitution with "extra" substitutions
  264. # provided in the .yas-setup.el file.
  265. #
  266. merged = @@known_substitutions[yas_directive].
  267. merge(@@extra_substitutions[yas_directive])
  268. #
  269. # First look for an uuid-based direct substitution for this
  270. # directive.
  271. #
  272. if direct = merged[uuid]
  273. return "# #{yas_directive}: "+ direct + "\n" unless direct.empty?
  274. else
  275. tm_directive = @@yas_to_tm_directives[yas_directive]
  276. val = tm_directive && @snippet[tm_directive]
  277. if val and !val.delete(" ").empty? then
  278. #
  279. # Sort merged substitutions by length (bigger ones first,
  280. # regexps last), and apply them to the value gotten for plist.
  281. #
  282. allsubs = merged.sort_by do |what, with|
  283. if what.respond_to? :length then -what.length else 0 end
  284. end
  285. allsubs.each do |sub|
  286. if val.gsub!(sub[0],sub[1])
  287. # puts "SUBBED #{sub[0]} for #{sub[1]}"
  288. return "# #{yas_directive}: "+ val + "\n" unless val.empty?
  289. end
  290. end
  291. #
  292. # If we get here, no substitution matched, so mark this an
  293. # unknown substitution.
  294. #
  295. @@unknown_substitutions[yas_directive][val] = self
  296. return "## #{yas_directive}: \""+ val + "\n"
  297. end
  298. end
  299. end
  300. end
  301. if __FILE__ == $PROGRAM_NAME
  302. # Read the the bundle's info.plist if can find it/guess it
  303. #
  304. info_plist_file = opts.plist_file || File.join(opts.bundle_dir,"info.plist")
  305. info_plist = TmSnippet::read_plist(info_plist_file) if info_plist_file and File.readable? info_plist_file;
  306. # Calculate the mode name
  307. #
  308. modename = File.basename opts.output_dir || "major-mode-name"
  309. # Read in .yas-setup.el looking for the separator between auto-generated
  310. #
  311. original_dir = Dir.pwd
  312. yas_setup_el_file = File.join(original_dir, opts.output_dir, ".yas-setup.el")
  313. separator = ";; --**--"
  314. whole, head , tail = "", "", ""
  315. if File::exists? yas_setup_el_file
  316. File.open yas_setup_el_file, 'r' do |file|
  317. whole = file.read
  318. head , tail = whole.split(separator)
  319. end
  320. else
  321. head = ";; .yas-setup.el for #{modename}\n" + ";; \n"
  322. end
  323. # Now iterate the tail part to find extra substitutions
  324. #
  325. tail ||= ""
  326. head ||= ""
  327. directive = nil
  328. # puts "get this head #{head}"
  329. head.each_line do |line|
  330. case line
  331. when /^;; Substitutions for:(.*)$/
  332. directive = $~[1].strip
  333. # puts "found the directove #{directive}"
  334. when /^;;(.*)[ ]+=yyas>(.*)$/
  335. replacewith = $~[2].strip
  336. lookfor = $~[1]
  337. lookfor.gsub!(/^[ ]*/, "")
  338. lookfor.gsub!(/[ ]*$/, "")
  339. # puts "found this wonderful substitution for #{directive} which is #{lookfor} => #{replacewith}"
  340. unless !directive or replacewith =~ /yas\/unknown/ then
  341. TmSnippet.extra_substitutions[directive][lookfor] = replacewith
  342. end
  343. end
  344. end
  345. # Glob snippets into snippet_files, going into subdirs
  346. #
  347. Dir.chdir opts.bundle_dir
  348. snippet_files_glob = File.join("**", opts.glob)
  349. snippet_files = Dir.glob(snippet_files_glob)
  350. # Attempt to convert each snippet files in snippet_files
  351. #
  352. puts "Will try to convert #{snippet_files.length} snippets...\n" unless opts.quiet
  353. # Iterate the globbed files
  354. #
  355. snippet_files.each do |file|
  356. begin
  357. $stdout.print "Processing \"#{File.join(opts.bundle_dir,file)}\"..." unless opts.quiet
  358. snippet = TmSnippet.new(file,info_plist)
  359. file_to_create = File.join(original_dir, opts.output_dir, snippet.yas_file)
  360. FileUtils.mkdir_p(File.dirname(file_to_create))
  361. File.open(file_to_create, 'w') do |f|
  362. f.write(snippet.to_yas)
  363. end
  364. $stdout.print "done\n" unless opts.quiet
  365. rescue SkipSnippet => e
  366. $stdout.print "skipped! #{e.message}\n" unless opts.quiet
  367. rescue RuntimeError => e
  368. $stderr.print "failed! #{e.message}\n"
  369. $strerr.print "#{e.backtrace.join("\n")}" unless opts.quiet
  370. end
  371. end
  372. # Attempt to decypher the menu
  373. #
  374. menustr = TmSubmenu::main_menu_to_lisp(info_plist, modename) if info_plist
  375. puts menustr if $DEBUG
  376. # Write some basic .yas-* files
  377. #
  378. if opts.output_dir
  379. FileUtils.mkdir_p opts.output_dir
  380. FileUtils.touch File.join(original_dir, opts.output_dir, ".yas-make-groups") unless menustr
  381. # Now, output head + a new tail in (possibly new) .yas-setup.el
  382. # file
  383. #
  384. File.open yas_setup_el_file, 'w' do |file|
  385. file.puts head
  386. file.puts separator
  387. file.puts ";; Automatically generated code, do not edit this part"
  388. file.puts ";; "
  389. file.puts ";; Translated menu"
  390. file.puts ";; "
  391. file.puts menustr
  392. file.puts
  393. file.puts ";; Unknown substitutions"
  394. file.puts ";; "
  395. ["content", "condition", "binding"].each do |type|
  396. file.puts ";; Substitutions for: #{type}"
  397. file.puts ";; "
  398. # TmSnippet::extra_substitutions[type].
  399. # each_pair do |k,v|
  400. # file.puts ";; " + k + "" + (" " * [1, 90-k.length].max) + " =yyas> " + v
  401. # end
  402. unknown = TmSnippet::unknown_substitutions[type];
  403. unknown.keys.uniq.each do |k|
  404. file.puts ";; # as in " + unknown[k].yas_file
  405. file.puts ";; " + k + "" + (" " * [1, 90-k.length].max) + " =yyas> (yas/unknown)"
  406. file.puts ";; "
  407. end
  408. file.puts ";; "
  409. file.puts
  410. end
  411. file.puts ";; .yas-setup.el for #{modename} ends here"
  412. end
  413. end
  414. end