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

Lsp/inlay hints on pipe #3290

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions compiler-core/src/ast/typed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,36 @@ pub enum TypedExpr {
}

impl TypedExpr {
/// Determines if the expression is a simple literal whose inlayHints must not be showed
/// in a pipeline chain
pub fn is_simple_lit(&self) -> bool {
match self {
TypedExpr::Int { .. }
| TypedExpr::Float { .. }
| TypedExpr::String { .. }
| TypedExpr::BitArray { .. } => true,
TypedExpr::Block { .. }
| TypedExpr::Pipeline { .. }
| TypedExpr::Var { .. }
| TypedExpr::Fn { .. }
| TypedExpr::List { .. }
| TypedExpr::Call { .. }
| TypedExpr::BinOp { .. }
| TypedExpr::Case { .. }
| TypedExpr::RecordAccess { .. }
| TypedExpr::ModuleSelect { .. }
| TypedExpr::Tuple { .. }
| TypedExpr::TupleIndex { .. }
| TypedExpr::Todo { .. }
| TypedExpr::Panic { .. }
| TypedExpr::RecordUpdate { .. }
| TypedExpr::NegateBool { .. }
| TypedExpr::NegateInt { .. }
| TypedExpr::Invalid { .. }
| TypedExpr::Echo { .. } => false,
}
}

pub fn is_println(&self) -> bool {
let fun = match self {
TypedExpr::Call { fun, args, .. } if args.len() == 1 => fun.as_ref(),
Expand Down
14 changes: 9 additions & 5 deletions compiler-core/src/language_server.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
mod code_action;
mod compiler;
mod completer;
mod configuration;
mod edits;
mod engine;
mod feedback;
mod files;
mod inlay_hints;
mod messages;
mod progress;
mod rename;
Expand Down Expand Up @@ -40,13 +42,15 @@ pub trait DownloadDependencies {
fn download_dependencies(&self, paths: &ProjectPaths) -> Result<Manifest>;
}

pub fn src_span_to_lsp_range(location: SrcSpan, line_numbers: &LineNumbers) -> Range {
let start = line_numbers.line_and_column_number(location.start);
let end = line_numbers.line_and_column_number(location.end);
pub fn src_offset_to_lsp_position(offset: u32, line_numbers: &LineNumbers) -> Position {
let line_col = line_numbers.line_and_column_number(offset);
Position::new(line_col.line - 1, line_col.column - 1)
}

pub fn src_span_to_lsp_range(location: SrcSpan, line_numbers: &LineNumbers) -> Range {
Range::new(
Position::new(start.line - 1, start.column - 1),
Position::new(end.line - 1, end.column - 1),
src_offset_to_lsp_position(location.start, line_numbers),
src_offset_to_lsp_position(location.end, line_numbers),
)
}

Expand Down
25 changes: 25 additions & 0 deletions compiler-core/src/language_server/configuration.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
use serde::Deserialize;
use std::sync::{Arc, RwLock};

pub type SharedConfig = Arc<RwLock<Configuration>>;

#[derive(Debug, Default, Clone, Deserialize, PartialEq, Eq)]
#[serde(default)]
#[serde(rename_all = "camelCase")]
pub struct Configuration {
pub inlay_hints: InlayHintsConfig,
}

#[derive(Debug, Default, Clone, Deserialize, PartialEq, Eq)]
#[serde(default)]
#[serde(rename_all = "camelCase")]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

snake_case please, we never use camelCase

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update: I noticed that for a better integration with vscode, it's better to use camelCase instead
you can take a look at how it is rendered in the client side pr: gleam-lang/vscode-gleam#82

pub struct InlayHintsConfig {
/// Whether to show type inlay hints of multiline pipelines
pub pipelines: bool,

/// Whether to show type inlay hints of function parameters
pub parameter_types: bool,

/// Whether to show type inlay hints of return types of functions
pub return_types: bool,
}
29 changes: 28 additions & 1 deletion compiler-core/src/language_server/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use ecow::EcoString;
use itertools::Itertools;
use lsp::CodeAction;
use lsp_types::{
self as lsp, DocumentSymbol, Hover, HoverContents, MarkedString, Position,
self as lsp, DocumentSymbol, Hover, HoverContents, InlayHint, MarkedString, Position,
PrepareRenameResponse, Range, SignatureHelp, SymbolKind, SymbolTag, TextEdit, Url,
WorkspaceEdit,
};
Expand All @@ -42,6 +42,8 @@ use super::{
code_action_inexhaustive_let_to_case,
},
completer::Completer,
configuration::SharedConfig,
inlay_hints,
rename::{VariableRenameKind, rename_local_variable},
signature_help, src_span_to_lsp_range,
};
Expand Down Expand Up @@ -82,6 +84,9 @@ pub struct LanguageServerEngine<IO, Reporter> {
/// Used to know if to show the "View on HexDocs" link
/// when hovering on an imported value
hex_deps: HashSet<EcoString>,

/// Configuration the user has set in their editor.
pub(crate) user_config: SharedConfig,
}

impl<'a, IO, Reporter> LanguageServerEngine<IO, Reporter>
Expand All @@ -102,6 +107,7 @@ where
progress_reporter: Reporter,
io: FileSystemProxy<IO>,
paths: ProjectPaths,
user_config: SharedConfig,
) -> Result<Self> {
let locker = io.inner().make_locker(&paths, config.target)?;

Expand Down Expand Up @@ -138,6 +144,7 @@ where
paths,
error: None,
hex_deps,
user_config,
})
}

