Skip to content

Commit

Permalink
Limited autocomplete for expressions (FuelLabs#4207)
Browse files Browse the repository at this point in the history
## Description

This PR adds autocomplete functionality for expressions to the LSP. It's
triggered by entering `.` after a variable.

Closes FuelLabs#2408

### Use cases

- Works inside of `main` functions
- Works inside of other functions outside of an impl
- Supports function chaining
- Finds all methods for a struct anywhere in the namespace

### Limitations

- Does not work inside of impl methods. This is because "incomplete"
impl methods apparently cause the compiler to error out before
generating tokens for the file
- Can be slow to populate. This slowness is due to the compile time
after every keystroke.
- Doesn't show up in certain cases, depending on what other errors are
inside the file. This is because the compiler won't generate tokens when
certain errors are present.

### Screen captures

![Feb-28-2023
16-36-04](https://user-images.githubusercontent.com/47993817/222015034-e488180e-4389-4bdc-9b98-f4f3c646a11a.gif)

method chaining

![Feb-28-2023
16-39-35](https://user-images.githubusercontent.com/47993817/222015426-eea06066-d07d-4fdb-a488-3e20802de79e.gif)

## Checklist

- [x] I have linked to any relevant issues.
- [x] I have commented my code, particularly in hard-to-understand
areas.
- [ ] I have updated the documentation where relevant (API docs, the
reference, and the Sway book).
- [x] I have added tests that prove my fix is effective or that my
feature works.
- [ ] I have added (or requested a maintainer to add) the necessary
`Breaking*` or `New Feature` labels where relevant.
- [x] I have done my best to ensure that my PR adheres to [the Fuel Labs
Code Review
Standards](https://github.com/FuelLabs/rfcs/blob/master/text/code-standards/external-contributors.md).
- [x] I have requested a review from the relevant team or maintainers.
  • Loading branch information
sdankel authored Mar 9, 2023
1 parent 49eae2d commit dd01aef
Show file tree
Hide file tree
Showing 16 changed files with 434 additions and 106 deletions.
2 changes: 1 addition & 1 deletion sway-core/src/decl_engine/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#[allow(clippy::module_inception)]
pub(crate) mod engine;
pub(crate) mod functional_decl_id;
pub mod functional_decl_id;
pub mod id;
pub(crate) mod interface_decl_id;
pub(crate) mod mapping;
Expand Down
2 changes: 1 addition & 1 deletion sway-core/src/language/ty/ast_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ pub trait GetDeclIdent {
#[derive(Clone, Debug)]
pub struct TyAstNode {
pub content: TyAstNodeContent,
pub(crate) span: Span,
pub span: Span,
}

impl EqWithEngines for TyAstNode {}
Expand Down
2 changes: 1 addition & 1 deletion sway-core/src/semantic_analysis/namespace/items.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ impl Items {
self.implemented_traits.insert_for_type(engines, type_id);
}

pub(crate) fn get_methods_for_type(
pub fn get_methods_for_type(
&self,
engines: Engines<'_>,
type_id: TypeId,
Expand Down
15 changes: 15 additions & 0 deletions sway-core/src/type_system/info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,21 @@ impl DisplayWithEngines for TypeInfo {
}
}

impl TypeInfo {
pub fn display_name(&self) -> String {
match self {
TypeInfo::UnknownGeneric { name, .. } => name.to_string(),
TypeInfo::Placeholder(type_param) => type_param.name_ident.to_string(),
TypeInfo::Enum(decl_ref) => decl_ref.name.to_string(),
TypeInfo::Struct(decl_ref) => decl_ref.name.to_string(),
TypeInfo::ContractCaller { abi_name, .. } => abi_name.to_string(),
TypeInfo::Custom { call_path, .. } => call_path.to_string(),
TypeInfo::Storage { .. } => "storage".into(),
_ => format!("{self:?}"),
}
}
}

impl UnconstrainedTypeParameters for TypeInfo {
fn type_parameter_is_unconstrained(
&self,
Expand Down
211 changes: 170 additions & 41 deletions sway-lsp/src/capabilities/completion.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,86 @@
use crate::core::{
token::{AstToken, SymbolKind, Token, TypedAstToken},
token_map::TokenMap,
use sway_core::{
language::ty::{TyAstNodeContent, TyDeclaration, TyFunctionDeclaration},
namespace::Items,
Engines, TypeId, TypeInfo,
};
use tower_lsp::lsp_types::{CompletionItem, CompletionItemKind};
use sway_types::Ident;
use tower_lsp::lsp_types::{
CompletionItem, CompletionItemKind, CompletionItemLabelDetails, CompletionTextEdit, Position,
Range, TextEdit,
};

pub(crate) fn to_completion_items(
namespace: &Items,
engines: Engines<'_>,
ident_to_complete: &Ident,
fn_decl: &TyFunctionDeclaration,
position: Position,
) -> Vec<CompletionItem> {
type_id_of_raw_ident(engines, namespace, ident_to_complete, fn_decl)
.map(|type_id| completion_items_for_type_id(engines, namespace, type_id, position))
.unwrap_or_default()
}

pub(crate) fn to_completion_items(token_map: &TokenMap) -> Vec<CompletionItem> {
/// Gathers the given [TypeId] struct's fields and methods and builds completion items.
fn completion_items_for_type_id(
engines: Engines<'_>,
namespace: &Items,
type_id: TypeId,
position: Position,
) -> Vec<CompletionItem> {
let mut completion_items = vec![];
let type_info = engines.te().get(type_id);

for item in token_map.iter() {
let ((ident, _), token) = item.pair();
if is_initial_declaration(token) {
if let TypeInfo::Struct(decl_ref) = type_info.clone() {
let struct_decl = engines.de().get_struct(&decl_ref.id);
for field in struct_decl.fields {
let item = CompletionItem {
label: ident.as_str().to_string(),
kind: completion_item_kind(&token.kind),
kind: Some(CompletionItemKind::FIELD),
label: field.name.as_str().to_string(),
label_details: Some(CompletionItemLabelDetails {
description: Some(field.type_argument.span.str()),
detail: None,
}),
..Default::default()
};
completion_items.push(item);
}
}

for method in namespace.get_methods_for_type(engines, type_id) {
let fn_decl = engines.de().get_function(&method.id);
let params = fn_decl.clone().parameters;

// Only show methods that take `self` as the first parameter.
if params.first().map(|p| p.is_self()).unwrap_or(false) {
let params_short = match params.is_empty() {
true => "()".to_string(),
false => "(…)".to_string(),
};
let params_edit_str = params
.iter()
.filter_map(|p| {
if p.is_self() {
return None;
}
Some(p.name.as_str())
})
.collect::<Vec<&str>>()
.join(", ");
let item = CompletionItem {
kind: Some(CompletionItemKind::METHOD),
label: format!("{}{}", method.name.as_str(), params_short),
text_edit: Some(CompletionTextEdit::Edit(TextEdit {
range: Range {
start: position,
end: position,
},
new_text: format!("{}({})", method.name.as_str(), params_edit_str),
})),
label_details: Some(CompletionItemLabelDetails {
description: Some(fn_signature_string(&fn_decl, &type_info)),
detail: None,
}),
..Default::default()
};
completion_items.push(item);
Expand All @@ -22,40 +90,101 @@ pub(crate) fn to_completion_items(token_map: &TokenMap) -> Vec<CompletionItem> {
completion_items
}

/// Given a `SymbolKind`, return the `lsp_types::CompletionItemKind` that corresponds to it.
pub fn completion_item_kind(symbol_kind: &SymbolKind) -> Option<CompletionItemKind> {
match symbol_kind {
SymbolKind::Field => Some(CompletionItemKind::FIELD),
SymbolKind::BuiltinType => Some(CompletionItemKind::TYPE_PARAMETER),
SymbolKind::ValueParam => Some(CompletionItemKind::VALUE),
SymbolKind::Function | SymbolKind::DeriveHelper => Some(CompletionItemKind::FUNCTION),
SymbolKind::Const => Some(CompletionItemKind::CONSTANT),
SymbolKind::Struct => Some(CompletionItemKind::STRUCT),
SymbolKind::Trait => Some(CompletionItemKind::INTERFACE),
SymbolKind::Module => Some(CompletionItemKind::MODULE),
SymbolKind::Enum => Some(CompletionItemKind::ENUM),
SymbolKind::Variant => Some(CompletionItemKind::ENUM_MEMBER),
SymbolKind::TypeParameter => Some(CompletionItemKind::TYPE_PARAMETER),
SymbolKind::BoolLiteral
| SymbolKind::ByteLiteral
| SymbolKind::StringLiteral
| SymbolKind::NumericLiteral => Some(CompletionItemKind::VALUE),
SymbolKind::Variable => Some(CompletionItemKind::VARIABLE),
SymbolKind::Keyword => Some(CompletionItemKind::KEYWORD),
SymbolKind::Unknown => None,
/// Returns the [String] of the shortened function signature to display in the completion item's label details.
fn fn_signature_string(fn_decl: &TyFunctionDeclaration, parent_type_info: &TypeInfo) -> String {
let params_str = fn_decl
.parameters
.iter()
.map(|p| replace_self_with_type_str(p.type_argument.clone().span.str(), parent_type_info))
.collect::<Vec<String>>()
.join(", ");
format!(
"fn({}) -> {}",
params_str,
replace_self_with_type_str(fn_decl.return_type.clone().span.str(), parent_type_info)
)
}

/// Given a [String] representing a type, replaces `Self` with the display name of the type.
fn replace_self_with_type_str(type_str: String, parent_type_info: &TypeInfo) -> String {
if type_str == "Self" {
return parent_type_info.display_name();
}
type_str
}

fn is_initial_declaration(token_type: &Token) -> bool {
match &token_type.typed {
Some(typed_ast_token) => {
matches!(
typed_ast_token,
TypedAstToken::TypedDeclaration(_) | TypedAstToken::TypedFunctionDeclaration(_)
)
}
None => {
matches!(token_type.parsed, AstToken::Declaration(_))
/// Returns the [TypeId] of an ident that may include field accesses and may be incomplete.
/// For the first part of the ident, it looks for instantiation in the scope of the given
/// [TyFunctionDeclaration]. For example, given `a.b.c`, it will return the type ID of `c`
/// if it can resolve `a` in the given function.
fn type_id_of_raw_ident(
engines: Engines,
namespace: &Items,
ident: &Ident,
fn_decl: &TyFunctionDeclaration,
) -> Option<TypeId> {
let full_ident = ident.as_str();

// If this ident has no field accesses or chained methods, look for it in the local function scope.
if !full_ident.contains('.') {
return type_id_of_local_ident(full_ident, fn_decl);
}

// Otherwise, start with the first part of the ident and follow the subsequent types.
let parts = full_ident.split('.').collect::<Vec<&str>>();
let mut curr_type_id = type_id_of_local_ident(parts[0], fn_decl);
let mut i = 1;

while (i < parts.len()) && curr_type_id.is_some() {
if parts[i].ends_with(')') {
let method_name = parts[i].split_at(parts[i].find('(').unwrap_or(0)).0;
curr_type_id = namespace
.get_methods_for_type(engines, curr_type_id?)
.iter()
.find_map(|decl_ref| {
if decl_ref.name.as_str() == method_name {
return Some(engines.de().get_function(&decl_ref.id).return_type.type_id);
}
None
});
} else if let TypeInfo::Struct(decl_ref) = engines.te().get(curr_type_id.unwrap()) {
let struct_decl = engines.de().get_struct(&decl_ref.id);
curr_type_id = struct_decl
.fields
.iter()
.find(|field| field.name.to_string() == parts[i])
.map(|field| field.type_argument.type_id);
}
i += 1;
}
curr_type_id
}

/// Returns the [TypeId] of an ident by looking for its instantiation within the scope of the
/// given [TyFunctionDeclaration].
fn type_id_of_local_ident(ident_name: &str, fn_decl: &TyFunctionDeclaration) -> Option<TypeId> {
fn_decl
.parameters
.iter()
.find_map(|param| {
// Check if this ident is a function parameter
if param.name.as_str() == ident_name {
return Some(param.type_argument.type_id);
}
None
})
.or_else(|| {
// Check if there is a variable declaration for this ident
fn_decl.body.contents.iter().find_map(|node| {
if let TyAstNodeContent::Declaration(TyDeclaration::VariableDeclaration(
variable_decl,
)) = node.content.clone()
{
if variable_decl.name.as_str() == ident_name {
return Some(variable_decl.return_type);
}
}
None
})
})
}
39 changes: 33 additions & 6 deletions sway-lsp/src/core/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ use crate::{
runnable::{Runnable, RunnableMainFn, RunnableTestFn},
},
core::{
document::TextDocument, sync::SyncWorkspace, token::get_range_from_span,
document::TextDocument,
sync::SyncWorkspace,
token::{get_range_from_span, TypedAstToken},
token_map::TokenMap,
},
error::{DocumentError, LanguageServerError},
Expand Down Expand Up @@ -75,6 +77,7 @@ impl Session {

pub fn init(&self, uri: &Url) -> Result<ProjectDirectory, LanguageServerError> {
let manifest_dir = PathBuf::from(uri.path());

// Create a new temp dir that clones the current workspace
// and store manifest and temp paths
self.sync.create_temp_dir_from_workspace(&manifest_dir)?;
Expand Down Expand Up @@ -244,10 +247,34 @@ impl Session {
})
}

pub fn completion_items(&self) -> Option<Vec<CompletionItem>> {
Some(capabilities::completion::to_completion_items(
self.token_map(),
))
pub fn completion_items(
&self,
uri: &Url,
position: Position,
trigger_char: String,
) -> Option<Vec<CompletionItem>> {
let shifted_position = Position {
line: position.line,
character: position.character - trigger_char.len() as u32 - 1,
};
let (ident_to_complete, _) = self.token_map.token_at_position(uri, shifted_position)?;
let fn_tokens = self
.token_map
.tokens_at_position(uri, shifted_position, Some(true));
let (_, fn_token) = fn_tokens.first()?;
let compiled_program = &*self.compiled_program.read();

if let Some(TypedAstToken::TypedFunctionDeclaration(fn_decl)) = fn_token.typed.clone() {
let program = compiled_program.typed.clone()?;
return Some(capabilities::completion::to_completion_items(
&program.root.namespace,
Engines::new(&self.type_engine.read(), &self.decl_engine.read()),
&ident_to_complete,
&fn_decl,
position,
));
}
None
}

pub fn symbol_information(&self, url: &Url) -> Option<Vec<SymbolInformation>> {
Expand Down Expand Up @@ -399,7 +426,7 @@ impl Session {
/// Create runnables if the `TyProgramKind` of the `TyProgram` is a script.
fn create_runnables(&self, typed_program: &ty::TyProgram) {
// Insert runnable test functions.
let decl_engine = &*self.decl_engine.read();
let decl_engine = &self.decl_engine.read();
for (decl, _) in typed_program.test_fns(decl_engine) {
// Get the span of the first attribute if it exists, otherwise use the span of the function name.
let span = decl
Expand Down
39 changes: 20 additions & 19 deletions sway-lsp/src/core/token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,33 +24,34 @@ use tower_lsp::lsp_types::{Position, Range};
/// useful to the language server.
#[derive(Debug, Clone)]
pub enum AstToken {
AbiCastExpression(AbiCastExpression),
AmbiguousPathExpression(AmbiguousPathExpression),
Attribute(Attribute),
Declaration(Declaration),
DelineatedPathExpression(DelineatedPathExpression),
EnumVariant(EnumVariant),
ErrorRecovery(Span),
Expression(Expression),
StructExpression(StructExpression),
StructExpressionField(StructExpressionField),
StructScrutineeField(StructScrutineeField),
FunctionParameter(FunctionParameter),
FunctionApplicationExpression(FunctionApplicationExpression),
FunctionParameter(FunctionParameter),
Ident(Ident),
IncludeStatement,
Intrinsic(Intrinsic),
Keyword(Ident),
LibraryName(Ident),
MethodApplicationExpression(MethodApplicationExpression),
AmbiguousPathExpression(AmbiguousPathExpression),
DelineatedPathExpression(DelineatedPathExpression),
AbiCastExpression(AbiCastExpression),
Scrutinee(Scrutinee),
StorageField(StorageField),
StructExpression(StructExpression),
StructExpressionField(StructExpressionField),
StructField(StructField),
EnumVariant(EnumVariant),
TraitFn(TraitFn),
StructScrutineeField(StructScrutineeField),
Supertrait(Supertrait),
TraitConstraint(TraitConstraint),
StorageField(StorageField),
Scrutinee(Scrutinee),
Keyword(Ident),
Intrinsic(Intrinsic),
Attribute(Attribute),
LibraryName(Ident),
IncludeStatement,
UseStatement(UseStatement),
TraitFn(TraitFn),
TypeArgument(TypeArgument),
TypeParameter(TypeParameter),
Supertrait(Supertrait),
Ident(Ident),
UseStatement(UseStatement),
}

/// The `TypedAstToken` holds the types produced by the [sway_core::language::ty::TyProgram].
Expand Down
Loading

0 comments on commit dd01aef

Please sign in to comment.