Skip to content

Commit

Permalink
chore: refactor LSP code for readability and dev ex (noir-lang#2969)
Browse files Browse the repository at this point in the history
  • Loading branch information
TomAFrench authored and Sakapoi committed Oct 19, 2023
1 parent e7df49b commit 8a0a9e8
Show file tree
Hide file tree
Showing 4 changed files with 376 additions and 353 deletions.
198 changes: 198 additions & 0 deletions tooling/lsp/src/codelens/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
use std::future::{self, Future};

use async_lsp::{ErrorCode, LanguageClient, ResponseError};

use fm::FILE_EXTENSION;
use nargo::{package::Package, prepare_package, workspace::Workspace};
use nargo_toml::{find_package_manifest, resolve_workspace_from_toml, PackageSelection};
use noirc_driver::check_crate;
use noirc_frontend::hir::FunctionNameMatch;

use crate::{
byte_span_to_range, get_non_stdlib_asset,
types::{CodeLens, CodeLensParams, CodeLensResult, Command, LogMessageParams, MessageType},
LspState,
};

mod test_run;
mod tests;

pub(crate) use {test_run::on_test_run_request, tests::on_tests_request};

const ARROW: &str = "▶\u{fe0e}";
const TEST_COMMAND: &str = "nargo.test";
const TEST_CODELENS_TITLE: &str = "Run Test";
const COMPILE_COMMAND: &str = "nargo.compile";
const COMPILE_CODELENS_TITLE: &str = "Compile";
const EXECUTE_COMMAND: &str = "nargo.execute";
const EXECUTE_CODELENS_TITLE: &str = "Execute";

fn with_arrow(title: &str) -> String {
format!("{ARROW} {title}")
}

fn package_selection_args(workspace: &Workspace, package: &Package) -> Vec<serde_json::Value> {
vec![
"--program-dir".into(),
workspace.root_dir.display().to_string().into(),
"--package".into(),
package.name.to_string().into(),
]
}

pub(super) fn on_code_lens_request(
state: &mut LspState,
params: CodeLensParams,
) -> impl Future<Output = Result<CodeLensResult, ResponseError>> {
future::ready(on_code_lens_request_inner(state, params))
}

fn on_code_lens_request_inner(
state: &mut LspState,
params: CodeLensParams,
) -> Result<CodeLensResult, ResponseError> {
let file_path = params.text_document.uri.to_file_path().map_err(|_| {
ResponseError::new(ErrorCode::REQUEST_FAILED, "URI is not a valid file path")
})?;

let root_path = state.root_path.as_deref().ok_or_else(|| {
ResponseError::new(ErrorCode::REQUEST_FAILED, "Could not find project root")
})?;

let toml_path = match find_package_manifest(root_path, &file_path) {
Ok(toml_path) => toml_path,
Err(err) => {
// If we cannot find a manifest, we log a warning but return no code lenses
// We can reconsider this when we can build a file without the need for a Nargo.toml file to resolve deps
let _ = state.client.log_message(LogMessageParams {
typ: MessageType::WARNING,
message: err.to_string(),
});
return Ok(None);
}
};
let workspace =
resolve_workspace_from_toml(&toml_path, PackageSelection::All).map_err(|err| {
// If we found a manifest, but the workspace is invalid, we raise an error about it
ResponseError::new(ErrorCode::REQUEST_FAILED, err)
})?;

let mut lenses: Vec<CodeLens> = vec![];

for package in &workspace {
let (mut context, crate_id) = prepare_package(package, Box::new(get_non_stdlib_asset));
// We ignore the warnings and errors produced by compilation for producing code lenses
// because we can still get the test functions even if compilation fails
let _ = check_crate(&mut context, crate_id, false);

let fm = &context.file_manager;
let files = fm.as_file_map();
let tests = context
.get_all_test_functions_in_crate_matching(&crate_id, FunctionNameMatch::Anything);

for (func_name, test_function) in tests {
let location = context.function_meta(&test_function.get_id()).name.location;
let file_id = location.file;

// Ignore diagnostics for any file that wasn't the file we saved
// TODO: In the future, we could create "related" diagnostics for these files
// TODO: This currently just appends the `.nr` file extension that we store as a constant,
// but that won't work if we accept other extensions
if fm.path(file_id).with_extension(FILE_EXTENSION) != file_path {
continue;
}

let range =
byte_span_to_range(files, file_id, location.span.into()).unwrap_or_default();

let test_command = Command {
title: with_arrow(TEST_CODELENS_TITLE),
command: TEST_COMMAND.into(),
arguments: Some(
[
package_selection_args(&workspace, package),
vec!["--exact".into(), func_name.into()],
]
.concat(),
),
};

let test_lens = CodeLens { range, command: Some(test_command), data: None };

lenses.push(test_lens);
}

if package.is_binary() {
if let Some(main_func_id) = context.get_main_function(&crate_id) {
let location = context.function_meta(&main_func_id).name.location;
let file_id = location.file;

// Ignore diagnostics for any file that wasn't the file we saved
// TODO: In the future, we could create "related" diagnostics for these files
// TODO: This currently just appends the `.nr` file extension that we store as a constant,
// but that won't work if we accept other extensions
if fm.path(file_id).with_extension(FILE_EXTENSION) != file_path {
continue;
}

let range =
byte_span_to_range(files, file_id, location.span.into()).unwrap_or_default();

let compile_command = Command {
title: with_arrow(COMPILE_CODELENS_TITLE),
command: COMPILE_COMMAND.into(),
arguments: Some(package_selection_args(&workspace, package)),
};

let compile_lens = CodeLens { range, command: Some(compile_command), data: None };

lenses.push(compile_lens);

let execute_command = Command {
title: EXECUTE_CODELENS_TITLE.to_string(),
command: EXECUTE_COMMAND.into(),
arguments: Some(package_selection_args(&workspace, package)),
};

let execute_lens = CodeLens { range, command: Some(execute_command), data: None };

lenses.push(execute_lens);
}
}

if package.is_contract() {
// Currently not looking to deduplicate this since we don't have a clear decision on if the Contract stuff is staying
for contract in context.get_all_contracts(&crate_id) {
let location = contract.location;
let file_id = location.file;

// Ignore diagnostics for any file that wasn't the file we saved
// TODO: In the future, we could create "related" diagnostics for these files
// TODO: This currently just appends the `.nr` file extension that we store as a constant,
// but that won't work if we accept other extensions
if fm.path(file_id).with_extension(FILE_EXTENSION) != file_path {
continue;
}

let range =
byte_span_to_range(files, file_id, location.span.into()).unwrap_or_default();

let compile_command = Command {
title: with_arrow(COMPILE_CODELENS_TITLE),
command: COMPILE_COMMAND.into(),
arguments: Some(package_selection_args(&workspace, package)),
};

let compile_lens = CodeLens { range, command: Some(compile_command), data: None };

lenses.push(compile_lens);
}
}
}

if lenses.is_empty() {
Ok(None)
} else {
Ok(Some(lenses))
}
}
99 changes: 99 additions & 0 deletions tooling/lsp/src/codelens/test_run.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
use std::future::{self, Future};

use async_lsp::{ErrorCode, ResponseError};
use nargo::{
ops::{run_test, TestStatus},
prepare_package,
};
use nargo_toml::{find_package_manifest, resolve_workspace_from_toml, PackageSelection};
use noirc_driver::{check_crate, CompileOptions};
use noirc_frontend::hir::FunctionNameMatch;

use crate::{
get_non_stdlib_asset,
types::{NargoTestRunParams, NargoTestRunResult},
LspState,
};

pub(crate) fn on_test_run_request(
state: &mut LspState,
params: NargoTestRunParams,
) -> impl Future<Output = Result<NargoTestRunResult, ResponseError>> {
future::ready(on_test_run_request_inner(state, params))
}

fn on_test_run_request_inner(
state: &mut LspState,
params: NargoTestRunParams,
) -> Result<NargoTestRunResult, ResponseError> {
let root_path = state.root_path.as_deref().ok_or_else(|| {
ResponseError::new(ErrorCode::REQUEST_FAILED, "Could not find project root")
})?;

let toml_path = find_package_manifest(root_path, root_path).map_err(|err| {
// If we cannot find a manifest, we can't run the test
ResponseError::new(ErrorCode::REQUEST_FAILED, err)
})?;

let crate_name = params.id.crate_name();
let function_name = params.id.function_name();

let workspace =
resolve_workspace_from_toml(&toml_path, PackageSelection::Selected(crate_name.clone()))
.map_err(|err| {
// If we found a manifest, but the workspace is invalid, we raise an error about it
ResponseError::new(ErrorCode::REQUEST_FAILED, err)
})?;

// Since we filtered on crate name, this should be the only item in the iterator
match workspace.into_iter().next() {
Some(package) => {
let (mut context, crate_id) = prepare_package(package, Box::new(get_non_stdlib_asset));
if check_crate(&mut context, crate_id, false).is_err() {
let result = NargoTestRunResult {
id: params.id.clone(),
result: "error".to_string(),
message: Some("The project failed to compile".into()),
};
return Ok(result);
};

let test_functions = context.get_all_test_functions_in_crate_matching(
&crate_id,
FunctionNameMatch::Exact(function_name),
);

let (_, test_function) = test_functions.into_iter().next().ok_or_else(|| {
ResponseError::new(
ErrorCode::REQUEST_FAILED,
format!("Could not locate test named: {function_name} in {crate_name}"),
)
})?;

let test_result =
run_test(&state.solver, &context, test_function, false, &CompileOptions::default());
let result = match test_result {
TestStatus::Pass => NargoTestRunResult {
id: params.id.clone(),
result: "pass".to_string(),
message: None,
},
TestStatus::Fail { message, .. } => NargoTestRunResult {
id: params.id.clone(),
result: "fail".to_string(),
message: Some(message),
},
TestStatus::CompileError(diag) => NargoTestRunResult {
id: params.id.clone(),
result: "error".to_string(),
message: Some(diag.diagnostic.message),
},
};
Ok(result)
}
None => Err(ResponseError::new(
ErrorCode::REQUEST_FAILED,
format!("Could not locate package named: {crate_name}"),
)),
}
}
68 changes: 68 additions & 0 deletions tooling/lsp/src/codelens/tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use std::future::{self, Future};

use async_lsp::{ErrorCode, LanguageClient, ResponseError};
use lsp_types::{LogMessageParams, MessageType};
use nargo::prepare_package;
use nargo_toml::{find_package_manifest, resolve_workspace_from_toml, PackageSelection};
use noirc_driver::check_crate;

use crate::{
get_non_stdlib_asset, get_package_tests_in_crate,
types::{NargoPackageTests, NargoTestsParams, NargoTestsResult},
LspState,
};

pub(crate) fn on_tests_request(
state: &mut LspState,
params: NargoTestsParams,
) -> impl Future<Output = Result<NargoTestsResult, ResponseError>> {
future::ready(on_tests_request_inner(state, params))
}

fn on_tests_request_inner(
state: &mut LspState,
_params: NargoTestsParams,
) -> Result<NargoTestsResult, ResponseError> {
let root_path = state.root_path.as_deref().ok_or_else(|| {
ResponseError::new(ErrorCode::REQUEST_FAILED, "Could not find project root")
})?;

let toml_path = match find_package_manifest(root_path, root_path) {
Ok(toml_path) => toml_path,
Err(err) => {
// If we cannot find a manifest, we log a warning but return no code lenses
// We can reconsider this when we can build a file without the need for a Nargo.toml file to resolve deps
let _ = state.client.log_message(LogMessageParams {
typ: MessageType::WARNING,
message: err.to_string(),
});
return Ok(None);
}
};

let workspace =
resolve_workspace_from_toml(&toml_path, PackageSelection::All).map_err(|err| {
// If we found a manifest, but the workspace is invalid, we raise an error about it
ResponseError::new(ErrorCode::REQUEST_FAILED, err)
})?;

let mut package_tests = Vec::new();

for package in &workspace {
let (mut context, crate_id) = prepare_package(package, Box::new(get_non_stdlib_asset));
// We ignore the warnings and errors produced by compilation for producing tests
// because we can still get the test functions even if compilation fails
let _ = check_crate(&mut context, crate_id, false);

// We don't add test headings for a package if it contains no `#[test]` functions
if let Some(tests) = get_package_tests_in_crate(&context, &crate_id, &package.name) {
package_tests.push(NargoPackageTests { package: package.name.to_string(), tests });
}
}

if package_tests.is_empty() {
Ok(None)
} else {
Ok(Some(package_tests))
}
}
Loading

0 comments on commit 8a0a9e8

Please sign in to comment.