Skip to main content

forge/cmd/
lsp.rs

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/// Open a Solidity project in VS Code or run its language server.
15#[derive(Debug, Default, Parser)]
16pub struct LspArgs {
17    /// Run the language server over standard input/output.
18    #[arg(long, conflicts_with_all = ["vscode", "path", "code_path"])]
19    pub stdio: bool,
20
21    /// Open a VS Code Extension Development Host even when input is redirected.
22    #[arg(long)]
23    pub vscode: bool,
24
25    /// Project directory to open in VS Code. Defaults to the current directory.
26    #[arg(value_hint = clap::ValueHint::DirPath)]
27    pub path: Option<PathBuf>,
28
29    /// VS Code executable or command. Defaults to `code` on PATH.
30    #[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}