cairo-language-server 2.19.0-rc.2

The Cairo Language Server
Documentation
use std::collections::{HashMap, HashSet};

use cairo_lang_semantic::lsp_helpers::LspHelpers;
use cairo_lang_syntax::node::TypedSyntaxNode;
use cairo_lang_syntax::node::ast::ExprPath;
use cairo_lang_syntax::node::helpers::GetIdentifier;
use cairo_lang_syntax::node::kind::SyntaxKind;
use cairo_language_common::CommonGroup;
use lsp_types::{CodeAction, CodeActionKind, TextEdit, Url, WorkspaceEdit};

use crate::lang::analysis_context::AnalysisContext;
use crate::lang::db::AnalysisDatabase;
use crate::lang::lsp::ToLsp;
use crate::lang::text_matching::text_matches;

pub fn suggest_similar_identifier<'db>(
    db: &'db AnalysisDatabase,
    ctx: &AnalysisContext<'db>,
    uri: &Url,
) -> Option<Vec<CodeAction>> {
    let typed_path_generic = ctx.node.ancestor_of_type::<ExprPath>(db)?;
    let typed_path_segments: Vec<_> = typed_path_generic
        .segments(db)
        .elements(db)
        .map(|e| e.identifier(db).to_string(db))
        .collect();

    db.get_node_resultants(typed_path_generic.as_syntax_node())?.iter().find_map(|resultant_node| {
        let resultant_expression_path = match resultant_node.kind(db) {
            SyntaxKind::ExprPath => ExprPath::from_syntax_node(db, *resultant_node),
            SyntaxKind::ExprPathInner => ExprPath::from_syntax_node(db, resultant_node.parent(db)?),
            _ => return None,
        };
        let expression_path_segments: Vec<_> = resultant_expression_path
            .segments(db)
            .as_syntax_node()
            .get_children(db)
            .iter()
            .filter(|node| node.kind(db) == SyntaxKind::PathSegmentSimple)
            .map(|segment| segment.get_text_without_trivia(db).to_string(db))
            .collect();

        if typed_path_segments != expression_path_segments {
            return None;
        }

        let expression_span =
            ctx.node.span(db).position_in_file(db, ctx.node.stable_ptr(db).file_id(db))?.to_lsp();

        let module_id = db.find_module_containing_node(*resultant_node)?;
        let items = db.visible_importables_from_module(module_id)?;

        let mut seen = HashSet::new();
        let suggestions: Vec<(String, TextEdit)> = items
            .iter()
            .filter_map(|(_item, proposed_path)| {
                segment_suggestion(proposed_path, &expression_path_segments).map(|suggestion| {
                    let edit =
                        TextEdit { range: expression_span, new_text: suggestion.to_string() };
                    (suggestion.to_string(), edit)
                })
            })
            .filter(|(suggestion, _)| seen.insert(suggestion.to_owned()))
            .collect();

        let code_actions = suggestions
            .into_iter()
            .map(|(identifier, edit)| CodeAction {
                title: format!("Did you mean `{identifier}`?"),
                kind: Some(CodeActionKind::QUICKFIX),
                edit: Some(WorkspaceEdit {
                    changes: Some(HashMap::from([(uri.clone(), vec![edit])])),
                    document_changes: None,
                    change_annotations: None,
                }),
                ..Default::default()
            })
            .collect();

        Some(code_actions)
    })
}

fn segment_suggestion<'a>(proposed_path: &'a str, typed_segments: &[String]) -> Option<&'a str> {
    let proposed_segments: Vec<_> = proposed_path.split("::").collect();

    if proposed_segments.len() != typed_segments.len() {
        return None;
    }

    proposed_segments
        .iter()
        .zip(typed_segments)
        .find(|(p, t)| *p != *t)
        .and_then(|(p, t)| text_matches(p, t).then_some(*p))
}