use "collections"
use "files"
use "logger"
use ast = "../../ast"
use ".."
interface tag FindDefinitionNotify
be definition_found(
task_id: USize,
canonical_path: FilePath,
range: SrcRange)
be definition_failed(
task_id: USize,
message: String)
class SearchFileItem
let canonical_path: FilePath
let syntax_tree: (ast.Node | None)
let nodes_by_index: Map[USize, ast.Node] val
let scope: Scope
new create(
canonical_path': FilePath,
syntax_tree': (ast.Node | None),
nodes_by_index': Map[USize, ast.Node] val,
scope': Scope)
=>
canonical_path = canonical_path'
syntax_tree = syntax_tree'
nodes_by_index = nodes_by_index'
scope = scope'
actor FindDefinition is AnalyzerRequestNotify
// we keep an ordered list of paths to search, starting with the original one
// when they come in from the analyzer, we see if the first one is done
let log: Logger[String]
let analyzer: Analyzer
let task_id: USize
let canonical_path: FilePath
let line: USize
let column: USize
let notify: FindDefinitionNotify
var span: String = "!INVALID!"
let paths_to_search: Array[(FilePath, (SearchFileItem | None))] =
paths_to_search.create()
var finished: Bool = false
new create(
log': Logger[String],
analyzer': Analyzer,
task_id': USize,
canonical_path': FilePath,
line': USize,
column': USize,
notify': FindDefinitionNotify)
=>
log = log'
analyzer = analyzer'
task_id = task_id'
canonical_path = canonical_path'
line = line'
column = column'
notify = notify'
paths_to_search.push((canonical_path, None))
analyzer.request_info(task_id, canonical_path, this)
be request_succeeded(
task_id': USize,
canonical_path': FilePath,
syntax_tree': (ast.Node | None),
nodes_by_index': Map[USize, ast.Node] val,
scope': Scope val)
=>
// find array index and update data
for (i, pending) in paths_to_search.pairs() do
if pending._1.path == canonical_path'.path then
try
let item = SearchFileItem(
canonical_path',
syntax_tree',
nodes_by_index',
scope')
paths_to_search(i)? = (canonical_path', item)
end
end
end
_process_next_path()
be request_failed(
task_id': USize,
canonical_path': FilePath,
message': String)
=>
log(Error) and log.log(
task_id'.string() + ": analysis request failed: " + message')
for (i, pending) in paths_to_search.pairs() do
if canonical_path'.path == pending._1.path then
try paths_to_search.delete(i)? end
break
end
end
// if there are no more pending requests, notify of failure
if paths_to_search.size() == 0 then
log(Error) and log.log(
task_id'.string() + ": unable to find definition: " + message')
finished = true
notify.definition_failed(task_id', message')
end
fun ref _process_next_path() =>
if not finished then
// is there data in the first item in the array?
match try paths_to_search(0)? end
| (let cp: FilePath, let sfi: SearchFileItem) =>
if cp.path == canonical_path.path then
match sfi.syntax_tree
| let st: ast.Node =>
// this is our original file
match _find_definition_span(st)
| let span': String =>
span = span'
_find_definition_location(sfi where orig_file = true)
else
finished = true
notify.definition_failed(task_id, "span not found")
end
else
finished = true
notify.definition_failed(
task_id, "no syntax tree for " + canonical_path.path)
end
else
// we're in a sibling, or import, or builtin
// if package scope, go into children
// TODO:
None
end
try
paths_to_search.shift()?
end
else
finished = true
notify.definition_failed(task_id, "definition not found: " + span)
end
end
fun ref _find_definition_span(node: ast.Node) : (String | None) =>
let si = node.src_info()
match (si.line, si.column, si.next_line, si.next_column)
| (let l: USize, let c: USize, let nl: USize, let nc: USize) =>
// log(Fine) and log.log(
// task_id.string() + ": " + canonical_path + ": is (" + line.string()
// + "," + column.string() + ") in " + node.name() + " (" + l.string()
// + "," + c.string() + ") - (" + nl.string() + "," + nc.string() + ")?")
if _is_in_range(l, c, nl, nc) then
// log(Fine) and log.log(
// task_id.string() + ": " + canonical_path + ": yes")
match node
| let id: ast.NodeWith[ast.Identifier] =>
// log(Fine) and log.log(
// task_id.string() + ": " + canonical_path + ": found "
// + id.data().string + " (" + l.string() + "," + c.string() + ") - ("
// + nl.string() + "," + nc.string() + ")")
return id.data().string
end
if node.children().size() > 0 then
// TODO: do a binary search here
for child in node.children().values() do
match _find_definition_span(child)
| let span': String =>
return span'
end
end
end
else
// log(Fine) and log.log(
// task_id.string() + ": " + canonical_path + ": no")
None
end
end
None
fun _is_in_range(l: USize, c: USize, nl: USize, nc: USize): Bool =>
if (line >= l) and (line <= nl) then
if (line == nl) and (column <= nc) then
return true
elseif (line == l) and (line == nl) then
return (column >= c) and (column < nc)
elseif (line == l) and (line < nl) then
return true
elseif (line > l) and (line < nl) then
return true
end
end
false
fun ref _find_definition_location(
item: SearchFileItem,
orig_file: Bool)
: Bool
=>
let scope = item.scope
var scope': (Scope val | None) =
if orig_file then
_find_child_scope(scope)
else
// log(Fine) and log.log(
// task_id.string() + ": original scope " + scope.name + "("
// + scope.range._1.string() + "," + scope.range._2.string() + ") - ("
// + scope.range._3.string() + "," + scope.range._4.string() + ")")
scope
end
while true do
match scope'
| let cur_scope: Scope val =>
// log(Fine) and log.log(
// task_id.string() + ": search scope " + cur_scope.name + " ("
// + cur_scope.range._1.string() + "," + cur_scope.range._2.string()
// + ") - (" + cur_scope.range._3.string() + ","
// + cur_scope.range._4.string() + ")")
for defs in cur_scope.definitions.values() do
for definition in defs.values() do
let identifier = definition._2
if identifier != span then
continue
end
let node_index = definition._1
let doc_string = definition._3
match try item.nodes_by_index(node_index)? end
| let node: ast.Node =>
let si = node.src_info()
match (si.line, si.column, si.next_line, si.next_column)
| (let l: USize, let c: USize, let nl: USize, let nc: USize) =>
log(Fine) and log.log(
task_id.string() + ": found definition for '" + span +
"': " + l.string() + ":" + c.string() + "-" + nl.string() +
":" + nc.string())
finished = true
notify.definition_found(
task_id, cur_scope.canonical_path, (l, c, nl, nc))
return true
end
end
end
end
match cur_scope.kind
| FileScope =>
// TODO: add siblings, then imports, then builtin
None
end
scope' = cur_scope.parent
else
break
end
end
false
fun _find_child_scope(scope: Scope val): (Scope val | None) =>
if
_is_in_range(
scope.range._1, scope.range._2, scope.range._3, scope.range._4)
then
// log(Fine) and log.log(
// task_id.string() + ": drill into scope " + scope.name + " ("
// + scope.range._1.string() + "," + scope.range._2.string() + ") - ("
// + scope.range._3.string() + "," + scope.range._4.string() + ")")
for child in scope.children.values() do
match _find_child_scope(child)
| let scope': Scope val =>
return scope'
end
end
// log(Fine) and log.log(
// task_id.string() + ": found scope " + scope.name + " ("
// + scope.range._1.string() + "," + scope.range._2.string() + ") - ("
// + scope.range._3.string() + "," + scope.range._4.string() + ")")
scope
end