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
@@ -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,
});

3 changes: 1 addition & 2 deletions compiler/noirc_frontend/src/hir_def/traits.rs
Original file line number Diff line number Diff line change
@@ -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,
}

1 change: 1 addition & 0 deletions compiler/noirc_frontend/src/lib.rs
Original file line number Diff line number Diff line change
@@ -16,6 +16,7 @@ pub mod lexer;
pub mod monomorphization;
pub mod node_interner;
pub mod parser;
pub mod resolve_locations;

pub mod hir;
pub mod hir_def;
132 changes: 6 additions & 126 deletions compiler/noirc_frontend/src/node_interner.rs
Original file line number Diff line number Diff line change
@@ -39,8 +39,8 @@ type StructAttributes = Vec<SecondaryAttribute>;
/// monomorphization - and it is not useful afterward.
#[derive(Debug)]
pub struct NodeInterner {
nodes: Arena<Node>,
func_meta: HashMap<FuncId, FuncMeta>,
pub(crate) nodes: Arena<Node>,
pub(crate) func_meta: HashMap<FuncId, FuncMeta>,
function_definition_ids: HashMap<FuncId, DefinitionId>,

// For a given function ID, this gives the function's modifiers which includes
@@ -52,7 +52,7 @@ pub struct NodeInterner {
function_modules: HashMap<FuncId, ModuleId>,

// Map each `Index` to it's own location
id_to_location: HashMap<Index, Location>,
pub(crate) id_to_location: HashMap<Index, Location>,

// Maps each DefinitionId to a DefinitionInfo.
definitions: Vec<DefinitionInfo>,
@@ -85,14 +85,14 @@ pub struct NodeInterner {
// Each trait definition is possibly shared across multiple type nodes.
// It is also mutated through the RefCell during name resolution to append
// methods from impls to the type.
traits: HashMap<TraitId, Trait>,
pub(crate) traits: HashMap<TraitId, Trait>,

// Trait implementation map
// For each type that implements a given Trait ( corresponding TraitId), there should be an entry here
// The purpose for this hashmap is to detect duplication of trait implementations ( if any )
//
// Indexed by TraitImplIds
trait_implementations: Vec<Shared<TraitImpl>>,
pub(crate) trait_implementations: Vec<Shared<TraitImpl>>,

/// Trait implementations on each type. This is expected to always have the same length as
/// `self.trait_implementations`.
@@ -350,7 +350,7 @@ partialeq!(StmtId);
/// This data structure is never accessed directly, so API wise there is no difference between using
/// Multiple arenas and a single Arena
#[derive(Debug, Clone)]
enum Node {
pub(crate) enum Node {
Function(HirFunction),
Statement(HirStatement),
Expression(HirExpression),
@@ -463,31 +463,6 @@ impl NodeInterner {
self.id_to_location.insert(expr_id.into(), Location::new(span, file));
}

/// Scans the interner for the item which is located at that [Location]
///
/// 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;

// Note: we can modify this in the future to not do a linear
// scan by storing a separate map of the spans or by sorting the locations.
for (index, interned_location) in self.id_to_location.iter() {
if interned_location.contains(&location) {
if let Some(current_location) = location_candidate {
if interned_location.span.is_smaller(&current_location.1.span) {
location_candidate = Some((index, interned_location));
}
} else {
location_candidate = Some((index, interned_location));
}
}
}
location_candidate.map(|(index, _location)| *index)
}

/// Interns a HIR Function.
pub fn push_fn(&mut self, func: HirFunction) -> FuncId {
FuncId(self.nodes.insert(Node::Function(func)))
@@ -1146,7 +1121,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,
@@ -1276,82 +1250,6 @@ impl NodeInterner {
self.selected_trait_implementations.get(&ident_id).cloned()
}

/// Returns the [Location] of the definition of the given Ident found at [Span] of the given [FileId].
/// Returns [None] when definition is not found.
pub fn get_definition_location_from(&self, location: Location) -> Option<Location> {
self.find_location_index(location)
.and_then(|index| self.resolve_location(index))
.or_else(|| self.try_resolve_trait_impl_location(location))
}

/// For a given [Index] we return [Location] to which we resolved to
/// We currently return None for features not yet implemented
/// TODO(#3659): LSP goto def should error when Ident at Location could not resolve
fn resolve_location(&self, index: impl Into<Index>) -> Option<Location> {
let node = self.nodes.get(index.into())?;

match node {
Node::Function(func) => self.resolve_location(func.as_expr()),
Node::Expression(expression) => self.resolve_expression_location(expression),
_ => None,
}
}

/// Resolves the [Location] of the definition for a given [HirExpression]
///
/// Note: current the code returns None because some expressions are not yet implemented.
fn resolve_expression_location(&self, expression: &HirExpression) -> Option<Location> {
match expression {
HirExpression::Ident(ident) => {
let definition_info = self.definition(ident.id);
match definition_info.kind {
DefinitionKind::Function(func_id) => {
Some(self.function_meta(&func_id).location)
}
DefinitionKind::Local(_local_id) => Some(definition_info.location),
_ => None,
}
}
HirExpression::Constructor(expr) => {
let struct_type = &expr.r#type.borrow();
Some(struct_type.location)
}
HirExpression::MemberAccess(expr_member_access) => {
self.resolve_struct_member_access(expr_member_access)
}
HirExpression::Call(expr_call) => {
let func = expr_call.func;
self.resolve_location(func)
}

_ => None,
}
}

/// Resolves the [Location] of the definition for a given [crate::hir_def::expr::HirMemberAccess]
/// This is used to resolve the location of a struct member access.
/// For example, in the expression `foo.bar` we want to resolve the location of `bar`
/// to the location of the definition of `bar` in the struct `foo`.
fn resolve_struct_member_access(
&self,
expr_member_access: &crate::hir_def::expr::HirMemberAccess,
) -> Option<Location> {
let expr_lhs = &expr_member_access.lhs;
let expr_rhs = &expr_member_access.rhs;

let lhs_self_struct = match self.id_type(expr_lhs) {
Type::Struct(struct_type, _) => struct_type,
_ => return None,
};

let struct_type = lhs_self_struct.borrow();
let field_names = struct_type.field_names();

field_names.iter().find(|field_name| field_name.0 == expr_rhs.0).map(|found_field_name| {
Location::new(found_field_name.span(), struct_type.location.file)
})
}

/// Retrieves the trait id for a given binary operator.
/// All binary operators correspond to a trait - although multiple may correspond
/// to the same trait (such as `==` and `!=`).
@@ -1436,24 +1334,6 @@ impl NodeInterner {
pub(crate) fn ordering_type(&self) -> Type {
self.ordering_type.clone().expect("Expected ordering_type to be set in the NodeInterner")
}

/// Attempts to resolve [Location] of [Trait] based on [Location] of [TraitImpl]
/// This is used by LSP to resolve the location of a trait based on the location of a trait impl.
///
/// Example:
/// impl Foo for Bar { ... } -> trait Foo { ... }
fn try_resolve_trait_impl_location(&self, location: Location) -> Option<Location> {
self.trait_implementations
.iter()
.find(|shared_trait_impl| {
let trait_impl = shared_trait_impl.borrow();
trait_impl.file == location.file && trait_impl.ident.span().contains(&location.span)
})
.and_then(|shared_trait_impl| {
let trait_impl = shared_trait_impl.borrow();
self.traits.get(&trait_impl.trait_id).map(|trait_| trait_.location)
})
}
}

impl Methods {
169 changes: 169 additions & 0 deletions compiler/noirc_frontend/src/resolve_locations.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
use arena::Index;
use noirc_errors::Location;

use crate::hir_def::expr::HirExpression;
use crate::hir_def::types::Type;

use crate::node_interner::{DefinitionKind, Node, NodeInterner};

impl NodeInterner {
/// Scans the interner for the item which is located at that [Location]
///
/// 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]
pub fn find_location_index(&self, location: Location) -> Option<impl Into<Index>> {
let mut location_candidate: Option<(&Index, &Location)> = None;

// Note: we can modify this in the future to not do a linear
// scan by storing a separate map of the spans or by sorting the locations.
for (index, interned_location) in self.id_to_location.iter() {
if interned_location.contains(&location) {
if let Some(current_location) = location_candidate {
if interned_location.span.is_smaller(&current_location.1.span) {
location_candidate = Some((index, interned_location));
}
} else {
location_candidate = Some((index, interned_location));
}
}
}
location_candidate.map(|(index, _location)| *index)
}

/// Returns the [Location] of the definition of the given Ident found at [Span] of the given [FileId].
/// Returns [None] when definition is not found.
pub fn get_definition_location_from(&self, location: Location) -> Option<Location> {
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)
})
})
}

/// For a given [Index] we return [Location] to which we resolved to
/// We currently return None for features not yet implemented
/// TODO(#3659): LSP goto def should error when Ident at Location could not resolve
fn resolve_location(&self, index: impl Into<Index>) -> Option<Location> {
let node = self.nodes.get(index.into())?;

match node {
Node::Function(func) => self.resolve_location(func.as_expr()),
Node::Expression(expression) => self.resolve_expression_location(expression),
_ => None,
}
}

/// Resolves the [Location] of the definition for a given [HirExpression]
///
/// Note: current the code returns None because some expressions are not yet implemented.
fn resolve_expression_location(&self, expression: &HirExpression) -> Option<Location> {
match expression {
HirExpression::Ident(ident) => {
let definition_info = self.definition(ident.id);
match definition_info.kind {
DefinitionKind::Function(func_id) => {
Some(self.function_meta(&func_id).location)
}
DefinitionKind::Local(_local_id) => Some(definition_info.location),
_ => None,
}
}
HirExpression::Constructor(expr) => {
let struct_type = &expr.r#type.borrow();
Some(struct_type.location)
}
HirExpression::MemberAccess(expr_member_access) => {
self.resolve_struct_member_access(expr_member_access)
}
HirExpression::Call(expr_call) => {
let func = expr_call.func;
self.resolve_location(func)
}

_ => None,
}
}

/// Resolves the [Location] of the definition for a given [crate::hir_def::expr::HirMemberAccess]
/// This is used to resolve the location of a struct member access.
/// For example, in the expression `foo.bar` we want to resolve the location of `bar`
/// to the location of the definition of `bar` in the struct `foo`.
fn resolve_struct_member_access(
&self,
expr_member_access: &crate::hir_def::expr::HirMemberAccess,
) -> Option<Location> {
let expr_lhs = &expr_member_access.lhs;
let expr_rhs = &expr_member_access.rhs;

let lhs_self_struct = match self.id_type(expr_lhs) {
Type::Struct(struct_type, _) => struct_type,
_ => return None,
};

let struct_type = lhs_self_struct.borrow();
let field_names = struct_type.field_names();

field_names.iter().find(|field_name| field_name.0 == expr_rhs.0).map(|found_field_name| {
Location::new(found_field_name.span(), struct_type.location.file)
})
}

/// Attempts to resolve [Location] of [Trait] based on [Location] of [TraitImpl]
/// This is used by LSP to resolve the location of a trait based on the location of a trait impl.
///
/// Example:
/// impl Foo for Bar { ... } -> trait Foo { ... }
fn try_resolve_trait_impl_location(&self, location: Location) -> Option<Location> {
self.trait_implementations
.iter()
.find(|shared_trait_impl| {
let trait_impl = shared_trait_impl.borrow();
trait_impl.file == location.file && trait_impl.ident.span().contains(&location.span)
})
.and_then(|shared_trait_impl| {
let trait_impl = shared_trait_impl.borrow();
self.traits.get(&trait_impl.trait_id).map(|trait_| trait_.location)
})
}

/// Attempts to resolve [Location] of [Trait]'s [TraitFunction] declaration based on [Location] of [TraitFunction] call.
///
/// This is used by LSP to resolve the location.
///
/// ### Example:
/// ```nr
/// trait Fieldable {
/// fn to_field(self) -> Field;
/// ^------------------------------\
/// } |
/// |
/// fn main_func(x: u32) { |
/// assert(x.to_field() == 15); |
/// \......................./
/// }
/// ```
///
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)?;

let mut methods = self.traits.get(&trait_id)?.methods.iter();
let method =
methods.find(|method| method.name.0.contents == self.function_name(func_id));
method.map(|method| method.location)
})
}
}
5 changes: 3 additions & 2 deletions tooling/lsp/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
@@ -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)
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
@@ -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,
@@ -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;
@@ -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);
}
}
98 changes: 95 additions & 3 deletions tooling/lsp/src/requests/mod.rs
Original file line number Diff line number Diff line change
@@ -2,7 +2,11 @@ use std::future::Future;

