1use cloud_llm_client::predict_edits_v3::{self, Line};
  2use language::{Language, LanguageId};
  3use project::ProjectEntryId;
  4use std::ops::Range;
  5use std::sync::Arc;
  6use std::{borrow::Cow, path::Path};
  7use text::{Bias, BufferId, Rope};
  8use util::paths::{path_ends_with, strip_path_suffix};
  9use util::rel_path::RelPath;
 10
 11use crate::outline::OutlineDeclaration;
 12
 13#[derive(Debug, Clone, Eq, PartialEq, Hash)]
 14pub struct Identifier {
 15    pub name: Arc<str>,
 16    pub language_id: LanguageId,
 17}
 18
 19slotmap::new_key_type! {
 20    pub struct DeclarationId;
 21}
 22
 23#[derive(Debug, Clone)]
 24pub enum Declaration {
 25    File {
 26        project_entry_id: ProjectEntryId,
 27        declaration: FileDeclaration,
 28        cached_path: CachedDeclarationPath,
 29    },
 30    Buffer {
 31        project_entry_id: ProjectEntryId,
 32        buffer_id: BufferId,
 33        rope: Rope,
 34        declaration: BufferDeclaration,
 35        cached_path: CachedDeclarationPath,
 36    },
 37}
 38
 39const ITEM_TEXT_TRUNCATION_LENGTH: usize = 1024;
 40
 41impl Declaration {
 42    pub fn identifier(&self) -> &Identifier {
 43        match self {
 44            Declaration::File { declaration, .. } => &declaration.identifier,
 45            Declaration::Buffer { declaration, .. } => &declaration.identifier,
 46        }
 47    }
 48
 49    pub fn parent(&self) -> Option<DeclarationId> {
 50        match self {
 51            Declaration::File { declaration, .. } => declaration.parent,
 52            Declaration::Buffer { declaration, .. } => declaration.parent,
 53        }
 54    }
 55
 56    pub fn as_buffer(&self) -> Option<&BufferDeclaration> {
 57        match self {
 58            Declaration::File { .. } => None,
 59            Declaration::Buffer { declaration, .. } => Some(declaration),
 60        }
 61    }
 62
 63    pub fn as_file(&self) -> Option<&FileDeclaration> {
 64        match self {
 65            Declaration::Buffer { .. } => None,
 66            Declaration::File { declaration, .. } => Some(declaration),
 67        }
 68    }
 69
 70    pub fn project_entry_id(&self) -> ProjectEntryId {
 71        match self {
 72            Declaration::File {
 73                project_entry_id, ..
 74            } => *project_entry_id,
 75            Declaration::Buffer {
 76                project_entry_id, ..
 77            } => *project_entry_id,
 78        }
 79    }
 80
 81    pub fn cached_path(&self) -> &CachedDeclarationPath {
 82        match self {
 83            Declaration::File { cached_path, .. } => cached_path,
 84            Declaration::Buffer { cached_path, .. } => cached_path,
 85        }
 86    }
 87
 88    pub fn item_range(&self) -> Range<usize> {
 89        match self {
 90            Declaration::File { declaration, .. } => declaration.item_range.clone(),
 91            Declaration::Buffer { declaration, .. } => declaration.item_range.clone(),
 92        }
 93    }
 94
 95    pub fn item_line_range(&self) -> Range<Line> {
 96        match self {
 97            Declaration::File { declaration, .. } => declaration.item_line_range.clone(),
 98            Declaration::Buffer {
 99                declaration, rope, ..
100            } => {
101                Line(rope.offset_to_point(declaration.item_range.start).row)
102                    ..Line(rope.offset_to_point(declaration.item_range.end).row)
103            }
104        }
105    }
106
107    pub fn item_text(&self) -> (Cow<'_, str>, bool) {
108        match self {
109            Declaration::File { declaration, .. } => (
110                declaration.text.as_ref().into(),
111                declaration.text_is_truncated,
112            ),
113            Declaration::Buffer {
114                rope, declaration, ..
115            } => (
116                rope.chunks_in_range(declaration.item_range.clone())
117                    .collect::<Cow<str>>(),
118                declaration.item_range_is_truncated,
119            ),
120        }
121    }
122
123    pub fn signature_text(&self) -> (Cow<'_, str>, bool) {
124        match self {
125            Declaration::File { declaration, .. } => (
126                declaration.text[self.signature_range_in_item_text()].into(),
127                declaration.signature_is_truncated,
128            ),
129            Declaration::Buffer {
130                rope, declaration, ..
131            } => (
132                rope.chunks_in_range(declaration.signature_range.clone())
133                    .collect::<Cow<str>>(),
134                declaration.signature_range_is_truncated,
135            ),
136        }
137    }
138
139    pub fn signature_range(&self) -> Range<usize> {
140        match self {
141            Declaration::File { declaration, .. } => declaration.signature_range.clone(),
142            Declaration::Buffer { declaration, .. } => declaration.signature_range.clone(),
143        }
144    }
145
146    pub fn signature_line_range(&self) -> Range<Line> {
147        match self {
148            Declaration::File { declaration, .. } => declaration.signature_line_range.clone(),
149            Declaration::Buffer {
150                declaration, rope, ..
151            } => {
152                Line(rope.offset_to_point(declaration.signature_range.start).row)
153                    ..Line(rope.offset_to_point(declaration.signature_range.end).row)
154            }
155        }
156    }
157
158    pub fn signature_range_in_item_text(&self) -> Range<usize> {
159        let signature_range = self.signature_range();
160        let item_range = self.item_range();
161        signature_range.start.saturating_sub(item_range.start)
162            ..(signature_range.end.saturating_sub(item_range.start)).min(item_range.len())
163    }
164}
165
166fn expand_range_to_line_boundaries_and_truncate(
167    range: &Range<usize>,
168    limit: usize,
169    rope: &Rope,
170) -> (Range<usize>, Range<predict_edits_v3::Line>, bool) {
171    let mut point_range = rope.offset_to_point(range.start)..rope.offset_to_point(range.end);
172    point_range.start.column = 0;
173    point_range.end.row += 1;
174    point_range.end.column = 0;
175
176    let mut item_range =
177        rope.point_to_offset(point_range.start)..rope.point_to_offset(point_range.end);
178    let is_truncated = item_range.len() > limit;
179    if is_truncated {
180        item_range.end = item_range.start + limit;
181    }
182    item_range.end = rope.clip_offset(item_range.end, Bias::Left);
183
184    let line_range =
185        predict_edits_v3::Line(point_range.start.row)..predict_edits_v3::Line(point_range.end.row);
186    (item_range, line_range, is_truncated)
187}
188
189#[derive(Debug, Clone)]
190pub struct FileDeclaration {
191    pub parent: Option<DeclarationId>,
192    pub identifier: Identifier,
193    /// offset range of the declaration in the file, expanded to line boundaries and truncated
194    pub item_range: Range<usize>,
195    /// line range of the declaration in the file, potentially truncated
196    pub item_line_range: Range<predict_edits_v3::Line>,
197    /// text of `item_range`
198    pub text: Arc<str>,
199    /// whether `text` was truncated
200    pub text_is_truncated: bool,
201    /// offset range of the signature in the file, expanded to line boundaries and truncated
202    pub signature_range: Range<usize>,
203    /// line range of the signature in the file, truncated
204    pub signature_line_range: Range<Line>,
205    /// whether `signature` was truncated
206    pub signature_is_truncated: bool,
207}
208
209impl FileDeclaration {
210    pub fn from_outline(declaration: OutlineDeclaration, rope: &Rope) -> FileDeclaration {
211        let (item_range_in_file, item_line_range_in_file, text_is_truncated) =
212            expand_range_to_line_boundaries_and_truncate(
213                &declaration.item_range,
214                ITEM_TEXT_TRUNCATION_LENGTH,
215                rope,
216            );
217
218        let (mut signature_range_in_file, signature_line_range, mut signature_is_truncated) =
219            expand_range_to_line_boundaries_and_truncate(
220                &declaration.signature_range,
221                ITEM_TEXT_TRUNCATION_LENGTH,
222                rope,
223            );
224
225        if signature_range_in_file.start < item_range_in_file.start {
226            signature_range_in_file.start = item_range_in_file.start;
227            signature_is_truncated = true;
228        }
229        if signature_range_in_file.end > item_range_in_file.end {
230            signature_range_in_file.end = item_range_in_file.end;
231            signature_is_truncated = true;
232        }
233
234        FileDeclaration {
235            parent: None,
236            identifier: declaration.identifier,
237            signature_range: signature_range_in_file,
238            signature_line_range,
239            signature_is_truncated,
240            text: rope
241                .chunks_in_range(item_range_in_file.clone())
242                .collect::<String>()
243                .into(),
244            text_is_truncated,
245            item_range: item_range_in_file,
246            item_line_range: item_line_range_in_file,
247        }
248    }
249}
250
251#[derive(Debug, Clone)]
252pub struct BufferDeclaration {
253    pub parent: Option<DeclarationId>,
254    pub identifier: Identifier,
255    pub item_range: Range<usize>,
256    pub item_range_is_truncated: bool,
257    pub signature_range: Range<usize>,
258    pub signature_range_is_truncated: bool,
259}
260
261impl BufferDeclaration {
262    pub fn from_outline(declaration: OutlineDeclaration, rope: &Rope) -> Self {
263        let (item_range, _item_line_range, item_range_is_truncated) =
264            expand_range_to_line_boundaries_and_truncate(
265                &declaration.item_range,
266                ITEM_TEXT_TRUNCATION_LENGTH,
267                rope,
268            );
269        let (signature_range, _signature_line_range, signature_range_is_truncated) =
270            expand_range_to_line_boundaries_and_truncate(
271                &declaration.signature_range,
272                ITEM_TEXT_TRUNCATION_LENGTH,
273                rope,
274            );
275        Self {
276            parent: None,
277            identifier: declaration.identifier,
278            item_range,
279            item_range_is_truncated,
280            signature_range,
281            signature_range_is_truncated,
282        }
283    }
284}
285
286#[derive(Debug, Clone)]
287pub struct CachedDeclarationPath {
288    pub worktree_abs_path: Arc<Path>,
289    pub rel_path: Arc<RelPath>,
290    /// The relative path of the file, possibly stripped according to `import_path_strip_regex`.
291    pub rel_path_after_regex_stripping: Arc<RelPath>,
292}
293
294impl CachedDeclarationPath {
295    pub fn new(
296        worktree_abs_path: Arc<Path>,
297        path: &Arc<RelPath>,
298        language: Option<&Arc<Language>>,
299    ) -> Self {
300        let rel_path = path.clone();
301        let rel_path_after_regex_stripping = if let Some(language) = language
302            && let Some(strip_regex) = language.config().import_path_strip_regex.as_ref()
303            && let Ok(stripped) = RelPath::unix(&Path::new(
304                strip_regex.replace_all(rel_path.as_unix_str(), "").as_ref(),
305            )) {
306            Arc::from(stripped)
307        } else {
308            rel_path.clone()
309        };
310        CachedDeclarationPath {
311            worktree_abs_path,
312            rel_path,
313            rel_path_after_regex_stripping,
314        }
315    }
316
317    #[cfg(test)]
318    pub fn new_for_test(worktree_abs_path: &str, rel_path: &str) -> Self {
319        let rel_path: Arc<RelPath> = util::rel_path::rel_path(rel_path).into();
320        CachedDeclarationPath {
321            worktree_abs_path: std::path::PathBuf::from(worktree_abs_path).into(),
322            rel_path_after_regex_stripping: rel_path.clone(),
323            rel_path,
324        }
325    }
326
327    pub fn ends_with_posix_path(&self, path: &Path) -> bool {
328        if path.as_os_str().len() <= self.rel_path_after_regex_stripping.as_unix_str().len() {
329            path_ends_with(self.rel_path_after_regex_stripping.as_std_path(), path)
330        } else {
331            if let Some(remaining) =
332                strip_path_suffix(path, self.rel_path_after_regex_stripping.as_std_path())
333            {
334                path_ends_with(&self.worktree_abs_path, remaining)
335            } else {
336                false
337            }
338        }
339    }
340
341    pub fn equals_absolute_path(&self, path: &Path) -> bool {
342        if let Some(remaining) =
343            strip_path_suffix(path, &self.rel_path_after_regex_stripping.as_std_path())
344        {
345            self.worktree_abs_path.as_ref() == remaining
346        } else {
347            false
348        }
349    }
350}