///|
/// Traverse all nodes in module index
pub fn traverse_module_index(
index : IndexNode,
f : (IndexNode, String) -> Unit,
) -> Unit {
fn go(node : IndexNode, current_path : String) {
f(node, current_path)
for child in node.childs {
go(child, "\{current_path}/\{child.name}")
}
}
go(index, index.name)
}
///|
/// Get all package nodes from module index
pub fn get_package_nodes(index : IndexNode) -> Array[(PackageIndex, String)] {
let result : Array[(PackageIndex, String)] = []
traverse_module_index(index, fn(node, path) {
match node.pkg {
Some(p) => result.push((p, path))
None => ()
}
})
result
}
///|
/// Get all package paths from module index
pub fn get_all_package_paths(index : IndexNode) -> Array[String] {
let paths : Array[String] = []
for pkg_path in get_package_nodes(index) {
paths.push(pkg_path.0.path)
}
paths
}
///|
/// Find a node by path in module index
pub fn find_node_by_path(index : IndexNode, path : String) -> IndexNode? {
let parts : Array[String] = path
.split("/")
.map(fn(sv) { sv.to_string() })
.collect()
if parts.length() == 0 {
return None
}
if parts[0] != index.name {
return None
}
fn go(node : IndexNode, start_idx : Int, arr : Array[String]) -> IndexNode? {
if start_idx >= arr.length() {
return Some(node)
}
let target = arr[start_idx]
for child in node.childs {
if child.name == target {
return go(child, start_idx + 1, arr)
}
}
None
}
go(index, 1, parts)
}
///|
/// Get all descendant packages of a node (not just direct children)
pub fn get_descendant_packages(node : IndexNode) -> Array[PackageIndex] {
let packages : Array[PackageIndex] = []
fn go(n : IndexNode) {
match n.pkg {
Some(p) => packages.push(p)
None => ()
}
for child in n.childs {
go(child)
}
}
go(node)
packages
}
///|
/// Get direct child packages of a node
pub fn get_child_packages(node : IndexNode) -> Array[PackageIndex] {
let packages : Array[PackageIndex] = []
for child in node.childs {
match child.pkg {
Some(p) => packages.push(p)
None => ()
}
}
packages
}
///|
/// Build a map of nodes by path
pub fn build_node_map(index : IndexNode) -> Map[String, IndexNode] {
let map : Map[String, IndexNode] = {}
traverse_module_index(index, fn(node, path) { map[path] = node })
map
}
///|
/// Convert TypePath to string
pub fn type_path_to_string(tp : TypePath) -> String {
if tp.path.is_empty() {
tp.name
} else {
"@\{tp.path}.\{tp.name}"
}
}
///|
/// Convert Stype to string
pub fn stype_to_string(stype : Stype) -> String {
match stype {
Stype::Constr(constr~, arguments~) => {
let name = type_path_to_string(constr)
if arguments.length() == 0 {
name
} else {
let args = arguments.map(stype_to_string).join(", ")
"\{name}[\{args}]"
}
}
Stype::Arrow(parameters~, return_type~, error_type~, is_async~) => {
let params = parameters.map(stype_to_string).join(", ")
let ret = stype_to_string(return_type)
let prefix = if is_async { "async " } else { "" }
let error_suffix = match error_type {
Some(e) => " raise(\{stype_to_string(e)})"
None => ""
}
"\{prefix}(\{params}) -> \{ret}\{error_suffix}"
}
Stype::Param(name~) => name
Stype::Path(path~, name~) =>
if path.is_empty() {
name
} else {
"@\{path}.\{name}"
}
}
}
///|
/// Build search entries from package data
pub fn build_search_entries(
pkg : PackageData,
path : String,
) -> Array[SearchEntry] {
let entries : Array[SearchEntry] = []
// Add types
for t in pkg.types {
entries.push(SearchEntry::{
name: t.name,
path,
kind: "type",
signature: Some(t.signature),
})
// Add type methods
for m in t.methods {
entries.push(SearchEntry::{
name: "\{t.name}::\{m.name}",
path,
kind: "method",
signature: Some(m.signature),
})
}
}
// Add traits
for t in pkg.traits {
entries.push(SearchEntry::{
name: t.name,
path,
kind: "trait",
signature: Some(t.signature),
})
}
// Add type aliases
for t in pkg.type_aliases {
entries.push(SearchEntry::{
name: t.name,
path,
kind: "typealias",
signature: Some(t.signature),
})
}
// Add values
for v in pkg.values {
entries.push(SearchEntry::{
name: v.name,
path,
kind: "value",
signature: Some(v.signature),
})
}
// Add errors
for e in pkg.errors {
entries.push(SearchEntry::{
name: e.name,
path,
kind: "error",
signature: Some(e.signature),
})
}
entries
}
///|
/// Build all search entries from module index
pub fn build_all_search_entries(
index : IndexNode,
packages : Map[String, PackageData],
) -> Array[SearchEntry] {
let entries : Array[SearchEntry] = []
for path in get_all_package_paths(index) {
match packages.get(path) {
Some(pkg) => entries.append(build_search_entries(pkg, path))
None => ()
}
}
entries
}
///|
/// Simple fuzzy search (case-insensitive substring match)
pub fn fuzzy_search(
entries : Array[SearchEntry],
query : String,
limit : Int,
) -> Array[SearchEntry] {
let query_lower = query.to_lower()
let results : Array[SearchEntry] = []
for entry in entries {
if results.length() >= limit {
break
}
if entry.name.to_lower().contains(query_lower) {
results.push(entry)
}
}
results
}
///|
/// Strip HTML tags from string
pub fn strip_html_tags(s : String) -> String {
let result = StringBuilder::new()
let mut in_tag = false
for c in s {
if c == '<' {
in_tag = true
} else if c == '>' {
in_tag = false
} else if not(in_tag) {
result.write_char(c)
}
}
result.to_string()
}
///|
/// Format docstring (strip HTML and trim)
pub fn format_docstring(docstring : String) -> String {
strip_html_tags(docstring).trim(chars=" \t\n\r").to_string()
}