use crate::types::{CodeLensOptions, InitializeParams};
use async_lsp::ResponseError;
use lsp_types::{Position, TextDocumentSyncCapability, TextDocumentSyncKind};
use fm::codespan_files::Error;
use lsp_types::{
DeclarationCapability, Location, Position, TextDocumentSyncCapability, TextDocumentSyncKind,
Url,
};
use nargo_fmt::Config;
use serde::{Deserialize, Serialize};

@@ -22,15 +26,16 @@ use crate::{
// and params passed in.

mod code_lens_request;
mod goto_declaration;
mod goto_definition;
mod profile_run;
mod test_run;
mod tests;

pub(crate) use {
code_lens_request::collect_lenses_for_package, code_lens_request::on_code_lens_request,
goto_definition::on_goto_definition_request, profile_run::on_profile_run_request,
test_run::on_test_run_request, tests::on_tests_request,
goto_declaration::on_goto_declaration_request, goto_definition::on_goto_definition_request,
profile_run::on_profile_run_request, test_run::on_test_run_request, tests::on_tests_request,
};

/// LSP client will send initialization request after the server has started.
@@ -88,6 +93,7 @@ pub(crate) fn on_initialize(
document_formatting_provider: true,
nargo: Some(nargo),
definition_provider: Some(lsp_types::OneOf::Left(true)),
declaration_provider: Some(DeclarationCapability::Simple(true)),
},
server_info: None,
})
@@ -130,6 +136,70 @@ fn on_formatting_inner(
}
}

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 })
}
}

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() })
}
}

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 on_shutdown(
_state: &mut LspState,
_params: (),
@@ -170,3 +240,25 @@ mod initialization {
assert!(response.server_info.is_none());
}
}

