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 const fn identifier(&self) -> &Identifier {
43 match self {
44 Declaration::File { declaration, .. } => &declaration.identifier,
45 Declaration::Buffer { declaration, .. } => &declaration.identifier,
46 }
47 }
48
49 pub const 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 const fn as_buffer(&self) -> Option<&BufferDeclaration> {
57 match self {
58 Declaration::File { .. } => None,
59 Declaration::Buffer { declaration, .. } => Some(declaration),
60 }
61 }
62
63 pub const fn as_file(&self) -> Option<&FileDeclaration> {
64 match self {
65 Declaration::Buffer { .. } => None,
66 Declaration::File { declaration, .. } => Some(declaration),
67 }
68 }
69
70 pub const 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 const 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}