els 0.1.65

An Erg compiler frontend for IDEs, implements LSP.
Documentation
use erg_common::consts::PYTHON_MODE;
use erg_common::lang::LanguageCode;
use erg_common::trim_eliminate_top_indent;
use erg_compiler::artifact::BuildRunnable;
use erg_compiler::erg_parser::parse::Parsable;
use erg_compiler::erg_parser::token::{Token, TokenCategory, TokenKind};
use erg_compiler::hir::Expr;
use erg_compiler::ty::HasType;
use erg_compiler::varinfo::{AbsLocation, VarInfo};

use lsp_types::{Hover, HoverContents, HoverParams, MarkedString, Url};

#[allow(unused)]
use crate::_log;
use crate::server::{ELSResult, RedirectableStdout, Server};
use crate::util::{self, NormalizedUrl};

const PROG_LANG: &str = if PYTHON_MODE { "python" } else { "erg" };

const ERG_LANG: &str = "erg";

fn lang_code(code: &str) -> LanguageCode {
    code.lines()
        .next()
        .unwrap_or("")
        .parse()
        .unwrap_or(LanguageCode::English)
}

fn language(marked: &MarkedString) -> Option<&str> {
    match marked {
        MarkedString::String(_) => None,
        MarkedString::LanguageString(ls) => Some(&ls.language),
    }
}

fn sort_hovers(mut contents: Vec<MarkedString>) -> Vec<MarkedString> {
    let erg_count = contents
        .iter()
        .filter(|marked| language(marked) == Some("erg"))
        .count();
    // only location & type definition, no "erg" code block
    if erg_count == 2 {
        return contents;
    }
    // swap "erg" code block (not type definition) and the last element
    let erg_idx = contents
        .iter()
        .rposition(|marked| language(marked) == Some("erg"));
    if let Some(erg_idx) = erg_idx {
        let last = contents.len().saturating_sub(1);
        if erg_idx != last {
            contents.swap(erg_idx, last);
        }
    }
    contents
}

macro_rules! next {
    ($def_pos: ident, $default_code_block: ident, $contents: ident, $prev_token: ident, $token: ident) => {
        if $def_pos.line == 0 {
            if !$default_code_block.is_empty() {
                let code_block = trim_eliminate_top_indent($default_code_block);
                $contents.push(MarkedString::from_markdown(code_block));
            }
            break;
        }
        $prev_token = $token;
        $def_pos.line -= 1;
        continue;
    };
    ($def_pos: ident, $default_code_block: ident, $contents: ident) => {
        if $def_pos.line == 0 {
            if !$default_code_block.is_empty() {
                let code_block = trim_eliminate_top_indent($default_code_block);
                $contents.push(MarkedString::from_markdown(code_block));
            }
            break;
        }
        $def_pos.line -= 1;
        continue;
    };
}