Expand Down Expand Up @@ -546,6 +553,26 @@ where
})
}

pub fn inlay_hints(&mut self, params: lsp::InlayHintParams) -> Response<Vec<InlayHint>> {
self.respond(|this| {
let Ok(config) = this.user_config.read() else {
return Ok(vec![]);
};

let Some(module) = this.module_for_uri(&params.text_document.uri) else {
return Ok(vec![]);
};

let hints = inlay_hints::get_inlay_hints(
config.inlay_hints.clone(),
module.ast.clone(),
&LineNumbers::new(&module.code),
);

Ok(hints)
})
}

pub fn prepare_rename(
&mut self,
params: lsp::TextDocumentPositionParams,
Expand Down
221 changes: 221 additions & 0 deletions compiler-core/src/language_server/inlay_hints.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
use lsp_types::{InlayHint, InlayHintKind, InlayHintLabel};

use crate::{
ast::{
PipelineAssignmentKind, SrcSpan, TypeAst, TypedExpr, TypedModule, TypedPipelineAssignment,
visit::Visit,
},
line_numbers::LineNumbers,
type_::{self, Type},
};

use super::{configuration::InlayHintsConfig, src_offset_to_lsp_position};

struct InlayHintsVisitor<'a> {
config: InlayHintsConfig,
module_names: &'a type_::printer::Names,
current_declaration_printer: type_::printer::Printer<'a>,

hints: Vec<InlayHint>,
line_numbers: &'a LineNumbers,
}

fn default_inlay_hint(line_numbers: &LineNumbers, offset: u32, label: String) -> InlayHint {
let position = src_offset_to_lsp_position(offset, line_numbers);

InlayHint {
position,
label: InlayHintLabel::String(label),
kind: Some(InlayHintKind::TYPE),
text_edits: None,
tooltip: None,
padding_left: Some(true),
padding_right: None,
data: None,
}
}

impl InlayHintsVisitor<'_> {
pub fn push_binding_annotation(
&mut self,
type_: &Type,
type_annotation_ast: Option<&TypeAst>,
span: &SrcSpan,
) {
if type_annotation_ast.is_some() {
return;
}

let label = format!(": {}", self.current_declaration_printer.print_type(type_));

let mut hint = default_inlay_hint(self.line_numbers, span.end, label);
hint.padding_left = Some(false);

self.hints.push(hint);
}

pub fn push_return_annotation(
&mut self,
type_: &Type,
type_annotation_ast: Option<&TypeAst>,
span: &SrcSpan,
) {
if type_annotation_ast.is_some() {
return;
}

let label = format!("-> {}", self.current_declaration_printer.print_type(type_));

let hint = default_inlay_hint(self.line_numbers, span.end, label);

self.hints.push(hint);
}
}

