swiftide_integrations/treesitter/
chunk_code.rs1use anyhow::{Context as _, Result};
3use async_trait::async_trait;
4use derive_builder::Builder;
5
6use crate::treesitter::{ChunkSize, CodeSplitter, SupportedLanguages};
7use swiftide_core::{
8 ChunkerTransformer,
9 indexing::{IndexingStream, TextNode},
10};
11
12#[derive(Debug, Clone, Builder)]
31#[builder(pattern = "owned", setter(into, strip_option))]
32pub struct ChunkCode {
33 chunker: CodeSplitter,
34 #[builder(default)]
35 concurrency: Option<usize>,
36}
37
38impl ChunkCode {
39 pub fn builder() -> ChunkCodeBuilder {
40 ChunkCodeBuilder::default()
41 }
42
43 pub fn try_for_language(lang: impl TryInto<SupportedLanguages>) -> Result<Self> {
56 Ok(Self {
57 chunker: CodeSplitter::builder().try_language(lang)?.build()?,
58 concurrency: None,
59 })
60 }
61
62 pub fn try_for_language_and_chunk_size(
77 lang: impl TryInto<SupportedLanguages>,
78 chunk_size: impl Into<ChunkSize>,
79 ) -> Result<Self> {
80 Ok(Self {
81 chunker: CodeSplitter::builder()
82 .try_language(lang)?
83 .chunk_size(chunk_size)
84 .build()?,
85 concurrency: None,
86 })
87 }
88
89 #[must_use]
90 pub fn with_concurrency(mut self, concurrency: usize) -> Self {
91 self.concurrency = Some(concurrency);
92 self
93 }
94}
95
96#[async_trait]
97impl ChunkerTransformer for ChunkCode {
98 type Input = String;
99 type Output = String;
100 #[tracing::instrument(skip_all, name = "transformers.chunk_code")]
112 async fn transform_node(&self, node: TextNode) -> IndexingStream<String> {
113 let split_result = self.chunker.split(&node.chunk);
114
115 if let Ok(split) = split_result {
116 let mut offset = 0;
117
118 IndexingStream::iter(node.into_chunks(split).map(move |mut chunk| {
119 let chunk_size = chunk.chunk.len();
120 chunk.offset = offset;
121 offset += chunk_size;
122 Ok(chunk)
123 }))
124 } else {
125 IndexingStream::iter(vec![Err(split_result
127 .with_context(|| format!("Failed to chunk {}", node.path.display()))
128 .unwrap_err())])
129 }
130 }
131
132 fn concurrency(&self) -> Option<usize> {
133 self.concurrency
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use futures_util::stream::TryStreamExt;
140 use swiftide_core::{ChunkerTransformer, indexing::Metadata};
141
142 use super::*;
143
144 #[tokio::test]
145 async fn preserves_code_chunks_offsets_and_first_ancestor() {
146 let code = "fn alpha() { println!(\"α\"); }\nfn beta() { println!(\"β\"); }\nfn gamma() { println!(\"γ\"); }";
147 let transformer =
148 ChunkCode::try_for_language_and_chunk_size(SupportedLanguages::Rust, 32).unwrap();
149 let source: TextNode = TextNode::builder()
150 .path("fixtures/日本語.rs")
151 .chunk(code.to_owned())
152 .metadata(Metadata::from([("language", "Rust")]))
153 .original_size(code.len())
154 .offset(99usize)
155 .build()
156 .unwrap();
157 let chunks = transformer.chunker.split(&source.chunk).unwrap();
158 assert!(chunks.len() > 1);
159 let outputs: Vec<TextNode> = transformer
160 .transform_node(source.clone())
161 .await
162 .try_collect()
163 .await
164 .unwrap();
165
166 let mut offset = 0;
167 for (output, chunk) in outputs.iter().zip(&chunks) {
168 let expected = TextNode::build_from_other(&source)
169 .chunk(chunk.clone())
170 .offset(offset)
171 .build()
172 .unwrap();
173 assert_eq!(output, &expected);
174 offset += chunk.len();
175 }
176 assert_eq!(outputs.len(), chunks.len());
177
178 let parent = uuid::Uuid::new_v4();
179 let mut descendant = source;
180 descendant.parent_id = Some(parent);
181 let child_outputs: Vec<TextNode> = transformer
182 .transform_node(descendant)
183 .await
184 .try_collect()
185 .await
186 .unwrap();
187 assert_eq!(child_outputs.len(), outputs.len());
188 assert!(
189 child_outputs
190 .iter()
191 .all(|output| output.parent_id == Some(parent))
192 );
193 }
194}