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
42pub 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
70pub 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
90pub 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#[must_use]
116pub fn find_word_at_pos(line: &str, col: Column) -> ((Column, Column), usize) {
117 let line_ = format!("{line} ");
118 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#[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 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 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
209pub 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#[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#[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#[must_use]
292fn get_default_include_dirs() -> Vec<PathBuf> {
293 let mut include_dirs = HashSet::new();
294 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#[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 match args {
370 CompileArgs::Flags(args) | CompileArgs::Arguments(args) => {
371 for arg in args.iter().map(|arg| arg.trim()) {
372 if check_dir {
373 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 check_dir = true;
391 } else if arg.len() > 2 && arg.starts_with("-I") {
392 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 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
433pub fn get_compile_cmds_from_file(params: &InitializeParams) -> Option<CompilationDatabase> {
439 if let Some(mut path) = get_project_root(params) {
440 let db = get_compilation_db_files(&path);
442 if db.is_some() {
443 return db;
444 }
445
446 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 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 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
475pub 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 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 let mut args = vec![compiler.to_owned()];
527 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 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 compile_cmds.clone()
560 }
561 }
562}
563
564pub 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, directory: PathBuf::new(), arguments: Some(CompileArgs::Flags(vec![uri.path().to_string()])),
579 command: None,
580 output: None,
581 },
582 |compiler| CompileCommand {
583 file: SourceFile::All, directory: PathBuf::new(), 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
595pub 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 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) .args(flags) .arg(uri.path().as_str()) .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
664fn get_diagnostics(diagnostics: &mut Vec<Diagnostic>, tool_output: &str, cfg: &Config) {
675 if cfg.is_assembler_enabled(Assembler::Fasm) {
677 static FASM_SOURCE_LOC: LazyLock<Regex> =
679 LazyLock::new(|| Regex::new(r"^(.+) \[(\d+)\]:$").unwrap());
680 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 if let Some(caps) = FASM_SOURCE_LOC.captures(line) {
693 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 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 if let Some(caps) = DIAG_REG_LINE_COLUMN.captures(line) {
743 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 let Some(caps) = DIAG_REG_LINE_ONLY.captures(line) {
773 if caps.len() < 3 {
774 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 if let Some(caps) = ALT_DIAG_REG_LINE_ONLY.captures(line) {
800 if caps.len() < 3 {
801 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#[allow(clippy::needless_pass_by_value)]
829pub fn tree_sitter_logger(log_type: tree_sitter::LogType, message: &str) {
830 let log_level = match log_type {
832 tree_sitter::LogType::Parse | tree_sitter::LogType::Lex => log::Level::Trace,
833 };
834
835 if log_enabled!(log_level) {
838 log!(log_level, "{}", message);
839 }
840}
841
842pub 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#[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 {
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 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 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 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 let main_register = &word[0..dot];
959 get_hover_resp_by_arch(main_register, &store.names_to_info.registers, config)
960 } else {
961 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 ¶ms.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 ¶ms.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 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
1072fn 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 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
1210fn 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
1235fn 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 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 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 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 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 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 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 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 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
1509fn 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 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
1581pub 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
1612pub 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 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, ¶ms.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, ¶ms.text_document_position);
1790 let uri = ¶ms.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 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 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
1849pub 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 (_, 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 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 if opts.diagnostics.is_none() {
1901 opts.diagnostics = Some(true);
1902 }
1903 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 projects.sort_unstable_by(|c1, c2| {
1921 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 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 if let Some(ref mut default_cfg) = config.default_config {
1960 if let Some(ref mut opts) = default_cfg.opts {
1961 if opts.diagnostics.is_none() {
1963 opts.diagnostics = Some(true);
1964 }
1965 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 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 vec![config_dir(), alt_mac_config_dir()]
1987 } else {
1988 vec![config_dir()]
1989 }
1990}
1991
1992fn 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#[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
2044fn get_project_root(params: &InitializeParams) -> Option<PathBuf> {
2052 if let Some(folders) = ¶ms.workspace_folders {
2054 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 #[allow(deprecated)]
2066 if let Some(root_uri) = ¶ms.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 #[allow(deprecated)]
2076 if let Some(root_path) = ¶ms.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
2087fn 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 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}