merman-editor-core 0.8.0-alpha.3

Protocol-neutral editor intelligence core for Merman.
Documentation
use crate::snapshot::{DocumentSnapshot, FenceSnapshot};
use merman_analysis::{
    ByteSpan, EditorSymbolKind, FenceSemanticItem, FenceSemanticRole, FenceTextIndexSource,
    SourceMap,
};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SemanticToken {
    pub line: u32,
    pub start: u32,
    pub length: u32,
    pub kind: SemanticTokenKind,
    pub modifier: SemanticTokenModifier,
    pub fact_source: FenceTextIndexSource,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SemanticTokenKind {
    Namespace,
    Class,
    Struct,
    Variable,
    Property,
    Event,
    Function,
    String,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SemanticTokenModifier {
    Entity,
    Outline,
    Payload,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SemanticTokenLegend {
    pub token_types: Vec<SemanticTokenKind>,
    pub token_modifiers: Vec<SemanticTokenModifier>,
}

pub fn semantic_token_legend() -> SemanticTokenLegend {
    SemanticTokenLegend {
        token_types: vec![
            SemanticTokenKind::Namespace,
            SemanticTokenKind::Class,
            SemanticTokenKind::Struct,
            SemanticTokenKind::Variable,
            SemanticTokenKind::Property,
            SemanticTokenKind::Event,
            SemanticTokenKind::Function,
            SemanticTokenKind::String,
        ],
        token_modifiers: vec![
            SemanticTokenModifier::Entity,
            SemanticTokenModifier::Outline,
            SemanticTokenModifier::Payload,
        ],
    }
}

pub fn semantic_tokens_for_snapshot(snapshot: &DocumentSnapshot) -> Vec<SemanticToken> {
    semantic_tokens_for_fences(&snapshot.source_map, &snapshot.fences, None)
}

pub fn semantic_tokens_for_snapshot_range(
    snapshot: &DocumentSnapshot,
    start_line: u32,
    end_line: u32,
) -> Vec<SemanticToken> {
    let Some(line_range) = RequestedLineRange::new(&snapshot.source_map, start_line, end_line)
    else {
        return Vec::new();
    };
    semantic_tokens_for_fences(&snapshot.source_map, &snapshot.fences, Some(line_range))
}

fn semantic_tokens_for_fences<'a>(
    source_map: &SourceMap,
    fences: impl IntoIterator<Item = &'a FenceSnapshot>,
    line_range: Option<RequestedLineRange>,
) -> Vec<SemanticToken> {
    let mut tokens = Vec::new();

    for fence in fences {
        if line_range.is_some_and(|range| !fence_overlaps_line_range(source_map, fence, range)) {
            continue;
        }
        for item in fence.text_index.semantic_items() {
            let Some(span) = absolute_span_for_item(fence, item) else {
                continue;
            };
            if line_range.is_some_and(|range| !range.overlaps_byte_span(span)) {
                continue;
            }
            tokens.extend(tokens_for_item(source_map, fence, item, span, line_range));
        }
    }

    tokens.sort_by(|left, right| {
        (
            left.line,
            left.start,
            left.length,
            token_type_index(left.kind),
            token_modifier_index(left.modifier),
        )
            .cmp(&(
                right.line,
                right.start,
                right.length,
                token_type_index(right.kind),
                token_modifier_index(right.modifier),
            ))
    });
    tokens.dedup();
    tokens
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct RequestedLineRange {
    start_line: u32,
    end_line: u32,
    byte_span: ByteSpan,
}

impl RequestedLineRange {
    fn new(source_map: &SourceMap, start_line: u32, end_line: u32) -> Option<Self> {
        let requested_start_line = start_line.min(end_line);
        let requested_end_line = start_line.max(end_line);
        let line_count = source_map.line_starts().len();
        if line_count == 0 || requested_start_line as usize >= line_count {
            return None;
        }

        let end_line = requested_end_line.min((line_count - 1) as u32);
        let (byte_start, _) = source_map.line_bounds(requested_start_line as usize)?;
        let (_, byte_end) = source_map.line_bounds(end_line as usize)?;
        Some(Self {
            start_line: requested_start_line,
            end_line,
            byte_span: ByteSpan {
                start: byte_start,
                end: byte_end,
            },
        })
    }

    fn contains_line(self, line: u32) -> bool {
        line >= self.start_line && line <= self.end_line
    }

    fn overlaps_byte_span(self, span: ByteSpan) -> bool {
        span.start < self.byte_span.end && span.end > self.byte_span.start
    }
}

fn fence_overlaps_line_range(
    source_map: &SourceMap,
    fence: &FenceSnapshot,
    line_range: RequestedLineRange,
) -> bool {
    let Ok(fence_start) = source_map.utf16_position(fence.body_start) else {
        return true;
    };
    let Ok(fence_end) = source_map.utf16_position(fence.body_end) else {
        return true;
    };

    (fence_start.line as u32) <= line_range.end_line
        && (fence_end.line as u32) >= line_range.start_line
}

fn absolute_span_for_item(fence: &FenceSnapshot, item: &FenceSemanticItem) -> Option<ByteSpan> {
    let span = ByteSpan {
        start: fence.body_start + item.selection.start,
        end: fence.body_start + item.selection.end,
    };
    if span.start >= span.end {
        None
    } else {
        Some(span)
    }
}

fn tokens_for_item(
    source_map: &SourceMap,
    fence: &FenceSnapshot,
    item: &FenceSemanticItem,
    span: ByteSpan,
    line_range: Option<RequestedLineRange>,
) -> Vec<SemanticToken> {
    token_pieces_for_span(source_map, span)
        .into_iter()
        .filter(|piece| line_range.is_none_or(|range| range.contains_line(piece.line)))
        .map(|piece| SemanticToken {
            line: piece.line,
            start: piece.start,
            length: piece.length,
            kind: token_kind_for_symbol(item.kind),
            modifier: token_modifier_for_role(item.role),
            fact_source: fence.text_index.source(),
        })
        .collect()
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct TokenPiece {
    line: u32,
    start: u32,
    length: u32,
}

fn token_pieces_for_span(source_map: &SourceMap, span: ByteSpan) -> Vec<TokenPiece> {
    let Ok(start) = source_map.utf16_position(span.start) else {
        return Vec::new();
    };
    let Ok(end) = source_map.utf16_position(span.end) else {
        return Vec::new();
    };

    let mut pieces = Vec::new();
    for line in start.line..=end.line {
        let Some((line_start, line_end)) = source_map.line_bounds(line) else {
            continue;
        };
        let segment_start = span.start.max(line_start);
        let segment_end = span.end.min(line_end);
        if segment_start >= segment_end {
            continue;
        }

        let Ok(segment_start_pos) = source_map.utf16_position(segment_start) else {
            continue;
        };
        let Ok(segment_end_pos) = source_map.utf16_position(segment_end) else {
            continue;
        };
        if segment_start_pos.line != segment_end_pos.line
            || segment_end_pos.character <= segment_start_pos.character
        {
            continue;
        }

        pieces.push(TokenPiece {
            line: segment_start_pos.line as u32,
            start: segment_start_pos.character as u32,
            length: (segment_end_pos.character - segment_start_pos.character) as u32,
        });
    }

    pieces
}

fn token_kind_for_symbol(kind: EditorSymbolKind) -> SemanticTokenKind {
    match kind {
        EditorSymbolKind::Class => SemanticTokenKind::Class,
        EditorSymbolKind::Event => SemanticTokenKind::Event,
        EditorSymbolKind::Function => SemanticTokenKind::Function,
        EditorSymbolKind::Module | EditorSymbolKind::Namespace | EditorSymbolKind::Package => {
            SemanticTokenKind::Namespace
        }
        EditorSymbolKind::Object | EditorSymbolKind::Variable => SemanticTokenKind::Variable,
        EditorSymbolKind::Property => SemanticTokenKind::Property,
        EditorSymbolKind::String => SemanticTokenKind::String,
        EditorSymbolKind::Struct => SemanticTokenKind::Struct,
    }
}

fn token_modifier_for_role(role: FenceSemanticRole) -> SemanticTokenModifier {
    match role {
        FenceSemanticRole::Entity => SemanticTokenModifier::Entity,
        FenceSemanticRole::Outline => SemanticTokenModifier::Outline,
        FenceSemanticRole::Payload => SemanticTokenModifier::Payload,
    }
}

pub fn token_type_index(kind: SemanticTokenKind) -> u32 {
    match kind {
        SemanticTokenKind::Namespace => 0,
        SemanticTokenKind::Class => 1,
        SemanticTokenKind::Struct => 2,
        SemanticTokenKind::Variable => 3,
        SemanticTokenKind::Property => 4,
        SemanticTokenKind::Event => 5,
        SemanticTokenKind::Function => 6,
        SemanticTokenKind::String => 7,
    }
}

pub fn token_modifier_index(modifier: SemanticTokenModifier) -> u32 {
    match modifier {
        SemanticTokenModifier::Entity => 0,
        SemanticTokenModifier::Outline => 1,
        SemanticTokenModifier::Payload => 2,
    }
}