Skip to content

Instantly share code, notes, and snippets.

@fernandes
Last active March 24, 2017 21:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save fernandes/469c1ac1f076c2e3e3a28fde1088bc85 to your computer and use it in GitHub Desktop.
Save fernandes/469c1ac1f076c2e3e3a28fde1088bc85 to your computer and use it in GitHub Desktop.
Generates ctags for a Crystal source - Crystal 0.17.4 [9d258f6] (2016-06-05)
# Generates ctags for a Crystal source
# Based on asterite script https://github.com/crystal-lang/crystal/issues/577#issuecomment-97054600
# This works on Crystal 0.17.4 [9d258f6] (2016-06-05)
#
require "compiler/crystal/**"
require "option_parser"
include Crystal
class CTagsVisitor < Visitor
record Tag, name : String, filename : String, regex : String, kind : String
getter tags
def initialize(@base_dir : String)
@files_to_lines = {} of String => Array(String)
@tags = [] of Tag
end
def process(filename)
contents = File.read(filename)
@files_to_lines[filename] = contents.lines
parser = Parser.new contents
parser.filename = filename
parser.parse.accept self
end
def visit(node : ClassDef)
process_node node, node.name.names.last, "c"
true
end
def visit(node : ModuleDef)
process_node node, node.name.names.last, "m"
true
end
def visit(node : Def)
process_node node, node.name, "d"
false
end
def visit(node : Macro)
process_node node, node.name, "x"
true
end
def visit(node : LibDef)
process_node node, node.name, "l"
true
end
def visit(node : StructOrUnionDef)
process_node node, node.name, "s"
true
end
def visit(node : FunDef)
process_node node, node.name, "l"
true
end
def visit(node : Expressions)
true
end
def visit(node : ASTNode)
true
end
def process_node(node, name, kind)
location = node.location
return true unless location
filename = location.filename
return true unless filename.is_a?(String)
line = @files_to_lines[filename][location.line_number - 1]
regex = escape_regex(line)
@tags << Tag.new(name, relativize(filename), regex, kind)
true
end
def relativize(filename)
if filename.starts_with?(@base_dir)
filename = filename[@base_dir.size .. -1]
filename = filename[1 .. -1] if filename.starts_with? '/'
filename
else
filename
end
end
REPLACEMENTS = {
"(": "\\(",
")": "\\)",
"[": "\\[",
"]": "\\]",
"{": "\\{",
"}": "\\}",
}
def escape_regex(string)
string.gsub(/\(\)\[\]\{\}/, REPLACEMENTS)
end
end
directory = ""
output = ""
def fetch_directory(args)
directory = args.empty? ? "." : args.shift
unless Dir.exists?(directory) || File.exists?(directory)
puts "file or directory #{directory} does not exists"
exit 1
end
if Dir.exists?(directory)
directory = directory.try &.+ "/**/*.cr"
end
directory
end
OptionParser.parse! do |opts|
opts.banner = <<-USAGE
Usage: crystaltags -o [FILE] [DIR]
DIR - directory will be read to generate ctags,
default: NAME, eg: ./custom/path/example
USAGE
opts.on("-o OUTPUT", "--output=OUTPUT", "output to (- STDIN)") do |opt|
output = opt
end
opts.on("--help", "show this help") do
puts opts
exit
end
opts.unknown_args do |args, after_dash|
directory = fetch_directory(args)
end
end
output = output.empty? ? "tags" : output
visitor = CTagsVisitor.new(Dir.current)
Dir.glob(directory) do |file|
visitor.process File.expand_path(file)
end
tags = visitor.tags
tags.sort_by! &.name
tags_output = String.build do |io|
io.puts %(!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/)
io.puts %(!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/)
tags.each do |tag|
io << tag.name << "\t" << tag.filename << "\t/^" << tag.regex.chomp << "$/;\"\t" << tag.kind
io.puts
end
end
if output == "-"
puts tags_output
else
File.open(output, "w") do |file|
file.puts tags_output
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment