diff options
Diffstat (limited to 'tools')
-rw-r--r-- | tools/nimfind.nim | 228 |
1 files changed, 228 insertions, 0 deletions
diff --git a/tools/nimfind.nim b/tools/nimfind.nim new file mode 100644 index 000000000..097ee599c --- /dev/null +++ b/tools/nimfind.nim @@ -0,0 +1,228 @@ +# +# +# The Nim Compiler +# (c) Copyright 2018 Andreas Rumpf +# +# See the file "copying.txt", included in this +# distribution, for details about the copyright. +# + +## Nimfind is a tool that helps to give editors IDE like capabilities. + +when not defined(nimcore): + {.error: "nimcore MUST be defined for Nim's core tooling".} +when not defined(nimfind): + {.error: "nimfind MUST be defined for Nim's nimfind tool".} + +const Usage = """ +Nimfind - Tool to find declarations or usages for Nim symbols +Usage: + nimfind [options] file.nim:line:col + +Options: + --help, -h show this help + --rebuild rebuild the index + --project:file.nim use file.nim as the entry point + +In addition, all command line options of Nim that do not affect code generation +are supported. +""" + +import strutils, os, parseopt, parseutils + +import "../compiler" / [options, commands, modules, sem, + passes, passaux, msgs, nimconf, + extccomp, condsyms, + ast, scriptconfig, + idents, modulegraphs, vm, prefixmatches, lineinfos, cmdlinehelper, + pathutils] + +import db_sqlite + +proc createDb(db: DbConn) = + db.exec(sql""" + create table if not exists filenames( + id integer primary key, + fullpath varchar(8000) not null + ); + """) + db.exec sql"create index if not exists FilenameIx on filenames(fullpath);" + + # every sym can have potentially 2 different definitions due to forward + # declarations. + db.exec(sql""" + create table if not exists syms( + id integer primary key, + nimid integer not null, + name varchar(256) not null, + defline integer not null, + defcol integer not null, + deffile integer not null, + deflineB integer not null default 0, + defcolB integer not null default 0, + deffileB integer not null default 0, + foreign key (deffile) references filenames(id), + foreign key (deffileB) references filenames(id) + ); + """) + + db.exec(sql""" + create table if not exists usages( + id integer primary key, + nimid integer not null, + line integer not null, + col integer not null, + file integer not null, + foreign key (file) references filenames(id), + foreign key (nimid) references syms(nimid) + ); + """) + +proc toDbFileId*(db: DbConn; conf: ConfigRef; fileIdx: FileIndex): int = + if fileIdx == FileIndex(-1): return -1 + let fullpath = toFullPath(conf, fileIdx) + let row = db.getRow(sql"select id from filenames where fullpath = ?", fullpath) + let id = row[0] + if id.len == 0: + result = int db.insertID(sql"insert into filenames(fullpath) values (?)", + fullpath) + else: + result = parseInt(id) + +type + FinderRef = ref object of RootObj + db: DbConn + +proc writeDef(graph: ModuleGraph; s: PSym; info: TLineInfo) = + let f = FinderRef(graph.backend) + f.db.exec(sql"""insert into syms(nimid, name, defline, defcol, deffile) values (?, ?, ?, ?, ?)""", + s.id, s.name.s, info.line, info.col, + toDbFileId(f.db, graph.config, info.fileIndex)) + +proc writeDefResolveForward(graph: ModuleGraph; s: PSym; info: TLineInfo) = + let f = FinderRef(graph.backend) + f.db.exec(sql"""update syms set deflineB = ?, defcolB = ?, deffileB = ? + where nimid = ?""", info.line, info.col, + toDbFileId(f.db, graph.config, info.fileIndex), s.id) + +proc writeUsage(graph: ModuleGraph; s: PSym; info: TLineInfo) = + let f = FinderRef(graph.backend) + f.db.exec(sql"""insert into usages(nimid, line, col, file) values (?, ?, ?, ?)""", + s.id, info.line, info.col, + toDbFileId(f.db, graph.config, info.fileIndex)) + +proc performSearch(conf: ConfigRef; dbfile: AbsoluteFile) = + var db = open(connection=string dbfile, user="nim", password="", + database="nim") + let pos = conf.m.trackPos + let fid = toDbFileId(db, conf, pos.fileIndex) + var row = db.getRow(sql"""select max(col) from usages where line = ? and file = ? and ? >= col""", + pos.line, fid, pos.col) + if row.len > 0: + let known = toFullPath(conf, pos.fileIndex) + let nimid = db.getRow(sql"""select nimid from usages where line = ? and file = ? and col = ?""", + pos.line, fid, row[0]) + for r in db.rows(sql"""select line, col, filenames.fullpath from usages + inner join filenames on filenames.id = file + where nimid = ?""", nimid): + let line = parseInt(r[0]) + let col = parseInt(r[1]) + let file = r[2] + if file == known and line == pos.line.int: + discard "don't output the line we already know" + else: + echo file, ":", line, ":", col+1 + close(db) + +proc setupDb(g: ModuleGraph; dbfile: AbsoluteFile) = + var f = FinderRef() + removeFile(dbfile) + f.db = open(connection=string dbfile, user="nim", password="", + database="nim") + createDb(f.db) + f.db.exec(sql"pragma journal_mode=off") + # This MUST be turned off, otherwise it's way too slow even for testing purposes: + f.db.exec(sql"pragma SYNCHRONOUS=off") + f.db.exec(sql"pragma LOCKING_MODE=exclusive") + g.backend = f + +proc mainCommand(graph: ModuleGraph) = + let conf = graph.config + let dbfile = getNimcacheDir(conf) / RelativeFile"nimfind.db" + if not fileExists(dbfile) or optForceFullMake in conf.globalOptions: + clearPasses(graph) + registerPass graph, verbosePass + registerPass graph, semPass + conf.cmd = cmdIdeTools + wantMainModule(conf) + setupDb(graph, dbfile) + + graph.onDefinition = writeUsage # writeDef + graph.onDefinitionResolveForward = writeUsage # writeDefResolveForward + graph.onUsage = writeUsage + + if not fileExists(conf.projectFull): + quit "cannot find file: " & conf.projectFull.string + add(conf.searchPaths, conf.libpath) + # do not stop after the first error: + conf.errorMax = high(int) + compileProject(graph) + close(FinderRef(graph.backend).db) + performSearch(conf, dbfile) + +proc processCmdLine*(pass: TCmdLinePass, cmd: string; conf: ConfigRef) = + var p = parseopt.initOptParser(cmd) + while true: + parseopt.next(p) + case p.kind + of cmdEnd: break + of cmdLongoption, cmdShortOption: + case p.key.normalize + of "help", "h": + stdout.writeline(Usage) + quit() + of "project": + conf.projectName = p.val + of "rebuild": + incl conf.globalOptions, optForceFullMake + else: processSwitch(pass, p, conf) + of cmdArgument: + let info = p.key.split(':') + if info.len == 3: + let (dir, file, ext) = info[0].splitFile() + conf.projectName = findProjectNimFile(conf, dir) + if conf.projectName.len == 0: conf.projectName = info[0] + try: + conf.m.trackPos = newLineInfo(conf, AbsoluteFile info[0], + parseInt(info[1]), parseInt(info[2])) + except ValueError: + quit "invalid command line" + else: + quit "invalid command line" + +proc handleCmdLine(cache: IdentCache; conf: ConfigRef) = + let self = NimProg( + suggestMode: true, + processCmdLine: processCmdLine, + mainCommand: mainCommand + ) + self.initDefinesProg(conf, "nimfind") + + if paramCount() == 0: + stdout.writeline(Usage) + return + + self.processCmdLineAndProjectPath(conf) + + # Find Nim's prefix dir. + let binaryPath = findExe("nim") + if binaryPath == "": + raise newException(IOError, + "Cannot find Nim standard library: Nim compiler not in PATH") + conf.prefixDir = AbsoluteDir binaryPath.splitPath().head.parentDir() + if not dirExists(conf.prefixDir / RelativeDir"lib"): + conf.prefixDir = AbsoluteDir"" + + discard self.loadConfigsAndRunMainCommand(cache, conf) + +handleCmdline(newIdentCache(), newConfigRef()) |