1use crate::snapshot::{DocumentSnapshot, FenceSnapshot};
2use crate::types::{DocumentUri, Position, Range};
3use merman_analysis::{
4 ByteSpan, EditorSymbolKind, FenceLineItem, FenceSemanticItem, FenceTextIndexSource, SourceMap,
5};
6use std::collections::HashMap;
7use std::fmt;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10struct OutlineItem {
11 name: String,
12 detail: Option<String>,
13 kind: EditorSymbolKind,
14 fact_source: FenceTextIndexSource,
15 span: ByteSpan,
16 selection: ByteSpan,
17 children: Vec<OutlineItem>,
18}
19
20impl OutlineItem {
21 fn to_document_symbol(&self, source_map: &SourceMap) -> Option<EditorDocumentSymbol> {
22 let range = range_from_span(source_map, self.span)?;
23 let selection_range = range_from_span(source_map, self.selection)?;
24
25 Some(EditorDocumentSymbol {
26 name: self.name.clone(),
27 detail: self.detail.clone(),
28 kind: self.kind,
29 fact_source: self.fact_source,
30 range,
31 selection_range,
32 children: self
33 .children
34 .iter()
35 .filter_map(|child| child.to_document_symbol(source_map))
36 .collect(),
37 })
38 }
39
40 fn hover_markdown(&self, fence: &FenceSnapshot) -> EditorMarkupContent {
41 let mut value = format!("### {}\n\n", markdown_plain_text(&self.name));
42 if let Some(detail) = &self.detail {
43 value.push_str(&markdown_plain_text(detail));
44 value.push_str("\n\n");
45 }
46 if let Some(kind) = fence.diagram_type.as_deref() {
47 value.push_str(&format!("Diagram: {}\n", markdown_plain_text(kind)));
48 }
49 value.push_str(&format!("Scope: fence {}\n", fence.index + 1));
50
51 EditorMarkupContent { value }
52 }
53
54 fn find_deepest(&self, offset: usize) -> Option<&OutlineItem> {
55 if !self.span.contains(offset) {
56 return None;
57 }
58
59 for child in &self.children {
60 if let Some(found) = child.find_deepest(offset) {
61 return Some(found);
62 }
63 }
64
65 Some(self)
66 }
67}
68
69fn markdown_plain_text(value: &str) -> String {
70 let mut escaped = String::with_capacity(value.len());
71 let mut pending_space = false;
72 for char in value.chars() {
73 if matches!(char, '\r' | '\n') {
74 pending_space = true;
75 continue;
76 }
77 if pending_space && !escaped.is_empty() && !escaped.ends_with(' ') {
78 escaped.push(' ');
79 }
80 pending_space = false;
81 if is_markdown_control(char) {
82 escaped.push('\\');
83 }
84 escaped.push(char);
85 }
86 escaped
87}
88
89fn is_markdown_control(char: char) -> bool {
90 matches!(
91 char,
92 '\\' | '`'
93 | '*'
94 | '_'
95 | '{'
96 | '}'
97 | '['
98 | ']'
99 | '('
100 | ')'
101 | '#'
102 | '+'
103 | '-'
104 | '.'
105 | '!'
106 | '~'
107 | '|'
108 | '>'
109 )
110}
111
112pub fn document_symbols(snapshot: &DocumentSnapshot) -> Vec<EditorDocumentSymbol> {
113 snapshot
114 .fences
115 .iter()
116 .map(outline_for_fence)
117 .filter_map(|item| item.to_document_symbol(&snapshot.source_map))
118 .collect()
119}
120
121pub fn workspace_symbols(snapshot: &DocumentSnapshot, query: &str) -> Vec<EditorSymbolInformation> {
122 let query = query.trim();
123 let query = if query.is_empty() {
124 None
125 } else {
126 Some(query.to_lowercase())
127 };
128 let mut symbols = Vec::new();
129 for fence in &snapshot.fences {
130 let outline = outline_for_fence(fence);
131 collect_workspace_symbols(snapshot, &outline, None, query.as_deref(), &mut symbols);
132 }
133
134 sort_workspace_symbols(&mut symbols);
135 symbols
136}
137
138pub fn workspace_symbols_for_snapshots<'a>(
139 snapshots: impl IntoIterator<Item = &'a DocumentSnapshot>,
140 query: &str,
141) -> Vec<EditorSymbolInformation> {
142 let mut symbols = snapshots
143 .into_iter()
144 .flat_map(|snapshot| workspace_symbols(snapshot, query))
145 .collect::<Vec<_>>();
146 sort_workspace_symbols(&mut symbols);
147 symbols
148}
149
150pub fn selection_ranges(
151 snapshot: &DocumentSnapshot,
152 positions: &[Position],
153) -> Vec<Option<EditorSelectionRange>> {
154 positions
155 .iter()
156 .copied()
157 .map(|position| selection_range(snapshot, position))
158 .collect()
159}
160
161pub fn selection_range(
162 snapshot: &DocumentSnapshot,
163 position: Position,
164) -> Option<EditorSelectionRange> {
165 let fence = snapshot.fence_at_position(position)?;
166 let absolute_offset = snapshot.byte_offset_for_position(position)?;
167 let outline = outline_for_fence(fence);
168 let mut spans = Vec::new();
169
170 if let Some(relative_offset) = fence_relative_offset(snapshot, fence, position) {
171 if let Some(item) = fence.text_index.semantic_item_at_offset(relative_offset) {
172 push_selection_span(
173 &mut spans,
174 absolute_offset,
175 absolute_span(fence, item.selection),
176 );
177 push_selection_span(&mut spans, absolute_offset, absolute_span(fence, item.span));
178 }
179 }
180
181 if let Some(item) = outline.find_deepest(absolute_offset) {
182 push_selection_span(&mut spans, absolute_offset, item.selection);
183 push_selection_span(&mut spans, absolute_offset, item.span);
184 }
185
186 push_selection_span(
187 &mut spans,
188 absolute_offset,
189 ByteSpan {
190 start: fence.body_start,
191 end: fence.body_end,
192 },
193 );
194 push_selection_span(
195 &mut spans,
196 absolute_offset,
197 ByteSpan {
198 start: fence.start,
199 end: fence.end,
200 },
201 );
202
203 spans_to_selection_range(&snapshot.source_map, fence.text_index.source(), spans)
204}
205
206pub fn folding_ranges(snapshot: &DocumentSnapshot) -> Vec<EditorFoldingRange> {
207 let mut ranges = Vec::new();
208
209 for fence in &snapshot.fences {
210 if fence.fence_delimiter.is_some() {
211 push_folding_span(
212 &mut ranges,
213 &snapshot.source_map,
214 fence.text_index.source(),
215 trim_folding_span(
216 &snapshot.text,
217 ByteSpan {
218 start: fence.start,
219 end: fence.end,
220 },
221 ),
222 );
223 }
224
225 for child in outline_children(fence) {
226 push_folding_span(
227 &mut ranges,
228 &snapshot.source_map,
229 child.fact_source,
230 trim_folding_span(&snapshot.text, child.span),
231 );
232 }
233 }
234
235 ranges.sort_by(|left, right| compare_range(&left.range, &right.range));
236 ranges.dedup_by(|left, right| left.range == right.range);
237 ranges
238}
239
240fn trim_folding_span(text: &str, mut span: ByteSpan) -> ByteSpan {
241 while span.end > span.start {
242 let previous = text.as_bytes()[span.end - 1];
243 if !matches!(previous, b'\n' | b'\r') {
244 break;
245 }
246 span.end -= 1;
247 }
248 span
249}
250
251pub fn hover(snapshot: &DocumentSnapshot, position: Position) -> Option<EditorHover> {
252 let fence = snapshot.fence_at_position(position)?;
253 let absolute_offset = snapshot.byte_offset_for_position(position)?;
254 let relative_offset = fence_relative_offset(snapshot, fence, position)?;
255 let outline = outline_for_fence(fence);
256 let item = fence
257 .text_index
258 .semantic_item_at_offset(relative_offset)
259 .map(|item| outline_item_from_semantic(fence, item))
260 .or_else(|| outline.find_deepest(absolute_offset).cloned())
261 .unwrap_or(outline);
262 let range = range_from_span(&snapshot.source_map, item.selection).or_else(|| {
263 range_from_span(
264 &snapshot.source_map,
265 ByteSpan {
266 start: fence.start,
267 end: fence.end,
268 },
269 )
270 });
271
272 Some(EditorHover {
273 contents: item.hover_markdown(fence),
274 fact_source: item.fact_source,
275 range,
276 })
277}
278
279pub fn goto_definition(snapshot: &DocumentSnapshot, position: Position) -> Option<EditorLocation> {
280 let fence = snapshot.fence_at_position(position)?;
281 let offset = fence_relative_offset(snapshot, fence, position)?;
282 let item = fence.text_index.entity_item_at_offset(offset)?;
283 let span = absolute_span(fence, fence.text_index.first_reference_span_for_item(item)?);
284 let range = range_from_span(&snapshot.source_map, span)?;
285 Some(EditorLocation {
286 uri: snapshot.uri.clone(),
287 fact_source: fence.text_index.source(),
288 range,
289 })
290}
291
292pub fn references(
293 snapshot: &DocumentSnapshot,
294 position: Position,
295 include_declaration: bool,
296) -> Option<Vec<EditorLocation>> {
297 let fence = snapshot.fence_at_position(position)?;
298 let offset = fence_relative_offset(snapshot, fence, position)?;
299 let item = fence.text_index.entity_item_at_offset(offset)?;
300 let mut locations = fence
301 .text_index
302 .reference_spans_for_item(item)
303 .iter()
304 .copied()
305 .filter_map(|span| {
306 let span = absolute_span(fence, span);
307 range_from_span(&snapshot.source_map, span).map(|range| EditorLocation {
308 uri: snapshot.uri.clone(),
309 fact_source: fence.text_index.source(),
310 range,
311 })
312 })
313 .collect::<Vec<_>>();
314
315 if !include_declaration
316 && let Some(def_span) = fence.text_index.first_reference_span_for_item(item)
317 {
318 let def_span = absolute_span(fence, def_span);
319 locations.retain(|location| !same_span(&snapshot.source_map, location.range, def_span));
320 }
321
322 locations.sort_by(|a, b| compare_range(&a.range, &b.range));
323 Some(locations)
324}
325
326pub fn prepare_rename(
327 snapshot: &DocumentSnapshot,
328 position: Position,
329) -> Option<EditorPrepareRename> {
330 let fence = snapshot.fence_at_position(position)?;
331 let offset = fence_relative_offset(snapshot, fence, position)?;
332 let item = fence.text_index.entity_item_at_offset(offset)?;
333 let selection = absolute_span(fence, item.selection);
334 let range = range_from_span(&snapshot.source_map, selection)?;
335 let placeholder = snapshot
336 .text
337 .get(selection.start..selection.end)?
338 .to_string();
339 Some(EditorPrepareRename {
340 fact_source: fence.text_index.source(),
341 range,
342 placeholder,
343 })
344}
345
346pub fn rename(
347 snapshot: &DocumentSnapshot,
348 position: Position,
349 new_name: &str,
350) -> Result<Option<EditorWorkspaceEdit>, RenameError> {
351 if !is_valid_rename_name(new_name) {
352 return Err(RenameError::InvalidName);
353 }
354 let fence = snapshot
355 .fence_at_position(position)
356 .ok_or(RenameError::OutsideFence)?;
357 let offset =
358 fence_relative_offset(snapshot, fence, position).ok_or(RenameError::NoRenameableSymbol)?;
359 let item = fence
360 .text_index
361 .entity_item_at_offset(offset)
362 .ok_or(RenameError::NoRenameableSymbol)?;
363 Ok(rename_edits(snapshot, fence, item, new_name))
364}
365
366fn rename_edits(
367 snapshot: &DocumentSnapshot,
368 fence: &FenceSnapshot,
369 item: &FenceSemanticItem,
370 new_name: &str,
371) -> Option<EditorWorkspaceEdit> {
372 let spans = fence.text_index.reference_spans_for_item(item);
373 let mut edits = spans
374 .iter()
375 .copied()
376 .filter_map(|span| range_from_span(&snapshot.source_map, absolute_span(fence, span)))
377 .map(|range| EditorTextEdit {
378 fact_source: fence.text_index.source(),
379 range,
380 new_text: new_name.to_string(),
381 })
382 .collect::<Vec<_>>();
383 if edits.is_empty() {
384 return None;
385 }
386 edits.sort_by(|a, b| compare_range(&a.range, &b.range));
387
388 let mut changes = HashMap::new();
389 changes.insert(snapshot.uri.clone(), edits);
390 Some(EditorWorkspaceEdit {
391 fact_source: fence.text_index.source(),
392 changes,
393 })
394}
395
396fn outline_for_fence(fence: &FenceSnapshot) -> OutlineItem {
397 OutlineItem {
398 name: fence_name(fence),
399 detail: fence_detail(fence),
400 kind: generic_kind(fence.diagram_type.as_deref()),
401 fact_source: fence.text_index.source(),
402 span: ByteSpan {
403 start: fence.start,
404 end: fence.end,
405 },
406 selection: ByteSpan {
407 start: fence.body_start,
408 end: fence.body_end,
409 },
410 children: outline_children(fence),
411 }
412}
413
414fn outline_children(fence: &FenceSnapshot) -> Vec<OutlineItem> {
415 fence
416 .text_index
417 .outline_items()
418 .iter()
419 .map(|item| outline_item_from_index(fence, item))
420 .collect()
421}
422
423fn outline_item_from_index(fence: &FenceSnapshot, item: &FenceLineItem) -> OutlineItem {
424 OutlineItem {
425 name: item.name.clone(),
426 detail: item.detail.clone(),
427 kind: item.kind,
428 fact_source: fence.text_index.source(),
429 span: absolute_span(fence, item.span),
430 selection: absolute_span(fence, item.selection),
431 children: Vec::new(),
432 }
433}
434
435fn outline_item_from_semantic(fence: &FenceSnapshot, item: &FenceSemanticItem) -> OutlineItem {
436 OutlineItem {
437 name: item.name.clone(),
438 detail: item.detail.clone(),
439 kind: item.kind,
440 fact_source: fence.text_index.source(),
441 span: absolute_span(fence, item.span),
442 selection: absolute_span(fence, item.selection),
443 children: Vec::new(),
444 }
445}
446
447fn collect_workspace_symbols(
448 snapshot: &DocumentSnapshot,
449 item: &OutlineItem,
450 container_name: Option<&str>,
451 query: Option<&str>,
452 symbols: &mut Vec<EditorSymbolInformation>,
453) {
454 if query.is_none_or(|query| workspace_symbol_matches(&item.name, query))
455 && let Some(location) = workspace_symbol_location(snapshot, item)
456 {
457 symbols.push(EditorSymbolInformation {
458 name: item.name.clone(),
459 kind: item.kind,
460 fact_source: item.fact_source,
461 location,
462 container_name: container_name.map(str::to_string),
463 });
464 }
465
466 let container_name = Some(item.name.as_str());
467 for child in &item.children {
468 collect_workspace_symbols(snapshot, child, container_name, query, symbols);
469 }
470}
471
472fn workspace_symbol_matches(name: &str, query: &str) -> bool {
473 name.to_lowercase().contains(query)
474}
475
476fn workspace_symbol_location(
477 snapshot: &DocumentSnapshot,
478 item: &OutlineItem,
479) -> Option<EditorLocation> {
480 let range = range_from_span(&snapshot.source_map, item.selection)?;
481 Some(EditorLocation {
482 uri: snapshot.uri.clone(),
483 fact_source: item.fact_source,
484 range,
485 })
486}
487
488pub fn sort_workspace_symbols(symbols: &mut [EditorSymbolInformation]) {
489 symbols.sort_by(|left, right| {
490 left.name
491 .cmp(&right.name)
492 .then_with(|| left.location.uri.as_str().cmp(right.location.uri.as_str()))
493 .then_with(|| compare_range(&left.location.range, &right.location.range))
494 .then_with(|| left.container_name.cmp(&right.container_name))
495 });
496}
497
498fn fence_relative_offset(
499 snapshot: &DocumentSnapshot,
500 fence: &FenceSnapshot,
501 position: Position,
502) -> Option<usize> {
503 let offset = snapshot.byte_offset_for_position(position)?;
504 (offset >= fence.body_start).then_some(offset - fence.body_start)
505}
506
507fn absolute_span(fence: &FenceSnapshot, span: ByteSpan) -> ByteSpan {
508 ByteSpan {
509 start: fence.body_start + span.start,
510 end: fence.body_start + span.end,
511 }
512}
513
514fn push_selection_span(spans: &mut Vec<ByteSpan>, offset: usize, span: ByteSpan) {
515 if span.start >= span.end || !span.contains(offset) || spans.iter().any(|item| *item == span) {
516 return;
517 }
518
519 spans.push(span);
520}
521
522fn spans_to_selection_range(
523 source_map: &SourceMap,
524 fact_source: FenceTextIndexSource,
525 spans: Vec<ByteSpan>,
526) -> Option<EditorSelectionRange> {
527 let mut parent = None;
528
529 for span in spans.into_iter().rev() {
530 let range = range_from_span(source_map, span)?;
531 parent = Some(EditorSelectionRange {
532 range,
533 fact_source,
534 parent: parent.map(Box::new),
535 });
536 }
537
538 parent
539}
540
541fn push_folding_span(
542 ranges: &mut Vec<EditorFoldingRange>,
543 source_map: &SourceMap,
544 fact_source: FenceTextIndexSource,
545 span: ByteSpan,
546) {
547 let Some(range) = range_from_span(source_map, span) else {
548 return;
549 };
550 if range.start.line >= range.end.line {
551 return;
552 }
553
554 ranges.push(EditorFoldingRange {
555 range,
556 kind: EditorFoldingRangeKind::Region,
557 fact_source,
558 });
559}
560
561fn fence_name(fence: &FenceSnapshot) -> String {
562 match fence.diagram_type.as_deref() {
563 Some(kind) => format!("{kind} diagram"),
564 None => "Mermaid diagram".to_string(),
565 }
566}
567
568fn fence_detail(fence: &FenceSnapshot) -> Option<String> {
569 let mut parts = Vec::new();
570
571 if let Some(kind) = fence.diagram_type.as_deref() {
572 parts.push(format!("diagram type `{kind}`"));
573 }
574
575 let prefixes = fence
576 .text_index
577 .directive_prefixes()
578 .cloned()
579 .collect::<Vec<_>>();
580 if !prefixes.is_empty() {
581 parts.push(format!("directives {}", prefixes.join(", ")));
582 }
583
584 if parts.is_empty() {
585 None
586 } else {
587 Some(parts.join(" ยท "))
588 }
589}
590
591fn generic_kind(diagram_type: Option<&str>) -> EditorSymbolKind {
592 match diagram_type {
593 Some("sequence") => EditorSymbolKind::Event,
594 Some("state") => EditorSymbolKind::Class,
595 Some("mindmap") => EditorSymbolKind::Namespace,
596 Some("class") => EditorSymbolKind::Class,
597 Some("er") => EditorSymbolKind::Struct,
598 Some("block") => EditorSymbolKind::Object,
599 Some("flowchart-v2") | Some("flowchart-elk") => EditorSymbolKind::Module,
600 _ => EditorSymbolKind::Variable,
601 }
602}
603
604fn range_from_span(source_map: &SourceMap, span: ByteSpan) -> Option<Range> {
605 source_map.span(span.start, span.end).ok().map(|span| {
606 Range::new(
607 Position::new(span.lsp_range.start.line, span.lsp_range.start.character),
608 Position::new(span.lsp_range.end.line, span.lsp_range.end.character),
609 )
610 })
611}
612
613fn same_span(source_map: &SourceMap, range: Range, span: ByteSpan) -> bool {
614 let Some(expected) = range_from_span(source_map, span) else {
615 return false;
616 };
617 expected == range
618}
619
620fn compare_range(left: &Range, right: &Range) -> std::cmp::Ordering {
621 (
622 left.start.line,
623 left.start.character,
624 left.end.line,
625 left.end.character,
626 )
627 .cmp(&(
628 right.start.line,
629 right.start.character,
630 right.end.line,
631 right.end.character,
632 ))
633}
634
635fn is_valid_rename_name(new_name: &str) -> bool {
636 !new_name.is_empty()
637 && new_name
638 .chars()
639 .all(|ch| ch.is_alphanumeric() || matches!(ch, '_' | '-'))
640}
641
642#[derive(Debug, Clone, PartialEq, Eq)]
643pub struct EditorDocumentSymbol {
644 pub name: String,
645 pub detail: Option<String>,
646 pub kind: EditorSymbolKind,
647 pub fact_source: FenceTextIndexSource,
648 pub range: Range,
649 pub selection_range: Range,
650 pub children: Vec<EditorDocumentSymbol>,
651}
652
653#[derive(Debug, Clone, PartialEq, Eq)]
654pub struct EditorSelectionRange {
655 pub range: Range,
656 pub parent: Option<Box<EditorSelectionRange>>,
657 pub fact_source: FenceTextIndexSource,
658}
659
660#[derive(Debug, Clone, PartialEq, Eq)]
661pub struct EditorFoldingRange {
662 pub range: Range,
663 pub kind: EditorFoldingRangeKind,
664 pub fact_source: FenceTextIndexSource,
665}
666
667#[derive(Debug, Clone, Copy, PartialEq, Eq)]
668pub enum EditorFoldingRangeKind {
669 Region,
670}
671
672#[derive(Debug, Clone, PartialEq, Eq)]
673pub struct EditorSymbolInformation {
674 pub name: String,
675 pub kind: EditorSymbolKind,
676 pub fact_source: FenceTextIndexSource,
677 pub location: EditorLocation,
678 pub container_name: Option<String>,
679}
680
681#[derive(Debug, Clone, PartialEq, Eq)]
682pub struct EditorLocation {
683 pub uri: DocumentUri,
684 pub fact_source: FenceTextIndexSource,
685 pub range: Range,
686}
687
688#[derive(Debug, Clone, PartialEq, Eq)]
689pub struct EditorHover {
690 pub contents: EditorMarkupContent,
691 pub fact_source: FenceTextIndexSource,
692 pub range: Option<Range>,
693}
694
695#[derive(Debug, Clone, PartialEq, Eq)]
696pub struct EditorMarkupContent {
697 pub value: String,
698}
699
700#[derive(Debug, Clone, PartialEq, Eq)]
701pub struct EditorPrepareRename {
702 pub fact_source: FenceTextIndexSource,
703 pub range: Range,
704 pub placeholder: String,
705}
706
707#[derive(Debug, Clone, PartialEq, Eq)]
708pub struct EditorWorkspaceEdit {
709 pub fact_source: FenceTextIndexSource,
710 pub changes: HashMap<DocumentUri, Vec<EditorTextEdit>>,
711}
712
713#[derive(Debug, Clone, PartialEq, Eq)]
714pub struct EditorTextEdit {
715 pub fact_source: FenceTextIndexSource,
716 pub range: Range,
717 pub new_text: String,
718}
719
720#[derive(Debug, Clone, Copy, PartialEq, Eq)]
721pub enum RenameError {
722 InvalidName,
723 OutsideFence,
724 NoRenameableSymbol,
725}
726
727impl fmt::Display for RenameError {
728 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
729 let message = match self {
730 Self::InvalidName => "new name must use letters, numbers, underscore, or dash",
731 Self::OutsideFence => "position is outside a Mermaid fence",
732 Self::NoRenameableSymbol => "no renameable symbol at the requested position",
733 };
734 formatter.write_str(message)
735 }
736}