├── .coveralls.yml ├── .rspec ├── .rvmrc ├── spec ├── fixtures │ ├── class_with_at.coffee │ ├── class_with_dot.coffee │ ├── exported_class.coffee │ ├── blockcomment.coffee │ ├── blockcomment.ctags │ ├── test.coffee │ ├── campfire.js.tags │ ├── campfire.coffee │ ├── campfire.js │ ├── test_tree.yaml │ ├── append.ctags │ ├── out.test.ctags │ ├── tree.yaml │ ├── out.test-two.ctags │ ├── append-expected.ctags │ ├── out.test-relative-append.ctags │ └── out.test-relative.ctags ├── coveralls-setup.rb ├── spec_helper.rb ├── formatter_spec.rb ├── coffeetags_spec.rb └── parser_spec.rb ├── lib ├── CoffeeTags │ ├── version.rb │ ├── formatter.rb │ └── parser.rb └── CoffeeTags.rb ├── .gitignore ├── Rakefile ├── bin └── coffeetags ├── .travis.yml ├── test.rb ├── Guardfile ├── Gemfile ├── script └── bump_version ├── CoffeeTags.gemspec ├── tagbar-info.markdown ├── plugin └── coffee-autotag.vim └── README.md /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-ci 2 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format progress 3 | -------------------------------------------------------------------------------- /.rvmrc: -------------------------------------------------------------------------------- 1 | rvm ruby-1.8.7-p302@coffeetags --create 2 | -------------------------------------------------------------------------------- /spec/fixtures/class_with_at.coffee: -------------------------------------------------------------------------------- 1 | class @Campfire 2 | constructor: -> 3 | -------------------------------------------------------------------------------- /spec/fixtures/class_with_dot.coffee: -------------------------------------------------------------------------------- 1 | class App.Campfire 2 | constructor: -> 3 | -------------------------------------------------------------------------------- /lib/CoffeeTags/version.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | module Coffeetags 3 | VERSION = "0.6.0" 4 | end 5 | -------------------------------------------------------------------------------- /spec/fixtures/exported_class.coffee: -------------------------------------------------------------------------------- 1 | modules.export = class LolWut 2 | constructor : -> @wut = 'lol' 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | Gemfile.lock 4 | pkg/* 5 | vendor 6 | testout/ 7 | test.out 8 | tags 9 | coverage/ 10 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/gem_tasks' 3 | 4 | task :default => [ :test ] 5 | task :test do 6 | exec "bundle exec rspec spec/" 7 | end 8 | -------------------------------------------------------------------------------- /bin/coffeetags: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'CoffeeTags' 3 | 4 | options = Coffeetags::Utils.option_parser ARGV 5 | if options 6 | Coffeetags::Utils.run options 7 | end 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | # 1.8.x and 1.9 < 1.9.3 are not supported anymore by Ruby core 4 | # - 1.8.7 5 | # - 1.9.2 6 | - 1.9.3 7 | - 2.0.0 8 | - 2.1.0 9 | - 2.2.0 10 | -------------------------------------------------------------------------------- /test.rb: -------------------------------------------------------------------------------- 1 | require './lib/CoffeeTags' 2 | require 'yaml' 3 | 4 | out = Coffeetags::Utils.option_parser ARGV 5 | if out 6 | output, include_vars, files = out 7 | Coffeetags::Utils.run output, include_vars, files 8 | end 9 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | guard 'rspec', :version => 2, :bundler => true, :cli => ' --color', :all_on_start =>true do 2 | watch(%r{^spec/(.*).rb$}) { |m| m[0] } 3 | watch(%r{^lib/(.*).rb$}) do |m| 4 | "spec/#{m[0].split('/').last.split('.').first.downcase}_spec.rb" 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/coveralls-setup.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | require 'coveralls' 3 | 4 | SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[ 5 | SimpleCov::Formatter::HTMLFormatter, 6 | Coveralls::SimpleCov::Formatter 7 | ] 8 | 9 | SimpleCov.start do 10 | add_filter 'spec/' 11 | end 12 | 13 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'coveralls-setup' 2 | require 'yaml' 3 | require './lib/CoffeeTags' 4 | 5 | def capture_stdout(&block) 6 | original_stdout = $stdout 7 | $stdout = fake = StringIO.new 8 | begin 9 | yield 10 | ensure 11 | $stdout = original_stdout 12 | end 13 | fake.string 14 | end 15 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gemspec 4 | gem 'rake' 5 | 6 | gem 'CoffeeTags', path: '.' 7 | 8 | group :development do 9 | gem 'listen', '1.3.1' 10 | gem 'guard' 11 | gem 'growl' 12 | gem 'guard-rspec' 13 | gem 'coveralls', :require => false 14 | gem 'simplecov', :require => false 15 | 16 | gem 'pry' 17 | # osx 18 | gem 'rb-fsevent' 19 | end 20 | 21 | group :test do 22 | gem 'rspec', '2.8.0' 23 | end 24 | -------------------------------------------------------------------------------- /script/bump_version: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | readonly currentVersion=$(ruby -r ./lib/CoffeeTags/version.rb -e 'puts Coffeetags::VERSION') 4 | readonly newVersion=$1 5 | 6 | if [[ "$newVersion" == '' ]] ; then 7 | echo "$(basename $0) " 8 | echo "Current version: $currentVersion" 9 | exit 1 10 | fi 11 | 12 | git grep "$currentVersion" | cut -d: -f1 | tee /dev/stderr | uniq | xargs sed -i "s/$currentVersion/$newVersion/g" 13 | -------------------------------------------------------------------------------- /spec/fixtures/blockcomment.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | Lol wut 3 | echo : -> 4 | echoes 5 | ### 6 | 7 | echo2 :-> console.log 'echo' 8 | 9 | ### One line blockcomment ### 10 | foo2 : (x) -> console.log 'bar #{x}' 11 | 12 | ### 13 | not so well formatted blockcomment 14 | echo4 :-> console.log 'echo4' ### 15 | baz : (x, y) -> 16 | console.log 'baz #{x} : #{y}' 17 | 18 | ### 19 | well formatted 20 | block comment 21 | ### 22 | 23 | echo3 :-> console.log 'echo' 24 | 25 | ### One line blockcomment ### 26 | foo : (x) -> console.log 'bar #{x}' 27 | -------------------------------------------------------------------------------- /CoffeeTags.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $LOAD_PATH.push File.expand_path('../lib', __FILE__) 3 | require 'CoffeeTags/version' 4 | 5 | Gem::Specification.new do |s| 6 | s.name = 'CoffeeTags' 7 | s.version = Coffeetags::VERSION 8 | s.authors = ['Łukasz Korecki', 'Matthew Smith'] 9 | s.email = ['lukasz@korecki.me', 'mtscout6@gmail.com'] 10 | s.homepage = 'http://github.com/lukaszkorecki/CoffeeTags' 11 | s.summary = 'tags generator for CoffeeScript' 12 | s.description = 'CoffeeTags generates ctags compatibile tags for CoffeeScript.' 13 | 14 | s.rubyforge_project = 'CoffeeTags' 15 | 16 | s.files = `git ls-files`.split("\n") 17 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 18 | s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) } 19 | s.require_paths = ['lib'] 20 | s.licenses = ['MIT'] 21 | end 22 | -------------------------------------------------------------------------------- /spec/fixtures/blockcomment.ctags: -------------------------------------------------------------------------------- 1 | !_TAG_FILE_FORMAT 2 /extended format/ 2 | !_TAG_FILE_SORTED 0 /0=unsorted, 1=sorted, 2=foldcase/ 3 | !_TAG_PROGRAM_AUTHOR Łukasz Korecki /lukasz@korecki.me/ 4 | !_TAG_PROGRAM_NAME CoffeeTags // 5 | !_TAG_PROGRAM_URL https://github.com/lukaszkorecki/CoffeeTags /GitHub repository/ 6 | !_TAG_PROGRAM_VERSION 0.6.0 // 7 | baz spec/fixtures/blockcomment.coffee /^baz : (x, y) ->$/;" f line:15 language:coffee object:window 8 | echo2 spec/fixtures/blockcomment.coffee /^echo2 :-> console.log 'echo'$/;" f line:7 language:coffee object:window 9 | echo3 spec/fixtures/blockcomment.coffee /^echo3 :-> console.log 'echo'$/;" f line:23 language:coffee object:window 10 | foo spec/fixtures/blockcomment.coffee /^foo : (x) -> console.log 'bar #{x}'$/;" f line:26 language:coffee object:window 11 | foo2 spec/fixtures/blockcomment.coffee /^foo2 : (x) -> console.log 'bar #{x}'$/;" f line:10 language:coffee object:window 12 | -------------------------------------------------------------------------------- /spec/formatter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | describe 'CoffeeTags::Formatter' do 3 | before :each do 4 | @tree = YAML::load_file './spec/fixtures/tree.yaml' 5 | end 6 | 7 | it "works!" do 8 | lambda { Coffeetags::Formatter.new 'lol.coffee' }.should_not raise_exception 9 | end 10 | 11 | before :each do 12 | @instance = Coffeetags::Formatter.new 'test.coffee', @tree 13 | end 14 | 15 | it "generates a line for method definition" do 16 | exp = [ 17 | 'constructor', 18 | 'test.coffee', 19 | "/^ constructor: (api_key, host) ->$/;\"", 20 | 'f', 21 | 'line:8', 22 | 'language:coffee', 23 | 'object:Campfire' 24 | ].join("\t") 25 | @instance.parse_tree.first.should == exp 26 | end 27 | 28 | it "generates line for second class" do 29 | exp = [ 30 | 'bump', 31 | 'test.coffee', 32 | "/^ bump : ->$/;\"", 33 | 'f', 34 | 'line:46', 35 | 'language:coffee', 36 | 'object:Test' 37 | ].join "\t" 38 | @instance.parse_tree.last.should == exp 39 | end 40 | 41 | end 42 | -------------------------------------------------------------------------------- /spec/fixtures/test.coffee: -------------------------------------------------------------------------------- 1 | bump = (wat) -> 2 | v = 'test' 3 | 4 | Wat = 5 | ho : (x) -> 6 | x = 'o' 7 | x = 'z' if bamp 8 | console.log 'ahhhh' 9 | @lolWat = 10 | bump : -> 11 | console.log 'bump' 12 | bump_up : -> 13 | console.log 'bump up' 14 | @filter = -> 15 | filter.getElements('input[type="checkbox"]').addEvent('change', (e) -> 16 | Wat.trackEvent('Track', "Filter::#{e.target.name}", e.target.value) 17 | ) 18 | 19 | _loop = (x) -> 20 | woop = 1 21 | if z.isSomethingRidic 22 | fu = 1 23 | 24 | if wat 25 | nice = 'ok' 26 | 27 | 28 | for element in lol 29 | do (element) -> 30 | forVariable = 2 * element 31 | # for testing with issue #1 examples 32 | dir = fs.readdirSync __dirname 33 | x = dir + '/foo' 34 | 35 | for f in dir 36 | do (f) -> 37 | console.log f 38 | 39 | zorb = get['x=\\/f'].getLast('/woot$') 40 | 41 | bound_func = (ok) => wat(ok) 42 | 43 | # TODO: beam_shield -> deployed(true) 44 | beam_magnum : -> deployed(true) 45 | 46 | Array::loop = (x) -> 47 | x 48 | 49 | obj = 50 | className: 'notAClass' 51 | -------------------------------------------------------------------------------- /spec/fixtures/campfire.js.tags: -------------------------------------------------------------------------------- 1 | !_TAG_FILE_FORMAT 2 /extended format/ 2 | !_TAG_FILE_SORTED 0 /0=unsorted, 1=sorted, 2=foldcase/ 3 | !_TAG_PROGRAM_AUTHOR Łukasz Korecki /lukasz@korecki.me/ 4 | !_TAG_PROGRAM_NAME CoffeeTags // 5 | !_TAG_PROGRAM_URL https://github.com/lukaszkorecki/CoffeeTags /GitHub repository/ 6 | !_TAG_PROGRAM_VERSION 0.6.0 // 7 | bump spec/fixtures/campfire.coffee /^ bump : ->$/;" f lineno:46 object:Test 8 | constructor spec/fixtures/campfire.coffee /^ constructor: (api_key, host) ->$/;" f lineno:8 object:Campfire 9 | handlers spec/fixtures/campfire.coffee /^ handlers: (callbacks) ->$/;" f lineno:14 object:Campfire 10 | onFailure spec/fixtures/campfire.coffee /^ onFailure: (response) ->$/;" f lineno:24 object:Campfire.handlers.resp 11 | onSuccess spec/fixtures/campfire.coffee /^ onSuccess : (response) ->$/;" f lineno:16 object:Campfire.handlers.resp 12 | recent spec/fixtures/campfire.coffee /^ recent: (id, since, callbacks) ->$/;" f lineno:40 object:Campfire 13 | roomInfo spec/fixtures/campfire.coffee /^ roomInfo: (id, callbacks) ->$/;" f lineno:34 object:Campfire 14 | rooms spec/fixtures/campfire.coffee /^ rooms: (callbacks) ->$/;" f lineno:29 object:Campfire 15 | -------------------------------------------------------------------------------- /tagbar-info.markdown: -------------------------------------------------------------------------------- 1 | # How to use CoffeeTags with [TagBar ](https://github.com/majutsushi/tagbar) 2 | 3 | ## Config types 4 | 5 | CoffeeTags can work in 2 modes: 6 | 7 | - tags only for functions (default) 8 | - tags for functions and objects containing them 9 | 10 | Second mode is activated by adding --include-vars to command line arguments 11 | 12 | # CoffeeTags + TagBar + Vim 13 | 14 | ## Config in vimrc 15 | 16 | You can add the config to your .vimrc (making sure that the old one is removed) 17 | by: 18 | 19 | coffeetags --vim-conf >> ~/.vimrc 20 | 21 | or (for 2nd mode) 22 | 23 | coffeetags --include-vars --vim-conf >> ~/.vimrc 24 | 25 | 26 | ## Config as a filetype plugin 27 | 28 | You can generate a special filetype plugin and tagbar will use that 29 | automatically. 30 | 31 | This option is preferable if you want to keep your vimrc short. 32 | 33 | coffeetags --vim-conf > ~/vim/ftplugin/coffee/tagbar-coffee.vim 34 | coffeetags [--include-vars] --vim-conf > ~/vim/ftplugin/coffee/tagbar-coffee.vim 35 | 36 | or if you're using pathogen 37 | 38 | coffeetags [--include-vars] --vim-conf > ~/vim/bundle/coffeetags/ftplugin/coffee/tagbar-coffee.vim 39 | coffeetags --vim-conf > ~/vim/bundle/coffeetags/ftplugin/coffee/tagbar-coffee.vim 40 | 41 | -------------------------------------------------------------------------------- /spec/fixtures/campfire.coffee: -------------------------------------------------------------------------------- 1 | # this example is not complete, but shows how to 2 | # implement an API client using Request class 3 | class Campfire 4 | 5 | # @api_key - Campfire API keys 6 | # @host - your campifre host, ie if you're using trololo.campfirenow.com, 7 | # then host is 'trololo 8 | constructor: (api_key, host) -> 9 | @url = "https://#{host}.campfirenow.com/" 10 | @auth = { 'username' : api_key, 'password' : 'X'} 11 | @headers = { 'Content-Type' : 'application/json' } 12 | 13 | # private function used for parsing JSON responses 14 | handlers: (callbacks) -> 15 | resp = 16 | onSuccess : (response) -> 17 | try 18 | obj = JSON.parse(response.responseText) 19 | catch error 20 | console.dir(error) 21 | callbacks.onFailure(error) 22 | callbacks.onSuccess(obj) 23 | 24 | onFailure: (response) -> 25 | console.dir(response) 26 | callbacks.onFailure(response) 27 | 28 | # get list of rooms 29 | rooms: (callbacks) -> 30 | new Request(@url, @headers, @auth).get 'rooms.json', this.handlers(callbacks) 31 | 32 | # get information about a room 33 | # @id - room id 34 | roomInfo: (id, callbacks) -> 35 | new Request(@url, @headers, @auth).get "room/#{id}.json", this.handlers(callbacks) 36 | 37 | # get latest messages and events from a room 38 | # @id - room id 39 | # @since - optional since id parameter 40 | recent: (id, since, callbacks) -> 41 | url = "room/#{id}/recent.json" 42 | url += "?since_message_id=#{since}" if since 43 | new Request(@url, @headers, @auth).get url, this.handlers(callbacks) 44 | 45 | class Test 46 | bump : -> 47 | -------------------------------------------------------------------------------- /spec/fixtures/campfire.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var Campfire; 3 | window.Campfire = Campfire = (function() { 4 | function Campfire(api_key, host) { 5 | this.url = "https://" + host + ".campfirenow.com/"; 6 | this.auth = { 7 | 'username': api_key, 8 | 'password': 'X' 9 | }; 10 | this.headers = { 11 | 'Content-Type': 'application/json' 12 | }; 13 | } 14 | Campfire.prototype.handlers = function(callbacks) { 15 | var resp; 16 | return resp = { 17 | onSuccess: function(response) { 18 | var obj; 19 | try { 20 | obj = JSON.parse(response.responseText); 21 | } catch (error) { 22 | console.dir(error); 23 | callbacks.onFailure(error); 24 | } 25 | return callbacks.onSuccess(obj); 26 | }, 27 | onFailure: function(response) { 28 | console.dir(response); 29 | return callbacks.onFailure(response); 30 | } 31 | }; 32 | }; 33 | Campfire.prototype.rooms = function(callbacks) { 34 | return new Request(this.url, this.headers, this.auth).get('rooms.json', this.handlers(callbacks)); 35 | }; 36 | Campfire.prototype.roomInfo = function(id, callbacks) { 37 | return new Request(this.url, this.headers, this.auth).get("room/" + id + ".json", this.handlers(callbacks)); 38 | }; 39 | Campfire.prototype.recent = function(id, since, callbacks) { 40 | var url; 41 | url = "room/" + id + "/recent.json"; 42 | if (since) { 43 | url += "?since_message_id=" + since; 44 | } 45 | return new Request(this.url, this.headers, this.auth).get(url, this.handlers(callbacks)); 46 | }; 47 | return Campfire; 48 | })(); 49 | }).call(this); 50 | -------------------------------------------------------------------------------- /spec/fixtures/test_tree.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - :source: bump = (wat) -> 3 | :parent: "window" 4 | :kind: f 5 | :name: bump 6 | :line: 1 7 | :level: 0 8 | - :source: " v = 'test'" 9 | :parent: 'bump' 10 | :kind: v 11 | :name: v 12 | :line: 2 13 | :level: 2 14 | - :name: Wat 15 | :source: 'Wat =' 16 | :parent: window 17 | :kind: v 18 | :level: 0 19 | :line: 4 20 | - :source: " ho : (x) ->" 21 | :parent: Wat 22 | :kind: f 23 | :name: ho 24 | :line: 5 25 | :level: 2 26 | - :source: " x = 'o'" 27 | :level: 4 28 | :kind: v 29 | :line: 6 30 | :name: x 31 | - :name: '@lolWat' 32 | :level: 4 33 | :line: 8 34 | :source: ' @lolWat =' 35 | :kind: v 36 | - :source: " bump : ->" 37 | :parent: 'Wat.ho.@lolWat' 38 | :kind: f 39 | :name: bump 40 | :line: 9 41 | :level: 6 42 | - :source: " bump_up : ->" 43 | :parent: 'Wat.ho.@lolWat' 44 | :kind: f 45 | :name: bump_up 46 | :line: 11 47 | :level: 6 48 | - :source: " @BOOO__up = -> z()" 49 | :parent: Wat.ho 50 | :kind: f 51 | :name: '@BOOO__up' 52 | :line: 13 53 | :level: 4 54 | - :source: 'Array::loop = (x) ->' 55 | :line: 46 56 | :kind: p 57 | :parent: Array 58 | :name: loop 59 | :level: 0 60 | - :source: 'bound_func = (ok) => wat(ok)' 61 | :line: 41 62 | :level: 0 63 | :kind: f 64 | :name: 'bound_func' 65 | :parent: window 66 | - :source: 'beam_magnum : -> deployed(true)' 67 | :line: 44 68 | :level: 0 69 | :kind: f 70 | :name: 'beam_magnum' 71 | :parent: window 72 | - :source: ' forVariable = 2 * element' 73 | :parent: '_loop.element' 74 | :line: 30 75 | :kind: v 76 | :name: forVariable 77 | :level: 6 78 | - :source: " className: 'notAClass'" 79 | :parent: obj 80 | :kind: v 81 | :name: 'className' 82 | :line: 50 83 | :level: 2 84 | 85 | -------------------------------------------------------------------------------- /spec/fixtures/append.ctags: -------------------------------------------------------------------------------- 1 | !_TAG_FILE_FORMAT 2 /extended format/ 2 | !_TAG_FILE_SORTED 0 /0=unsorted, 1=sorted, 2=foldcase/ 3 | !_TAG_PROGRAM_AUTHOR Łukasz Korecki /lukasz@korecki.me/ 4 | !_TAG_PROGRAM_NAME CoffeeTags // 5 | !_TAG_PROGRAM_URL https://github.com/lukaszkorecki/CoffeeTags /GitHub repository/ 6 | !_TAG_PROGRAM_VERSION 0.6.0 // 7 | @filter spec/fixtures/test.coffee /^ @filter = ->$/;" f line:14 language:coffee object:Wat.ho 8 | Wat spec/fixtures/test.coffee /^Wat =$/;" o line:4 language:coffee object:window 9 | _loop spec/fixtures/test.coffee /^_loop = (x) ->$/;" f line:19 language:coffee object:window 10 | beam_magnum spec/fixtures/test.coffee /^beam_magnum : -> deployed(true)$/;" f line:44 language:coffee object:window 11 | bound_func spec/fixtures/test.coffee /^bound_func = (ok) => wat(ok)$/;" f line:41 language:coffee object:window 12 | bump spec/fixtures/test.coffee /^bump = (wat) ->$/;" f line:1 language:coffee object:window 13 | bump_up spec/fixtures/test.coffee /^ bump_up : ->$/;" f line:12 language:coffee object:Wat.ho.@lolWat 14 | do spec/fixtures/test.coffee /^ do (element) ->$/;" b line:29 language:coffee 15 | do spec/fixtures/test.coffee /^ do (f) ->$/;" b line:36 language:coffee 16 | f spec/fixtures/test.coffee /^for f in dir$/;" o line:35 language:coffee object:window 17 | for spec/fixtures/test.coffee /^ for element in lol$/;" b line:28 language:coffee 18 | for spec/fixtures/test.coffee /^for f in dir$/;" b line:35 language:coffee 19 | ho spec/fixtures/test.coffee /^ ho : (x) ->$/;" f line:5 language:coffee object:Wat 20 | if spec/fixtures/test.coffee /^ if wat$/;" b line:24 language:coffee 21 | if spec/fixtures/test.coffee /^ if z.isSomethingRidic$/;" b line:21 language:coffee 22 | loop spec/fixtures/test.coffee /^Array::loop = (x) ->$/;" p line:46 language:coffee object:Array 23 | obj spec/fixtures/test.coffee /^obj =$/;" o line:49 language:coffee object:window 24 | -------------------------------------------------------------------------------- /spec/fixtures/out.test.ctags: -------------------------------------------------------------------------------- 1 | !_TAG_FILE_FORMAT 2 /extended format/ 2 | !_TAG_FILE_SORTED 0 /0=unsorted, 1=sorted, 2=foldcase/ 3 | !_TAG_PROGRAM_AUTHOR Łukasz Korecki /lukasz@korecki.me/ 4 | !_TAG_PROGRAM_NAME CoffeeTags // 5 | !_TAG_PROGRAM_URL https://github.com/lukaszkorecki/CoffeeTags /GitHub repository/ 6 | !_TAG_PROGRAM_VERSION 0.6.0 // 7 | @filter spec/fixtures/test.coffee /^ @filter = ->$/;" f line:14 language:coffee object:Wat.ho 8 | Wat spec/fixtures/test.coffee /^Wat =$/;" o line:4 language:coffee object:window 9 | _loop spec/fixtures/test.coffee /^_loop = (x) ->$/;" f line:19 language:coffee object:window 10 | beam_magnum spec/fixtures/test.coffee /^beam_magnum : -> deployed(true)$/;" f line:44 language:coffee object:window 11 | bound_func spec/fixtures/test.coffee /^bound_func = (ok) => wat(ok)$/;" f line:41 language:coffee object:window 12 | bump spec/fixtures/test.coffee /^bump = (wat) ->$/;" f line:1 language:coffee object:window 13 | bump_up spec/fixtures/test.coffee /^ bump_up : ->$/;" f line:12 language:coffee object:Wat.ho.@lolWat 14 | do spec/fixtures/test.coffee /^ do (element) ->$/;" b line:29 language:coffee 15 | do spec/fixtures/test.coffee /^ do (f) ->$/;" b line:36 language:coffee 16 | f spec/fixtures/test.coffee /^for f in dir$/;" o line:35 language:coffee object:window 17 | for spec/fixtures/test.coffee /^ for element in lol$/;" b line:28 language:coffee 18 | for spec/fixtures/test.coffee /^for f in dir$/;" b line:35 language:coffee 19 | ho spec/fixtures/test.coffee /^ ho : (x) ->$/;" f line:5 language:coffee object:Wat 20 | if spec/fixtures/test.coffee /^ if wat$/;" b line:24 language:coffee 21 | if spec/fixtures/test.coffee /^ if z.isSomethingRidic$/;" b line:21 language:coffee 22 | loop spec/fixtures/test.coffee /^Array::loop = (x) ->$/;" p line:46 language:coffee object:Array 23 | obj spec/fixtures/test.coffee /^obj =$/;" o line:49 language:coffee object:window 24 | -------------------------------------------------------------------------------- /spec/fixtures/tree.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - :name: Campfire 3 | :level: 0 4 | :kind: c 5 | - :source: " constructor: (api_key, host) ->" 6 | :parent: Campfire 7 | :kind: f 8 | :name: constructor 9 | :line: 8 10 | :level: 2 11 | - :source: ' @url = "https://#{host}.campfirenow.com/"' 12 | :name: '@url' 13 | :parent: 'Campfire.constructor' 14 | :kind: v 15 | :level: 4 16 | :line: 9 17 | - :source: " handlers: (callbacks) ->" 18 | :parent: Campfire 19 | :kind: f 20 | :name: handlers 21 | :line: 14 22 | :level: 2 23 | - :name: resp 24 | :level: 4 25 | :source: " resp =" 26 | :parent: "Campfire.handlers" 27 | :line: 15 28 | :kind: v 29 | - :source: " onSuccess : (response) ->" 30 | :parent: Campfire.handlers.resp 31 | :kind: f 32 | :name: onSuccess 33 | :line: 16 34 | :level: 6 35 | - :source: " onFailure: (response) ->" 36 | :parent: Campfire.handlers.resp 37 | :kind: f 38 | :name: onFailure 39 | :line: 24 40 | :level: 6 41 | - :source: " rooms: (callbacks) ->" 42 | :parent: Campfire 43 | :kind: f 44 | :name: rooms 45 | :line: 29 46 | :level: 2 47 | - :source: " roomInfo: (id, callbacks) ->" 48 | :parent: Campfire 49 | :kind: f 50 | :name: roomInfo 51 | :line: 34 52 | :level: 2 53 | - :source: " recent: (id, since, callbacks) ->" 54 | :parent: Campfire 55 | :kind: f 56 | :name: recent 57 | :line: 40 58 | :level: 2 59 | - :source: ' url = "room/#{id}/recent.json"' 60 | :parent: Campfire.recent 61 | :kind: v 62 | :name: url 63 | :line: 41 64 | :level: 4 65 | 66 | - :name: App.Campfire 67 | :level: 0 68 | :source: "class App.Campfire" 69 | :line: 1 70 | :kind: c 71 | - :source: " constructor: ->" 72 | :parent: App.Campfire 73 | :kind: f 74 | :name: constructor 75 | :line: 8 76 | :level: 2 77 | 78 | 79 | - :name: Test 80 | :level: 0 81 | :source: "class Test" 82 | :line: 45 83 | :kind: c 84 | - :source: " bump : ->" 85 | :parent: Test 86 | :kind: f 87 | :name: bump 88 | :line: 46 89 | :level: 2 90 | -------------------------------------------------------------------------------- /plugin/coffee-autotag.vim: -------------------------------------------------------------------------------- 1 | if exists("g:loaded_coffee_autotag") 2 | finish 3 | endif 4 | 5 | let g:loaded_coffee_autotag=1 6 | 7 | if !has("ruby") && !has("nvim") 8 | echohl WarningMsg 9 | echo "Coffee auto tag requires Vim to be compiled with Ruby support" 10 | echohl none 11 | finish 12 | endif 13 | 14 | let s:CoffeeAutoTagFile="./tags" 15 | let s:CoffeeAutoTagIncludeVars=0 16 | let s:CoffeeAutoTagTagRelative=1 17 | 18 | if !exists("g:CoffeeAutoTagDisabled") 19 | let g:CoffeeAutoTagDisabled = 0 20 | endif 21 | 22 | if exists("g:CoffeeAutoTagFile") 23 | let s:CoffeeAutoTagFile = g:CoffeeAutoTagFile 24 | endif 25 | 26 | if exists("g:CoffeeAutoTagIncludeVars") 27 | let s:CoffeeAutoTagIncludeVars = g:CoffeeAutoTagIncludeVars 28 | endif 29 | 30 | if exists("g:CoffeeAutoTagTagRelative") 31 | let s:CoffeeAutoTagTagRelative = g:CoffeeAutoTagTagRelative 32 | endif 33 | 34 | if s:CoffeeAutoTagIncludeVars 35 | let s:raw_args="--include-vars" 36 | else 37 | let s:raw_args="" 38 | endif 39 | 40 | let g:tagbar_type_coffee = { 41 | \ 'ctagsbin' : 'coffeetags', 42 | \ 'ctagsargs' : s:raw_args, 43 | \ 'kinds' : [ 44 | \ 'f:functions', 45 | \ 'c:classes', 46 | \ 'o:object', 47 | \ 'v:variables', 48 | \ 'p:prototypes', 49 | \ 'b:blocks' 50 | \ ], 51 | \ 'sro' : ".", 52 | \ 'kind2scope' : { 53 | \ 'f' : 'object', 54 | \ 'o' : 'object', 55 | \ } 56 | \ } 57 | 58 | 59 | function! CoffeeAutoTag() 60 | if g:CoffeeAutoTagDisabled 61 | return 62 | endif 63 | 64 | let cmd = 'coffeetags --append -f ' . s:CoffeeAutoTagFile . ' ' 65 | 66 | if s:CoffeeAutoTagIncludeVars 67 | let cmd .= '--include-vars ' 68 | endif 69 | 70 | if s:CoffeeAutoTagTagRelative 71 | let cmd .= '--tag-relative ' 72 | endif 73 | 74 | let cmd .= expand("%:p") 75 | 76 | let output = system(cmd) 77 | 78 | if exists(":TlistUpdate") 79 | TlistUpdate 80 | endif 81 | endfunction 82 | 83 | augroup CoffeeAutoTag 84 | au! 85 | autocmd BufWritePost,FileWritePost *.coffee call CoffeeAutoTag() 86 | augroup END 87 | -------------------------------------------------------------------------------- /lib/CoffeeTags/formatter.rb: -------------------------------------------------------------------------------- 1 | module Coffeetags 2 | class Formatter 3 | # Generates CTAGS header 4 | # @returns [String] ctags header 5 | def self.header 6 | return [ 7 | "!_TAG_FILE_FORMAT 2 /extended format/", 8 | "!_TAG_FILE_SORTED 0 /0=unsorted, 1=sorted, 2=foldcase/", 9 | "!_TAG_PROGRAM_AUTHOR #{::Coffeetags::AUTHOR}", 10 | "!_TAG_PROGRAM_NAME #{::Coffeetags::NAME} //", 11 | "!_TAG_PROGRAM_URL #{::Coffeetags::URL} /GitHub repository/", 12 | "!_TAG_PROGRAM_VERSION #{::Coffeetags::VERSION} //" 13 | ].map { |h| "#{h}\n"}.join '' 14 | end 15 | 16 | def self.kinds 17 | return { 18 | 'f' => 'function', 19 | 'c' => 'class', 20 | 'o' => 'object', 21 | 'v' => 'var', 22 | 'p' => 'proto', 23 | 'b' => 'block' 24 | } 25 | end 26 | 27 | # New Formatter class 28 | # 29 | # @param [String] file file name 30 | # @param [Array] tree parse tree for given file generated by Coffeetags::Parser 31 | def initialize file, tree =[] 32 | @file = file 33 | @tree = tree 34 | 35 | @header = Formatter.header 36 | end 37 | 38 | 39 | # Helper function for formatting a source line into regex 40 | def regex_line line 41 | "/^#{line.gsub(/([\\^$])/, '\\\\\1')}$/;\"" 42 | end 43 | 44 | def line_to_string entry 45 | namespace = (entry[:parent].blank?) ? entry[:name]: entry[:parent] 46 | namespace = namespace == entry[:name] ? '' : "object:#{namespace}" 47 | 48 | output = [ 49 | entry[:name], 50 | @file, 51 | regex_line(entry[:source]), 52 | entry[:kind], 53 | "line:#{entry[:line]}", 54 | "language:coffee" 55 | ].join("\t") 56 | unless namespace.empty? then output << "\t#{namespace}" end 57 | output 58 | end 59 | 60 | def parse_tree 61 | @lines = @tree.map do | content| 62 | line_to_string content unless content[:line].nil? or content[:name].blank? 63 | end 64 | @lines.reject!{|l| l.nil? } 65 | end 66 | 67 | def tags 68 | @lines.map { |l| "#{l}\n"}.join '' 69 | end 70 | 71 | def lines 72 | @lines 73 | end 74 | 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /spec/fixtures/out.test-two.ctags: -------------------------------------------------------------------------------- 1 | !_TAG_FILE_FORMAT 2 /extended format/ 2 | !_TAG_FILE_SORTED 0 /0=unsorted, 1=sorted, 2=foldcase/ 3 | !_TAG_PROGRAM_AUTHOR Łukasz Korecki /lukasz@korecki.me/ 4 | !_TAG_PROGRAM_NAME CoffeeTags // 5 | !_TAG_PROGRAM_URL https://github.com/lukaszkorecki/CoffeeTags /GitHub repository/ 6 | !_TAG_PROGRAM_VERSION 0.6.0 // 7 | @filter spec/fixtures/test.coffee /^ @filter = ->$/;" f line:14 language:coffee object:Wat.ho 8 | Campfire spec/fixtures/campfire.coffee /^class Campfire$/;" c line:3 language:coffee 9 | Test spec/fixtures/campfire.coffee /^class Test$/;" c line:45 language:coffee 10 | Wat spec/fixtures/test.coffee /^Wat =$/;" o line:4 language:coffee object:window 11 | _loop spec/fixtures/test.coffee /^_loop = (x) ->$/;" f line:19 language:coffee object:window 12 | beam_magnum spec/fixtures/test.coffee /^beam_magnum : -> deployed(true)$/;" f line:44 language:coffee object:window 13 | bound_func spec/fixtures/test.coffee /^bound_func = (ok) => wat(ok)$/;" f line:41 language:coffee object:window 14 | bump spec/fixtures/campfire.coffee /^ bump : ->$/;" p line:46 language:coffee object:Test 15 | bump spec/fixtures/test.coffee /^bump = (wat) ->$/;" f line:1 language:coffee object:window 16 | bump_up spec/fixtures/test.coffee /^ bump_up : ->$/;" f line:12 language:coffee object:Wat.ho.@lolWat 17 | constructor spec/fixtures/campfire.coffee /^ constructor: (api_key, host) ->$/;" p line:8 language:coffee object:Campfire 18 | do spec/fixtures/test.coffee /^ do (element) ->$/;" b line:29 language:coffee 19 | do spec/fixtures/test.coffee /^ do (f) ->$/;" b line:36 language:coffee 20 | f spec/fixtures/test.coffee /^for f in dir$/;" o line:35 language:coffee object:window 21 | for spec/fixtures/test.coffee /^ for element in lol$/;" b line:28 language:coffee 22 | for spec/fixtures/test.coffee /^for f in dir$/;" b line:35 language:coffee 23 | handlers spec/fixtures/campfire.coffee /^ handlers: (callbacks) ->$/;" p line:14 language:coffee object:Campfire 24 | ho spec/fixtures/test.coffee /^ ho : (x) ->$/;" f line:5 language:coffee object:Wat 25 | if spec/fixtures/test.coffee /^ if wat$/;" b line:24 language:coffee 26 | if spec/fixtures/test.coffee /^ if z.isSomethingRidic$/;" b line:21 language:coffee 27 | loop spec/fixtures/test.coffee /^Array::loop = (x) ->$/;" p line:46 language:coffee object:Array 28 | obj spec/fixtures/test.coffee /^obj =$/;" o line:49 language:coffee object:window 29 | onFailure spec/fixtures/campfire.coffee /^ onFailure: (response) ->$/;" f line:24 language:coffee object:Campfire.handlers.resp 30 | onSuccess spec/fixtures/campfire.coffee /^ onSuccess : (response) ->$/;" f line:16 language:coffee object:Campfire.handlers.resp 31 | recent spec/fixtures/campfire.coffee /^ recent: (id, since, callbacks) ->$/;" p line:40 language:coffee object:Campfire 32 | roomInfo spec/fixtures/campfire.coffee /^ roomInfo: (id, callbacks) ->$/;" p line:34 language:coffee object:Campfire 33 | rooms spec/fixtures/campfire.coffee /^ rooms: (callbacks) ->$/;" p line:29 language:coffee object:Campfire 34 | -------------------------------------------------------------------------------- /spec/fixtures/append-expected.ctags: -------------------------------------------------------------------------------- 1 | !_TAG_FILE_FORMAT 2 /extended format/ 2 | !_TAG_FILE_SORTED 0 /0=unsorted, 1=sorted, 2=foldcase/ 3 | !_TAG_PROGRAM_AUTHOR Łukasz Korecki /lukasz@korecki.me/ 4 | !_TAG_PROGRAM_NAME CoffeeTags // 5 | !_TAG_PROGRAM_URL https://github.com/lukaszkorecki/CoffeeTags /GitHub repository/ 6 | !_TAG_PROGRAM_VERSION 0.6.0 // 7 | @filter spec/fixtures/test.coffee /^ @filter = ->$/;" f line:14 language:coffee object:Wat.ho 8 | Campfire spec/fixtures/campfire.coffee /^class Campfire$/;" c line:3 language:coffee 9 | Test spec/fixtures/campfire.coffee /^class Test$/;" c line:45 language:coffee 10 | Wat spec/fixtures/test.coffee /^Wat =$/;" o line:4 language:coffee object:window 11 | _loop spec/fixtures/test.coffee /^_loop = (x) ->$/;" f line:19 language:coffee object:window 12 | beam_magnum spec/fixtures/test.coffee /^beam_magnum : -> deployed(true)$/;" f line:44 language:coffee object:window 13 | bound_func spec/fixtures/test.coffee /^bound_func = (ok) => wat(ok)$/;" f line:41 language:coffee object:window 14 | bump spec/fixtures/campfire.coffee /^ bump : ->$/;" p line:46 language:coffee object:Test 15 | bump spec/fixtures/test.coffee /^bump = (wat) ->$/;" f line:1 language:coffee object:window 16 | bump_up spec/fixtures/test.coffee /^ bump_up : ->$/;" f line:12 language:coffee object:Wat.ho.@lolWat 17 | constructor spec/fixtures/campfire.coffee /^ constructor: (api_key, host) ->$/;" p line:8 language:coffee object:Campfire 18 | do spec/fixtures/test.coffee /^ do (element) ->$/;" b line:29 language:coffee 19 | do spec/fixtures/test.coffee /^ do (f) ->$/;" b line:36 language:coffee 20 | f spec/fixtures/test.coffee /^for f in dir$/;" o line:35 language:coffee object:window 21 | for spec/fixtures/test.coffee /^ for element in lol$/;" b line:28 language:coffee 22 | for spec/fixtures/test.coffee /^for f in dir$/;" b line:35 language:coffee 23 | handlers spec/fixtures/campfire.coffee /^ handlers: (callbacks) ->$/;" p line:14 language:coffee object:Campfire 24 | ho spec/fixtures/test.coffee /^ ho : (x) ->$/;" f line:5 language:coffee object:Wat 25 | if spec/fixtures/test.coffee /^ if wat$/;" b line:24 language:coffee 26 | if spec/fixtures/test.coffee /^ if z.isSomethingRidic$/;" b line:21 language:coffee 27 | loop spec/fixtures/test.coffee /^Array::loop = (x) ->$/;" p line:46 language:coffee object:Array 28 | obj spec/fixtures/test.coffee /^obj =$/;" o line:49 language:coffee object:window 29 | onFailure spec/fixtures/campfire.coffee /^ onFailure: (response) ->$/;" f line:24 language:coffee object:Campfire.handlers.resp 30 | onSuccess spec/fixtures/campfire.coffee /^ onSuccess : (response) ->$/;" f line:16 language:coffee object:Campfire.handlers.resp 31 | recent spec/fixtures/campfire.coffee /^ recent: (id, since, callbacks) ->$/;" p line:40 language:coffee object:Campfire 32 | roomInfo spec/fixtures/campfire.coffee /^ roomInfo: (id, callbacks) ->$/;" p line:34 language:coffee object:Campfire 33 | rooms spec/fixtures/campfire.coffee /^ rooms: (callbacks) ->$/;" p line:29 language:coffee object:Campfire 34 | -------------------------------------------------------------------------------- /spec/fixtures/out.test-relative-append.ctags: -------------------------------------------------------------------------------- 1 | !_TAG_FILE_FORMAT 2 /extended format/ 2 | !_TAG_FILE_SORTED 0 /0=unsorted, 1=sorted, 2=foldcase/ 3 | !_TAG_PROGRAM_AUTHOR Łukasz Korecki /lukasz@korecki.me/ 4 | !_TAG_PROGRAM_NAME CoffeeTags // 5 | !_TAG_PROGRAM_URL https://github.com/lukaszkorecki/CoffeeTags /GitHub repository/ 6 | !_TAG_PROGRAM_VERSION 0.6.0 // 7 | @filter spec/fixtures/test.coffee /^ @filter = ->$/;" f line:14 language:coffee object:bump 8 | @lolWat spec/fixtures/test.coffee /^ @lolWat =$/;" v line:9 language:coffee object:Wat.ho 9 | Wat spec/fixtures/test.coffee /^Wat =$/;" v line:4 language:coffee object:window 10 | _loop spec/fixtures/test.coffee /^_loop = (x) ->$/;" f line:19 language:coffee object:window 11 | beam_magnum spec/fixtures/test.coffee /^beam_magnum : -> deployed(true)$/;" f line:44 language:coffee object:window 12 | bound_func spec/fixtures/test.coffee /^bound_func = (ok) => wat(ok)$/;" f line:41 language:coffee object:window 13 | bump spec/fixtures/test.coffee /^bump = (wat) ->$/;" f line:1 language:coffee object:window 14 | bump spec/fixtures/test.coffee /^bump = (wat) ->$/;" f line:1 language:coffee object:window 15 | bump_up spec/fixtures/test.coffee /^ bump_up : ->$/;" f line:12 language:coffee object:bump 16 | className spec/fixtures/test.coffee /^ className: 'notAClass'$/;" v line:50 language:coffee object:obj 17 | dir spec/fixtures/test.coffee /^dir = fs.readdirSync __dirname$/;" v line:32 language:coffee object:window 18 | do spec/fixtures/test.coffee /^ do (element) ->$/;" b line:29 language:coffee 19 | do spec/fixtures/test.coffee /^ do (f) ->$/;" b line:36 language:coffee 20 | for spec/fixtures/test.coffee /^ for element in lol$/;" b line:28 language:coffee 21 | for spec/fixtures/test.coffee /^for f in dir$/;" b line:35 language:coffee 22 | forVariable spec/fixtures/test.coffee /^ forVariable = 2 * element$/;" v line:30 language:coffee object:_loop 23 | fu spec/fixtures/test.coffee /^ fu = 1$/;" v line:22 language:coffee object:_loop 24 | ho spec/fixtures/test.coffee /^ ho : (x) ->$/;" f line:5 language:coffee object:Wat 25 | if spec/fixtures/test.coffee /^ if wat$/;" b line:24 language:coffee 26 | if spec/fixtures/test.coffee /^ if z.isSomethingRidic$/;" b line:21 language:coffee 27 | loop spec/fixtures/test.coffee /^Array::loop = (x) ->$/;" p line:46 language:coffee object:Array 28 | nice spec/fixtures/test.coffee /^ nice = 'ok'$/;" v line:25 language:coffee object:_loop 29 | obj spec/fixtures/test.coffee /^obj =$/;" v line:49 language:coffee object:window 30 | v spec/fixtures/test.coffee /^ v = 'test'$/;" v line:2 language:coffee object:bump 31 | woop spec/fixtures/test.coffee /^ woop = 1$/;" v line:20 language:coffee object:_loop 32 | x spec/fixtures/test.coffee /^ x = 'o'$/;" v line:6 language:coffee object:Wat.ho 33 | x spec/fixtures/test.coffee /^ x = 'z' if bamp$/;" v line:7 language:coffee 34 | x spec/fixtures/test.coffee /^ zorb = get['x=\\\\/f'].getLast('/woot\$')$/;" v line:39 language:coffee 35 | x spec/fixtures/test.coffee /^x = dir + '/foo'$/;" v line:33 language:coffee 36 | -------------------------------------------------------------------------------- /spec/fixtures/out.test-relative.ctags: -------------------------------------------------------------------------------- 1 | !_TAG_FILE_FORMAT 2 /extended format/ 2 | !_TAG_FILE_SORTED 0 /0=unsorted, 1=sorted, 2=foldcase/ 3 | !_TAG_PROGRAM_AUTHOR Łukasz Korecki /lukasz@korecki.me/ 4 | !_TAG_PROGRAM_NAME CoffeeTags // 5 | !_TAG_PROGRAM_URL https://github.com/lukaszkorecki/CoffeeTags /GitHub repository/ 6 | !_TAG_PROGRAM_VERSION 0.6.0 // 7 | @filter ../spec/fixtures/test.coffee /^ @filter = ->$/;" f line:14 language:coffee object:Wat.ho 8 | Campfire ../spec/fixtures/campfire.coffee /^class Campfire$/;" c line:3 language:coffee 9 | Test ../spec/fixtures/campfire.coffee /^class Test$/;" c line:45 language:coffee 10 | Wat ../spec/fixtures/test.coffee /^Wat =$/;" o line:4 language:coffee object:window 11 | _loop ../spec/fixtures/test.coffee /^_loop = (x) ->$/;" f line:19 language:coffee object:window 12 | beam_magnum ../spec/fixtures/test.coffee /^beam_magnum : -> deployed(true)$/;" f line:44 language:coffee object:window 13 | bound_func ../spec/fixtures/test.coffee /^bound_func = (ok) => wat(ok)$/;" f line:41 language:coffee object:window 14 | bump ../spec/fixtures/campfire.coffee /^ bump : ->$/;" p line:46 language:coffee object:Test 15 | bump ../spec/fixtures/test.coffee /^bump = (wat) ->$/;" f line:1 language:coffee object:window 16 | bump_up ../spec/fixtures/test.coffee /^ bump_up : ->$/;" f line:12 language:coffee object:Wat.ho.@lolWat 17 | constructor ../spec/fixtures/campfire.coffee /^ constructor: (api_key, host) ->$/;" p line:8 language:coffee object:Campfire 18 | do ../spec/fixtures/test.coffee /^ do (element) ->$/;" b line:29 language:coffee 19 | do ../spec/fixtures/test.coffee /^ do (f) ->$/;" b line:36 language:coffee 20 | f ../spec/fixtures/test.coffee /^for f in dir$/;" o line:35 language:coffee object:window 21 | for ../spec/fixtures/test.coffee /^ for element in lol$/;" b line:28 language:coffee 22 | for ../spec/fixtures/test.coffee /^for f in dir$/;" b line:35 language:coffee 23 | handlers ../spec/fixtures/campfire.coffee /^ handlers: (callbacks) ->$/;" p line:14 language:coffee object:Campfire 24 | ho ../spec/fixtures/test.coffee /^ ho : (x) ->$/;" f line:5 language:coffee object:Wat 25 | if ../spec/fixtures/test.coffee /^ if wat$/;" b line:24 language:coffee 26 | if ../spec/fixtures/test.coffee /^ if z.isSomethingRidic$/;" b line:21 language:coffee 27 | loop ../spec/fixtures/test.coffee /^Array::loop = (x) ->$/;" p line:46 language:coffee object:Array 28 | obj ../spec/fixtures/test.coffee /^obj =$/;" o line:49 language:coffee object:window 29 | onFailure ../spec/fixtures/campfire.coffee /^ onFailure: (response) ->$/;" f line:24 language:coffee object:Campfire.handlers.resp 30 | onSuccess ../spec/fixtures/campfire.coffee /^ onSuccess : (response) ->$/;" f line:16 language:coffee object:Campfire.handlers.resp 31 | recent ../spec/fixtures/campfire.coffee /^ recent: (id, since, callbacks) ->$/;" p line:40 language:coffee object:Campfire 32 | roomInfo ../spec/fixtures/campfire.coffee /^ roomInfo: (id, callbacks) ->$/;" p line:34 language:coffee object:Campfire 33 | rooms ../spec/fixtures/campfire.coffee /^ rooms: (callbacks) ->$/;" p line:29 language:coffee object:Campfire 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CoffeeTags 2 | 3 | ### Latest version: [![Gem version][ruby-gems-image]][ruby-gems-url] 4 | 5 | A simple tool for generating CoffeeScript tags (Ctags compatible). 6 | 7 | [![Build Status][travis-image]][travis-url] [![Coverage Status][coveralls-image]][coveralls-url] 8 | 9 | ### [Watch a quick demo](https://asciinema.org/a/26) 10 | 11 | ## Huh? 12 | 13 | CoffeeTags was created for use with Vim and [TagBar plugin](https://github.com/majutsushi/tagbar), however it 14 | accepts most common ctags arguments, therefore the following: 15 | 16 | `coffeetags -R -f TAGS` 17 | 18 | 19 | will generate standard TAGS file which later can be used with Vim (standard `:tag` command works as expected) 20 | 21 | # Requirements 22 | 23 | * ruby 1.8.7 and up 24 | 25 | ### Windows support 26 | 27 | [Yup, we got it!](https://github.com/lukaszkorecki/CoffeeTags/issues/28#issuecomment-44046429) 28 | 29 | 30 | ### Editors supported 31 | 32 | * [NeoVim](https://neovim.io) with [TagBar](https://github.com/majutsushi/tagbar) 33 | * Vim with [TagBar](https://github.com/majutsushi/tagbar) 34 | * [Sublime Text](http://www.sublimetext.com/) and [CTags plugin](https://github.com/SublimeText/CTags) 35 | 36 | 37 | # Halp! 38 | 39 | Just use `coffeetags --help` 40 | 41 | # Ruby Gem 42 | 43 | ## Installation 44 | 45 | `gem install CoffeeTags` (or `sudo gem install CoffeeTags`) 46 | 47 | ## Usage 48 | 49 | `$ coffeetags --help` 50 | 51 | # Vim 52 | 53 | This can also be used as a vim plugin that will update tag files on save, and support visualization with [TagBar](https://github.com/majutsushi/tagbar). You will still need to install the gem as described above as well as install the plugin to vim. You can install it via: 54 | 55 | ## Install 56 | 57 | * [Pathogen](https://github.com/tpope/vim-pathogen) 58 | * `git clone https://github.com/lukaszkorecki/CoffeeTags ~/.vim/bundle/CoffeeTags` 59 | * [NeoBundle](https://github.com/Shougo/neobundle.vim) 60 | * `NeoBundle 'lukaszkorecki/CoffeeTags'` 61 | * [Vundle](https://github.com/gmarik/vundle) 62 | * `Bundle 'lukaszkorecki/CoffeeTags'` 63 | * manual 64 | * copy all of the files into your `~/.vim` directory 65 | 66 | ## Configuration 67 | 68 | In you `~/.vimrc` you can configure the plugin with: 69 | 70 | ``` 71 | let g:CoffeeAutoTagDisabled=<0 or 1> " Disables autotaging on save (Default: 0 [false]) 72 | let g:CoffeeAutoTagFile= " Name of the generated tag file (Default: ./tags) 73 | let g:CoffeeAutoTagIncludeVars=<0 or 1> " Includes variables (Default: 0 [false]) 74 | let g:CoffeeAutoTagTagRelative=<0 or 1> " Sets file names to the relative path from the tag file location to the tag file location (Default: 1 [true]) 75 | ``` 76 | 77 | # Sublime Text 78 | 79 | See [this issue on SublimeText/Ctags](https://github.com/SublimeText/CTags/issues/33) 80 | 81 | # Config types 82 | 83 | CoffeeTags can work in 2 modes: 84 | 85 | - tags only for functions (default) 86 | - tags for functions and objects containing them 87 | 88 | Second mode is activated by: 89 | 90 | - Adding `--include-vars` to command line arguments 91 | - Setting `let g:CoffeeAutoTagIncludeVars=1` in your `~/.vimrc` for vim 92 | 93 | # TODO 94 | 95 | - squash all bugs 96 | 97 | # License 98 | 99 | MIT 100 | 101 | [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/lukaszkorecki/coffeetags/trend.png)](https://bitdeli.com/free "Bitdeli Badge") 102 | 103 | [travis-url]: https://travis-ci.org/lukaszkorecki/CoffeeTags 104 | [travis-image]: https://travis-ci.org/lukaszkorecki/CoffeeTags.svg?branch=master 105 | 106 | [ruby-gems-url]: http://rubygems.org/gems/CoffeeTags 107 | [ruby-gems-image]: https://badge.fury.io/rb/CoffeeTags.svg 108 | 109 | [coveralls-url]: https://coveralls.io/r/lukaszkorecki/CoffeeTags?branch=master 110 | [coveralls-image]: https://img.shields.io/coveralls/lukaszkorecki/CoffeeTags.svg 111 | -------------------------------------------------------------------------------- /lib/CoffeeTags.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require "CoffeeTags/version" 3 | require "CoffeeTags/parser" 4 | require "CoffeeTags/formatter" 5 | require "pathname" 6 | require 'optparse' 7 | require 'erb' 8 | 9 | class Object 10 | def blank? 11 | if self.respond_to? :"empty?" 12 | self.nil? or self.empty? 13 | else 14 | self.nil? 15 | end 16 | end 17 | end 18 | 19 | module Coffeetags 20 | AUTHOR = "Łukasz Korecki /lukasz@korecki.me/" 21 | NAME = "CoffeeTags" 22 | URL = "https://github.com/lukaszkorecki/CoffeeTags" 23 | 24 | class Utils 25 | 26 | def self.option_parser args 27 | args << '-h' if args.empty? 28 | options = {} 29 | optparse = OptionParser.new do |opts| 30 | opts.banner = (<<-BAN 31 | --------------------------------------------------------------------- 32 | #{NAME} #{Coffeetags::VERSION} 33 | --------------------------------------------------------------------- 34 | by #{AUTHOR} ( #{URL} ) 35 | Usage: 36 | coffeetags [OPTIONS] 37 | 38 | CoffeeTags + TagBar + Vim ---> https://gist.github.com/1935512 39 | --------------------------------------------------------------------- 40 | BAN 41 | ).gsub(/^\s*/,'') 42 | 43 | 44 | opts.on('--include-vars', "Include variables in generated tags") do |o| 45 | options[:include_vars] = true 46 | end 47 | 48 | opts.on('-f', '--file FILE', 'Write tags to FILE (use - for std out)') do |o| 49 | options[:output] = o unless o == '-' 50 | end 51 | 52 | opts.on('-a', '--append', 'Append tags to existing tags file') do |o| 53 | options[:append] = true 54 | end 55 | 56 | opts.on('-R', '--recursive', 'Process current directory recursively') do |o| 57 | options[:recur] = true 58 | end 59 | 60 | opts.on('--tag-relative', 'Should paths be relative to location of tag file?') do |o| 61 | options[:tag_relative] = true 62 | end 63 | 64 | opts.on('-v', '--version', 'Current version') do 65 | puts Coffeetags::VERSION 66 | exit 67 | end 68 | 69 | opts.on('--list-kinds', 'Lists the tag kinds') do 70 | Coffeetags::Formatter.kinds().map { |k, v| puts "#{k} #{v}" } 71 | options[:exit] = true 72 | end 73 | 74 | opts.on('-h','--help','HELP') do 75 | puts opts 76 | options[:exit] = true 77 | end 78 | 79 | end 80 | 81 | optparse.parse! args 82 | 83 | options[:files] = args.to_a 84 | options[:files] += Dir['./**/*.coffee', './**/Cakefile'] if options[:recur] 85 | 86 | options 87 | end 88 | 89 | 90 | def self.run options 91 | exit if options[:exit] 92 | 93 | output = options[:output] 94 | include_vars = options[:include_vars] 95 | files = options[:files] 96 | 97 | files = [files] if files.is_a? String 98 | files = files.reject { |f| f =~ /^-/} 99 | 100 | lines = setup_tag_lines output, files, options[:append] 101 | 102 | __out = if output.nil? 103 | STDOUT 104 | else 105 | File.open output, 'w' 106 | end 107 | 108 | __out << Coffeetags::Formatter.header 109 | 110 | files.each do |file| 111 | sc = File.read file 112 | parser = Coffeetags::Parser.new sc, include_vars 113 | parser.execute! 114 | 115 | tag_file_path = file_path file, output, options[:tag_relative] 116 | formatter = Coffeetags::Formatter.new tag_file_path, parser.tree 117 | 118 | formatter.parse_tree 119 | 120 | lines.concat(formatter.lines) 121 | end 122 | 123 | lines.sort! 124 | __out << lines.map { |l| "#{l}\n"}.join('') 125 | 126 | __out.close if __out.respond_to? :close 127 | 128 | __out.join("\n") if __out.is_a? Array 129 | end 130 | 131 | def self.setup_tag_lines output, files, append 132 | return [] unless append 133 | return [] if output.nil? 134 | return [] unless File.exists? output 135 | 136 | files = [] if files.nil? 137 | absolute_files = files.map {|f| Pathname.new(f).expand_path } 138 | 139 | output_dir = Pathname.new(output).dirname 140 | 141 | File.readlines(output). 142 | map {|l| l.strip}. 143 | reject {|l| l =~ /^!_/ }. 144 | reject {|l| 145 | raw_file_path = l.split("\t")[1] 146 | tag_file = Pathname.new(raw_file_path) 147 | 148 | tag_file = output_dir + tag_file if raw_file_path =~ /^\./ 149 | 150 | absolute_files.include? tag_file.expand_path 151 | } 152 | end 153 | 154 | def self.file_path file, output, tag_relative 155 | return file if output.nil? 156 | return file unless tag_relative 157 | 158 | output_path = Pathname.new(output).expand_path 159 | file_path = Pathname.new(file).expand_path 160 | 161 | file_path.relative_path_from(output_path.dirname).to_s 162 | end 163 | 164 | end 165 | end 166 | -------------------------------------------------------------------------------- /spec/coffeetags_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | include Coffeetags 3 | describe Utils do 4 | context 'Argument parsing' do 5 | it "returns nil when nothing is passed" do 6 | Utils.option_parser([]).should == { :exit => true, :files => [] } 7 | end 8 | 9 | it "returns files list" do 10 | Utils.option_parser(['lol.coffee']).should == {:files => ['lol.coffee']} 11 | end 12 | 13 | it "parses --include-vars option" do 14 | Utils.option_parser( [ '--include-vars', 'lol.coffee']).should == { :include_vars => true, :files => ["lol.coffee"]} 15 | end 16 | 17 | it "parses --append option" do 18 | Utils.option_parser( ['--append', 'lol.coffee']).should == { :append => true, :files => ["lol.coffee"]} 19 | end 20 | 21 | it "parses --tag-relative option" do 22 | Utils.option_parser( [ '--tag-relative', 'lol.coffee']).should == { :tag_relative => true, :files => ['lol.coffee'] } 23 | end 24 | 25 | it "parses -f option" do 26 | Utils.option_parser( [ '-f','tags' ,'lol.coffee']).should == { :output => 'tags', :files => ['lol.coffee'] } 27 | end 28 | 29 | it "outputs list-kinds to console" do 30 | options = nil 31 | output = capture_stdout { 32 | options = Utils.option_parser( ['--list-kinds']) 33 | } 34 | output.should == <<-TAG 35 | f function 36 | c class 37 | o object 38 | v var 39 | p proto 40 | b block 41 | TAG 42 | 43 | options[:exit].should == true 44 | end 45 | end 46 | 47 | context 'Relative file path' do 48 | it "returns file with nil output" do 49 | Utils.file_path("some/file", nil, nil).should == "some/file" 50 | end 51 | 52 | it "returns file with non relative path" do 53 | Utils.file_path("some/file", "some/output", nil).should == "some/file" 54 | end 55 | 56 | it "returns relative path to file from output" do 57 | Utils.file_path("/some/path/to/file", "/some/path/for/output", true).should == "../to/file" 58 | end 59 | 60 | it "returns relative path from cwd" do 61 | Utils.file_path("some/path/to/file", ".git/tags", true).should == "../some/path/to/file" 62 | end 63 | end 64 | 65 | context 'Parser runner' do 66 | 67 | before do 68 | @fake_parser = mock('Parser') 69 | @fake_parser.stub! :"execute!" 70 | @fake_parser.stub!( :tree).and_return [] 71 | 72 | Parser.stub!(:new).and_return @fake_parser 73 | 74 | @fake_formatter = mock 'Formatter' 75 | @fake_formatter.stub! :parse_tree 76 | @fake_formatter.stub!(:lines).and_return %w{ tag tag2 tag3 } 77 | @fake_formatter.stub!(:tags).and_return <<-TAG 78 | tag 79 | tag2 80 | tag3 81 | TAG 82 | Coffeetags::Formatter.stub!(:new).and_return @fake_formatter 83 | Coffeetags::Formatter.stub!(:header).and_return "header\n" 84 | File.stub!(:read).and_return 'woot@' 85 | end 86 | 87 | 88 | it "opens the file and writes tags to it" do 89 | Utils.run({ :output => 'test.tags', :files => ['woot'] }) 90 | 91 | `cat test.tags`.should== <<-FF 92 | header 93 | tag 94 | tag2 95 | tag3 96 | FF 97 | 98 | end 99 | 100 | after :each do 101 | `rm test.tags` 102 | end 103 | 104 | end 105 | 106 | context 'Runner exit' do 107 | it 'exits when the exit flag is present' do 108 | expect { 109 | Coffeetags::Utils.run({ :exit => true }) 110 | }.to raise_error SystemExit 111 | end 112 | end 113 | 114 | context "Complete output" do 115 | it "generates tags for given file" do 116 | Coffeetags::Utils.run({ :output => 'test.out', :files => 'spec/fixtures/test.coffee' }) 117 | 118 | File.read("test.out").should == File.read("./spec/fixtures/out.test.ctags") 119 | end 120 | 121 | it "generates tags for given files" do 122 | Coffeetags::Utils.run({ :output => 'test.out', :files => ['spec/fixtures/test.coffee', 'spec/fixtures/campfire.coffee'] }) 123 | 124 | File.read("test.out").should == File.read("./spec/fixtures/out.test-two.ctags") 125 | 126 | end 127 | 128 | it "generates tags with relative path from tags file" do 129 | files = [ "spec/fixtures/test.coffee", 'spec/fixtures/campfire.coffee'] 130 | 131 | FileUtils.mkdir "testout" unless File.directory? "testout" 132 | output = "testout/test.out" 133 | 134 | Coffeetags::Utils.run({ :output => output, :files => files, :tag_relative => true }) 135 | 136 | File.read(output).should == File.read("./spec/fixtures/out.test-relative.ctags") 137 | end 138 | 139 | it "appends tags for given file" do 140 | FileUtils.cp 'spec/fixtures/append.ctags', 'test.out' 141 | 142 | Coffeetags::Utils.run({ :output => 'test.out', :files => ['spec/fixtures/campfire.coffee'], :append => true }) 143 | 144 | File.read("test.out").should == File.read("./spec/fixtures/append-expected.ctags") 145 | end 146 | 147 | it "appends tags with tag relative for given file" do 148 | FileUtils.mkdir "testout" unless File.directory? "testout" 149 | output = "testout/test.out" 150 | 151 | append_tags = 'testout/append_test.out' 152 | Coffeetags::Utils.run({ :output => append_tags, :files => ["spec/fixtures/test.coffee"], :tag_relative => true }) 153 | FileUtils.cp append_tags, output 154 | 155 | lines = Coffeetags::Utils.run({ :output => output, :files => ["spec/fixtures/campfire.coffee"], :append => true, :tag_relative => true }) 156 | File.read(output).should == File.read("./spec/fixtures/out.test-relative.ctags") 157 | end 158 | 159 | after :each do 160 | `rm test.out` if File.exists? 'test.out' 161 | `rm -rf testout` if File.directory? 'testout' 162 | end 163 | end 164 | 165 | context "setup tag lines" do 166 | it "returns empty array when append flag is false" do 167 | Coffeetags::Utils.setup_tag_lines("spec/fixtures/out.test-two.ctags", ["spec/fixtures/test.coffee"], false).should == [] 168 | end 169 | 170 | it "returns empty array for nil output" do 171 | Coffeetags::Utils.setup_tag_lines(nil, nil, true).should == [] 172 | end 173 | 174 | it "returns empty array for nil output" do 175 | Coffeetags::Utils.setup_tag_lines("file/does/not/exist", nil, true).should == [] 176 | end 177 | 178 | it "returns contents of output file without header" do 179 | lines = Coffeetags::Utils.setup_tag_lines("spec/fixtures/blockcomment.ctags", nil, true).map {|l| l.split("\t")[0]} 180 | lines.should == %w{baz echo2 echo3 foo foo2} 181 | end 182 | 183 | it "returns contents of output file without header and without tags for files that will be indexed" do 184 | lines = Coffeetags::Utils.setup_tag_lines("spec/fixtures/out.test-two.ctags", ["spec/fixtures/test.coffee"], true).map {|l| l.split("\t")[0]} 185 | lines.should == %w{Campfire Test bump constructor handlers onFailure onSuccess recent roomInfo rooms} 186 | end 187 | 188 | it "returns contents of output file with relative file paths without header and without tags for files that will be indexed" do 189 | FileUtils.mkdir "testout" unless File.directory? "testout" 190 | output = "testout/test.out" 191 | 192 | FileUtils.cp "spec/fixtures/out.test-relative.ctags", output 193 | 194 | lines = Coffeetags::Utils.setup_tag_lines(output, ["spec/fixtures/test.coffee"], true).map {|l| l.split("\t")[0]} 195 | lines.should == %w{Campfire Test bump constructor handlers onFailure onSuccess recent roomInfo rooms} 196 | end 197 | 198 | # FIXME: setup_tag_lines will reject some lines ... 199 | it "returns contents of output file with relative file paths from absolute file path" do 200 | FileUtils.mkdir "testout" unless File.directory? "testout" 201 | output = "testout/test.out" 202 | 203 | FileUtils.cp "spec/fixtures/out.test-relative.ctags", output 204 | 205 | expanded_path = Pathname.new("spec/fixtures/test.coffee").expand_path.to_s 206 | 207 | lines = Coffeetags::Utils.setup_tag_lines(output, [expanded_path], true).map {|l| l.split("\t")[0]} 208 | lines.should == %w{Campfire Test bump constructor handlers onFailure onSuccess recent roomInfo rooms} 209 | end 210 | 211 | it "returns contents of output file with relative file paths from absolution output path" do 212 | FileUtils.mkdir "testout" unless File.directory? "testout" 213 | output = "testout/test.out" 214 | 215 | FileUtils.cp "spec/fixtures/out.test-relative.ctags", output 216 | 217 | lines = Coffeetags::Utils.setup_tag_lines(Pathname.new(output).expand_path.to_s, ["spec/fixtures/test.coffee"], true).map {|l| l.split("\t")[0]} 218 | lines.should == %w{Campfire Test bump constructor handlers onFailure onSuccess recent roomInfo rooms} 219 | end 220 | end 221 | 222 | end 223 | -------------------------------------------------------------------------------- /spec/parser_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | describe 'CoffeeTags::Parser' do 3 | before :all do 4 | @campfire_class = File.read File.expand_path('./spec/fixtures/campfire.coffee') 5 | @class_with_dot = File.read File.expand_path('./spec/fixtures/class_with_dot.coffee') 6 | @class_with_at = File.read File.expand_path('./spec/fixtures/class_with_at.coffee') 7 | @test_file = File.read File.expand_path('./spec/fixtures/test.coffee') 8 | 9 | @cf_tree = YAML::load_file File.expand_path('./spec/fixtures/tree.yaml') 10 | @test_tree = YAML::load_file File.expand_path('./spec/fixtures/test_tree.yaml') 11 | 12 | end 13 | 14 | it "should work" do 15 | lambda { Coffeetags::Parser.new "\n" }.should_not raise_exception 16 | end 17 | 18 | 19 | context 'detect item level' do 20 | before :each do 21 | @parser = Coffeetags::Parser.new '' 22 | end 23 | 24 | it 'gets level from a string with no indent' do 25 | @parser.line_level("zooo").should == 0 26 | end 27 | 28 | it "gets level from spaces" do 29 | @parser.line_level(" def lol").should == 4 30 | end 31 | 32 | it "gets level from tabs" do 33 | @parser.line_level("\t\t\tdef lol").should == 3 34 | end 35 | end 36 | 37 | 38 | context "Creating scope path" do 39 | before(:each) do 40 | @parser = Coffeetags::Parser.new '' 41 | end 42 | it 'gets the scope path for function' do 43 | @parser.scope_path(@cf_tree[1], @cf_tree[0...1] ).should == 'Campfire' 44 | end 45 | 46 | it 'gets the scope path for second function' do 47 | @parser.scope_path(@cf_tree[3], @cf_tree[0..2] ).should == 'Campfire' 48 | end 49 | 50 | it "gets the scope for nested function" do 51 | @parser.scope_path(@cf_tree[5], @cf_tree[0..4]).should == 'Campfire.handlers.resp' 52 | end 53 | 54 | it "gets the scope of a function which comes after nested function" do 55 | @parser.scope_path(@cf_tree[7], @cf_tree[0..6]).should == 'Campfire' 56 | end 57 | 58 | it 'gets scope for last method defined in diff class' do 59 | @parser.scope_path(@cf_tree.last, @cf_tree).should == 'Test' 60 | end 61 | end 62 | 63 | context 'Parsing' do 64 | context 'Scoping' do 65 | before(:each) do 66 | @coffee_parser = Coffeetags::Parser.new @campfire_class, true 67 | @test_parser = Coffeetags::Parser.new @test_file, true 68 | @coffee_parser.execute! 69 | end 70 | 71 | def cf_defined_values_should_equal(cf, c) 72 | cf.each do |k, v| 73 | c[k].should == v 74 | end 75 | end 76 | 77 | it "parses the class" do 78 | c = @coffee_parser.tree.find { |i| i[:name] == 'Campfire'} 79 | cf = @cf_tree.find {|i| i[:name] == 'Campfire'} 80 | cf_defined_values_should_equal cf, c 81 | end 82 | 83 | it "parses the 2nd class" do 84 | c = @coffee_parser.tree.find { |i| i[:name] == 'Test'} 85 | c.should == @cf_tree.find {|i| i[:name] == 'Test'} 86 | end 87 | 88 | it "parses the class with dot in name" do 89 | @coffee_parser = Coffeetags::Parser.new @class_with_dot, true 90 | @coffee_parser.execute! 91 | 92 | c = @coffee_parser.tree.find { |i| i[:name] == 'App.Campfire'} 93 | c.should == @cf_tree.find {|i| i[:name] == 'App.Campfire'} 94 | end 95 | 96 | it "parses the class with @ in name" do 97 | @coffee_parser = Coffeetags::Parser.new @class_with_at, true 98 | @coffee_parser.execute! 99 | 100 | c = @coffee_parser.tree.find { |i| i[:name] == 'Campfire'} 101 | cf = @cf_tree.find {|i| i[:name] == 'Campfire'} 102 | cf_defined_values_should_equal cf, c 103 | end 104 | 105 | it "should distinguish from a variable that contains 'class'" do 106 | @test_parser.execute! 107 | c = @test_parser.tree.find { |i| i[:name] == 'className'} 108 | c.should == @test_tree.find { |i| i[:name] == 'className'} 109 | end 110 | 111 | it "parses the instance variable" do 112 | c = @coffee_parser.tree.find { |i| i[:name] == '@url'} 113 | c.should == @cf_tree.find {|i| i[:name] == '@url'} 114 | end 115 | 116 | it "parses the object literal with functions" do 117 | c = @coffee_parser.tree.find { |i| i[:name] == 'resp'} 118 | c.should == @cf_tree.find {|i| i[:name] == 'resp'} 119 | end 120 | 121 | it "parses a nested function" do 122 | c = @coffee_parser.tree.find { |i| i[:name] == 'onSuccess'} 123 | c.should == @cf_tree.find {|i| i[:name] == 'onSuccess'} 124 | end 125 | 126 | it "parses a method var" do 127 | c = @coffee_parser.tree.find { |i| i[:name] == 'url'} 128 | c.should == @cf_tree.find {|i| i[:name] == 'url'} 129 | end 130 | end 131 | end 132 | 133 | context 'Test.coffee parsing' do 134 | before(:each) do 135 | @parser_test = Coffeetags::Parser.new @test_file, true 136 | @parser_test.execute! 137 | end 138 | 139 | it "doesnt extract a variable from a tricky line" do 140 | @parser_test.tree.find { |i| i[:name] == 'Filter'}.should == nil 141 | end 142 | 143 | it 'correctly recognizes an object in if block' do 144 | pro = @parser_test.tree.find { |i| i[:name] == 'fu'} 145 | pro[:parent].should == '_loop' 146 | 147 | pro = @parser_test.tree.find { |i| i[:name] == 'nice'} 148 | pro[:parent].should == '_loop' 149 | end 150 | 151 | it 'correctly recognizes an object in for block' do 152 | pro = @parser_test.tree.find { |i| i[:name] == 'forVariable'} 153 | pro[:parent].should == '_loop.element' 154 | 155 | end 156 | it "detects a fat arrow function" do 157 | c = @parser_test.tree.find { |i| i[:name] == 'bound_func'} 158 | c.should == @test_tree.find {|i| i[:name] == 'bound_func'} 159 | 160 | end 161 | it "extracts a method defined in a prototype" do 162 | pro = @parser_test.tree.find { |i| i[:name] == 'loop'} 163 | exp = @test_tree.find { |i| i[:name] == 'loop'} 164 | pro.should_not be nil 165 | pro.should == exp 166 | end 167 | 168 | it 'Ignores #TODO: -> (and any other comment line)' do 169 | @parser_test.tree.find { |i| i[:name] == 'TODO' }.should be_nil 170 | 171 | end 172 | 173 | context 'for loop' do 174 | subject { 175 | @parser_test = Coffeetags::Parser.new @test_file, true 176 | @parser_test.execute! 177 | @parser_test.tree.find { |i| i[:name] == 'element'} 178 | } 179 | it "finds variables defined in for loop" do 180 | subject[:line].should == 28 181 | end 182 | 183 | it "extracts the name" do 184 | subject[:name].should == 'element' 185 | end 186 | 187 | it "detects the scope" do 188 | subject[:parent].should == '_loop' 189 | end 190 | 191 | it "should distinguish from a variable that contains 'for'" do 192 | e = @test_tree.find { |i| i[:name] == 'forVariable'} 193 | g = @parser_test.tree.find { |i| i[:line] == 30} 194 | g.should == e 195 | end 196 | end 197 | context 'Block comments' do 198 | let(:bc_file) { 'spec/fixtures/blockcomment.coffee'} 199 | let(:bc_ctags_file) { 'spec/fixtures/blockcomment.ctags'} 200 | let(:blockcomment_file) { File.read(bc_file)} 201 | let(:blockcomment_tree) { 202 | [{:level=>0, 203 | :parent=>"window", 204 | :source=>"echo2 :-> console.log 'echo'", 205 | :line=>7, 206 | :kind=>"f", 207 | :name=>"echo2"}, 208 | {:level=>0, 209 | :parent=>"window", 210 | :source=>"foo2 : (x) -> console.log 'bar \#{x}'", 211 | :line=>10, 212 | :kind=>"f", 213 | :name=>"foo2"}, 214 | {:level=>0, 215 | :parent=>"window", 216 | :source=>"baz : (x, y) ->", 217 | :line=>15, 218 | :kind=>"f", 219 | :name=>"baz"}, 220 | {:level=>0, 221 | :parent=>"window", 222 | :source=>"echo3 :-> console.log 'echo'", 223 | :line=>23, 224 | :kind=>"f", 225 | :name=>"echo3"}, 226 | {:level=>0, 227 | :parent=>"window", 228 | :source=>"foo : (x) -> console.log 'bar \#{x}'", 229 | :line=>26, 230 | :kind=>"f", 231 | :name=>"foo"}] 232 | } 233 | 234 | subject { 235 | parser = Coffeetags::Parser.new(blockcomment_file, true) 236 | parser.execute! 237 | parser 238 | } 239 | 240 | it 'ignores block comments when parsing the contents' do 241 | subject.tree.zip(blockcomment_tree).each do |subject_node, blockcomment_node| 242 | subject_node.should == blockcomment_node 243 | end 244 | end 245 | 246 | 247 | context 'top-down test' do 248 | let(:blockcomment_ctags){ File.read(bc_ctags_file) } 249 | subject { 250 | Coffeetags::Utils.run({ :output => 'test_bc.out', :files => bc_file}) 251 | File.read 'test_bc.out' 252 | } 253 | 254 | after(:all) { `rm test_bc.out` } 255 | 256 | it 'ignores block comments in output' do 257 | subject.should == blockcomment_ctags 258 | end 259 | end 260 | end 261 | end 262 | end 263 | -------------------------------------------------------------------------------- /lib/CoffeeTags/parser.rb: -------------------------------------------------------------------------------- 1 | module Coffeetags 2 | class Parser 3 | attr_reader :tree 4 | # Creates a new parser 5 | # 6 | # @param [String] source source of the CoffeeScript file 7 | # @param [Bool] include_vars include objects in generated tree (default false) 8 | # @return [Coffeetags::Parser] 9 | def initialize source, include_vars = false 10 | @include_vars = include_vars 11 | @source = source 12 | 13 | @fake_parent = 'window' 14 | 15 | # tree maps the ... tree :-) 16 | @tree = [] 17 | 18 | # regexes 19 | @block = /^\s*(if\s+|unless\s+|switch\s+|loop\s+|do\s+|for\s+)/ 20 | @class_regex = /\s*class\s+(?:@)?([\w\.]*)/ 21 | @func_regex = /^\s*(?[a-zA-Z0-9_]+)\s?[=:]\s?(?\([@a-zA-Z0-9_]*\))?\s?[=-]>/ 22 | @proto_meths = /^\s*(?[A-Za-z]+)::(?[@a-zA-Z0-9_]*)/ 23 | @var_regex = /([@a-zA-Z0-9_]+)\s*[:=]\s*[^-=]*$/ 24 | @token_regex = /([@a-zA-Z0-9_]+)\s*[:=]/ 25 | #@iterator_regex = /^\s*for\s+([a-zA-Z0-9_]*)\s*/ # for in/of 26 | @iterator_regex = /^\s*for\s+(?[a-zA-Z0-9_]+)\s+(in|of)(?.)*/ # use named captures too specify parent variable in iterator 27 | @comment_regex = /^\s*#/ 28 | @start_block_comment_regex = /^\s*###/ 29 | @end_block_comment_regex = /^.*###/ 30 | @oneline_block_comment_regex = /^\s*###.*###/ 31 | @comment_lines = mark_commented_lines 32 | end 33 | 34 | # Mark line numbers as commented out 35 | # either by single line comment (#) 36 | # or block comments (###~###) 37 | def mark_commented_lines 38 | [].tap do |reg| 39 | in_block_comment = false 40 | line_no = 0 41 | start_block = 0 42 | end_block = 0 43 | @source.each_line do |line| 44 | line_no = line_no+1 45 | 46 | start_block = line_no if !in_block_comment and line =~ @start_block_comment_regex 47 | end_block = line_no if start_block < line_no and line =~ @end_block_comment_regex 48 | end_block = line_no if line =~ @oneline_block_comment_regex 49 | 50 | in_block_comment = end_block < start_block 51 | 52 | reg << line_no if in_block_comment or end_block == line_no or line =~ @comment_regex 53 | end 54 | end 55 | end 56 | 57 | # Detect current line level based on indentation 58 | # very useful in parsing, since CoffeeScript's syntax 59 | # depends on whitespace 60 | # @param [String] line currently parsed line 61 | # @return [Integer] 62 | def line_level line 63 | line.match(/^[ \t]*/)[0].gsub("\t", " ").split('').length 64 | end 65 | 66 | # Generate current scope path, for example: 67 | # e -> 68 | # f -> 69 | # z -> 70 | # Scope path for function z would be: 71 | # window.e.f 72 | # @param [Hash, nil] _el element of a prase tree (last one for given tree is used by default) 73 | # @param [Array, nil] _tree parse tree (or currently built) 74 | # @returns [String] string representation of scope for given element 75 | def scope_path _el = nil, _tree = nil 76 | bf = [] 77 | tree = (_tree || @tree) 78 | element = (_el || tree.last) 79 | idx = tree.index(element) || -1 80 | 81 | current_level = element[:level] 82 | tree[0..idx].reverse.each_with_index do |item, index| 83 | # uhmmmmmm 84 | if item[:level] < current_level 85 | if item[:kind] == 'b' 86 | true 87 | elsif 88 | bf << item[:name] 89 | end 90 | current_level = item[:level] 91 | end 92 | end 93 | sp = bf.uniq.reverse.join('.') 94 | sp 95 | end 96 | 97 | # Helper function for generating parse tree elements for given 98 | # line and regular expression 99 | # 100 | # @param [String] line source line currently being parsed 101 | # @param [RegExp] regex regular expression for matching a syntax element 102 | # @param [Integer] level current indentation/line level 103 | # @param [Hash, {}] additional_fields additional fields which need to be added to generated element 104 | # @returns [Hash,nil] returns a parse tree element consiting of: 105 | # :name of the element 106 | # indentation :level of the element 107 | def item_for_regex line, regex, level, additional_fields={} 108 | if item = line.match(regex) 109 | entry_for_item = { 110 | :level => level 111 | } 112 | if item.length > 2 # proto method or func 113 | if regex == @proto_meths 114 | entry_for_item[:parent] = item[1] 115 | entry_for_item[:name] = item[2] 116 | elsif regex == @func_regex 117 | entry_for_item[:name] = item[1] 118 | #entry_for_item[:params] = item[2] # TODO: when formatting, show params in name ? 119 | end 120 | else 121 | entry_for_item[:name] = item[1] 122 | end 123 | entry_for_item.merge(additional_fields) 124 | end 125 | end 126 | 127 | # Look through the tree to see if it's possible to change an entry's kind 128 | # Differ the objects from simple variables 129 | # Should execute after the whole tree has been generated 130 | def comb_kinds tree 131 | entries_with_parent = tree.reject {|c| c[:parent].nil? } 132 | tree.each do |c| 133 | next c unless c[:kind] == 'v' 134 | maybe_child = entries_with_parent.select {|e| e[:parent] == c[:name]} 135 | unless maybe_child.empty? 136 | c[:kind] = 'o' 137 | end 138 | end 139 | tree 140 | end 141 | 142 | # trim the bloated tree 143 | # - when not required to include_vars, reject the variables 144 | def trim_tree tree 145 | unless @include_vars 146 | tree = tree.reject do |c| 147 | ['v'].include? c[:kind] 148 | end 149 | end 150 | tree 151 | end 152 | 153 | # Parse the source and create a tags tree 154 | # @note this method mutates @tree instance variable of Coffeetags::Parser instance 155 | # @returns self it can be chained 156 | def execute! 157 | line_n = 0 158 | level = 0 159 | classes = [] 160 | @source.each_line do |line| 161 | line_n += 1 162 | line.chomp! 163 | # indentify scopes 164 | level = line_level line 165 | 166 | # ignore comments! 167 | next if @comment_lines.include? line_n 168 | 169 | [ 170 | [@class_regex, 'c'], 171 | [@proto_meths, 'p'], 172 | [@func_regex, 'f'], 173 | [@var_regex, 'v'], 174 | [@block, 'b'] 175 | ].each do |regex, kind| 176 | mt = item_for_regex line, regex, level, :source => line, :line => line_n, :kind => kind 177 | unless mt.nil? 178 | # TODO: one token should not fit for multiple regex 179 | classes.push mt if kind == 'c' 180 | next if kind == 'f' # wait for later to determine whether it is a class method 181 | @tree << mt 182 | end 183 | end 184 | 185 | # instance variable or iterator (for/in)? 186 | token = line.match(@token_regex ) 187 | token ||= line.match(@iterator_regex) 188 | 189 | # we have found something! 190 | if not token.nil? 191 | # should find token through the tree first 192 | token_name = token[1] 193 | existing_token = @tree.find {|o| o[:name] == token_name} 194 | if existing_token 195 | o = existing_token 196 | else 197 | o = { 198 | :name => token_name, 199 | :level => level, 200 | :parent => '', 201 | :source => line, 202 | :line => line_n 203 | } 204 | end 205 | 206 | # Remove edge cases for now 207 | 208 | # - if a line containes a line like: element.getElement('type=[checkbox]').lol() 209 | token_match_in_line = false 210 | token_match_in_line = line.match token_name 211 | unless token_match_in_line.nil? 212 | offset = token_match_in_line.offset 0 213 | str_before = line.slice 0, offset[0] 214 | str_after = line.slice offset[1], line.size 215 | [str_before, str_after].map do |str| 216 | # if there are unmatch quotes, our token is in a string 217 | token_match_in_line = ['"', '\''].any? { |q| str.scan(q).size % 2 == 1 } 218 | end 219 | end 220 | 221 | if token_match_in_line 222 | @tree = @tree.reject {|c| c[:name] == o[:name]} 223 | next 224 | end 225 | 226 | # - scope access and comparison in if x == 'lol' 227 | is_in_comparison = line =~ /::|==/ 228 | 229 | # - objects with blank parent (parser bug?) 230 | has_blank_parent = o[:parent] =~ /\.$/ 231 | 232 | # - multiple consecutive assignments 233 | is_previous_not_the_same = !(@tree.last and @tree.last[:name] == o[:name] and @tree.last[:level] == o[:level]) 234 | 235 | if !token_match_in_line and is_in_comparison.nil? and (has_blank_parent.nil? or is_previous_not_the_same) 236 | unless o[:kind] 237 | o[:kind] = line =~ /[:=]{1}.*[-=]\s?\>/ ? 'f' : 'v' 238 | end 239 | o[:parent] = scope_path o 240 | o[:parent] = @fake_parent if o[:parent].empty? 241 | 242 | # treat variable and function with a class as parent as property 243 | if ['f', 'v', 'o'].include? o[:kind] 244 | # TODO: process func params 245 | maybe_parent_class = classes.find {|c| c[:name] == o[:parent] } 246 | if maybe_parent_class 247 | o[:kind] = 'p' 248 | end 249 | end 250 | 251 | @tree << o unless @tree.include? o 252 | end 253 | end 254 | end 255 | 256 | @tree = comb_kinds @tree 257 | 258 | @tree = trim_tree @tree 259 | 260 | # P.S when found a token, first lookup in the tree, thus the duplicate won't appear 261 | # so there is no need of uniq_tree 262 | self # chain! 263 | end 264 | end 265 | end 266 | --------------------------------------------------------------------------------