impl<Checker: BuildRunnable, Parser: Parsable> Server<Checker, Parser> {
    pub(crate) fn handle_hover(&mut self, params: HoverParams) -> ELSResult<Option<Hover>> {
        self.send_log(format!("hover requested : {params:?}"))?;
        let uri = NormalizedUrl::new(params.text_document_position_params.text_document.uri);
        let pos = params.text_document_position_params.position;
        let mut contents = vec![];
        let opt_tok = self.file_cache.get_token(&uri, pos);
        let opt_token = if let Some(token) = opt_tok {
            match token.category() {
                TokenCategory::StrInterpRight => {
                    self.file_cache.get_token_relatively(&uri, pos, -1)
                }
                TokenCategory::StrInterpLeft => self.file_cache.get_token_relatively(&uri, pos, 1),
                // TODO: StrInterpMid
                _ => Some(token),
            }
        } else {
            None
        };
        if let Some(token) = opt_token {
            match self.get_definition(&uri, &token)? {
                Some(vi) => {
                    if let Some(line) = vi.def_loc.loc.ln_begin() {
                        let line0 = line.saturating_sub(1);
                        let Some(file_path) = vi.def_loc.module.as_ref() else {
                            return Ok(None);
                        };
                        let mut code_block = if cfg!(not(windows)) {
                            let relative = file_path
                                .strip_prefix(&self.home)
                                .unwrap_or(file_path.as_path());
                            let relative =
                                relative.strip_prefix(&self.erg_path).unwrap_or(relative);
                            format!("# {}, line {line}\n", relative.display())
                        } else {
                            // windows' file paths are case-insensitive, so we need to normalize them
                            let lower = file_path.as_os_str().to_ascii_lowercase();
                            let verbatim_removed =
                                lower.to_str().unwrap_or_default().replace("\\\\?\\", "");
                            let relative = verbatim_removed
                                .strip_prefix(
                                    self.home
                                        .as_os_str()
                                        .to_ascii_lowercase()
                                        .to_str()
                                        .unwrap_or_default(),
                                )
                                .unwrap_or_else(|| file_path.as_path().to_str().unwrap_or_default())
                                .trim_start_matches(['\\', '/']);
                            let relative = relative
                                .strip_prefix(self.erg_path.to_str().unwrap_or_default())
                                .unwrap_or(relative);
                            format!("# {relative}, line {line}\n")
                        };
                        // display the definition line
                        if vi.kind.is_defined() {
                            let uri = NormalizedUrl::try_from(file_path.as_path())?;
                            code_block += self
                                .file_cache
                                .get_line(&uri, line0)
                                .unwrap_or_default()
                                .trim_start();
                            match code_block.chars().last() {
                                Some('=' | '>') => {
                                    code_block += " ...";
                                }
                                Some('(') => {
                                    code_block += "...) = ...";
                                }
                                _ => {}
                            }
                        }
                        let definition =
                            MarkedString::from_language_code(PROG_LANG.into(), code_block);
                        contents.push(definition);
                    }
                    let typ = MarkedString::from_language_code(
                        ERG_LANG.into(),
                        format!("{}: {}", token.content, vi.t),
                    );
                    contents.push(typ);
                    self.show_type_defs(&vi, &mut contents)?;
                    self.show_doc_comment(Some(token), &mut contents, &vi.def_loc)?;
                }
                // not found or not symbol, etc.
                None => {
                    if let Some(visitor) = self.get_visitor(&uri) {
                        if let Some(typ) = visitor.get_min_expr(pos) {
                            let typ = MarkedString::from_language_code(
                                ERG_LANG.into(),
                                format!("{}: {typ}", token.content),
                            );
                            contents.push(typ);
                        }
                    }
                }
            }
            if let Some(visitor) = self.get_visitor(&uri) {
                let url = match visitor.get_min_expr(pos) {
                    Some(Expr::Def(def)) => def
                        .sig
                        .ref_t()
                        .module_path()
                        .and_then(|path| Url::from_file_path(path).ok()),
                    Some(Expr::Call(call)) => {
                        call.call_signature_t().return_t().and_then(|ret_t| {
                            ret_t
                                .module_path()
                                .and_then(|path| Url::from_file_path(path).ok())
                        })
                    }
                    Some(expr) => expr
                        .ref_t()
                        .module_path()
                        .or_else(|| {
                            expr.ref_t()
                                .inner_ts()
                                .into_iter()
                                .find_map(|t| t.module_path())
                        })
                        .and_then(|path| Url::from_file_path(path).ok()),
                    _ => None,
                };
                if let Some(url) = url {
                    let path = url.to_file_path().unwrap().with_extension("");
                    let name = if path.ends_with("__init__") || path.ends_with("__init__.d") {
                        path.parent()
                            .unwrap()
                            .file_stem()
                            .unwrap()
                            .to_string_lossy()
                    } else {
                        path.file_stem().unwrap().to_string_lossy()
                    };
                    contents.push(MarkedString::from_markdown(format!(
                        "Go to [{name}]({url})",
                    )));
                }
            }
        } else {
            self.send_log("lex error")?;
        }
        Ok(Some(Hover {
            contents: HoverContents::Array(sort_hovers(contents)),
            range: None,
        }))
    }