#[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);
}
}
9 changes: 7 additions & 2 deletions tooling/lsp/src/types.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use fm::FileId;
use lsp_types::{DefinitionOptions, OneOf};
use lsp_types::{DeclarationCapability, DefinitionOptions, OneOf};
use noirc_driver::DebugFile;
use noirc_errors::{debug_info::OpCodesCount, Location};
use noirc_frontend::graph::CrateName;
@@ -25,7 +25,7 @@ pub(crate) mod request {

// Re-providing lsp_types that we don't need to override
pub(crate) use lsp_types::request::{
CodeLensRequest as CodeLens, Formatting, GotoDefinition, Shutdown,
CodeLensRequest as CodeLens, Formatting, GotoDeclaration, GotoDefinition, Shutdown,
};

#[derive(Debug)]
@@ -110,6 +110,10 @@ pub(crate) struct ServerCapabilities {
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) text_document_sync: Option<TextDocumentSyncCapability>,

/// The server provides go to declaration support.
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) declaration_provider: Option<DeclarationCapability>,

/// The server provides goto definition support.
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) definition_provider: Option<OneOf<bool, DefinitionOptions>>,
@@ -222,3 +226,4 @@ pub(crate) struct NargoProfileRunResult {

pub(crate) type CodeLensResult = Option<Vec<CodeLens>>;
pub(crate) type GotoDefinitionResult = Option<lsp_types::GotoDefinitionResponse>;
pub(crate) type GotoDeclarationResult = Option<lsp_types::request::GotoDeclarationResponse>;

Unchanged files with check annotations Beta

### ⚠ BREAKING CHANGES
* add backend-solvable blackboxes to brillig & unify implementations ([#422](https://github.com/noir-lang/acvm/issues/422))

Check warning on line 151 in acvm-repo/acir/CHANGELOG.md

GitHub Actions / Code

Unknown word (blackboxes)
* Returns index of failing opcode and transformation mapping ([#412](https://github.com/noir-lang/acvm/issues/412))
### Features
* add backend-solvable blackboxes to brillig & unify implementations ([#422](https://github.com/noir-lang/acvm/issues/422)) ([093342e](https://github.com/noir-lang/acvm/commit/093342ea9481a311fa71343b8b7a22774788838a))

Check warning on line 156 in acvm-repo/acir/CHANGELOG.md

GitHub Actions / Code

Unknown word (blackboxes)
* Returns index of failing opcode and transformation mapping ([#412](https://github.com/noir-lang/acvm/issues/412)) ([79950e9](https://github.com/noir-lang/acvm/commit/79950e943f60e4082e1cf5ec4442aa67ea91aade))
## [0.17.0](https://github.com/noir-lang/acvm/compare/acir-v0.16.0...acir-v0.17.0) (2023-07-07)
* **acir:** Add RAM and ROM opcodes
* **acir:** Add a public outputs field ([#56](https://github.com/noir-lang/acvm/issues/56))
* **acir:** remove `Linear` struct ([#145](https://github.com/noir-lang/acvm/issues/145))
* **acvm:** Remove truncate and oddrange directives ([#142](https://github.com/noir-lang/acvm/issues/142))

Check warning on line 461 in acvm-repo/acir/CHANGELOG.md

GitHub Actions / Code

Unknown word (oddrange)
### Features
### Miscellaneous Chores
* **acir:** remove `Linear` struct ([#145](https://github.com/noir-lang/acvm/issues/145)) ([bbb6d92](https://github.com/noir-lang/acvm/commit/bbb6d92e25c43dd33b12f5fcd639fc9ad2a9c9d8))
* **acvm:** Remove truncate and oddrange directives ([#142](https://github.com/noir-lang/acvm/issues/142)) ([85dd6e8](https://github.com/noir-lang/acvm/commit/85dd6e85bfba85bfb97651f7e30e1f75deb986d5))

Check warning on line 473 in acvm-repo/acir/CHANGELOG.md

GitHub Actions / Code

Unknown word (oddrange)
## [0.6.0](https://github.com/noir-lang/acvm/compare/acir-v0.5.0...acir-v0.6.0) (2023-03-03)
### Features
* **acvm_js:** Add `execute_circuit_with_black_box_solver` to prevent reinitialization of `BlackBoxFunctionSolver` ([3877e0e](https://github.com/noir-lang/acvm/commit/3877e0e438a8d0e5545a4da7210767dec05c342f))

Check warning on line 124 in acvm-repo/acvm/CHANGELOG.md

GitHub Actions / Code

Unknown word (reinitialization)
### Miscellaneous Chores
### ⚠ BREAKING CHANGES
* add backend-solvable blackboxes to brillig & unify implementations ([#422](https://github.com/noir-lang/acvm/issues/422))

Check warning on line 287 in acvm-repo/acvm/CHANGELOG.md

GitHub Actions / Code

Unknown word (blackboxes)
* **acvm:** Remove `CircuitSimplifier` ([#421](https://github.com/noir-lang/acvm/issues/421))
* **acvm:** Add `circuit: &Circuit` to `eth_contract_from_vk` function signature ([#420](https://github.com/noir-lang/acvm/issues/420))
* Returns index of failing opcode and transformation mapping ([#412](https://github.com/noir-lang/acvm/issues/412))
### Features
* **acvm:** Add `circuit: &Circuit` to `eth_contract_from_vk` function signature ([#420](https://github.com/noir-lang/acvm/issues/420)) ([744e9da](https://github.com/noir-lang/acvm/commit/744e9da71f7ca477a5390a63f47211dd4dffb8b3))
* add backend-solvable blackboxes to brillig & unify implementations ([#422](https://github.com/noir-lang/acvm/issues/422)) ([093342e](https://github.com/noir-lang/acvm/commit/093342ea9481a311fa71343b8b7a22774788838a))

Check warning on line 295 in acvm-repo/acvm/CHANGELOG.md

GitHub Actions / Code

Unknown word (blackboxes)
* Returns index of failing opcode and transformation mapping ([#412](https://github.com/noir-lang/acvm/issues/412)) ([79950e9](https://github.com/noir-lang/acvm/commit/79950e943f60e4082e1cf5ec4442aa67ea91aade))
* **stdlib:** Add fallback implementation of `SHA256` black box function ([#407](https://github.com/noir-lang/acvm/issues/407)) ([040369a](https://github.com/noir-lang/acvm/commit/040369adc8749fa5ec2edd255ff54c105c3140f5))
* **acir:** Add RAM and ROM opcodes
* **acir:** Add a public outputs field ([#56](https://github.com/noir-lang/acvm/issues/56))
* **acvm:** remove `prove_with_meta` and `verify_from_cs` from `ProofSystemCompiler` ([#140](https://github.com/noir-lang/acvm/issues/140))
* **acvm:** Remove truncate and oddrange directives ([#142](https://github.com/noir-lang/acvm/issues/142))

Check warning on line 609 in acvm-repo/acvm/CHANGELOG.md

GitHub Actions / Code

Unknown word (oddrange)
### Features
### Miscellaneous Chores
* **acvm:** remove `prove_with_meta` and `verify_from_cs` from `ProofSystemCompiler` ([#140](https://github.com/noir-lang/acvm/issues/140)) ([35dd181](https://github.com/noir-lang/acvm/commit/35dd181102203df17eef510666b327ef41f4b036))
* **acvm:** Remove truncate and oddrange directives ([#142](https://github.com/noir-lang/acvm/issues/142)) ([85dd6e8](https://github.com/noir-lang/acvm/commit/85dd6e85bfba85bfb97651f7e30e1f75deb986d5))

Check warning on line 621 in acvm-repo/acvm/CHANGELOG.md

GitHub Actions / Code

Unknown word (oddrange)
## [0.6.0](https://github.com/noir-lang/acvm/compare/acvm-v0.5.0...acvm-v0.6.0) (2023-03-03)
use pedersen::pedersen;
use range::solve_range_opcode;
use signature::{
ecdsa::{secp256k1_prehashed, secp256r1_prehashed},

Check warning on line 27 in acvm-repo/acvm/src/pwg/blackbox/mod.rs

GitHub Actions / Code

Unknown word (prehashed)
schnorr::schnorr_verify,
};