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 && !@scopes.empty? | |
| @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 | |
| !@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| | |
| 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) |