asm_lsp/
lsp.rs

1use std::{
2    borrow::ToOwned,
3    cmp::Ordering,
4    collections::{HashMap, HashSet},
5    convert::TryFrom as _,
6    fmt::Write as _,
7    fs::{File, create_dir_all},
8    io::BufRead,
9    path::{Path, PathBuf},
10    process::Command,
11    str::FromStr as _,
12    string::ToString as _,
13    sync::LazyLock,
14};
15
16use anyhow::{Result, anyhow};
17use compile_commands::{CompilationDatabase, CompileArgs, CompileCommand, SourceFile};
18use dirs::config_dir;
19use log::{error, info, log, log_enabled, warn};
20use lsp_server::{Connection, Message, RequestId, Response};
21use lsp_textdocument::FullTextDocument;
22use lsp_types::notification::Notification as _;
23use lsp_types::{
24    CompletionItem, CompletionItemKind, CompletionList, CompletionParams, CompletionTriggerKind,
25    Diagnostic, DocumentSymbol, DocumentSymbolParams, Documentation, GotoDefinitionParams,
26    GotoDefinitionResponse, Hover, HoverContents, HoverParams, InitializeParams, Location,
27    MarkupContent, MarkupKind, MessageType, Position, Range, ReferenceParams, SignatureHelp,
28    SignatureHelpParams, SignatureInformation, SymbolKind, TextDocumentContentChangeEvent,
29    TextDocumentPositionParams, Uri,
30};
31use regex::Regex;
32use symbolic::common::{Language, Name, NameMangling};
33use symbolic_demangle::{Demangle, DemangleOptions};
34use tree_sitter::InputEdit;
35
36use crate::{
37    Arch, ArchOrAssembler, Assembler, Completable, CompletionItems, Config, ConfigOptions,
38    Directive, DocumentStore, Hoverable, Instruction, NameToInstructionMap, RootConfig,
39    ServerStore, TreeEntry, types::Column, ustr,
40};
41
42/// Prints information about the server
43///
44/// - Server version
45/// - Attempts to find the global configuration directories on the host machine,
46///   and indicates whether a `.asm-lsp.toml` config file is present
47pub fn run_info() {
48    println!("asm-lsp-{}\n", env!("CARGO_PKG_VERSION"));
49    let mut global_cfgs: Vec<PathBuf> = get_global_cfg_dirs()
50        .iter()
51        .filter_map(|p| (*p).clone())
52        .collect();
53    println!("Default config architecture: {}", Arch::default());
54    println!(
55        "Global config director{}:",
56        if global_cfgs.len() > 1 { "ies" } else { "y" }
57    );
58    for cfg_path in &mut global_cfgs {
59        cfg_path.push("asm-lsp");
60        print!("\t{}", cfg_path.display());
61        cfg_path.push(".asm-lsp.toml");
62        if cfg_path.exists() {
63            println!(" -- Config detected");
64        } else {
65            println!(" -- No config detected");
66        }
67    }
68}
69
70/// Sends an  response indicating no information was available to
71/// the lsp client via `connection`
72///
73/// # Errors
74///
75/// Returns `Err` if the response fails to send via `connection`
76pub fn send_empty_resp(connection: &Connection, id: RequestId) -> Result<()> {
77    let empty_resp = Response {
78        id,
79        result: None,
80        error: Some(lsp_server::ResponseError {
81            code: lsp_server::ErrorCode::RequestFailed as i32,
82            message: "No information available".to_string(),
83            data: None,
84        }),
85    };
86
87    Ok(connection.sender.send(Message::Response(empty_resp))?)
88}
89
90/// Sends a notification with to the client
91/// Param `message` is the owned type `String` to help reduce redundant allocations
92///
93/// # Errors
94///
95/// Returns `Err` if the response fails to send via `connection`
96///
97/// # Panics
98///
99/// Panics if JSON encoding of the notification fails
100pub fn send_notification(message: String, typ: MessageType, connection: &Connection) -> Result<()> {
101    let msg_params = lsp_types::ShowMessageParams { typ, message };
102    let result = serde_json::to_value(msg_params).unwrap();
103    let err_notif = lsp_server::Notification {
104        method: lsp_types::notification::ShowMessage::METHOD.to_string(),
105        params: result,
106    };
107    Ok(connection.sender.send(Message::Notification(err_notif))?)
108}
109
110/// Find the ([start], [end]) indices and the cursor's offset in a word
111/// on the given line
112///
113/// Borrowed from RLS
114/// characters besides the default alphanumeric and '_'
115#[must_use]
116pub fn find_word_at_pos(line: &str, col: Column) -> ((Column, Column), usize) {
117    let line_ = format!("{line} ");
118    // TODO: Let's just pass in a config, this could get messy
119    // NOTE: '*' is added as an allowed character to account for the the program
120    // counter pseudo variable of the Ca65 assembler. It's included unconditionally
121    // here for simplicity, but if this proves to be an issue we can pass in a `config`
122    // and only use it if the Ca65 assembler is enabled
123    //
124    // NOTE: In a similar manner to above, '$' is added as an allowed character to account
125    // for mips registers
126    let is_ident_char =
127        |c: char| c.is_alphanumeric() || c == '_' || c == '.' || c == '*' || c == '$';
128
129    let start = line_
130        .chars()
131        .enumerate()
132        .take(col)
133        .filter(|&(_, c)| !is_ident_char(c))
134        .last()
135        .map_or(0, |(i, _)| i + 1);
136
137    let mut end = line_
138        .chars()
139        .enumerate()
140        .skip(col)
141        .filter(|&(_, c)| !is_ident_char(c));
142
143    let end = end.next();
144    ((start, end.map_or(col, |(i, _)| i)), col - start)
145}
146
147pub enum UriConversion {
148    Canonicalized(PathBuf),
149    Unchecked(PathBuf),
150}
151
152/// Sanitizes the URI path sent by an LSP client
153///
154/// - "%3A" is replaced by ':' on windows, as this is likely escaped
155///   by the emacs client on windows/mingw/msys2
156/// - Returning `UriConversion::Canonicalized` indicates a path was able to be
157///   canonicalized. This indicates the path is valid and said file exists on disk
158/// - Returning `UriConversion::Unchecked` indicates that the path couldn't be
159///   canonicalized
160///
161/// # Panics
162///
163/// Will panic if `uri` cannot be interpreted as valid utf-8 after being percent-decoded
164#[must_use]
165pub fn process_uri(uri: &Uri) -> UriConversion {
166    let mut clean_path: String =
167        url_escape::percent_encoding::percent_decode_str(uri.path().as_str())
168            .decode_utf8()
169            .unwrap_or_else(|e| {
170                panic!(
171                    "Invalid encoding for uri \"{}\" -- {e}",
172                    uri.path().as_str()
173                )
174            })
175            .to_string();
176
177    // HACK: On Windows, sometimes a leading '/',  e.g. /C:/Users/foo/bar/...
178    // is passed as part of the path -- Stuff like Git bash and MSYS2 will accept
179    // /C/Users/foo/bar/..., but *not* if the colon is present. Vanila windows
180    // will not accept a leading slash at all, but requires the colon after the
181    // drive letter, like C:/Users/foo/... So we do our best to clean up here
182    if cfg!(windows) && clean_path.contains(':') {
183        clean_path = clean_path.strip_prefix('/').unwrap_or(&clean_path).into();
184    }
185
186    let Ok(path) = PathBuf::from_str(&clean_path);
187    path.canonicalize()
188        .map_or(UriConversion::Unchecked(path), |canonicalized| {
189            // HACK: On Windows, when a path is canonicalized, sometimes it gets prefixed
190            // with "\\?\" -- https://stackoverflow.com/questions/41233684/why-does-my-canonicalized-path-get-prefixed-with
191            // That's great and all, but it looks like common tools (like gcc) don't handle
192            // this correctly, and you get something like the following:
193            // Error: can't open //test.s for reading: No such file or directory
194            // The solution? Just cut out the prefix and hope that doesn't break anything else
195            if cfg!(windows) {
196                #[allow(clippy::option_if_let_else)]
197                if let Some(tmp) = canonicalized.to_str().unwrap().strip_prefix("\\\\?\\") {
198                    warn!("Stripping Windows canonicalization prefix \"\\\\?\\\" from path");
199                    UriConversion::Canonicalized(tmp.into())
200                } else {
201                    UriConversion::Canonicalized(canonicalized)
202                }
203            } else {
204                UriConversion::Canonicalized(canonicalized)
205            }
206        })
207}
208
209/// Returns the word undernearth the cursor given the specified `TextDocumentPositionParams`
210///
211/// # Errors
212///
213/// Will return `Err` if the file cannot be opened
214///
215/// # Panics
216///
217/// Will panic if the position parameters specify a line past the end of the file's
218/// contents
219pub fn get_word_from_file_params(pos_params: &TextDocumentPositionParams) -> Result<String> {
220    let uri = &pos_params.text_document.uri;
221    let line = pos_params.position.line as usize;
222    let col = pos_params.position.character as usize;
223
224    let filepath = PathBuf::from(uri.as_str());
225    match filepath.canonicalize() {
226        Ok(file) => {
227            let file = match File::open(file) {
228                Ok(opened) => opened,
229                Err(e) => return Err(anyhow!("Couldn't open file -> {:?} -- Error: {e}", uri)),
230            };
231            let buf_reader = std::io::BufReader::new(file);
232
233            let line_conts = buf_reader.lines().nth(line).unwrap().unwrap();
234            let ((start, end), _) = find_word_at_pos(&line_conts, col);
235            Ok(String::from(&line_conts[start..end]))
236        }
237        Err(e) => Err(anyhow!("Filepath get error -- Error: {e}")),
238    }
239}
240
241/// Returns a string slice to the word in doc specified by the position params,
242/// and the cursor's offset into the word
243#[must_use]
244pub fn get_word_from_pos_params<'a>(
245    doc: &'a FullTextDocument,
246    pos_params: &TextDocumentPositionParams,
247) -> (&'a str, usize) {
248    let line_contents = doc.get_content(Some(Range {
249        start: Position {
250            line: pos_params.position.line,
251            character: 0,
252        },
253        end: Position {
254            line: pos_params.position.line,
255            character: u32::MAX,
256        },
257    }));
258
259    let ((word_start, word_end), cursor_offset) =
260        find_word_at_pos(line_contents, pos_params.position.character as usize);
261    (&line_contents[word_start..word_end], cursor_offset)
262}
263
264/// Fetches default include directories, as well as any additional directories
265/// as specified by a `compile_commands.json` or `compile_flags.txt` file in the
266/// appropriate location
267///
268/// # Panics
269#[must_use]
270pub fn get_include_dirs(compile_cmds: &CompilationDatabase) -> HashMap<SourceFile, Vec<PathBuf>> {
271    let mut include_map = HashMap::from([(SourceFile::All, Vec::new())]);
272
273    let global_dirs = include_map.get_mut(&SourceFile::All).unwrap();
274    for dir in get_default_include_dirs() {
275        global_dirs.push(dir);
276    }
277
278    for (source_file, ref dir) in get_additional_include_dirs(compile_cmds) {
279        include_map
280            .entry(source_file)
281            .and_modify(|dirs| dirs.push(dir.to_owned()))
282            .or_insert_with(|| vec![dir.to_owned()]);
283    }
284
285    info!("Include directory map: {:?}", include_map);
286
287    include_map
288}
289
290/// Returns a vector of default #include directories
291#[must_use]
292fn get_default_include_dirs() -> Vec<PathBuf> {
293    let mut include_dirs = HashSet::new();
294    // repeat "cpp" and "clang" so that each command can be run with
295    // both set of args specified in `cmd_args`
296    let cmds = &["cpp", "cpp", "clang", "clang"];
297    let cmd_args = &[
298        ["-v", "-E", "-x", "c", "/dev/null", "-o", "/dev/null"],
299        ["-v", "-E", "-x", "c++", "/dev/null", "-o", "/dev/null"],
300    ];
301
302    for (cmd, args) in cmds.iter().zip(cmd_args.iter().cycle()) {
303        if let Ok(cmd_output) = std::process::Command::new(cmd)
304            .args(args)
305            .stderr(std::process::Stdio::piped())
306            .output()
307            && cmd_output.status.success()
308        {
309            let output_str: String = ustr::get_string(cmd_output.stderr);
310
311            output_str
312                .lines()
313                .skip_while(|line| !line.contains("#include \"...\" search starts here:"))
314                .skip(1)
315                .take_while(|line| {
316                    !(line.contains("End of search list.")
317                        || line.contains("#include <...> search starts here:"))
318                })
319                .filter_map(|line| PathBuf::from(line.trim()).canonicalize().ok())
320                .for_each(|path| {
321                    include_dirs.insert(path);
322                });
323
324            output_str
325                .lines()
326                .skip_while(|line| !line.contains("#include <...> search starts here:"))
327                .skip(1)
328                .take_while(|line| !line.contains("End of search list."))
329                .filter_map(|line| PathBuf::from(line.trim()).canonicalize().ok())
330                .for_each(|path| {
331                    include_dirs.insert(path);
332                });
333        }
334    }
335
336    include_dirs.iter().cloned().collect::<Vec<PathBuf>>()
337}
338
339/// Returns a vector of source files and their associated additional include directories,
340/// as specified by `compile_cmds`
341#[must_use]
342fn get_additional_include_dirs(compile_cmds: &CompilationDatabase) -> Vec<(SourceFile, PathBuf)> {
343    let mut additional_dirs = Vec::new();
344
345    for entry in compile_cmds {
346        let Ok(entry_dir) = entry.directory.canonicalize() else {
347            continue;
348        };
349
350        let source_file = match &entry.file {
351            SourceFile::All => SourceFile::All,
352            SourceFile::File(file) => {
353                if file.is_absolute() {
354                    entry.file.clone()
355                } else if let Ok(dir) = entry_dir.join(file).canonicalize() {
356                    SourceFile::File(dir)
357                } else {
358                    continue;
359                }
360            }
361        };
362
363        let mut check_dir = false;
364        if let Some(args) = &entry.arguments {
365            // `arguments` run as the compilation step for the translation unit `file`
366            // We will try to canonicalize non-absolute paths as relative to `file`,
367            // but this isn't possible if we have a SourceFile::All. Just don't
368            // add the include directory and issue a warning in this case
369            match args {
370                CompileArgs::Flags(args) | CompileArgs::Arguments(args) => {
371                    for arg in args.iter().map(|arg| arg.trim()) {
372                        if check_dir {
373                            // current arg is preceeded by lone '-I'
374                            let dir = PathBuf::from(arg);
375                            if dir.is_absolute() {
376                                additional_dirs.push((source_file.clone(), dir));
377                            } else if let SourceFile::File(ref source_path) = source_file {
378                                if let Ok(full_include_path) = source_path.join(dir).canonicalize()
379                                {
380                                    additional_dirs.push((source_file.clone(), full_include_path));
381                                }
382                            } else {
383                                warn!(
384                                    "Additional relative include directories cannot be extracted for a compilation database entry targeting 'All'"
385                                );
386                            }
387                            check_dir = false;
388                        } else if arg.eq("-I") {
389                            // -Irelative is stored as two separate args if parsed from `compile_flags.txt`
390                            check_dir = true;
391                        } else if arg.len() > 2 && arg.starts_with("-I") {
392                            // '-Irelative'
393                            let dir = PathBuf::from(&arg[2..]);
394                            if dir.is_absolute() {
395                                additional_dirs.push((source_file.clone(), dir));
396                            } else if let SourceFile::File(ref source_path) = source_file {
397                                if let Ok(full_include_path) = source_path.join(dir).canonicalize()
398                                {
399                                    additional_dirs.push((source_file.clone(), full_include_path));
400                                }
401                            } else {
402                                warn!(
403                                    "Additional relative include directories cannot be extracted for a compilation database entry targeting 'All'"
404                                );
405                            }
406                        }
407                    }
408                }
409            }
410        } else if entry.command.is_some()
411            && let Some(args) = entry.args_from_cmd()
412        {
413            for arg in args {
414                if arg.starts_with("-I") && arg.len() > 2 {
415                    // "All paths specified in the `command` or `file` fields must be either absolute or relative to..." the `directory` field
416                    let incl_path = PathBuf::from(&arg[2..]);
417                    if incl_path.is_absolute() {
418                        additional_dirs.push((source_file.clone(), incl_path));
419                    } else {
420                        let dir = entry_dir.join(incl_path);
421                        if let Ok(full_include_path) = dir.canonicalize() {
422                            additional_dirs.push((source_file.clone(), full_include_path));
423                        }
424                    }
425                }
426            }
427        }
428    }
429
430    additional_dirs
431}
432
433/// Attempts to find either the `compile_commands.json` or `compile_flags.txt`
434/// file in the project's root or build directories, returning either file as a
435/// `CompilationDatabase` object
436///
437/// If both are present, `compile_commands.json` will override `compile_flags.txt`
438pub fn get_compile_cmds_from_file(params: &InitializeParams) -> Option<CompilationDatabase> {
439    if let Some(mut path) = get_project_root(params) {
440        // Check the project root directory first
441        let db = get_compilation_db_files(&path);
442        if db.is_some() {
443            return db;
444        }
445
446        // "The convention is to name the file compile_commands.json and put it at the top of the
447        // build directory."
448        path.push("build");
449        let db = get_compilation_db_files(&path);
450        if db.is_some() {
451            return db;
452        }
453    }
454
455    None
456}
457
458fn get_compilation_db_files(path: &Path) -> Option<CompilationDatabase> {
459    // first check for compile_commands.json
460    let cmp_cmd_path = path.join("compile_commands.json");
461    if let Ok(conts) = std::fs::read_to_string(cmp_cmd_path)
462        && let Ok(cmds) = serde_json::from_str(&conts)
463    {
464        return Some(cmds);
465    }
466    // then check for compile_flags.txt
467    let cmp_flag_path = path.join("compile_flags.txt");
468    if let Ok(conts) = std::fs::read_to_string(cmp_flag_path) {
469        return Some(compile_commands::from_compile_flags_txt(path, &conts));
470    }
471
472    None
473}
474
475/// Returns the compile command associated with `uri` if it exists, or the default
476/// one from `compile_cmds` otherwise
477///
478/// - If the user specified a `compiler` *and* flags in their config, use that
479/// - If the user specified a `compiler` but no flags in their config (`None`,
480///   *not* an empty `Vec`), try to find flags from `compile_flags.txt` in
481///   `compile_cmds` and combine the two
482/// - If the user didn't specify any compiler info in the relevant config, return
483///   the default commands from `compile_cmds`
484///
485/// # Panics
486///
487/// Will panic if `req_uri` can't be canonicalized
488///
489/// NOTE: Several fields within the returned `CompilationDatabase` are intentionally left
490/// uninitialized to avoid unnecessary allocations. If you're using this function
491/// in a new place, please reconsider this assumption
492pub fn get_compile_cmd_for_req(
493    config: &RootConfig,
494    req_uri: &Uri,
495    compile_cmds: &CompilationDatabase,
496) -> CompilationDatabase {
497    let request_path = match process_uri(req_uri) {
498        UriConversion::Canonicalized(p) => p,
499        UriConversion::Unchecked(p) => {
500            error!(
501                "Failed to canonicalize request path {}, using {}",
502                req_uri.path().as_str(),
503                p.display()
504            );
505            p
506        }
507    };
508    let config = config.get_config(req_uri);
509    match (config.get_compiler(), config.get_compile_flags_txt()) {
510        (Some(compiler), Some(flags)) => {
511            // Fill out the full command invocation
512            let mut args = vec![compiler.to_owned()];
513            args.append(&mut flags.clone());
514            args.push(request_path.to_str().unwrap_or_default().to_string());
515            vec![CompileCommand {
516                file: SourceFile::File(request_path),
517                directory: PathBuf::new(),
518                arguments: Some(CompileArgs::Arguments(args)),
519                command: None,
520                output: None,
521            }]
522        }
523        (Some(compiler), None) => {
524            // Fill out the full command invocation, check if `compile_cmds`
525            // has flags to tack on
526            let mut args = vec![compiler.to_owned()];
527            // Check if we have flags as the first compile command from files,
528            // `compile_flags.txt` files get loaded as a single `CompileCommand`
529            // object as structured in the below `if` block
530            if compile_cmds.len() == 1
531                && let CompileCommand {
532                    arguments: Some(CompileArgs::Flags(flags)),
533                    ..
534                } = &compile_cmds[0]
535            {
536                args.append(&mut flags.clone());
537            }
538            args.push(request_path.to_str().unwrap_or_default().to_string());
539            vec![CompileCommand {
540                file: SourceFile::File(request_path),
541                directory: PathBuf::new(),
542                arguments: Some(CompileArgs::Arguments(args)),
543                command: None,
544                output: None,
545            }]
546        }
547        (None, Some(flags)) => {
548            // Fill out flags, no compiler
549            vec![CompileCommand {
550                file: SourceFile::File(request_path),
551                directory: PathBuf::new(),
552                arguments: Some(CompileArgs::Flags(flags.clone())),
553                command: None,
554                output: None,
555            }]
556        }
557        (None, None) => {
558            // Grab the default command from `compile_cmds`
559            compile_cmds.clone()
560        }
561    }
562}
563
564/// Returns a default `CompileCommand` for the provided `uri`.
565///
566/// - If the user specified a compiler in their config, it will be used.
567/// - Otherwise, the command will be constructed with a single flag consisting of
568///   the provided `uri`
569///
570/// NOTE: Several fields within the returned `CompileCommand` are intentionally left
571/// uninitialized to avoid unnecessary allocations. If you're using this function
572/// in a new place, please reconsider this assumption
573pub fn get_default_compile_cmd(uri: &Uri, cfg: &Config) -> CompileCommand {
574    cfg.get_compiler().as_ref().map_or_else(
575        || CompileCommand {
576            file: SourceFile::All, // Field isn't checked when called, intentionally left in odd state here
577            directory: PathBuf::new(), // Field isn't checked when called, intentionally left uninitialized here
578            arguments: Some(CompileArgs::Flags(vec![uri.path().to_string()])),
579            command: None,
580            output: None,
581        },
582        |compiler| CompileCommand {
583            file: SourceFile::All, // Field isn't checked when called, intentionally left in odd state here
584            directory: PathBuf::new(), // Field isn't checked when called, intentionally left uninitialized here
585            arguments: Some(CompileArgs::Arguments(vec![
586                (*compiler).to_string(),
587                uri.path().to_string(),
588            ])),
589            command: None,
590            output: None,
591        },
592    )
593}
594
595/// Attempts to run the given compile command and parses the resulting output. Any
596/// relevant output will be translated into a `Diagnostic` object and pushed into
597/// `diagnostics`
598pub fn apply_compile_cmd(
599    cfg: &Config,
600    diagnostics: &mut Vec<Diagnostic>,
601    uri: &Uri,
602    compile_cmd: &CompileCommand,
603) {
604    info!("Attempting to apply compile command: {compile_cmd:?}");
605    // TODO: Consolidate this logic, a little tricky because we need to capture
606    // compile_cmd.arguments by reference, but we get an owned Vec out of args_from_cmd()...
607    if let Some(ref args) = compile_cmd.arguments {
608        match args {
609            CompileArgs::Flags(flags) => {
610                let compilers = cfg
611                    .get_compiler()
612                    .as_ref()
613                    .map_or_else(|| vec!["gcc", "clang"], |compiler| vec![compiler]);
614
615                for compiler in compilers {
616                    match Command::new(compiler) // default or user-supplied compiler
617                        .args(flags) // user supplied args
618                        .arg(uri.path().as_str()) // the source file in question
619                        .output()
620                    {
621                        Ok(result) => {
622                            let output_str = ustr::get_string(result.stderr);
623                            get_diagnostics(diagnostics, &output_str, cfg);
624                        }
625                        Err(e) => {
626                            warn!(
627                                "Failed to launch compile command process with {compiler} -- Error: {e}"
628                            );
629                        }
630                    }
631                }
632            }
633            CompileArgs::Arguments(arguments) => {
634                if arguments.len() < 2 {
635                    return;
636                }
637                let output = match Command::new(&arguments[0]).args(&arguments[1..]).output() {
638                    Ok(result) => result,
639                    Err(e) => {
640                        error!("Failed to launch compile command process -- Error: {e}");
641                        return;
642                    }
643                };
644                let output_str = ustr::get_string(output.stderr);
645                get_diagnostics(diagnostics, &output_str, cfg);
646            }
647        }
648    } else if let Some(args) = compile_cmd.args_from_cmd() {
649        if args.len() < 2 {
650            return;
651        }
652        let output = match Command::new(&args[0]).args(&args[1..]).output() {
653            Ok(result) => result,
654            Err(e) => {
655                error!("Failed to launch compile command process -- Error: {e}");
656                return;
657            }
658        };
659        let output_str = ustr::get_string(output.stderr);
660        get_diagnostics(diagnostics, &output_str, cfg);
661    }
662}
663
664/// Attempts to parse `tool_output`, translating it into `Diagnostic` objects
665/// and placing them into `diagnostics`
666///
667/// Looks for diagnostics of the following form:
668///
669/// <file name>:<line number>: Error: <Error message>
670///
671/// As more assemblers are incorporated, this can be updated
672///
673/// # Panics
674fn get_diagnostics(diagnostics: &mut Vec<Diagnostic>, tool_output: &str, cfg: &Config) {
675    // Special handingling for FASM assembler diagnostics
676    if cfg.is_assembler_enabled(Assembler::Fasm) {
677        // https://flatassembler.net/docs.php?article=manual - 1.1.3 Compile messages
678        static FASM_SOURCE_LOC: LazyLock<Regex> =
679            LazyLock::new(|| Regex::new(r"^(.+) \[(\d+)\]:$").unwrap());
680        // TODO: Look into including macro defintion locations as related information
681        // static FASM_MACRO_INSTR_LOC: LazyLock<Regex> =
682        //     LazyLock::new(|| Regex::new(r"^(.+) \[(\d+)\]: .+ \[(\d+\)]:$").unwrap());
683        static FASM_ERR_MSG: LazyLock<Regex> =
684            LazyLock::new(|| Regex::new(r"^error: (.+)").unwrap());
685
686        let mut source_line: Option<u32> = None;
687        let mut source_start_col: Option<u32> = None;
688        let mut source_end_col: Option<u32> = None;
689        let mut lines = tool_output.lines();
690        while let Some(line) = lines.next() {
691            // for line in tool_output.lines() {
692            if let Some(caps) = FASM_SOURCE_LOC.captures(line) {
693                // the entire capture is always at the 0th index,
694                // then we have 2 more explicit capture groups
695                if caps.len() == 3 {
696                    let Ok(line_number) = caps[2].parse::<u32>() else {
697                        continue;
698                    };
699                    source_line = Some(line_number);
700                    if let Some(src) = lines.next() {
701                        let len = src.len() as u32;
702                        source_start_col = Some(len - src.trim_start().len() as u32);
703                        source_end_col = Some(len);
704                    }
705                }
706            } else if let Some(caps) = FASM_ERR_MSG.captures(line) {
707                if caps.len() != 2 {
708                    continue;
709                }
710                if let Some(line_number) = source_line {
711                    let err_msg = caps[1].to_string();
712                    let start_col = source_start_col.unwrap_or(0);
713                    let end_col = source_end_col.unwrap_or(0);
714                    diagnostics.push(Diagnostic::new_simple(
715                        Range {
716                            start: Position {
717                                line: line_number - 1,
718                                character: start_col,
719                            },
720                            end: Position {
721                                line: line_number - 1,
722                                character: end_col,
723                            },
724                        },
725                        err_msg,
726                    ));
727                }
728                source_line = None;
729            }
730        }
731    } else {
732        // TODO: Consolidate/ clean this up...regexes are hard
733        static DIAG_REG_LINE_COLUMN: LazyLock<Regex> =
734            LazyLock::new(|| Regex::new(r"^.*:(\d+):(\d+):\s+(.*)$").unwrap());
735        static DIAG_REG_LINE_ONLY: LazyLock<Regex> =
736            LazyLock::new(|| Regex::new(r"^.*:(\d+):\s+(.*)$").unwrap());
737        static ALT_DIAG_REG_LINE_ONLY: LazyLock<Regex> =
738            LazyLock::new(|| Regex::new(r"^.*\((\d+)\):\s+(.*)$").unwrap());
739        for line in tool_output.lines() {
740            // first check if we have an error message of the form:
741            // :<line>:<column>: <error message here>
742            if let Some(caps) = DIAG_REG_LINE_COLUMN.captures(line) {
743                // the entire capture is always at the 0th index,
744                // then we have 3 more explicit capture groups
745                if caps.len() == 4 {
746                    let Ok(line_number) = caps[1].parse::<u32>() else {
747                        continue;
748                    };
749                    let Ok(column_number) = caps[2].parse::<u32>() else {
750                        continue;
751                    };
752                    let err_msg = &caps[3];
753                    diagnostics.push(Diagnostic::new_simple(
754                        Range {
755                            start: Position {
756                                line: line_number - 1,
757                                character: column_number,
758                            },
759                            end: Position {
760                                line: line_number - 1,
761                                character: column_number,
762                            },
763                        },
764                        String::from(err_msg),
765                    ));
766                    continue;
767                }
768            }
769            // if the above check for lines *and* columns didn't match, see if we
770            // have an error message of the form:
771            // :<line>: <error message here>
772            if let Some(caps) = DIAG_REG_LINE_ONLY.captures(line) {
773                if caps.len() < 3 {
774                    // the entire capture is always at the 0th index,
775                    // then we have 2 more explicit capture groups
776                    continue;
777                }
778                let Ok(line_number) = caps[1].parse::<u32>() else {
779                    continue;
780                };
781                let err_msg = &caps[2];
782                diagnostics.push(Diagnostic::new_simple(
783                    Range {
784                        start: Position {
785                            line: line_number - 1,
786                            character: 0,
787                        },
788                        end: Position {
789                            line: line_number - 1,
790                            character: 0,
791                        },
792                    },
793                    String::from(err_msg),
794                ));
795            }
796
797            // ca65 has a slightly different format
798            // file(<line>): <error message here>
799            if let Some(caps) = ALT_DIAG_REG_LINE_ONLY.captures(line) {
800                if caps.len() < 3 {
801                    // the entire capture is always at the 0th index,
802                    // then we have 2 more explicit capture groups
803                    continue;
804                }
805                let Ok(line_number) = caps[1].parse::<u32>() else {
806                    continue;
807                };
808                let err_msg = &caps[2];
809                diagnostics.push(Diagnostic::new_simple(
810                    Range {
811                        start: Position {
812                            line: line_number - 1,
813                            character: 0,
814                        },
815                        end: Position {
816                            line: line_number - 1,
817                            character: 0,
818                        },
819                    },
820                    String::from(err_msg),
821                ));
822            }
823        }
824    }
825}
826
827/// Function allowing us to connect tree sitter's logging with the log crate
828#[allow(clippy::needless_pass_by_value)]
829pub fn tree_sitter_logger(log_type: tree_sitter::LogType, message: &str) {
830    // map tree-sitter log types to log levels, for now set everything to Trace
831    let log_level = match log_type {
832        tree_sitter::LogType::Parse | tree_sitter::LogType::Lex => log::Level::Trace,
833    };
834
835    // tree-sitter logs are incredibly verbose, only forward them to the logger
836    // if we *really* need to see what's going on
837    if log_enabled!(log_level) {
838        log!(log_level, "{}", message);
839    }
840}
841
842/// Convert an `lsp_types::TextDocumentContentChangeEvent` to a `tree_sitter::InputEdit`
843///
844/// # Errors
845///
846/// Returns `Err` if `change.range` is `None`, or if a `usize`->`u32` numeric conversion
847/// failed
848pub fn text_doc_change_to_ts_edit(
849    change: &TextDocumentContentChangeEvent,
850    doc: &FullTextDocument,
851) -> Result<InputEdit> {
852    let range = change.range.ok_or_else(|| anyhow!("Invalid edit range"))?;
853    let start = range.start;
854    let end = range.end;
855
856    let start_byte = doc.offset_at(start) as usize;
857    let new_end_byte = start_byte + change.text.len();
858    let new_end_pos = doc.position_at(u32::try_from(new_end_byte)?);
859
860    Ok(tree_sitter::InputEdit {
861        start_byte,
862        old_end_byte: doc.offset_at(end) as usize,
863        new_end_byte,
864        start_position: tree_sitter::Point {
865            row: start.line as usize,
866            column: start.character as usize,
867        },
868        old_end_position: tree_sitter::Point {
869            row: end.line as usize,
870            column: end.character as usize,
871        },
872        new_end_position: tree_sitter::Point {
873            row: new_end_pos.line as usize,
874            column: new_end_pos.character as usize,
875        },
876    })
877}
878
879/// Given a `NameTo_SomeItem_` map, returns a `Vec<CompletionItem>` for the items
880/// contained within the map
881#[must_use]
882pub fn get_completes<T: Completable, U: ArchOrAssembler>(
883    map: &HashMap<(U, String), T>,
884    kind: Option<CompletionItemKind>,
885) -> Vec<(U, CompletionItem)> {
886    map.iter()
887        .map(|((arch_or_asm, name), item_info)| {
888            let value = item_info.to_string();
889
890            (
891                *arch_or_asm,
892                CompletionItem {
893                    label: (*name).to_string(),
894                    kind,
895                    documentation: Some(Documentation::MarkupContent(MarkupContent {
896                        kind: MarkupKind::Markdown,
897                        value,
898                    })),
899                    ..Default::default()
900                },
901            )
902        })
903        .collect()
904}
905
906#[must_use]
907pub fn get_hover_resp(
908    params: &HoverParams,
909    config: &Config,
910    word: &str,
911    cursor_offset: usize,
912    doc_store: &mut DocumentStore,
913    store: &ServerStore,
914) -> Option<Hover> {
915    let instr_lookup = get_hover_resp_by_arch(word, &store.names_to_info.instructions, config);
916    if instr_lookup.is_some() {
917        return instr_lookup;
918    }
919
920    // directive lookup
921    {
922        if config.is_assembler_enabled(Assembler::Gas)
923            || config.is_assembler_enabled(Assembler::Masm)
924            || config.is_assembler_enabled(Assembler::Ca65)
925            || config.is_assembler_enabled(Assembler::Avr)
926            || config.is_assembler_enabled(Assembler::Fasm)
927            || config.is_assembler_enabled(Assembler::Mars)
928        {
929            // all gas, AVR, and Mars directives have a '.' prefix, some masm directives do
930            let directive_lookup =
931                get_directive_hover_resp(word, &store.names_to_info.directives, config);
932            if directive_lookup.is_some() {
933                return directive_lookup;
934            }
935        } else if config.is_assembler_enabled(Assembler::Nasm) {
936            // most nasm directives have no prefix, 2 have a '.' prefix
937            let directive_lookup =
938                get_directive_hover_resp(word, &store.names_to_info.directives, config);
939            if directive_lookup.is_some() {
940                return directive_lookup;
941            }
942            // Some nasm directives have a % prefix
943            let prefixed = format!("%{word}");
944            let directive_lookup =
945                get_directive_hover_resp(&prefixed, &store.names_to_info.directives, config);
946            if directive_lookup.is_some() {
947                return directive_lookup;
948            }
949        }
950    }
951
952    let reg_lookup = if config.is_isa_enabled(Arch::ARM64) {
953        word.find('.').map_or_else(
954            || get_hover_resp_by_arch(word, &store.names_to_info.registers, config),
955            |dot| {
956                if cursor_offset <= dot {
957                    // main vector register info on ARM64
958                    let main_register = &word[0..dot];
959                    get_hover_resp_by_arch(main_register, &store.names_to_info.registers, config)
960                } else {
961                    // if Vector = V21.2D -> lower Register = D21
962                    // lower vector register info on ARM64
963                    let reg_len = 3;
964                    let mut lower_register = String::with_capacity(reg_len);
965                    let reg_letter = dot + 2;
966                    lower_register.push_str(&word[reg_letter..]);
967                    let reg_num = 1..dot;
968                    lower_register.push_str(&word[reg_num]);
969                    get_hover_resp_by_arch(&lower_register, &store.names_to_info.registers, config)
970                }
971            },
972        )
973    } else {
974        get_hover_resp_by_arch(word, &store.names_to_info.registers, config)
975    };
976
977    if reg_lookup.is_some() {
978        return reg_lookup;
979    }
980
981    let label_data = get_label_resp(
982        word,
983        &params.text_document_position_params.text_document.uri,
984        doc_store,
985    );
986    if label_data.is_some() {
987        return label_data;
988    }
989
990    let demang = get_demangle_resp(word);
991    if demang.is_some() {
992        return demang;
993    }
994
995    let include_path = get_include_resp(
996        &params.text_document_position_params.text_document.uri,
997        word,
998        &store.include_dirs,
999    );
1000    if include_path.is_some() {
1001        return include_path;
1002    }
1003
1004    None
1005}
1006
1007fn search_for_hoverable_by_arch<'a, T: Hoverable>(
1008    word: &'a str,
1009    map: &'a HashMap<(Arch, String), T>,
1010    config: &Config,
1011) -> (Option<&'a T>, Option<&'a T>) {
1012    match config.instruction_set {
1013        Arch::X86_AND_X86_64 => {
1014            let x86_resp = map.get(&(Arch::X86, word.to_string()));
1015            let x86_64_resp = map.get(&(Arch::X86_64, word.to_string()));
1016            (x86_resp, x86_64_resp)
1017        }
1018        arch => (map.get(&(arch, word.to_string())), None),
1019    }
1020}
1021
1022fn search_for_dir_by_assembler<'a>(
1023    word: &'a str,
1024    dir_map: &'a HashMap<(Assembler, String), Directive>,
1025    config: &Config,
1026) -> Option<&'a Directive> {
1027    dir_map.get(&(config.assembler, word.to_string()))
1028}
1029
1030fn get_hover_resp_by_arch<T: Hoverable>(
1031    word: &str,
1032    map: &HashMap<(Arch, String), T>,
1033    config: &Config,
1034) -> Option<Hover> {
1035    // ensure hovered text is always lowercase
1036    let hovered_word = word.to_ascii_lowercase();
1037    let instr_resp = search_for_hoverable_by_arch(&hovered_word, map, config);
1038    let value = match instr_resp {
1039        (Some(resp1), Some(resp2)) => {
1040            format!("{resp1}\n\n{resp2}")
1041        }
1042        (Some(resp), None) | (None, Some(resp)) => {
1043            format!("{resp}")
1044        }
1045        (None, None) => return None,
1046    };
1047
1048    Some(Hover {
1049        contents: HoverContents::Markup(MarkupContent {
1050            kind: MarkupKind::Markdown,
1051            value,
1052        }),
1053        range: None,
1054    })
1055}
1056
1057fn get_directive_hover_resp(
1058    word: &str,
1059    dir_map: &HashMap<(Assembler, String), Directive>,
1060    config: &Config,
1061) -> Option<Hover> {
1062    let hovered_word = word.to_ascii_lowercase();
1063    search_for_dir_by_assembler(&hovered_word, dir_map, config).map(|dir_resp| Hover {
1064        contents: HoverContents::Markup(MarkupContent {
1065            kind: MarkupKind::Markdown,
1066            value: dir_resp.to_string(),
1067        }),
1068        range: None,
1069    })
1070}
1071
1072/// Returns the data associated with a given label `word`
1073fn get_label_resp(word: &str, uri: &Uri, doc_store: &mut DocumentStore) -> Option<Hover> {
1074    if let Some(doc) = doc_store.text_store.get_document(uri) {
1075        let curr_doc = doc.get_content(None).as_bytes();
1076        if let Some(ref mut tree_entry) = doc_store.tree_store.get_mut(uri) {
1077            tree_entry.tree = tree_entry.parser.parse(curr_doc, tree_entry.tree.as_ref());
1078            if let Some(ref tree) = tree_entry.tree {
1079                static QUERY_LABEL_DATA: LazyLock<tree_sitter::Query> = LazyLock::new(|| {
1080                    tree_sitter::Query::new(
1081                        &tree_sitter_asm::language(),
1082                        "(
1083                            (label (ident) @label)
1084                            .
1085                            (meta
1086	                            (
1087                                    [
1088                                        (int)
1089                                        (string)
1090                                        (float)
1091                                    ]
1092                                )
1093                            ) @data
1094                        )",
1095                    )
1096                    .unwrap()
1097                });
1098                let mut cursor = tree_sitter::QueryCursor::new();
1099                let matches_iter = cursor.matches(&QUERY_LABEL_DATA, tree.root_node(), curr_doc);
1100
1101                for match_ in matches_iter {
1102                    let caps = match_.captures;
1103                    if caps.len() != 2
1104                        || caps[0].node.end_byte() >= curr_doc.len()
1105                        || caps[1].node.end_byte() >= curr_doc.len()
1106                    {
1107                        continue;
1108                    }
1109                    let label_text = caps[0].node.utf8_text(curr_doc);
1110                    let label_data = caps[1].node.utf8_text(curr_doc);
1111                    match (label_text, label_data) {
1112                        (Ok(label), Ok(data))
1113                            // Some labels have a preceding '.' that we need to account for
1114                            if label.eq(word) || label.trim_start_matches('.').eq(word) =>
1115                        {
1116                            return Some(Hover {
1117                                contents: HoverContents::Markup(MarkupContent {
1118                                    kind: MarkupKind::Markdown,
1119                                    value: format!("`{data}`"),
1120                                }),
1121                                range: None,
1122                            });
1123                        }
1124                        _ => {}
1125                    }
1126                }
1127            }
1128        }
1129    }
1130    None
1131}
1132
1133fn get_demangle_resp(word: &str) -> Option<Hover> {
1134    let name = Name::new(word, NameMangling::Mangled, Language::Unknown);
1135    let demangled = name.demangle(DemangleOptions::complete());
1136    if let Some(demang) = demangled {
1137        let value = demang;
1138        return Some(Hover {
1139            contents: HoverContents::Markup(MarkupContent {
1140                kind: MarkupKind::Markdown,
1141                value,
1142            }),
1143            range: None,
1144        });
1145    }
1146
1147    None
1148}
1149
1150fn get_include_resp(
1151    source_file: &Uri,
1152    filename: &str,
1153    include_dirs: &HashMap<SourceFile, Vec<PathBuf>>,
1154) -> Option<Hover> {
1155    let mut paths = String::new();
1156
1157    type DirIter<'a> = Box<dyn Iterator<Item = &'a PathBuf> + 'a>;
1158    let mut dir_iter = include_dirs.get(&SourceFile::All).map_or_else(
1159        || Box::new(std::iter::empty()) as DirIter,
1160        |dirs| Box::new(dirs.iter()) as DirIter,
1161    );
1162
1163    if let Ok(src_path) = PathBuf::from(source_file.as_str()).canonicalize()
1164        && let Some(dirs) = include_dirs.get(&SourceFile::File(src_path))
1165    {
1166        dir_iter = Box::new(dir_iter.chain(dirs.iter()));
1167    }
1168
1169    for dir in dir_iter {
1170        match std::fs::read_dir(dir) {
1171            Ok(dir_reader) => {
1172                for file in dir_reader {
1173                    match file {
1174                        Ok(f) => {
1175                            if f.file_name() == filename {
1176                                writeln!(&mut paths, "file://{}", f.path().display()).unwrap();
1177                            }
1178                        }
1179                        Err(e) => {
1180                            error!(
1181                                "Failed to read item in {} - Error {e}",
1182                                dir.as_path().display()
1183                            );
1184                        }
1185                    }
1186                }
1187            }
1188            Err(e) => {
1189                error!(
1190                    "Failed to create directory reader for {} - Error {e}",
1191                    dir.as_path().display()
1192                );
1193            }
1194        }
1195    }
1196
1197    if paths.is_empty() {
1198        None
1199    } else {
1200        Some(Hover {
1201            contents: HoverContents::Markup(MarkupContent {
1202                kind: MarkupKind::Markdown,
1203                value: paths,
1204            }),
1205            range: None,
1206        })
1207    }
1208}
1209
1210/// Filter out duplicate completion suggestions, and those that aren't allowed
1211/// by `config`
1212fn filtered_comp_list_arch(
1213    comps: &[(Arch, CompletionItem)],
1214    config: &Config,
1215) -> Vec<CompletionItem> {
1216    let mut seen = HashSet::new();
1217    comps
1218        .iter()
1219        .filter(|(arch, comp_item)| {
1220            if !config.is_isa_enabled(*arch) {
1221                return false;
1222            }
1223            if seen.contains(&comp_item.label) {
1224                false
1225            } else {
1226                seen.insert(&comp_item.label);
1227                true
1228            }
1229        })
1230        .map(|(_, comp_item)| comp_item)
1231        .cloned()
1232        .collect()
1233}
1234
1235/// Filter out duplicate completion suggestions, and those that aren't allowed
1236/// by `config`
1237/// 'prefix' allows the caller to optionally require completion items to start with
1238/// a given character
1239fn filtered_comp_list_assem(
1240    comps: &[(Assembler, CompletionItem)],
1241    config: &Config,
1242    prefix: Option<char>,
1243) -> Vec<CompletionItem> {
1244    let mut seen = HashSet::new();
1245    comps
1246        .iter()
1247        .filter(|(assem, comp_item)| {
1248            if !config.is_assembler_enabled(*assem) {
1249                return false;
1250            }
1251            if let Some(c) = prefix
1252                && !comp_item.label.starts_with(c)
1253            {
1254                return false;
1255            }
1256            if seen.contains(&comp_item.label) {
1257                false
1258            } else {
1259                seen.insert(&comp_item.label);
1260                true
1261            }
1262        })
1263        .map(|(_, comp_item)| comp_item)
1264        .cloned()
1265        .collect()
1266}
1267
1268macro_rules! cursor_matches {
1269    ($cursor_line:expr,$cursor_char:expr,$query_start:expr,$query_end:expr) => {{
1270        $query_start.row == $cursor_line
1271            && $query_end.row == $cursor_line
1272            && $query_start.column <= $cursor_char
1273            && $query_end.column >= $cursor_char
1274    }};
1275}
1276
1277pub fn get_comp_resp(
1278    curr_doc: &str,
1279    tree_entry: &mut TreeEntry,
1280    params: &CompletionParams,
1281    config: &Config,
1282    completion_items: &CompletionItems,
1283) -> Option<CompletionList> {
1284    let cursor_line = params.text_document_position.position.line as usize;
1285    let cursor_char = params.text_document_position.position.character as usize;
1286
1287    if let Some(ctx) = params.context.as_ref()
1288        && ctx.trigger_kind == CompletionTriggerKind::TRIGGER_CHARACTER
1289    {
1290        match ctx
1291            .trigger_character
1292            .as_ref()
1293            .map(std::convert::AsRef::as_ref)
1294        {
1295            // prepend GAS registers, some NASM directives with "%"
1296            Some("%") => {
1297                let mut items = Vec::new();
1298                if config.is_isa_enabled(Arch::X86) || config.is_isa_enabled(Arch::X86_64) {
1299                    items.append(&mut filtered_comp_list_arch(
1300                        &completion_items.registers,
1301                        config,
1302                    ));
1303                }
1304                if config.is_assembler_enabled(Assembler::Nasm) {
1305                    items.append(&mut filtered_comp_list_assem(
1306                        &completion_items.directives,
1307                        config,
1308                        Some('%'),
1309                    ));
1310                }
1311
1312                if !items.is_empty() {
1313                    return Some(CompletionList {
1314                        is_incomplete: true,
1315                        items,
1316                    });
1317                }
1318            }
1319            // prepend all GAS, all Ca65, all AVR, all Mars, some MASM, some NASM directives with "."
1320            Some(".") => {
1321                if config.is_assembler_enabled(Assembler::Gas)
1322                    || config.is_assembler_enabled(Assembler::Masm)
1323                    || config.is_assembler_enabled(Assembler::Nasm)
1324                    || config.is_assembler_enabled(Assembler::Ca65)
1325                    || config.is_assembler_enabled(Assembler::Avr)
1326                    || config.is_assembler_enabled(Assembler::Mars)
1327                {
1328                    return Some(CompletionList {
1329                        is_incomplete: true,
1330                        items: filtered_comp_list_assem(
1331                            &completion_items.directives,
1332                            config,
1333                            Some('.'),
1334                        ),
1335                    });
1336                }
1337            }
1338            // prepend all Mips registers with "$"
1339            Some("$") => {
1340                if config.is_isa_enabled(Arch::Mips) {
1341                    return Some(CompletionList {
1342                        is_incomplete: true,
1343                        items: filtered_comp_list_arch(&completion_items.registers, config),
1344                    });
1345                }
1346            }
1347            _ => {}
1348        }
1349    }
1350
1351    // TODO: filter register completions by width allowed by corresponding instruction
1352    tree_entry.tree = tree_entry.parser.parse(curr_doc, tree_entry.tree.as_ref());
1353    if let Some(ref tree) = tree_entry.tree {
1354        static QUERY_DIRECTIVE: LazyLock<tree_sitter::Query> = LazyLock::new(|| {
1355            tree_sitter::Query::new(
1356                &tree_sitter_asm::language(),
1357                "(meta kind: (meta_ident) @directive)",
1358            )
1359            .unwrap()
1360        });
1361        let mut line_cursor = tree_sitter::QueryCursor::new();
1362        line_cursor.set_point_range(std::ops::Range {
1363            start: tree_sitter::Point {
1364                row: cursor_line,
1365                column: 0,
1366            },
1367            end: tree_sitter::Point {
1368                row: cursor_line,
1369                column: usize::MAX,
1370            },
1371        });
1372        let curr_doc = curr_doc.as_bytes();
1373
1374        let matches_iter = line_cursor.matches(&QUERY_DIRECTIVE, tree.root_node(), curr_doc);
1375
1376        for match_ in matches_iter {
1377            let caps = match_.captures;
1378            for cap in caps {
1379                let arg_start = cap.node.range().start_point;
1380                let arg_end = cap.node.range().end_point;
1381                if cursor_matches!(cursor_line, cursor_char, arg_start, arg_end) {
1382                    let items =
1383                        filtered_comp_list_assem(&completion_items.directives, config, None);
1384                    return Some(CompletionList {
1385                        is_incomplete: true,
1386                        items,
1387                    });
1388                }
1389            }
1390        }
1391
1392        // tree-sitter-asm currently parses label arguments to instructions as *registers*
1393        // We'll collect all of labels in the document (that are being parsed as labels, at least)
1394        // and suggest those along with the register completions
1395        static QUERY_LABEL: LazyLock<tree_sitter::Query> = LazyLock::new(|| {
1396            tree_sitter::Query::new(&tree_sitter_asm::language(), "(label (ident) @label)").unwrap()
1397        });
1398
1399        // need a separate cursor to search the entire document
1400        let mut doc_cursor = tree_sitter::QueryCursor::new();
1401        let captures = doc_cursor.captures(&QUERY_LABEL, tree.root_node(), curr_doc);
1402        let mut labels = HashSet::new();
1403        for caps in captures.map(|c| c.0) {
1404            for cap in caps.captures {
1405                if cap.node.end_byte() >= curr_doc.len() {
1406                    continue;
1407                }
1408                if let Ok(text) = cap.node.utf8_text(curr_doc) {
1409                    labels.insert(text);
1410                }
1411            }
1412        }
1413
1414        static QUERY_INSTR_ANY: LazyLock<tree_sitter::Query> = LazyLock::new(|| {
1415            tree_sitter::Query::new(
1416                &tree_sitter_asm::language(),
1417                "[
1418                    (instruction kind: (word) @instr_name)
1419                    (
1420                        instruction kind: (word) @instr_name
1421                            [
1422                                (
1423                                    [
1424                                     (ident (reg) @r1)
1425                                     (ptr (int) (reg) @r1)
1426                                     (ptr (reg) @r1)
1427                                     (ptr (int))
1428                                     (ptr)
1429                                    ]
1430                                    [
1431                                     (ident (reg) @r2)
1432                                     (ptr (int) (reg) @r2)
1433                                     (ptr (reg) @r2)
1434                                     (ptr (int))
1435                                     (ptr)
1436                                    ]
1437                                )
1438                                (
1439                                    [
1440                                     (ident (reg) @r1)
1441                                     (ptr (int) (reg) @r1)
1442                                     (ptr (reg) @r1)
1443                                    ]
1444                                )
1445                            ]
1446                    )
1447                ]",
1448            )
1449            .unwrap()
1450        });
1451
1452        let matches_iter = line_cursor.matches(&QUERY_INSTR_ANY, tree.root_node(), curr_doc);
1453        for match_ in matches_iter {
1454            let caps = match_.captures;
1455            for (cap_num, cap) in caps.iter().enumerate() {
1456                let arg_start = cap.node.range().start_point;
1457                let arg_end = cap.node.range().end_point;
1458                if cursor_matches!(cursor_line, cursor_char, arg_start, arg_end) {
1459                    // an instruction is always capture #0 for this query, any capture
1460                    // number after must be a register or label
1461                    let is_instr = cap_num == 0;
1462                    let mut items = filtered_comp_list_arch(
1463                        if is_instr {
1464                            &completion_items.instructions
1465                        } else {
1466                            &completion_items.registers
1467                        },
1468                        config,
1469                    );
1470                    if is_instr {
1471                        // Sometimes tree-sitter-asm parses a directive as an instruction, so we'll
1472                        // suggest both in this case
1473                        items.append(&mut filtered_comp_list_assem(
1474                            &completion_items.directives,
1475                            config,
1476                            None,
1477                        ));
1478                    } else {
1479                        items.append(
1480                            &mut labels
1481                                .iter()
1482                                .map(|l| CompletionItem {
1483                                    label: (*l).to_string(),
1484                                    kind: Some(CompletionItemKind::VARIABLE),
1485                                    ..Default::default()
1486                                })
1487                                .collect(),
1488                        );
1489                    }
1490                    return Some(CompletionList {
1491                        is_incomplete: true,
1492                        items,
1493                    });
1494                }
1495            }
1496        }
1497    }
1498
1499    None
1500}
1501
1502const fn lsp_pos_of_point(pos: tree_sitter::Point) -> lsp_types::Position {
1503    Position {
1504        line: pos.row as u32,
1505        character: pos.column as u32,
1506    }
1507}
1508
1509/// Explore `node`, push immediate children into `res`.
1510fn explore_node(
1511    curr_doc: &str,
1512    node: tree_sitter::Node,
1513    res: &mut Vec<DocumentSymbol>,
1514    label_kind_id: u16,
1515    ident_kind_id: u16,
1516) {
1517    if node.kind_id() == label_kind_id {
1518        let mut children = vec![];
1519        let mut cursor = node.walk();
1520
1521        // description for this node
1522        let mut descr = String::new();
1523
1524        if cursor.goto_first_child() {
1525            loop {
1526                let sub_node = cursor.node();
1527                if sub_node.kind_id() == ident_kind_id
1528                    && let Ok(text) = sub_node.utf8_text(curr_doc.as_bytes())
1529                {
1530                    descr = text.to_string();
1531                }
1532
1533                explore_node(
1534                    curr_doc,
1535                    sub_node,
1536                    &mut children,
1537                    label_kind_id,
1538                    ident_kind_id,
1539                );
1540                if !cursor.goto_next_sibling() {
1541                    break;
1542                }
1543            }
1544        }
1545
1546        let range = lsp_types::Range::new(
1547            lsp_pos_of_point(node.start_position()),
1548            lsp_pos_of_point(node.end_position()),
1549        );
1550
1551        #[allow(deprecated)]
1552        let doc = DocumentSymbol {
1553            name: descr,
1554            detail: None,
1555            kind: SymbolKind::FUNCTION,
1556            tags: None,
1557            deprecated: Some(false),
1558            range,
1559            selection_range: range,
1560            children: if children.is_empty() {
1561                None
1562            } else {
1563                Some(children)
1564            },
1565        };
1566        res.push(doc);
1567    } else {
1568        let mut cursor = node.walk();
1569
1570        if cursor.goto_first_child() {
1571            loop {
1572                explore_node(curr_doc, cursor.node(), res, label_kind_id, ident_kind_id);
1573                if !cursor.goto_next_sibling() {
1574                    break;
1575                }
1576            }
1577        }
1578    }
1579}
1580
1581/// Get a tree of symbols describing the document's structure.
1582pub fn get_document_symbols(
1583    curr_doc: &str,
1584    tree_entry: &mut TreeEntry,
1585    _params: &DocumentSymbolParams,
1586) -> Option<Vec<DocumentSymbol>> {
1587    static LABEL_KIND_ID: LazyLock<u16> =
1588        LazyLock::new(|| tree_sitter_asm::language().id_for_node_kind("label", true));
1589    static IDENT_KIND_ID: LazyLock<u16> =
1590        LazyLock::new(|| tree_sitter_asm::language().id_for_node_kind("ident", true));
1591    tree_entry.tree = tree_entry.parser.parse(curr_doc, tree_entry.tree.as_ref());
1592
1593    tree_entry.tree.as_ref().map(|tree| {
1594        let mut res: Vec<DocumentSymbol> = vec![];
1595        let mut cursor = tree.walk();
1596        loop {
1597            explore_node(
1598                curr_doc,
1599                cursor.node(),
1600                &mut res,
1601                *LABEL_KIND_ID,
1602                *IDENT_KIND_ID,
1603            );
1604            if !cursor.goto_next_sibling() {
1605                break;
1606            }
1607        }
1608        res
1609    })
1610}
1611
1612/// Produces a signature help response if the appropriate instruction forms can
1613/// be found
1614pub fn get_sig_help_resp(
1615    curr_doc: &str,
1616    params: &SignatureHelpParams,
1617    config: &Config,
1618    tree_entry: &mut TreeEntry,
1619    instr_info: &NameToInstructionMap,
1620) -> Option<SignatureHelp> {
1621    let cursor_line = params.text_document_position_params.position.line as usize;
1622
1623    tree_entry.tree = tree_entry.parser.parse(curr_doc, tree_entry.tree.as_ref());
1624    if let Some(ref tree) = tree_entry.tree {
1625        // Instruction with any (including zero) argument(s)
1626        static QUERY_INSTR_ANY_ARGS: LazyLock<tree_sitter::Query> = LazyLock::new(|| {
1627            tree_sitter::Query::new(
1628                &tree_sitter_asm::language(),
1629                "(instruction kind: (word) @instr_name)",
1630            )
1631            .unwrap()
1632        });
1633
1634        let mut line_cursor = tree_sitter::QueryCursor::new();
1635        line_cursor.set_point_range(std::ops::Range {
1636            start: tree_sitter::Point {
1637                row: cursor_line,
1638                column: 0,
1639            },
1640            end: tree_sitter::Point {
1641                row: cursor_line,
1642                column: usize::MAX,
1643            },
1644        });
1645        let curr_doc = curr_doc.as_bytes();
1646
1647        let matches: Vec<tree_sitter::QueryMatch<'_, '_>> = line_cursor
1648            .matches(&QUERY_INSTR_ANY_ARGS, tree.root_node(), curr_doc)
1649            .collect();
1650        if let Some(match_) = matches.first() {
1651            let caps = match_.captures;
1652            if caps.len() == 1
1653                && caps[0].node.end_byte() < curr_doc.len()
1654                && let Ok(instr_name) = caps[0].node.utf8_text(curr_doc)
1655            {
1656                let mut value = String::new();
1657                let (instr1, instr2) = search_for_hoverable_by_arch(instr_name, instr_info, config);
1658                let instructions = vec![instr1, instr2];
1659                for instr in instructions.into_iter().flatten() {
1660                    for form in &instr.forms {
1661                        match instr.arch {
1662                            Arch::X86 | Arch::X86_64 => {
1663                                if let Some(ref gas_name) = form.gas_name
1664                                    && instr_name.eq_ignore_ascii_case(gas_name)
1665                                {
1666                                    writeln!(&mut value, "**{}**\n{form}", instr.arch).unwrap();
1667                                } else if let Some(ref go_name) = form.go_name
1668                                    && instr_name.eq_ignore_ascii_case(go_name)
1669                                {
1670                                    writeln!(&mut value, "**{}**\n{form}", instr.arch).unwrap();
1671                                }
1672                            }
1673                            Arch::Z80 => {
1674                                for form in &instr.forms {
1675                                    if let Some(ref z80_name) = form.z80_name
1676                                        && instr_name.eq_ignore_ascii_case(z80_name)
1677                                    {
1678                                        writeln!(&mut value, "{form}").unwrap();
1679                                    }
1680                                }
1681                            }
1682                            Arch::ARM | Arch::RISCV => {
1683                                for form in &instr.asm_templates {
1684                                    writeln!(&mut value, "{form}").unwrap();
1685                                }
1686                            }
1687                            _ => {}
1688                        }
1689                    }
1690                }
1691                if !value.is_empty() {
1692                    return Some(SignatureHelp {
1693                        signatures: vec![SignatureInformation {
1694                            label: instr_name.to_string(),
1695                            documentation: Some(Documentation::MarkupContent(MarkupContent {
1696                                kind: MarkupKind::Markdown,
1697                                value,
1698                            })),
1699                            parameters: None,
1700                            active_parameter: None,
1701                        }],
1702                        active_signature: None,
1703                        active_parameter: None,
1704                    });
1705                }
1706            }
1707        }
1708    }
1709
1710    None
1711}
1712
1713pub fn get_goto_def_resp(
1714    curr_doc: &FullTextDocument,
1715    tree_entry: &mut TreeEntry,
1716    params: &GotoDefinitionParams,
1717) -> Option<GotoDefinitionResponse> {
1718    let doc = curr_doc.get_content(None).as_bytes();
1719    tree_entry.tree = tree_entry.parser.parse(doc, tree_entry.tree.as_ref());
1720
1721    if let Some(ref tree) = tree_entry.tree {
1722        static QUERY_LABEL: LazyLock<tree_sitter::Query> = LazyLock::new(|| {
1723            tree_sitter::Query::new(&tree_sitter_asm::language(), "(label) @label").unwrap()
1724        });
1725
1726        let is_not_ident_char = |c: char| !(c.is_alphanumeric() || c == '_');
1727        let mut cursor = tree_sitter::QueryCursor::new();
1728        let matches = cursor.matches(&QUERY_LABEL, tree.root_node(), doc);
1729
1730        let (word, _) = get_word_from_pos_params(curr_doc, &params.text_document_position_params);
1731
1732        for match_ in matches {
1733            for cap in match_.captures {
1734                if cap.node.end_byte() >= doc.len() {
1735                    continue;
1736                }
1737                let text = cap
1738                    .node
1739                    .utf8_text(doc)
1740                    .unwrap_or("")
1741                    .trim()
1742                    .trim_matches(is_not_ident_char);
1743
1744                if word.eq(text) {
1745                    let start = cap.node.start_position();
1746                    let end = cap.node.end_position();
1747                    return Some(GotoDefinitionResponse::Scalar(Location {
1748                        uri: params
1749                            .text_document_position_params
1750                            .text_document
1751                            .uri
1752                            .clone(),
1753                        range: Range {
1754                            start: lsp_pos_of_point(start),
1755                            end: lsp_pos_of_point(end),
1756                        },
1757                    }));
1758                }
1759            }
1760        }
1761    }
1762
1763    None
1764}
1765
1766pub fn get_ref_resp(
1767    params: &ReferenceParams,
1768    curr_doc: &FullTextDocument,
1769    tree_entry: &mut TreeEntry,
1770) -> Vec<Location> {
1771    let mut refs: HashSet<Location> = HashSet::new();
1772    let doc = curr_doc.get_content(None).as_bytes();
1773    tree_entry.tree = tree_entry.parser.parse(doc, tree_entry.tree.as_ref());
1774
1775    if let Some(ref tree) = tree_entry.tree {
1776        static QUERY_LABEL: LazyLock<tree_sitter::Query> = LazyLock::new(|| {
1777            tree_sitter::Query::new(
1778                &tree_sitter_asm::language(),
1779                "(label (ident (reg (word)))) @label",
1780            )
1781            .unwrap()
1782        });
1783
1784        static QUERY_WORD: LazyLock<tree_sitter::Query> = LazyLock::new(|| {
1785            tree_sitter::Query::new(&tree_sitter_asm::language(), "(ident) @ident").unwrap()
1786        });
1787
1788        let is_not_ident_char = |c: char| !(c.is_alphanumeric() || c == '_');
1789        let (word, _) = get_word_from_pos_params(curr_doc, &params.text_document_position);
1790        let uri = &params.text_document_position.text_document.uri;
1791
1792        let mut cursor = tree_sitter::QueryCursor::new();
1793        if params.context.include_declaration {
1794            let label_matches = cursor.matches(&QUERY_LABEL, tree.root_node(), doc);
1795            for match_ in label_matches {
1796                for cap in match_.captures {
1797                    // HACK: Temporary solution for what I believe is a bug in tree-sitter core
1798                    if cap.node.end_byte() >= doc.len() {
1799                        continue;
1800                    }
1801                    let text = cap
1802                        .node
1803                        .utf8_text(doc)
1804                        .unwrap_or("")
1805                        .trim()
1806                        .trim_matches(is_not_ident_char);
1807
1808                    if word.eq(text) {
1809                        let start = lsp_pos_of_point(cap.node.start_position());
1810                        let end = lsp_pos_of_point(cap.node.end_position());
1811                        refs.insert(Location {
1812                            uri: uri.clone(),
1813                            range: Range { start, end },
1814                        });
1815                    }
1816                }
1817            }
1818        }
1819
1820        let word_matches = cursor.matches(&QUERY_WORD, tree.root_node(), doc);
1821        for match_ in word_matches {
1822            for cap in match_.captures {
1823                // HACK: Temporary solution for what I believe is a bug in tree-sitter core
1824                if cap.node.end_byte() >= doc.len() {
1825                    continue;
1826                }
1827                let text = cap
1828                    .node
1829                    .utf8_text(doc)
1830                    .unwrap_or("")
1831                    .trim()
1832                    .trim_matches(is_not_ident_char);
1833
1834                if word.eq(text) {
1835                    let start = lsp_pos_of_point(cap.node.start_position());
1836                    let end = lsp_pos_of_point(cap.node.end_position());
1837                    refs.insert(Location {
1838                        uri: uri.clone(),
1839                        range: Range { start, end },
1840                    });
1841                }
1842            }
1843        }
1844    }
1845
1846    refs.into_iter().collect()
1847}
1848
1849/// Searches for global config in ~/.config/asm-lsp, then the project's directory
1850/// Project specific configs will override global configs
1851///
1852/// If no valid config files can be found, this function will cause the program
1853/// to exit with code -1
1854///
1855/// # Errors
1856///
1857/// Will return `Err` if an invalid configuration file is found
1858pub fn get_root_config(params: &InitializeParams) -> Result<RootConfig> {
1859    let report_err = |msg: &str| -> Result<RootConfig> {
1860        error!("{msg}");
1861        Err(anyhow!(msg.to_string()))
1862    };
1863    let mut config = match (get_global_config(), get_project_config(params)) {
1864        // If we have a valid project config, ignore bad global configs
1865        (_, Some(Ok(proj_cfg))) => proj_cfg,
1866        (Some(Ok(global_cfg)), None) => global_cfg,
1867        (Some(Ok(_)) | None, Some(Err(e))) => {
1868            return report_err(&format!("Inavlid project config file -- {e}"));
1869        }
1870        (Some(Err(e)), None) => {
1871            return report_err(&format!("Invalid global config file -- {e}"));
1872        }
1873        (Some(Err(e_global)), Some(Err(e_project))) => {
1874            return report_err(&format!(
1875                "Invalid project and global config files -- {e_project} -- {e_global}"
1876            ));
1877        }
1878        (None, None) => {
1879            info!("No configuration files found, using default options");
1880            RootConfig::default()
1881        }
1882    };
1883
1884    // Validate project paths and enforce default diagnostics settings
1885    if let Some(ref mut projects) = config.projects {
1886        if let Some(ref project_root) = get_project_root(params) {
1887            let mut project_idx = 0;
1888            while project_idx < projects.len() {
1889                let mut project_path = project_root.clone();
1890                project_path.push(&projects[project_idx].path);
1891                let Ok(canonicalized_project_path) = project_path.canonicalize() else {
1892                    return report_err(&format!(
1893                        "Failed to canonicalize project path \"{}\".",
1894                        project_path.display()
1895                    ));
1896                };
1897                projects[project_idx].path = canonicalized_project_path;
1898                if let Some(ref mut opts) = projects[project_idx].config.opts {
1899                    // Want diagnostics enabled by default
1900                    if opts.diagnostics.is_none() {
1901                        opts.diagnostics = Some(true);
1902                    }
1903                    // Want default diagnostics enabled by default
1904                    if opts.default_diagnostics.is_none() {
1905                        opts.default_diagnostics = Some(true);
1906                    }
1907                } else {
1908                    projects[project_idx].config.opts = Some(ConfigOptions::default());
1909                }
1910
1911                project_idx += 1;
1912            }
1913        } else {
1914            return report_err("Unable to detect project root directory.");
1915        }
1916
1917        // sort project configurations so when we select a project config at request
1918        // time, we find configs controlling specific files first, and then configs
1919        // for a sub-directory of another config before the parent config
1920        projects.sort_unstable_by(|c1, c2| {
1921            // - If both are files, we don't care
1922            // - If one is file and other is directory, file goes first
1923            // - Else (just assuming both are directories for the default case),
1924            //   go by the length metric (parent directories get placed *after*
1925            //   their children)
1926            let c1_dir = c1.path.is_dir();
1927            let c1_file = c1.path.is_file();
1928            let c2_dir = c2.path.is_dir();
1929            let c2_file = c2.path.is_file();
1930            if c1_file && c2_file {
1931                Ordering::Equal
1932            } else if c1_dir && c2_file {
1933                Ordering::Greater
1934            } else if c1_file && c2_dir {
1935                Ordering::Less
1936            } else {
1937                c2.path
1938                    .to_string_lossy()
1939                    .len()
1940                    .cmp(&c1.path.to_string_lossy().len())
1941            }
1942        });
1943
1944        // Check if the user specified multiple configs pointing to the same
1945        // file or directory
1946        let mut path_check = HashSet::new();
1947        for project in projects {
1948            if path_check.contains(&project.path) {
1949                return report_err(&format!(
1950                    "Multiple project configurations for \"{}\".",
1951                    project.path.display()
1952                ));
1953            }
1954            path_check.insert(&project.path);
1955        }
1956    }
1957
1958    // Enforce default diagnostics settings for default config
1959    if let Some(ref mut default_cfg) = config.default_config {
1960        if let Some(ref mut opts) = default_cfg.opts {
1961            // Want diagnostics enabled by default
1962            if opts.diagnostics.is_none() {
1963                opts.diagnostics = Some(true);
1964            }
1965            // Want default diagnostics enabled by default
1966            if opts.default_diagnostics.is_none() {
1967                opts.default_diagnostics = Some(true);
1968            }
1969        } else {
1970            default_cfg.opts = Some(ConfigOptions::default());
1971        }
1972    } else {
1973        info!("No `default_config` specified, filling in default options");
1974        // provide a default empty configuration for sub-directories
1975        // not specified in `projects`
1976        config.default_config = Some(Config::default());
1977    }
1978
1979    Ok(config)
1980}
1981
1982#[must_use]
1983pub fn get_global_cfg_dirs() -> Vec<Option<PathBuf>> {
1984    if cfg!(target_os = "macos") {
1985        // `$HOME`/Library/Application Support/ and `$HOME`/.config/
1986        vec![config_dir(), alt_mac_config_dir()]
1987    } else {
1988        vec![config_dir()]
1989    }
1990}
1991
1992/// Checks ~/.config/asm-lsp for a config file, creating directories along the way as necessary
1993fn get_global_config() -> Option<Result<RootConfig>> {
1994    let mut paths = get_global_cfg_dirs();
1995
1996    for cfg_path in paths.iter_mut().flatten() {
1997        cfg_path.push("asm-lsp");
1998        info!(
1999            "Creating directories along {} as necessary...",
2000            cfg_path.display()
2001        );
2002        match create_dir_all(&cfg_path) {
2003            Ok(()) => {
2004                cfg_path.push(".asm-lsp.toml");
2005                if let Ok(config) = std::fs::read_to_string(&cfg_path) {
2006                    let cfg_path_s = cfg_path.display();
2007                    match toml::from_str::<RootConfig>(&config) {
2008                        Ok(config) => {
2009                            info!(
2010                                "Parsing global asm-lsp config from file -> {}",
2011                                cfg_path.display()
2012                            );
2013                            return Some(Ok(config));
2014                        }
2015                        Err(e) => {
2016                            error!("Failed to parse global config file {cfg_path_s} - Error: {e}");
2017                            return Some(Err(e.into()));
2018                        }
2019                    }
2020                }
2021            }
2022            Err(e) => {
2023                error!(
2024                    "Failed to create global config directory {} - Error: {e}",
2025                    cfg_path.display()
2026                );
2027            }
2028        }
2029    }
2030
2031    None
2032}
2033
2034// Returns `$HOME`/.config/ for usage on MacOS, as this isn't the default
2035// config directory
2036#[must_use]
2037pub fn alt_mac_config_dir() -> Option<PathBuf> {
2038    home::home_dir().map(|mut path| {
2039        path.push(".config");
2040        path
2041    })
2042}
2043
2044/// Attempts to find the project's root directory given its `InitializeParams`
2045// 1. if we have workspace folders, then iterate through them and assign the first valid one to
2046//    the root path
2047// 2. If we don't have worksace folders or none of them is a valid path, check the (deprecated)
2048//    root_uri field
2049// 3. If both workspace folders and root_uri didn't provide a path, check the (deprecated)
2050//    root_path field
2051fn get_project_root(params: &InitializeParams) -> Option<PathBuf> {
2052    // first check workspace folders
2053    if let Some(folders) = &params.workspace_folders {
2054        // if there's multiple, just visit in order until we find a valid folder
2055        for folder in folders {
2056            let Ok(parsed) = PathBuf::from_str(folder.uri.path().as_str());
2057            if let Ok(parsed_path) = parsed.canonicalize() {
2058                info!("Detected project root: {}", parsed_path.display());
2059                return Some(parsed_path);
2060            }
2061        }
2062    }
2063
2064    // if workspace folders weren't set or came up empty, we check the root_uri
2065    #[allow(deprecated)]
2066    if let Some(root_uri) = &params.root_uri {
2067        let Ok(parsed) = PathBuf::from_str(root_uri.path().as_str());
2068        if let Ok(parsed_path) = parsed.canonicalize() {
2069            info!("Detected project root: {}", parsed_path.display());
2070            return Some(parsed_path);
2071        }
2072    }
2073
2074    // if both `workspace_folders` and `root_uri` weren't set or came up empty, we check the root_path
2075    #[allow(deprecated)]
2076    if let Some(root_path) = &params.root_path {
2077        let Ok(parsed) = PathBuf::from_str(root_path.as_str());
2078        if let Ok(parsed_path) = parsed.canonicalize() {
2079            return Some(parsed_path);
2080        }
2081    }
2082
2083    warn!("Failed to detect project root");
2084    None
2085}
2086
2087/// checks for a config specific to the project's root directory
2088///
2089/// # Errors
2090///
2091/// Returns `Err` if the file cannot be deserialized
2092fn get_project_config(params: &InitializeParams) -> Option<Result<RootConfig>> {
2093    if let Some(mut path) = get_project_root(params) {
2094        path.push(".asm-lsp.toml");
2095        match std::fs::read_to_string(&path) {
2096            Ok(config) => match toml::from_str::<RootConfig>(&config) {
2097                Ok(config) => {
2098                    info!(
2099                        "Parsing asm-lsp project config from file -> {}",
2100                        path.display()
2101                    );
2102                    return Some(Ok(config));
2103                }
2104                Err(e) => {
2105                    error!(
2106                        "Failed to parse project config file {} - Error: {e}",
2107                        path.display()
2108                    );
2109                    return Some(Err(e.into()));
2110                }
2111            },
2112            Err(e) => {
2113                error!("Failed to read config file {} - Error: {e}", path.display());
2114            }
2115        }
2116    }
2117
2118    None
2119}
2120
2121#[must_use]
2122pub fn instr_filter_targets(instr: &Instruction, config: &Config) -> Instruction {
2123    let mut instr = instr.clone();
2124
2125    let forms = instr
2126        .forms
2127        .iter()
2128        .filter(|form| {
2129            (form.gas_name.is_some() && config.is_assembler_enabled(Assembler::Gas))
2130                || (form.go_name.is_some() && config.is_assembler_enabled(Assembler::Go))
2131        })
2132        .map(|form| {
2133            let mut filtered = form.clone();
2134            // handle cases where gas and go both have names on the same form
2135            if !config.is_assembler_enabled(Assembler::Gas) {
2136                filtered.gas_name = None;
2137            }
2138            if !config.is_assembler_enabled(Assembler::Go) {
2139                filtered.go_name = None;
2140            }
2141            filtered
2142        })
2143        .collect();
2144
2145    instr.forms = forms;
2146    instr
2147}