Skip to content

Instantly share code, notes, and snippets.

@rf-
Forked from datanoise/crystal-tags.cr
Last active March 8, 2019 06:39
Show Gist options
  • Save rf-/2b74152aae77a147a0dd1b7f102ea2ee to your computer and use it in GitHub Desktop.
Save rf-/2b74152aae77a147a0dd1b7f102ea2ee to your computer and use it in GitHub Desktop.
Crystal ctags generator

To compile with Homebrew-installed LLVM:

PATH=/usr/local/opt/llvm/bin:$PATH crystal build crystal-tags.cr
require "compiler/crystal/**"
require "option_parser"
include Crystal
class ToCtagsVisitor < Visitor
@filename : String
@lines : Array(String)
@io : IO
@opts : Generator
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 : CStructOrUnionDef)
visit_scope_node(node, "s")
end
def visit(node : LibDef)
visit_scope_node(node, "l")
end
def end_visit(node : ClassDef | ModuleDef | CStructOrUnionDef | 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'
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 && [email protected]?
@io << "\tclass:#{@scopes.join(".")}"
end
if @opts.show_line_tags
@io << "\tline:#{location.line_number}"
end
end
end
class Generator
@tag_fn : String
@fields : String
@excmd : String
@append : Bool
property :tag_fn, :excmd, :fields, :append
def initialize(@tag_fn, @fields, @excmd)
@append = false
end
def show_line_tags
@fields.index('n')
end
def show_short_tag
[email protected]('K')
end
def show_long_tag
@fields.index('K')
end
def show_scope
@fields.index('s')
end
def generate(filename)
output do |f|
unless append
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 //"
end
if File.directory?(filename)
walk_dir(filename) do |fn|
if File.extname(fn) == ".cr"
generate_tags(f, fn.sub(/^\.\//, ""))
end
end
else
if File.extname(filename) == ".cr"
generate_tags(f, filename)
else
STDERR.puts "#{filename} doesn't have .cr extension"
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
elsif @append
File.open(@tag_fn, "a") do |f|
yield f
end
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
p.on("--append", "append instead of replacing the tags file") do
generator.append = true
end
p.on("--exclude IGNORED", "ignored") do |v|
end
p.on("--options IGNORED", "ignored") do |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