df_ls_syntax_analysis 0.3.0-rc.1

A language server for Dwarf Fortress RAW files
Documentation
#![forbid(unsafe_code)]
#![deny(clippy::all)]
// TODO: Remove this at later point can create custom errors.
#![allow(clippy::result_unit_err)]

mod token_deserializers;
mod utils;

#[cfg(debug_assertions)]
pub mod test_utils;

use colored::*;
pub use df_ls_derive::TokenDeserialize;
use df_ls_diagnostics::lsp_types::{Diagnostic, DiagnosticSeverity};
use df_ls_diagnostics::{hash_map, DMExtraInfo, DiagnosticMessageSet, DiagnosticsInfo};
pub use df_ls_lexical_analysis::{Node, Tree, TreeCursor};
use std::collections::HashMap;
pub use token_deserializers::{
    Argument, LoopControl, Token, TokenArgument, TokenDeserialize, TokenDeserializeBasics,
    TryFromArgument, TryFromArgumentGroup,
};
pub use utils::mark_rest_of_token_as_unchecked;
pub(crate) use utils::*;

pub fn do_syntax_analysis<T: TokenDeserialize>(tree: &Tree, source: &str) -> (T, DiagnosticsInfo) {
    do_syntax_analysis_direct(tree, source, true)
}

pub fn do_syntax_analysis_direct<T: TokenDeserialize>(
    tree: &Tree,
    source: &str,
    load_diagnostic_messages: bool,
) -> (T, DiagnosticsInfo) {
    let mut tree_cursor = tree.walk();
    tree_cursor.goto_first_child();
    let mut diagnostic_info = if load_diagnostic_messages {
        DiagnosticsInfo::load_from_file(
            DiagnosticMessageSet::Syntax,
            Some("DF RAW Language Server".to_owned()),
        )
    } else {
        DiagnosticsInfo::new(HashMap::new(), Some("DF RAW Language Server".to_owned()))
    };
    let structure = match TokenDeserialize::deserialize_tokens(
        &mut tree_cursor,
        source,
        &mut diagnostic_info,
    ) {
        Ok(value) => {
            // Check if EOF is reached
            let new_node = tree_cursor.node();
            if new_node.next_sibling().is_none() {
                // EOF was reached
                tree_cursor.goto_parent();
            } else {
                // EOF was Not reached, there is still more to parse.
                // So parsing stopped on Error or unknown token
                // mark token as not expected too
                // TODO others errors might be possible here.
                if let Ok(token) =
                    Token::deserialize_tokens(&mut tree_cursor, source, &mut diagnostic_info)
                {
                    // TODO check how the token name can not be set when this code is reached.
                    let token_name = match token.get_token_name() {
                        Ok(token_name) => token_name.value.to_owned(),
                        Err(_) => "TokenName is missing".to_owned(),
                    };
                    diagnostic_info.add_message(
                        DMExtraInfo {
                            range: new_node.get_range(),
                            message_template_data: hash_map! {
                                "token_name" => format!("`{}`", token_name),
                            },
                        },
                        "unknown_token",
                    );
                }
                mark_rest_of_file_as_unchecked(&mut tree_cursor, &mut diagnostic_info, &new_node);
            }
            *value
        }
        Err(_) => {
            let new_node = tree_cursor.node();
            // mark token as not expected too
            diagnostic_info
                .add_message(DMExtraInfo::new(new_node.get_range()), "token_not_expected");
            mark_rest_of_file_as_unchecked(&mut tree_cursor, &mut diagnostic_info, &new_node);
            T::default()
        }
    };
    (structure, diagnostic_info)
}

pub fn print_source_with_diagnostics(source: &str, diagnostics: &[Diagnostic]) {
    println!("--------Output---------");
    for (line_nr, line) in source.split('\n').enumerate() {
        let mut new_line = "".to_owned();
        for (ch_nr, ch) in line.chars().enumerate() {
            match get_pos_severity(line_nr as u32, ch_nr as u32, diagnostics) {
                Some(severity) => {
                    let ch_str = ch.to_string();
                    #[allow(clippy::unnecessary_to_owned)]
                    match severity {
                        DiagnosticSeverity::INFORMATION => {
                            new_line.push_str(&ch_str.bright_blue().to_string())
                        }
                        DiagnosticSeverity::HINT => {
                            new_line.push_str(&ch_str.bright_blue().to_string())
                        }
                        DiagnosticSeverity::WARNING => {
                            new_line.push_str(&ch_str.bright_yellow().to_string())
                        }
                        DiagnosticSeverity::ERROR => {
                            new_line.push_str(&ch_str.bright_red().to_string())
                        }
                        _ => {
                            unreachable!("This severity does not exist currently");
                        }
                    }
                }
                None => new_line.push(ch),
            }
        }
        println!("{}", new_line);
    }
    println!("----------------------");
}

fn get_pos_severity(
    line: u32,
    character: u32,
    diagnostics: &[Diagnostic],
) -> Option<DiagnosticSeverity> {
    let mut severity = None;
    for item in diagnostics {
        // check start
        if line >= item.range.start.line && line <= item.range.end.line {
            // Middle of multiple lines
            if line > item.range.start.line && line < item.range.end.line {
                // get severity
                if severity < item.severity || severity.is_none() {
                    severity = item.severity;
                }
            }
            // First line of multiple
            else if line < item.range.end.line {
                if character >= item.range.start.character {
                    // get severity
                    if severity < item.severity || severity.is_none() {
                        severity = item.severity;
                    }
                }
            }
            // Last line of multiple
            else if line > item.range.start.line {
                if character < item.range.end.character {
                    // get severity
                    if severity < item.severity || severity.is_none() {
                        severity = item.severity;
                    }
                }
            }
            // Only one line
            else if character >= item.range.start.character
                && character < item.range.end.character
            {
                // get severity
                if severity < item.severity || severity.is_none() {
                    severity = item.severity;
                }
            }
        }
    }
    severity
}