impl<'ast> Visit<'ast> for InlayHintsVisitor<'_> {
fn visit_typed_function(&mut self, fun: &'ast crate::ast::TypedFunction) {
// This must be reset on every statement
self.current_declaration_printer = type_::printer::Printer::new(self.module_names);

for st in &fun.body {
self.visit_typed_statement(st);
}

if self.config.parameter_types {
for arg in &fun.arguments {
self.push_binding_annotation(&arg.type_, arg.annotation.as_ref(), &arg.location);
}
}

if self.config.return_types {
self.push_return_annotation(
&fun.return_type,
fun.return_annotation.as_ref(),
&fun.location,
);
}
}

fn visit_typed_expr_fn(
&mut self,
_location: &'ast SrcSpan,
type_: &'ast std::sync::Arc<Type>,
kind: &'ast crate::ast::FunctionLiteralKind,
args: &'ast [crate::ast::TypedArg],
body: &'ast vec1::Vec1<crate::ast::TypedStatement>,
return_annotation: &'ast Option<TypeAst>,
) {
for st in body {
self.visit_typed_statement(st);
}

let crate::ast::FunctionLiteralKind::Anonymous { head } = kind else {
return;
};

if self.config.parameter_types {
for arg in args {
self.push_binding_annotation(&arg.type_, arg.annotation.as_ref(), &arg.location);
}
}

if self.config.return_types {
if let Some((_args, ret_type)) = type_.fn_types() {
self.push_return_annotation(&ret_type, return_annotation.as_ref(), head);
}
}
}

fn visit_typed_expr_pipeline(
&mut self,
_location: &'ast SrcSpan,
first_value: &'ast TypedPipelineAssignment,
assignments: &'ast [(TypedPipelineAssignment, PipelineAssignmentKind)],
finally: &'ast TypedExpr,
_finally_kind: &'ast PipelineAssignmentKind,
) {
self.visit_typed_pipeline_assignment(first_value);
for (assignment, _kind) in assignments {
self.visit_typed_pipeline_assignment(assignment);
}
self.visit_typed_expr(finally);

if !self.config.pipelines {
return;
}

let mut prev_hint: Option<(u32, Option<InlayHint>)> = None;

let assigments_values =
std::iter::once(first_value).chain(assignments.iter().map(|p| &p.0));

for assign in assigments_values {
let this_line: u32 = self
.line_numbers
.line_and_column_number(assign.location.end)
.line;

if let Some((prev_line, prev_hint)) = prev_hint {
if prev_line != this_line {
if let Some(prev_hint) = prev_hint {
self.hints.push(prev_hint);
}
}
};

let this_hint = default_inlay_hint(
self.line_numbers,
assign.location.end,
self.current_declaration_printer
.print_type(assign.type_().as_ref())
.to_string(),
);

prev_hint = Some((
this_line,
if assign.value.is_simple_lit() {
None
} else {
Some(this_hint)
},
));
}

if let Some((prev_line, prev_hint)) = prev_hint {
let this_line = self
.line_numbers
.line_and_column_number(finally.location().end)
.line;
if this_line != prev_line {
if let Some(prev_hint) = prev_hint {
self.hints.push(prev_hint);
}
let hint = default_inlay_hint(
self.line_numbers,
finally.location().end,
self.current_declaration_printer
.print_type(finally.type_().as_ref())
.to_string(),
);
self.hints.push(hint);
}
}
}
}

pub fn get_inlay_hints(
config: InlayHintsConfig,
typed_module: TypedModule,
line_numbers: &LineNumbers,
) -> Vec<InlayHint> {
let mut visitor = InlayHintsVisitor {
config,
module_names: &typed_module.names,
current_declaration_printer: type_::printer::Printer::new(&typed_module.names),
hints: vec![],
line_numbers,
};

visitor.visit_typed_module(&typed_module);
visitor.hints
}
Loading
Loading