Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(lsp): goto trait method declaration #3991

Merged
merged 10 commits into from
Jan 11, 2024
3 changes: 1 addition & 2 deletions compiler/noirc_frontend/src/hir/resolution/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,9 +159,8 @@ fn resolve_trait_methods(
functions.push(TraitFunction {
name: name.clone(),
typ: Type::Forall(generics, Box::new(function_type)),
span: name.span(),
location: Location::new(name.span(), unresolved_trait.file_id),
default_impl,
default_impl_file_id: unresolved_trait.file_id,
default_impl_module_id: unresolved_trait.module_id,
});

Expand Down
3 changes: 1 addition & 2 deletions compiler/noirc_frontend/src/hir_def/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@ use noirc_errors::{Location, Span};
pub struct TraitFunction {
pub name: Ident,
pub typ: Type,
pub span: Span,
pub location: Location,
pub default_impl: Option<Box<NoirFunction>>,
pub default_impl_file_id: fm::FileId,
pub default_impl_module_id: crate::hir::def_map::LocalModuleId,
}

Expand Down
29 changes: 27 additions & 2 deletions compiler/noirc_frontend/src/node_interner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -468,7 +468,6 @@ impl NodeInterner {
/// The [Location] may not necessarily point to the beginning of the item
/// so we check if the location's span is contained within the start or end
/// of each items [Span]
#[tracing::instrument(skip(self))]
pub fn find_location_index(&self, location: Location) -> Option<impl Into<Index>> {
let mut location_candidate: Option<(&Index, &Location)> = None;

Expand Down Expand Up @@ -1146,7 +1145,6 @@ impl NodeInterner {
}

/// Adds a trait implementation to the list of known implementations.
#[tracing::instrument(skip(self))]
pub fn add_trait_implementation(
&mut self,
object_type: Type,
Expand Down Expand Up @@ -1282,6 +1280,17 @@ impl NodeInterner {
self.find_location_index(location)
.and_then(|index| self.resolve_location(index))
.or_else(|| self.try_resolve_trait_impl_location(location))
.or_else(|| self.try_resolve_trait_method_declaration(location))
}

pub fn get_declaration_location_from(&self, location: Location) -> Option<Location> {
self.try_resolve_trait_method_declaration(location).or_else(|| {
self.find_location_index(location)
.and_then(|index| self.resolve_location(index))
.and_then(|found_impl_location| {
self.try_resolve_trait_method_declaration(found_impl_location)
})
})
kobyhallx marked this conversation as resolved.
Show resolved Hide resolved
}

/// For a given [Index] we return [Location] to which we resolved to
Expand Down Expand Up @@ -1454,6 +1463,22 @@ impl NodeInterner {
self.traits.get(&trait_impl.trait_id).map(|trait_| trait_.location)
})
}

fn try_resolve_trait_method_declaration(&self, location: Location) -> Option<Location> {
self.func_meta
.iter()
.find(|(_, func_meta)| func_meta.location.contains(&location))
.and_then(|(func_id, _func_meta)| {
let (_, trait_id) = self.get_function_trait(func_id)?;

self.traits
.get(&trait_id)?
.methods
.iter()
.find(|method| method.name.0.contents == self.function_name(func_id))
.map(|method| method.location)
kobyhallx marked this conversation as resolved.
Show resolved Hide resolved
})
}
}

impl Methods {
Expand Down
5 changes: 3 additions & 2 deletions tooling/lsp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ use notifications::{
on_did_open_text_document, on_did_save_text_document, on_exit, on_initialized,
};
use requests::{
on_code_lens_request, on_formatting, on_goto_definition_request, on_initialize,
on_profile_run_request, on_shutdown, on_test_run_request, on_tests_request,
on_code_lens_request, on_formatting, on_goto_declaration_request, on_goto_definition_request,
on_initialize, on_profile_run_request, on_shutdown, on_test_run_request, on_tests_request,
};
use serde_json::Value as JsonValue;
use thiserror::Error;
Expand Down Expand Up @@ -97,6 +97,7 @@ impl NargoLspService {
.request::<request::NargoTestRun, _>(on_test_run_request)
.request::<request::NargoProfileRun, _>(on_profile_run_request)
.request::<request::GotoDefinition, _>(on_goto_definition_request)
.request::<request::GotoDeclaration, _>(on_goto_declaration_request)
.notification::<notification::Initialized>(on_initialized)
.notification::<notification::DidChangeConfiguration>(on_did_change_configuration)
.notification::<notification::DidOpenTextDocument>(on_did_open_text_document)
Expand Down
80 changes: 80 additions & 0 deletions tooling/lsp/src/requests/goto_declaration.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
use std::future::{self, Future};

use crate::resolve_workspace_for_source_path;
use crate::types::GotoDeclarationResult;
use crate::LspState;
use async_lsp::{ErrorCode, ResponseError};

use lsp_types::request::{GotoDeclarationParams, GotoDeclarationResponse};

use nargo::insert_all_files_for_workspace_into_file_manager;
use noirc_driver::file_manager_with_stdlib;

use super::{position_to_byte_index, to_lsp_location};

pub(crate) fn on_goto_declaration_request(
state: &mut LspState,
params: GotoDeclarationParams,
) -> impl Future<Output = Result<GotoDeclarationResult, ResponseError>> {
let result = on_goto_definition_inner(state, params);
future::ready(result)
}

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

let workspace = resolve_workspace_for_source_path(file_path.as_path()).unwrap();
let package = workspace.members.first().unwrap();

let package_root_path: String = package.root_dir.as_os_str().to_string_lossy().into();

let mut workspace_file_manager = file_manager_with_stdlib(&workspace.root_dir);
insert_all_files_for_workspace_into_file_manager(&workspace, &mut workspace_file_manager);

let (mut context, crate_id) = nargo::prepare_package(&workspace_file_manager, package);

let interner;
if let Some(def_interner) = _state.cached_definitions.get(&package_root_path) {
interner = def_interner;
} else {
// We ignore the warnings and errors produced by compilation while resolving the definition
let _ = noirc_driver::check_crate(&mut context, crate_id, false, false);
interner = &context.def_interner;
}

let files = context.file_manager.as_file_map();
let file_id = context.file_manager.name_to_id(file_path.clone()).ok_or(ResponseError::new(
ErrorCode::REQUEST_FAILED,
format!("Could not find file in file manager. File path: {:?}", file_path),
))?;
let byte_index =
position_to_byte_index(files, file_id, &params.text_document_position_params.position)
.map_err(|err| {
ResponseError::new(
ErrorCode::REQUEST_FAILED,
format!("Could not convert position to byte index. Error: {:?}", err),
)
})?;

let search_for_location = noirc_errors::Location {
file: file_id,
span: noirc_errors::Span::single_char(byte_index as u32),
};

let goto_declaration_response =
interner.get_declaration_location_from(search_for_location).and_then(|found_location| {
let file_id = found_location.file;
let definition_position = to_lsp_location(files, file_id, found_location.span)?;
let response: GotoDeclarationResponse =
GotoDeclarationResponse::from(definition_position).to_owned();
Some(response)
});

Ok(goto_declaration_response)
}
99 changes: 5 additions & 94 deletions tooling/lsp/src/requests/goto_definition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ use std::future::{self, Future};
use crate::resolve_workspace_for_source_path;
use crate::{types::GotoDefinitionResult, LspState};
use async_lsp::{ErrorCode, ResponseError};
use fm::codespan_files::Error;
use lsp_types::{GotoDefinitionParams, GotoDefinitionResponse, Location};
use lsp_types::{Position, Url};

use lsp_types::{GotoDefinitionParams, GotoDefinitionResponse};
use nargo::insert_all_files_for_workspace_into_file_manager;
use noirc_driver::file_manager_with_stdlib;

use super::{position_to_byte_index, to_lsp_location};

pub(crate) fn on_goto_definition_request(
state: &mut LspState,
params: GotoDefinitionParams,
Expand Down Expand Up @@ -76,79 +77,11 @@ fn on_goto_definition_inner(
Ok(goto_definition_response)
}

fn to_lsp_location<'a, F>(
files: &'a F,
file_id: F::FileId,
definition_span: noirc_errors::Span,
) -> Option<Location>
where
F: fm::codespan_files::Files<'a> + ?Sized,
{
let range = crate::byte_span_to_range(files, file_id, definition_span.into())?;
let file_name = files.name(file_id).ok()?;

let path = file_name.to_string();
let uri = Url::from_file_path(path).ok()?;

Some(Location { uri, range })
}

pub(crate) fn position_to_byte_index<'a, F>(
files: &'a F,
file_id: F::FileId,
position: &Position,
) -> Result<usize, Error>
where
F: fm::codespan_files::Files<'a> + ?Sized,
{
let source = files.source(file_id)?;
let source = source.as_ref();

let line_span = files.line_range(file_id, position.line as usize)?;

let line_str = source.get(line_span.clone());

if let Some(line_str) = line_str {
let byte_offset = character_to_line_offset(line_str, position.character)?;
Ok(line_span.start + byte_offset)
} else {
Err(Error::InvalidCharBoundary { given: position.line as usize })
}
}

/// Calculates the byte offset of a given character in a line.
/// LSP Clients (editors, eg. neovim) use a different coordinate (LSP Positions) system than the compiler.
///
/// LSP Positions navigate through line numbers and character numbers, eg. `(line: 1, character: 5)`
/// meanwhile byte indexes are used within the compiler to navigate through the source code.
fn character_to_line_offset(line: &str, character: u32) -> Result<usize, Error> {
let line_len = line.len();
let mut character_offset = 0;

let mut chars = line.chars();
while let Some(ch) = chars.next() {
if character_offset == character {
let chars_off = chars.as_str().len();
let ch_off = ch.len_utf8();

return Ok(line_len - chars_off - ch_off);
}

character_offset += ch.len_utf16() as u32;
}

// Handle positions after the last character on the line
if character_offset == character {
Ok(line_len)
} else {
Err(Error::ColumnTooLarge { given: character_offset as usize, max: line.len() })
}
}

#[cfg(test)]
mod goto_definition_tests {

use async_lsp::ClientSocket;
use lsp_types::{Position, Url};
use tokio::test;

use crate::solver::MockBackend;
Expand Down Expand Up @@ -204,25 +137,3 @@ mod goto_definition_tests {
assert!(&response.is_some());
}
}

#[cfg(test)]
mod character_to_line_offset_tests {
use super::*;

#[test]
fn test_character_to_line_offset() {
let line = "Hello, dark!";
let character = 8;

let result = character_to_line_offset(line, character).unwrap();
assert_eq!(result, 8);

// In the case of a multi-byte character, the offset should be the byte index of the character
// byte offset for 8 character (黑) is expected to be 10
let line = "Hello, 黑!";
let character = 8;

let result = character_to_line_offset(line, character).unwrap();
assert_eq!(result, 10);
}
}
Loading
Loading