Instantly share code, notes, and snippets.
Last active
March 5, 2021 03:24
-
Star
(6)
6
You must be signed in to star a gist -
Fork
(3)
3
You must be signed in to fork a gist
-
Save datanoise/c3e3465029009f55a6e4 to your computer and use it in GitHub Desktop.
ctags for crystal language
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
property :tag_fn, :excmd, :fields | |
def initialize(@tag_fn, @fields, @excmd) | |
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| | |
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
A very minor change:
https://gist.github.com/Fusion/44f2c2245f0e5afd2f42/revisions