1use clap::Parser;
2use eyre::Result;
3use foundry_compilers::compilers::multi::MultiCompilerLanguage;
4use foundry_config::{Config, load_config_with_root};
5use solar::config::ImportRemapping;
6use solar_lsp::FoundryWorkspaceConfig;
7use std::{
8 io::IsTerminal,
9 path::{MAIN_SEPARATOR, Path, PathBuf},
10};
11
12mod editor;
13
14#[derive(Debug, Default, Parser)]
16pub struct LspArgs {
17 #[arg(long, conflicts_with_all = ["vscode", "path", "code_path"])]
19 pub stdio: bool,
20
21 #[arg(long)]
23 pub vscode: bool,
24
25 #[arg(value_hint = clap::ValueHint::DirPath)]
27 pub path: Option<PathBuf>,
28
29 #[arg(long, value_hint = clap::ValueHint::ExecutablePath)]
31 pub code_path: Option<PathBuf>,
32}
33
34pub async fn run(args: LspArgs) -> Result<()> {
35 if !args.stdio
36 && (args.vscode
37 || args.path.is_some()
38 || args.code_path.is_some()
39 || std::io::stdin().is_terminal())
40 {
41 return editor::launch(args.path.as_deref(), args.code_path.as_deref());
42 }
43
44 let config = solar_lsp::LaunchConfig::from(solar::config::LspArgs { stdio: args.stdio })
45 .with_default_forge_path(std::env::current_exe()?)
46 .with_selected_profile(Config::selected_profile().to_string())
47 .with_foundry_workspace_config_loader(|root| {
48 foundry_workspace_config(root, load_config_with_root(Some(root))?)
49 });
50 solar_lsp::launch(config).await?;
51 Ok(())
52}
53
54fn foundry_workspace_config(root: &Path, config: Config) -> Result<FoundryWorkspaceConfig> {
55 let paths = config.project_paths::<MultiCompilerLanguage>();
56 let resolved_root = paths.root;
57 let sources = rebase_workspace_path(&resolved_root, root, paths.sources);
58 let tests = rebase_workspace_path(&resolved_root, root, paths.tests);
59 let scripts = rebase_workspace_path(&resolved_root, root, paths.scripts);
60
61 Ok(FoundryWorkspaceConfig::new(root)
62 .with_source_roots([sources.clone()])
63 .with_flycheck_source_roots([sources, tests, scripts])
64 .with_include_paths(
65 paths
66 .libraries
67 .into_iter()
68 .chain(paths.include_paths)
69 .map(|path| rebase_workspace_path(&resolved_root, root, path)),
70 )
71 .with_import_remappings(paths.remappings.into_iter().map(|remapping| ImportRemapping {
72 context: rebase_remapping_path(
73 &resolved_root,
74 root,
75 remapping.context.unwrap_or_default(),
76 ),
77 prefix: remapping.name,
78 path: rebase_remapping_path(&resolved_root, root, remapping.path),
79 }))
80 .with_evm_version(config.evm_version.to_string().parse()?))
81}
82
83fn rebase_workspace_path(resolved_root: &Path, root: &Path, path: PathBuf) -> PathBuf {
84 let Ok(relative) = path.strip_prefix(resolved_root) else { return path };
85 root.join(relative)
86}
87
88fn rebase_remapping_path(resolved_root: &Path, root: &Path, path: impl Into<String>) -> String {
89 let path = path.into();
90 let has_directory_boundary = path.ends_with(['/', '\\']);
91 let mut rebased =
92 rebase_workspace_path(resolved_root, root, PathBuf::from(path)).display().to_string();
93 if has_directory_boundary && !rebased.ends_with(['/', '\\']) {
94 rebased.push(MAIN_SEPARATOR);
95 }
96 rebased
97}