    fn show_type_defs(&mut self, vi: &VarInfo, contents: &mut Vec<MarkedString>) -> ELSResult<()> {
        let mut defs = "".to_string();
        for inner_t in vi.t.contained_ts() {
            if let Some(path) = &vi.def_loc.module {
                let Ok(def_uri) = util::NormalizedUrl::try_from(path.as_path()) else {
                    continue;
                };
                let Some(module) = ({
                    // self.quick_check_file(def_uri.clone())?;
                    self.get_mod_ctx(&def_uri)
                }) else {
                    continue;
                };
                if let Some((_, vi)) = module.context.get_type_info(&inner_t) {
                    if let Some(url) = vi
                        .def_loc
                        .module
                        .as_ref()
                        .and_then(|path| Url::from_file_path(path).ok())
                    {
                        defs += &format!(
                            "[{}]({url}#L{}) ",
                            inner_t.local_name(),
                            vi.def_loc.loc.ln_begin().unwrap_or(1)
                        );
                    }
                }
            }
        }
        if !defs.is_empty() {
            contents.push(MarkedString::from_markdown(format!("Go to {defs}")));
        }
        Ok(())
    }

    pub(crate) fn show_doc_comment(
        &self,
        var_token: Option<Token>,
        contents: &mut Vec<MarkedString>,
        def_loc: &AbsLocation,
    ) -> ELSResult<()> {
        if let Some(module) = def_loc.module.as_ref() {
            let mut def_pos = match util::loc_to_range(def_loc.loc) {
                Some(range) => range.end,
                None => {
                    return Ok(());
                }
            };
            def_pos.line = def_pos.line.saturating_sub(1);
            let Ok(def_uri) = NormalizedUrl::try_from(module.as_path()) else {
                return Ok(());
            };
            let mut default_code_block = "".to_string();
            let Some(stream) = self.file_cache.get_token_stream(&def_uri) else {
                return Ok(());
            };
            let mut prev_token = Token::DUMMY;
            loop {
                let Some(token) = util::get_token_from_stream(&stream, def_pos)? else {
                    next!(def_pos, default_code_block, contents);
                };
                if token.deep_eq(&prev_token) {
                    next!(def_pos, default_code_block, contents, prev_token, token);
                }
                if token.is(TokenKind::DocComment) {
                    let code_block = token
                        .content
                        .trim_start_matches("'''")
                        .trim_end_matches("'''")
                        .to_string();
                    let lang = lang_code(&code_block);
                    if lang.matches_feature() {
                        let code_block = trim_eliminate_top_indent(
                            code_block.trim_start_matches(lang.as_str()).to_string(),
                        );
                        let marked = match lang {
                            LanguageCode::Erg => {
                                MarkedString::from_language_code("erg".into(), code_block)
                            }
                            LanguageCode::Python | LanguageCode::ErgOrPython => {
                                MarkedString::from_language_code("python".into(), code_block)
                            }
                            _ => MarkedString::from_markdown(code_block),
                        };
                        contents.push(marked);
                        if lang.is_pl() {
                            next!(def_pos, default_code_block, contents, prev_token, token);
                        } else {
                            break;
                        }
                    } else {
                        if lang.is_en() {
                            default_code_block = code_block;
                        }
                        next!(def_pos, default_code_block, contents, prev_token, token);
                    }
                } else if var_token.as_ref() == Some(&token) {
                    // multiple pattern def
                    next!(def_pos, default_code_block, contents, prev_token, token);
                } else {
                    if token.category_is(TokenCategory::Separator) {
                        next!(def_pos, default_code_block, contents, prev_token, token);
                    }
                    if !default_code_block.is_empty() {
                        let code_block = trim_eliminate_top_indent(default_code_block);
                        contents.push(MarkedString::from_markdown(code_block));
                    }
                    break;
                }
            }
        }
        Ok(())
    }
}