1use gpui::{App, WeakEntity};
2use language::{Buffer, BufferSnapshot, LanguageId};
3use project::ProjectEntryId;
4use std::borrow::Cow;
5use std::ops::{Deref, Range};
6use std::sync::Arc;
7use text::{Anchor, Bias, OffsetRangeExt, ToOffset};
8
9use crate::outline::OutlineDeclaration;
10
11#[derive(Debug, Clone, Eq, PartialEq, Hash)]
12pub struct Identifier {
13 pub name: Arc<str>,
14 pub language_id: LanguageId,
15}
16
17slotmap::new_key_type! {
18 pub struct DeclarationId;
19}
20
21#[derive(Debug, Clone)]
22pub enum Declaration {
23 File {
24 project_entry_id: ProjectEntryId,
25 declaration: FileDeclaration,
26 },
27 Buffer {
28 buffer: WeakEntity<Buffer>,
29 declaration: BufferDeclaration,
30 },
31}
32
33const ITEM_TEXT_TRUNCATION_LENGTH: usize = 1024;
34
35impl Declaration {
36 pub fn identifier(&self) -> &Identifier {
37 match self {
38 Declaration::File { declaration, .. } => &declaration.identifier,
39 Declaration::Buffer { declaration, .. } => &declaration.identifier,
40 }
41 }
42
43 pub fn project_entry_id(&self, cx: &App) -> Option<ProjectEntryId> {
44 match self {
45 Declaration::File {
46 project_entry_id, ..
47 } => Some(*project_entry_id),
48 Declaration::Buffer { buffer, .. } => buffer
49 .read_with(cx, |buffer, _cx| {
50 project::File::from_dyn(buffer.file())
51 .and_then(|file| file.project_entry_id(cx))
52 })
53 .ok()
54 .flatten(),
55 }
56 }
57
58 pub fn item_text(&self, cx: &App) -> (Cow<'_, str>, bool) {
59 match self {
60 Declaration::File { declaration, .. } => (
61 declaration.text.as_ref().into(),
62 declaration.text_is_truncated,
63 ),
64 Declaration::Buffer {
65 buffer,
66 declaration,
67 } => buffer
68 .read_with(cx, |buffer, _cx| {
69 let (range, is_truncated) = expand_range_to_line_boundaries_and_truncate(
70 &declaration.item_range,
71 ITEM_TEXT_TRUNCATION_LENGTH,
72 buffer.deref(),
73 );
74 (
75 buffer.text_for_range(range).collect::<Cow<str>>(),
76 is_truncated,
77 )
78 })
79 .unwrap_or_default(),
80 }
81 }
82
83 pub fn signature_text(&self, cx: &App) -> (Cow<'_, str>, bool) {
84 match self {
85 Declaration::File { declaration, .. } => (
86 declaration.text[declaration.signature_range_in_text.clone()].into(),
87 declaration.signature_is_truncated,
88 ),
89 Declaration::Buffer {
90 buffer,
91 declaration,
92 } => buffer
93 .read_with(cx, |buffer, _cx| {
94 let (range, is_truncated) = expand_range_to_line_boundaries_and_truncate(
95 &declaration.signature_range,
96 ITEM_TEXT_TRUNCATION_LENGTH,
97 buffer.deref(),
98 );
99 (
100 buffer.text_for_range(range).collect::<Cow<str>>(),
101 is_truncated,
102 )
103 })
104 .unwrap_or_default(),
105 }
106 }
107}
108
109fn expand_range_to_line_boundaries_and_truncate<T: ToOffset>(
110 range: &Range<T>,
111 limit: usize,
112 buffer: &text::BufferSnapshot,
113) -> (Range<usize>, bool) {
114 let mut point_range = range.to_point(buffer);
115 point_range.start.column = 0;
116 point_range.end.row += 1;
117 point_range.end.column = 0;
118
119 let mut item_range = point_range.to_offset(buffer);
120 let is_truncated = item_range.len() > limit;
121 if is_truncated {
122 item_range.end = item_range.start + limit;
123 }
124 item_range.end = buffer.clip_offset(item_range.end, Bias::Left);
125 (item_range, is_truncated)
126}
127
128#[derive(Debug, Clone)]
129pub struct FileDeclaration {
130 pub parent: Option<DeclarationId>,
131 pub identifier: Identifier,
132 /// offset range of the declaration in the file, expanded to line boundaries and truncated
133 pub item_range_in_file: Range<usize>,
134 /// text of `item_range_in_file`
135 pub text: Arc<str>,
136 /// whether `text` was truncated
137 pub text_is_truncated: bool,
138 /// offset range of the signature within `text`
139 pub signature_range_in_text: Range<usize>,
140 /// whether `signature` was truncated
141 pub signature_is_truncated: bool,
142}
143
144impl FileDeclaration {
145 pub fn from_outline(
146 declaration: OutlineDeclaration,
147 snapshot: &BufferSnapshot,
148 ) -> FileDeclaration {
149 let (item_range_in_file, text_is_truncated) = expand_range_to_line_boundaries_and_truncate(
150 &declaration.item_range,
151 ITEM_TEXT_TRUNCATION_LENGTH,
152 snapshot,
153 );
154
155 // TODO: consider logging if unexpected
156 let signature_start = declaration
157 .signature_range
158 .start
159 .saturating_sub(item_range_in_file.start);
160 let mut signature_end = declaration
161 .signature_range
162 .end
163 .saturating_sub(item_range_in_file.start);
164 let signature_is_truncated = signature_end > item_range_in_file.len();
165 if signature_is_truncated {
166 signature_end = item_range_in_file.len();
167 }
168
169 FileDeclaration {
170 parent: None,
171 identifier: declaration.identifier,
172 signature_range_in_text: signature_start..signature_end,
173 signature_is_truncated,
174 text: snapshot
175 .text_for_range(item_range_in_file.clone())
176 .collect::<String>()
177 .into(),
178 text_is_truncated,
179 item_range_in_file,
180 }
181 }
182}
183
184#[derive(Debug, Clone)]
185pub struct BufferDeclaration {
186 pub parent: Option<DeclarationId>,
187 pub identifier: Identifier,
188 pub item_range: Range<Anchor>,
189 pub signature_range: Range<Anchor>,
190}
191
192impl BufferDeclaration {
193 pub fn from_outline(declaration: OutlineDeclaration, snapshot: &BufferSnapshot) -> Self {
194 // use of anchor_before is a guess that the proper behavior is to expand to include
195 // insertions immediately before the declaration, but not for insertions immediately after
196 Self {
197 parent: None,
198 identifier: declaration.identifier,
199 item_range: snapshot.anchor_before(declaration.item_range.start)
200 ..snapshot.anchor_before(declaration.item_range.end),
201 signature_range: snapshot.anchor_before(declaration.signature_range.start)
202 ..snapshot.anchor_before(declaration.signature_range.end),
203 }
204 }
205}