Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T05:56:23.741Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

symbol_locator.rs

237 lines · 7.9 KB · rust
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
Served at tenant.openagents/omega Member data and write actions are omitted.