use anyhow::{Context as _, Result};
use tree_sitter::{Node, Parser, TreeCursor};
use derive_builder::Builder;
use super::supported_languages::SupportedLanguages;
#[derive(Debug, Builder, Clone)]
#[builder(setter(into), build_fn(error = "anyhow::Error"))]
pub struct CodeOutliner {
#[builder(setter(custom))]
language: SupportedLanguages,
}
impl CodeOutlinerBuilder {
pub fn try_language(mut self, language: impl TryInto<SupportedLanguages>) -> Result<Self> {
self.language = Some(
language
.try_into()
.ok()
.context("Treesitter language not supported")?,
);
Ok(self)
}
}
impl CodeOutliner {
pub fn new(language: SupportedLanguages) -> Self {
Self { language }
}
pub fn builder() -> CodeOutlinerBuilder {
CodeOutlinerBuilder::default()
}
pub fn outline(&self, code: &str) -> Result<String> {
let mut parser = Parser::new();
parser.set_language(&self.language.into())?;
let tree = parser.parse(code, None).context("No nodes found")?;
let root_node = tree.root_node();
if root_node.has_error() {
anyhow::bail!("Root node has invalid syntax");
}
let mut cursor = root_node.walk();
let mut summary = String::with_capacity(code.len());
let mut last_end = 0;
self.outline_node(&mut cursor, code, &mut summary, &mut last_end);
Ok(summary)
}
fn is_unneeded_node(&self, node: Node) -> bool {
match self.language {
SupportedLanguages::Rust | SupportedLanguages::Java | SupportedLanguages::CSharp => {
matches!(node.kind(), "block")
}
SupportedLanguages::Typescript | SupportedLanguages::Javascript => {
matches!(node.kind(), "statement_block")
}
SupportedLanguages::Python => match node.kind() {
"block" => {
let parent = node.parent().expect("Python block node has no parent");
parent.kind() == "function_definition"
}
_ => false,
},
SupportedLanguages::Ruby => match node.kind() {
"body_statement" => {
let parent = node
.parent()
.expect("Ruby body_statement node has no parent");
parent.kind() == "method"
}
_ => false,
},
SupportedLanguages::Go => unimplemented!(),
SupportedLanguages::Solidity => unimplemented!(),
SupportedLanguages::C => unimplemented!(),
SupportedLanguages::Cpp => unimplemented!(),
SupportedLanguages::Elixir => unimplemented!(),
SupportedLanguages::HTML => unimplemented!(),
SupportedLanguages::PHP => unimplemented!(),
}
}
fn outline_node(
&self,
cursor: &mut TreeCursor,
source: &str,
summary: &mut String,
last_end: &mut usize,
) {
let node = cursor.node();
if self.is_unneeded_node(node) {
summary.push_str(&source[*last_end..node.start_byte()]);
*last_end = node.end_byte();
if cursor.goto_next_sibling() {
self.outline_node(cursor, source, summary, last_end);
}
return;
}
let mut next_cursor = cursor.clone();
if next_cursor.goto_first_child() {
self.outline_node(&mut next_cursor, source, summary, last_end);
} else {
summary.push_str(&source[*last_end..node.end_byte()]);
*last_end = node.end_byte();
}
if cursor.goto_next_sibling() {
self.outline_node(cursor, source, summary, last_end);
} else {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_outline_rust() {
let code = r#"
use anyhow::{Context as _, Result};
// This is a comment
fn main(a: usize, b: usize) -> usize {
println!("Hello, world!");
}
pub struct Bla {
a: usize
}
impl Bla {
fn ok(&mut self) {
self.a = 1;
}
}"#;
let outliner = CodeOutliner::new(SupportedLanguages::Rust);
let summary = outliner.outline(code).unwrap();
assert_eq!(
summary,
"\nuse anyhow::{Context as _, Result};\n// This is a comment\nfn main(a: usize, b: usize) -> usize \n\npub struct Bla {\n a: usize\n}\n\nimpl Bla {\n fn ok(&mut self) \n}"
);
}
#[test]
fn test_outline_typescript() {
let code = r#"
import { Context as _, Result } from 'anyhow';
// This is a comment
function main(a: number, b: number): number {
console.log("Hello, world!");
}
export class Bla {
a: number;
}
export interface Bla {
ok(): void;
}"#;
let outliner = CodeOutliner::new(SupportedLanguages::Typescript);
let summary = outliner.outline(code).unwrap();
assert_eq!(
summary,
"\nimport { Context as _, Result } from 'anyhow';\n// This is a comment\nfunction main(a: number, b: number): number \n\nexport class Bla {\n a: number;\n}\n\nexport interface Bla {\n ok(): void;\n}"
);
}
#[test]
fn test_outline_python() {
let code = r#"
import sys
# This is a comment
def main(a: int, b: int) -> int:
print("Hello, world!")
class Bla:
def __init__(self):
self.a = 1
def ok(self):
self.a = 1
"#;
let outliner = CodeOutliner::new(SupportedLanguages::Python);
let summary = outliner.outline(code).unwrap();
assert_eq!(
summary,
"\nimport sys\n# This is a comment\ndef main(a: int, b: int) -> int:\n \n\nclass Bla:\n def __init__(self):\n \n\n def ok(self):\n "
);
}
#[test]
fn test_outline_ruby() {
let code = r#"
require 'anyhow'
# This is a comment
def main(a, b)
puts "Hello, world!"
end
class Bla
def ok
@a = 1
end
end
"#;
let outliner = CodeOutliner::new(SupportedLanguages::Ruby);
let summary = outliner.outline(code).unwrap();
assert_eq!(
summary,
"\nrequire 'anyhow'\n# This is a comment\ndef main(a, b)\n \nend\n\nclass Bla\n def ok\n \n end\nend"
);
}
#[test]
fn test_outline_javascript() {
let code = r#"
import { Context as _, Result } from 'anyhow';
// This is a comment
function main(a, b) {
console.log("Hello, world!");
}
class Bla {
constructor() {
this.a = 1;
}
ok() {
this.a = 1;
}
}
"#;
let outliner = CodeOutliner::new(SupportedLanguages::Javascript);
let summary = outliner.outline(code).unwrap();
assert_eq!(
summary,
"\nimport { Context as _, Result } from 'anyhow';\n// This is a comment\nfunction main(a, b) \n\nclass Bla {\n constructor() \n\n ok() \n}"
);
}
#[test]
fn test_outline_java() {
let code = r#"
import java.io.PrintStream;
import java.util.Scanner;
public class HelloWorld {
// This is a comment
public static void main(String[] args) {
PrintStream out = System.out;
out.println("Hello, World!");
}
}
"#;
let outliner = CodeOutliner::new(SupportedLanguages::Java);
let summary = outliner.outline(code).unwrap();
println!("{summary}");
assert_eq!(
summary,
"\nimport java.io.PrintStream;\nimport java.util.Scanner;\n\npublic class HelloWorld {\n // This is a comment\n public static void main(String[] args) \n}"
);
}
}