Skip to main content

code_moniker_workspace/notes/
resolve.rs

1use std::collections::BTreeMap;
2
3use crate::snapshot::{SourceFileRecord, SourceId, SymbolRecord, WorkspaceSnapshot};
4
5use super::model::{Note, NoteResolution, ResolvedNote};
6
7pub fn resolve_notes(notes: &[Note], snapshot: &WorkspaceSnapshot) -> Vec<ResolvedNote> {
8	let sources = source_by_id(snapshot);
9	let sources_by_uri = source_by_uri(snapshot);
10	let symbols = symbol_by_identity(snapshot);
11	notes
12		.iter()
13		.cloned()
14		.map(|note| {
15			let resolution = resolve_note(&note, &symbols, &sources, &sources_by_uri);
16			ResolvedNote { note, resolution }
17		})
18		.collect()
19}
20
21fn resolve_note(
22	note: &Note,
23	symbols: &BTreeMap<&str, &SymbolRecord>,
24	sources: &BTreeMap<SourceId, &SourceFileRecord>,
25	sources_by_uri: &BTreeMap<&str, &SourceFileRecord>,
26) -> NoteResolution {
27	if let Some(target_label) = navigation_target_label(note.moniker.as_str()) {
28		return NoteResolution::Resolved {
29			target_label,
30			target_file: String::new(),
31			target_slice: None,
32		};
33	}
34	if let Some(target_label) = view_target_label(note.moniker.as_str()) {
35		return NoteResolution::Resolved {
36			target_label,
37			target_file: String::new(),
38			target_slice: None,
39		};
40	}
41	if let Some(source) = sources_by_uri.get(note.moniker.as_str()).copied() {
42		return NoteResolution::Resolved {
43			target_label: format!("file {}", source.rel_path),
44			target_file: source.rel_path.clone(),
45			target_slice: None,
46		};
47	}
48	let Some(symbol) = symbols.get(note.moniker.as_str()).copied() else {
49		return NoteResolution::Orphan;
50	};
51	let Some(source) = sources.get(&symbol.source) else {
52		return NoteResolution::Orphan;
53	};
54	NoteResolution::Resolved {
55		target_label: format!("{} {}", symbol.kind, symbol.name),
56		target_file: source.rel_path.clone(),
57		target_slice: symbol.line_range,
58	}
59}
60
61fn navigation_target_label(moniker: &str) -> Option<String> {
62	workspace_uri_suffix(moniker, "navigation/")
63		.map(|target| format!("navigation {}", decode_navigation_id(target)))
64}
65
66fn view_target_label(moniker: &str) -> Option<String> {
67	workspace_uri_suffix(moniker, "views/").map(|target| format!("view {target}"))
68}
69
70fn workspace_uri_suffix<'a>(moniker: &'a str, path: &str) -> Option<&'a str> {
71	let workspace_path = format!("workspace/{path}");
72	moniker.strip_prefix(&workspace_path).or_else(|| {
73		moniker
74			.split_once(&workspace_path)
75			.map(|(_, suffix)| suffix)
76	})
77}
78
79fn decode_navigation_id(value: &str) -> String {
80	let mut decoded = Vec::new();
81	let bytes = value.as_bytes();
82	let mut idx = 0;
83	while idx < bytes.len() {
84		if bytes[idx] == b'%' && idx + 2 < bytes.len() {
85			if let Ok(hex) = std::str::from_utf8(&bytes[idx + 1..idx + 3]) {
86				if let Ok(byte) = u8::from_str_radix(hex, 16) {
87					decoded.push(byte);
88					idx += 3;
89					continue;
90				}
91			}
92		}
93		decoded.push(bytes[idx]);
94		idx += 1;
95	}
96	String::from_utf8(decoded).unwrap_or_else(|_| value.to_string())
97}
98
99fn source_by_id(snapshot: &WorkspaceSnapshot) -> BTreeMap<SourceId, &SourceFileRecord> {
100	snapshot
101		.index
102		.sources
103		.iter()
104		.map(|source| (source.id, source))
105		.collect()
106}
107
108fn source_by_uri(snapshot: &WorkspaceSnapshot) -> BTreeMap<&str, &SourceFileRecord> {
109	snapshot
110		.index
111		.sources
112		.iter()
113		.map(|source| (source.uri.as_str(), source))
114		.collect()
115}
116
117fn symbol_by_identity(snapshot: &WorkspaceSnapshot) -> BTreeMap<&str, &SymbolRecord> {
118	snapshot
119		.index
120		.symbols
121		.iter()
122		.map(|symbol| (symbol.identity.as_ref(), symbol))
123		.collect()
124}
125
126#[cfg(test)]
127mod tests {
128	use std::sync::Arc;
129
130	use crate::snapshot::{
131		ChangeOverlay, CodeIndex, CodeIndexTimings, LinkageSnapshot, RecordTable,
132		ResourceGeneration, SourceCatalog, SourceFileRecord, SourceId, SymbolId,
133		SymbolInventoryIndex, SymbolRecord, WorkspaceSnapshot, WorkspaceTimings,
134	};
135
136	use super::*;
137	use crate::notes::{NoteAuthor, NoteId, NoteKind, NoteStatus};
138
139	#[test]
140	fn resolves_matching_moniker_and_keeps_orphans() {
141		let moniker = "code+moniker://./lang:rs/module:example/fn:run()";
142		let notes = vec![
143			sample_note("note_1", moniker),
144			sample_note("note_2", "code+moniker://./lang:rs/module:missing"),
145		];
146		let snapshot = snapshot_with_symbol(moniker);
147
148		let resolved = resolve_notes(&notes, &snapshot);
149
150		assert_eq!(resolved.len(), 2);
151		assert_eq!(
152			resolved[0].resolution,
153			NoteResolution::Resolved {
154				target_label: "fn run()".to_string(),
155				target_file: "src/lib.rs".to_string(),
156				target_slice: Some((3, 7)),
157			}
158		);
159		assert!(resolved[1].resolution.is_orphan());
160	}
161
162	#[test]
163	fn treats_symbol_without_source_as_orphan() {
164		let moniker = "code+moniker://./lang:rs/module:example/fn:run()";
165		let notes = vec![sample_note("note_1", moniker)];
166		let snapshot = snapshot_with_orphaned_symbol(moniker);
167
168		let resolved = resolve_notes(&notes, &snapshot);
169
170		assert!(resolved[0].resolution.is_orphan());
171	}
172
173	#[test]
174	fn resolves_navigation_note_targets() {
175		let notes = vec![sample_note(
176			"note_nav",
177			"code+moniker://workspace/navigation/root%3Aexplorer",
178		)];
179		let snapshot = snapshot_with_symbol("code+moniker://./lang:rs/module:example/fn:run()");
180
181		let resolved = resolve_notes(&notes, &snapshot);
182
183		assert_eq!(
184			resolved[0].resolution,
185			NoteResolution::Resolved {
186				target_label: "navigation root:explorer".to_string(),
187				target_file: String::new(),
188				target_slice: None,
189			}
190		);
191	}
192
193	fn sample_note(id: &str, moniker: &str) -> Note {
194		Note {
195			id: NoteId::new(id),
196			moniker: moniker.to_string(),
197			kind: NoteKind::Todo,
198			status: NoteStatus::Pending,
199			title: "Title".to_string(),
200			body: "Body".to_string(),
201			created_by: NoteAuthor::User,
202			created_at: "2026-06-02T00:00:00Z".to_string(),
203			updated_at: "2026-06-02T00:00:00Z".to_string(),
204		}
205	}
206
207	fn snapshot_with_symbol(moniker: &str) -> WorkspaceSnapshot {
208		let source = SourceFileRecord {
209			id: SourceId::at(1),
210			uri: "code+moniker://./lang:rs/path:src/path:lib.rs".to_string(),
211			source_root: 0,
212			path: "/tmp/src/lib.rs".to_string(),
213			rel_path: "src/lib.rs".to_string(),
214			anchor: ".".to_string(),
215			language: "rs".to_string(),
216			text: "pub fn run() {}\n".to_string(),
217		};
218		let symbol = SymbolRecord {
219			id: SymbolId::at(0, 1),
220			source: source.id,
221			identity: std::sync::Arc::from(moniker.to_string()),
222			name: "run()".to_string(),
223			kind: "fn".to_string(),
224			visibility: "public".to_string(),
225			signature: "run()".to_string(),
226			call_name: None,
227			call_arity: None,
228			navigable: true,
229			line_range: Some((3, 7)),
230			parent: None,
231		};
232		WorkspaceSnapshot {
233			generation: ResourceGeneration::new(1),
234			catalog: SourceCatalog::new(ResourceGeneration::new(1), Vec::new()),
235			index: CodeIndex {
236				generation: ResourceGeneration::new(1),
237				catalog_generation: ResourceGeneration::new(1),
238				identity_scheme: "code+moniker://".to_string(),
239				sources: vec![source],
240				symbols: RecordTable::from_records(vec![symbol]),
241				references: RecordTable::from_records(Vec::new()),
242				inventory: Arc::new(SymbolInventoryIndex::default()),
243				timings: CodeIndexTimings::default(),
244			},
245			linkage: LinkageSnapshot::new(
246				ResourceGeneration::new(1),
247				ResourceGeneration::new(1),
248				0,
249				0,
250			),
251			changes: ChangeOverlay::new(
252				ResourceGeneration::new(1),
253				ResourceGeneration::new(1),
254				ResourceGeneration::new(1),
255				Vec::new(),
256			),
257			timings: WorkspaceTimings::default(),
258		}
259	}
260
261	fn snapshot_with_orphaned_symbol(moniker: &str) -> WorkspaceSnapshot {
262		let symbol = SymbolRecord {
263			id: SymbolId::at(0, 1),
264			source: SourceId::at(9999),
265			identity: std::sync::Arc::from(moniker.to_string()),
266			name: "run()".to_string(),
267			kind: "fn".to_string(),
268			visibility: "public".to_string(),
269			signature: "run()".to_string(),
270			call_name: None,
271			call_arity: None,
272			navigable: true,
273			line_range: Some((3, 7)),
274			parent: None,
275		};
276		WorkspaceSnapshot {
277			generation: ResourceGeneration::new(1),
278			catalog: SourceCatalog::new(ResourceGeneration::new(1), Vec::new()),
279			index: CodeIndex {
280				generation: ResourceGeneration::new(1),
281				catalog_generation: ResourceGeneration::new(1),
282				identity_scheme: "code+moniker://".to_string(),
283				sources: Vec::new(),
284				symbols: RecordTable::from_records(vec![symbol]),
285				references: RecordTable::from_records(Vec::new()),
286				inventory: Arc::new(SymbolInventoryIndex::default()),
287				timings: CodeIndexTimings::default(),
288			},
289			linkage: LinkageSnapshot::new(
290				ResourceGeneration::new(1),
291				ResourceGeneration::new(1),
292				0,
293				0,
294			),
295			changes: ChangeOverlay::new(
296				ResourceGeneration::new(1),
297				ResourceGeneration::new(1),
298				ResourceGeneration::new(1),
299				Vec::new(),
300			),
301			timings: WorkspaceTimings::default(),
302		}
303	}
304}