To compile with Homebrew-installed LLVM:
PATH=/usr/local/opt/llvm/bin:$PATH crystal build crystal-tags.cr
Instantly share code, notes, and snippets.
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) |