Skip to repository content237 lines · 7.9 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T04:48:52.636Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
symbol_locator.rs
1use std::collections::VecDeque;
2use std::fmt;
3
4use gpui::{App, AsyncApp, Entity};
5use language::{Buffer, Location};
6use project::{CodeAction, Project};
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9use text::ToPoint as _;
10use text::{Anchor, Point};
11
12/// Identifies a specific symbol (declaration or usage) in the source code.
13///
14/// Use the file path, line number, and symbol name from file outlines, grep results, or other tool outputs to populate these fields.
15#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
16pub struct SymbolLocator {
17 /// The relative path of the file containing the symbol (e.g. "crates/editor/src/editor.rs").
18 pub file_path: String,
19
20 /// The 1-based line number where the symbol appears. Use the line numbers from file outlines or grep results.
21 pub line: u32,
22
23 /// The name of the symbol (function name, type name, variable name, etc.)
24 pub symbol_name: String,
25}
26
27pub struct PendingCodeActions {
28 pub actions: Vec<CodeAction>,
29 pub buffer: Entity<Buffer>,
30}
31
32pub type CodeActionStore = Entity<Option<PendingCodeActions>>;
33
34pub struct ResolvedSymbol {
35 pub buffer: Entity<Buffer>,
36 pub position: Anchor,
37 pub line_text: String,
38 pub truncated: bool,
39}
40
41pub const MAX_LINE_DISPLAY_LEN: usize = 200;
42
43pub struct LocationDisplay {
44 pub path: String,
45 pub start_line: u32,
46 pub end_line: u32,
47 pub snippet: String,
48 pub truncated: bool,
49}
50
51impl LocationDisplay {
52 pub fn from_location(location: &Location, cx: &App) -> Self {
53 let snapshot = location.buffer.read(cx).snapshot();
54 let range =
55 location.range.start.to_point(&snapshot)..location.range.end.to_point(&snapshot);
56 let path = location
57 .buffer
58 .read(cx)
59 .file()
60 .map(|f| f.full_path(cx).display().to_string())
61 .unwrap_or_else(|| "<untitled>".to_string());
62
63 let start_line = range.start.row + 1;
64 let end_line = range.end.row + 1;
65
66 let line_len = snapshot.line_len(range.start.row);
67 let truncated = line_len as usize > MAX_LINE_DISPLAY_LEN;
68 let snippet: String = snapshot
69 .text_for_range(Point::new(range.start.row, 0)..Point::new(range.start.row, line_len))
70 .flat_map(|chunk| chunk.chars())
71 .skip_while(|c| c.is_whitespace())
72 .take(MAX_LINE_DISPLAY_LEN)
73 .collect::<String>();
74 let snippet = snippet.trim_end().to_string();
75
76 Self {
77 path,
78 start_line,
79 end_line,
80 snippet,
81 truncated,
82 }
83 }
84}
85
86impl fmt::Display for LocationDisplay {
87 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88 let truncated_label = if self.truncated { " (truncated)" } else { "" };
89 if self.start_line == self.end_line {
90 writeln!(f, "{}#L{}{truncated_label}", self.path, self.start_line)?;
91 } else {
92 writeln!(
93 f,
94 "{}#L{}-{}{truncated_label}",
95 self.path, self.start_line, self.end_line
96 )?;
97 }
98 writeln!(f, "```")?;
99 writeln!(f, "{}", self.snippet)?;
100 write!(f, "```")
101 }
102}
103
104/// Searches for `needle` in a char iterator, returning the byte offset of the
105/// first occurrence without collecting the full iterator into a string.
106///
107/// Equivalent to [`str::find`]
108fn find_in_char_iter(chars: impl Iterator<Item = char>, needle: &str) -> Option<usize> {
109 let needle_chars: Vec<char> = needle.chars().collect();
110 if needle_chars.is_empty() {
111 return Some(0);
112 }
113
114 let mut window: VecDeque<char> = VecDeque::with_capacity(needle_chars.len());
115 let mut byte_offsets: VecDeque<usize> = VecDeque::with_capacity(needle_chars.len());
116 let mut byte_offset = 0usize;
117
118 for ch in chars {
119 window.push_back(ch);
120 byte_offsets.push_back(byte_offset);
121 byte_offset += ch.len_utf8();
122
123 if window.len() > needle_chars.len() {
124 window.pop_front();
125 byte_offsets.pop_front();
126 }
127
128 if window.len() == needle_chars.len()
129 && window.iter().zip(needle_chars.iter()).all(|(a, b)| a == b)
130 {
131 return byte_offsets.front().copied();
132 }
133 }
134
135 None
136}
137
138impl SymbolLocator {
139 /// Resolves this locator into a concrete buffer and position.
140 ///
141 /// Opens the file at `file_path`, then searches for `symbol_name` on the
142 /// specified `line`. Returns an error if the file can't be found, the line
143 /// is out of range, or the symbol name doesn't appear on that line.
144 /// If the symbol name appears multiple times on the line, uses the first
145 /// occurrence.
146 pub async fn resolve(
147 &self,
148 project: &Entity<Project>,
149 cx: &mut AsyncApp,
150 ) -> Result<ResolvedSymbol, String> {
151 let Self {
152 file_path,
153 line,
154 symbol_name,
155 } = self;
156
157 let open_buffer_task = project.update(cx, |project, cx| {
158 let Some(project_path) = project.find_project_path(file_path, cx) else {
159 return Err(format!("Could not find path '{file_path}' in project",));
160 };
161 Ok(project.open_buffer(project_path, cx))
162 })?;
163
164 let buffer = open_buffer_task
165 .await
166 .map_err(|e| format!("Failed to open '{}': {e}", self.file_path))?;
167
168 let (position, line_text, truncated) = buffer.read_with(cx, |buffer, _cx| {
169 let snapshot = buffer.snapshot();
170 let row = line.saturating_sub(1);
171
172 if row > snapshot.max_point().row {
173 let line_count = snapshot.max_point().row + 1;
174 return Err(format!(
175 "Line {line} is beyond the end of '{file_path}' (file has {line_count} lines)",
176 ));
177 }
178
179 let line_len = snapshot.line_len(row);
180 let truncated = line_len as usize > MAX_LINE_DISPLAY_LEN;
181 let line_start = Point::new(row, 0);
182 let line_end = Point::new(row, line_len);
183 let line_chars = || {
184 snapshot
185 .text_for_range(line_start..line_end)
186 .flat_map(|chunk| chunk.chars())
187 };
188
189 let byte_offset = find_in_char_iter(line_chars(), symbol_name).ok_or_else(|| {
190 let preview: String = line_chars()
191 .skip_while(|c| c.is_whitespace())
192 .take(MAX_LINE_DISPLAY_LEN)
193 .collect();
194 format!(
195 "Symbol '{symbol_name}' not found on line {line} of '{file_path}'. \
196 Line content: '{}'",
197 preview.trim_end()
198 )
199 })?;
200
201 let position = snapshot.anchor_before(Point::new(row, byte_offset as u32));
202 let display_text: String = line_chars()
203 .skip_while(|c| c.is_whitespace())
204 .take(MAX_LINE_DISPLAY_LEN)
205 .collect::<String>();
206 let display_text = display_text.trim_end().to_string();
207
208 Ok((position, display_text, truncated))
209 })?;
210
211 Ok(ResolvedSymbol {
212 buffer,
213 position,
214 line_text,
215 truncated,
216 })
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223 use gpui::proptest::prelude::*;
224
225 #[gpui::property_test]
226 fn find_in_char_iter_test(
227 // limited character sets to increase odds of finding matches
228 #[strategy = "[abcd]{100,1000}"] haystack: String,
229 #[strategy = "[abcd]{1,5}"] needle: String,
230 ) -> Result<(), TestCaseError> {
231 let expected = haystack.find(&needle);
232 let actual = find_in_char_iter(haystack.chars(), &needle);
233 prop_assert_eq!(actual, expected);
234 Ok::<_, TestCaseError>(())
235 }
236}
237