Skip to main content

merman_editor_core/
structure.rs

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}