semantic_code_edit_mcp/languages/
python.rs

1use crate::languages::{LanguageCommon, LanguageName, traits::LanguageEditor};
2use anyhow::Result;
3use rustpython_parser::ast::TextSize;
4
5pub fn language() -> Result<LanguageCommon> {
6    let language = tree_sitter_python::LANGUAGE.into();
7    let editor = Box::new(PythonEditor);
8
9    Ok(LanguageCommon {
10        name: LanguageName::Python,
11        file_extensions: &["py", "pyi"],
12        language,
13        editor,
14        validation_query: None,
15    })
16}
17
18pub struct PythonEditor;
19
20impl PythonEditor {
21    pub fn new() -> Self {
22        Self
23    }
24}
25
26impl Default for PythonEditor {
27    fn default() -> Self {
28        Self::new()
29    }
30}
31
32impl LanguageEditor for PythonEditor {
33    fn collect_errors(&self, _tree: &tree_sitter::Tree, content: &str) -> Vec<usize> {
34        if let Some(err) =
35            rustpython_parser::parse(content, rustpython_parser::Mode::Module, "anonymous.py").err()
36        {
37            let converter = LineConverter::new(content);
38            vec![converter.textsize_to_line(err.offset)]
39        } else {
40            vec![]
41        }
42    }
43}
44
45struct LineConverter {
46    newline_positions: Vec<usize>,
47}
48
49impl LineConverter {
50    fn new(text: &str) -> Self {
51        let newline_positions = std::iter::once(0)
52            .chain(text.match_indices('\n').map(|(i, _)| i + 1))
53            .chain(std::iter::once(text.len())) // End of file
54            .collect();
55
56        Self { newline_positions }
57    }
58
59    fn textsize_to_line(&self, offset: TextSize) -> usize {
60        let byte_offset = usize::from(offset); // Safe conversion
61        match self.newline_positions.binary_search(&byte_offset) {
62            Ok(line) => line + 1,
63            Err(line) => line,
64        }
65    }
66}