Skip to content

Instantly share code, notes, and snippets.

@Fusion
Forked from datanoise/crystal-tags.cr
Last active January 3, 2016 09:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Fusion/44f2c2245f0e5afd2f42 to your computer and use it in GitHub Desktop.
Save Fusion/44f2c2245f0e5afd2f42 to your computer and use it in GitHub Desktop.
ctags for crystal language
require "compiler/crystal/**"
require "option_parser"
include Crystal
class ToCtagsVisitor < Visitor
TAG_NAMES = {
"c" => "class",
"f" => "method",
"s" => "struct",
"m" => "module",
"l" => "library",
"t" => "type"
}
def initialize(@filename, @lines, @io, @opts)
@scopes = [] of String
@to_s_visitor = ToSVisitor.new(@io)
end
def visit(node)
true
end
def visit(node: ClassDef)
visit_scope_node(node, "c")
end
def visit(node: ModuleDef)
visit_scope_node(node, "m")
end
def visit(node: StructOrUnionDef)
visit_scope_node(node, "s")
end
def visit(node: LibDef)
visit_scope_node(node, "l")
end
def end_visit(node: ClassDef | ModuleDef | StructOrUnionDef | LibDef)
return unless node.location
pop_scope
end
def visit(node: Def | TypeDef | FunDef | Alias | Macro)
location = node.location
if location
@io << node.name
@io << "\t"
@io << @filename
@io << "\t"
print_location(location)
@io << ";\"\t"
case node
when TypeDef, Alias
print_type "t"
else
print_type "f"
end
print_flags location
@io << "\n"
end
true
end
private def print_location(location)
case @opts.excmd
when "pattern"
@io << "/^#{regexp_escape(@lines[location.line_number-1])}$/"
when "number"
@io << location.line_number
else
raise "Invalid option"
end
end
private def regexp_escape(str)
String.build do |result|
str.each_byte do |byte|
case byte.chr
when '\\', '?', '[', '^', ']', '$'
result << '\\'
result.write_byte byte
when '\n'
# get rid of spurious lf
else
result.write_byte byte
end
end
end
end
private def visit_scope_node(node, type)
location = node.location
if location
@io << node.name
@io << "\t"
@io << @filename
@io << "\t"
print_location location
@io << ";\"\t"
print_type type
print_flags location
@io << "\n"
push_scope node.name
end
true
end
private def push_scope(node)
case node
when Path
buf = [] of String
node.names.each_with_index do |name, i|
buf << "::" if i > 0 || node.global
buf << name
end
@scopes << buf.join
when String
@scopes << node
else
@scopes << node.class.name
end
end
private def pop_scope
@scopes.pop?
end
private def print_type(tag)
if @opts.show_short_tag
@io << tag
else
@io << TAG_NAMES[tag]
end
end
private def print_flags(location)
if @opts.show_scope && !@scopes.empty?
@io << "\tclass:#{@scopes.join(".")}"
end
if @opts.show_line_tags
@io << "\tline:#{location.line_number}"
end
end
end
class Generator
property :tag_fn, :excmd, :fields
def initialize(@tag_fn, @fields, @excmd)
end
def show_line_tags
@fields.index('n')
end
def show_short_tag
!@fields.index('K')
end
def show_long_tag
@fields.index('K')
end
def show_scope
@fields.index('s')
end
def generate(filename)
output do |f|
f.puts "!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;\" to lines/"
f.puts "!_TAG_FILE_SORTED 0 /0=unsorted, 1=sorted, 2=foldcase/"
f.puts "!_TAG_PROGRAM_AUTHOR Kent Sibilev //"
f.puts "!_TAG_PROGRAM_URL crystal-tags //"
f.puts "!_TAG_PROGRAM_VERSION 1.0 //"
if File.directory?(filename)
walk_dir(filename) do |fn|
if File.extname(fn) == ".cr"
generate_tags(f, fn)
end
end
else
if File.extname(filename) == ".cr"
generate_tags(f, filename)
else
STDERR.puts "#{filename} doesn't have .cr extention"
exit -1
end
end
end
end
private def generate_tags(io, filename)
data = File.read(filename)
parser = Parser.new(data)
parser.filename = filename
parser.wants_doc = false
node = parser.parse
node.accept(ToCtagsVisitor.new(filename, data.lines, io, self))
rescue ex : Crystal::Exception
STDERR.puts ex
end
private def walk_dir(dir, &block : String ->)
Dir.entries(dir).each do |fn|
filename = File.join(dir, fn)
block.call filename
if File.directory?(filename) && !fn.starts_with?(".")
walk_dir(filename, &block)
end
end
end
private def output
if @tag_fn == "-"
yield STDOUT
else
File.open(@tag_fn, "w") do |f|
yield f
end
end
end
end
generator = Generator.new "tags", "nks", "pattern"
begin
OptionParser.parse! do |p|
p.on("-f FILE", "specify the output file, '-' will output to standard output") do |v|
generator.tag_fn = v
end
p.on("--excmd EXCMD", "specify the type of EX command used. Either 'pattern' or 'number'") do |v|
unless %w[pattern number].includes?(v)
raise OptionParser::InvalidOption.new("Invalid excmd option #{v}")
end
generator.excmd = v
end
p.on("-n", "equivalent to --excmd number") do
generator.excmd = "number"
end
p.on("-N", "equivalent to --excmd pattern") do
generator.excmd = "pattern"
end
p.on("--fields FIELDS", "specify the available extention fields. k - single letter tag, K - full tag name, n - line number of tag definition, s - scope of tag definition") do |v|
generator.fields = v
end
end
rescue OptionParser::InvalidOption
STDOUT.puts "Usage: #{$0} -s <TAG_FILE> <FILE.CR>"
exit -1
end
filename = ARGV.first? || "."
generator.generate(filename)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment