Skip to content

Commit

Permalink
Lsp hover capability (#187)
Browse files Browse the repository at this point in the history
* Add hover capability to ante-ls

* Bump version

* Remove accidentally tracked file

* Apply code review suggestions
  • Loading branch information
ehllie authored Feb 7, 2024
1 parent 2fc0542 commit 7f6b458
Show file tree
Hide file tree
Showing 10 changed files with 272 additions and 55 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion ante-ls/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "ante-ls"
version = "0.1.0"
version = "0.1.1"
edition = "2021"

[dependencies]
Expand Down
265 changes: 232 additions & 33 deletions ante-ls/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ use ante::{
cache::{cached_read, ModuleCache},
error::{location::Locatable, ErrorType},
frontend,
parser::ast::Ast,
types::typeprinter,
};

use dashmap::DashMap;
Expand Down Expand Up @@ -37,6 +39,7 @@ impl LanguageServer for Backend {
Ok(InitializeResult {
capabilities: ServerCapabilities {
text_document_sync: Some(TextDocumentSyncCapability::Kind(TextDocumentSyncKind::INCREMENTAL)),
hover_provider: Some(HoverProviderCapability::Simple(true)),
..Default::default()
},
..Default::default()
Expand Down Expand Up @@ -88,58 +91,254 @@ impl LanguageServer for Backend {
self.update_diagnostics(params.text_document.uri, &rope).await;
};
}
}

fn lsp_range_to_rope_range(range: Range, rope: &Rope) -> std::ops::Range<usize> {
let start_line = range.start.line as usize;
let start_line = rope.line_to_char(start_line);
async fn hover(&self, params: HoverParams) -> Result<Option<Hover>> {
self.client.log_message(MessageType::LOG, format!("ante-ls hover: {:?}", params)).await;
let uri = params.text_document_position_params.text_document.uri;
let rope = match self.document_map.get(&uri) {
Some(rope) => rope,
None => return Ok(None),
};

let cache = self.create_cache(&uri, &rope);
let ast = cache.parse_trees.get_mut(0).unwrap();

let index = position_to_index(params.text_document_position_params.position, &rope);
let hovered_node = walk_ast(ast, index);

let start_char = range.start.character as usize;
let start_char = start_line + start_char;
match hovered_node {
Ast::Variable(v) => {
let info = match v.definition {
Some(definition_id) => &cache[definition_id],
_ => return Ok(None),
};

let end_line = range.end.line as usize;
let end_line = rope.line_to_char(end_line);
let typ = match &info.typ {
Some(typ) => typ,
None => return Ok(None),
};

let end_char = range.end.character as usize;
let end_char = end_line + end_char;
let name = v.kind.name();

start_char..end_char
let value =
typeprinter::show_type_and_traits(&name, typ, &info.required_traits, &info.trait_info, &cache);

let location = v.locate();
let range = Some(rope_range_to_lsp_range(location.start.index..location.end.index, &rope));

Ok(Some(Hover {
contents: HoverContents::Markup(MarkupContent { kind: MarkupKind::PlainText, value }),
range,
}))
},
_ => Ok(None),
}
}
}

fn rope_range_to_lsp_range(range: std::ops::Range<usize>, rope: &Rope) -> Range {
let start_line = rope.char_to_line(range.start);
let start_char = rope.line_to_char(start_line);
let start_char = (range.start - start_char) as u32;
let start_line = start_line as u32;

let end_line = rope.char_to_line(range.end);
let end_char = rope.line_to_char(end_line);
let end_char = (range.end - end_char) as u32;
let end_line = end_line as u32;

Range {
start: Position { line: start_line, character: start_char },
end: Position { line: end_line, character: end_char },
fn walk_ast<'a>(ast: &'a Ast<'a>, idx: usize) -> &'a Ast<'a> {
let mut ast = ast;
loop {
match ast {
Ast::Assignment(a) => {
if a.lhs.locate().contains_index(&idx) {
ast = &a.lhs;
} else if a.rhs.locate().contains_index(&idx) {
ast = &a.rhs;
} else {
break;
}
},
Ast::Definition(d) => {
if d.pattern.locate().contains_index(&idx) {
ast = &d.pattern;
} else if d.expr.locate().contains_index(&idx) {
ast = &d.expr;
} else {
break;
}
},
Ast::EffectDefinition(_) => {
break;
},
Ast::Extern(_) => {
break;
},
Ast::FunctionCall(f) => {
if let Some(arg) = f.args.iter().find(|&arg| arg.locate().contains_index(&idx)) {
ast = arg;
} else if f.function.locate().contains_index(&idx) {
ast = &f.function;
} else {
break;
}
},
Ast::Handle(h) => {
if let Some(branch) = h.branches.iter().find_map(|(pat, body)| {
if pat.locate().contains_index(&idx) {
return Some(pat);
};
if body.locate().contains_index(&idx) {
return Some(body);
};
None
}) {
ast = branch;
} else if h.expression.locate().contains_index(&idx) {
ast = &h.expression;
} else {
break;
}
},
Ast::If(i) => {
if i.condition.locate().contains_index(&idx) {
ast = &i.condition;
} else if i.then.locate().contains_index(&idx) {
ast = &i.then;
} else if i.otherwise.locate().contains_index(&idx) {
ast = &i.otherwise;
} else {
break;
}
},
Ast::Import(_) => {
break;
},
Ast::Lambda(l) => {
if let Some(arg) = l.args.iter().find(|&arg| arg.locate().contains_index(&idx)) {
ast = arg;
} else if l.body.locate().contains_index(&idx) {
ast = &l.body;
} else {
break;
}
},
Ast::Literal(_) => {
break;
},
Ast::Match(m) => {
if let Some(branch) = m.branches.iter().find_map(|(pat, body)| {
if pat.locate().contains_index(&idx) {
return Some(pat);
};
if body.locate().contains_index(&idx) {
return Some(body);
};
None
}) {
ast = branch;
} else {
break;
}
},
Ast::MemberAccess(m) => {
if m.lhs.locate().contains_index(&idx) {
ast = &m.lhs;
} else {
break;
}
},
Ast::NamedConstructor(n) => {
if let Some((_, arg)) = n.args.iter().find(|(_, arg)| arg.locate().contains_index(&idx)) {
ast = arg;
} else if n.constructor.locate().contains_index(&idx) {
ast = &n.constructor;
} else {
break;
}
},
Ast::Return(r) => {
if r.expression.locate().contains_index(&idx) {
ast = &r.expression;
} else {
break;
}
},
Ast::Sequence(s) => {
if let Some(stmt) = s.statements.iter().find(|&stmt| stmt.locate().contains_index(&idx)) {
ast = stmt;
} else {
break;
}
},
Ast::TraitDefinition(_) => {
break;
},
Ast::TraitImpl(t) => {
if let Some(def) = t.definitions.iter().find_map(|def| {
if def.pattern.locate().contains_index(&idx) {
return Some(&def.pattern);
};
if def.expr.locate().contains_index(&idx) {
return Some(&def.expr);
};
None
}) {
ast = def;
} else {
break;
}
},
Ast::TypeAnnotation(t) => {
if t.lhs.locate().contains_index(&idx) {
ast = &t.lhs;
} else {
break;
}
},
Ast::TypeDefinition(_) => {
break;
},
Ast::Variable(_) => {
break;
},
}
}
ast
}

fn position_to_index(position: Position, rope: &Rope) -> usize {
let line = position.line as usize;
let line = rope.line_to_char(line);
line + position.character as usize
}

fn index_to_position(index: usize, rope: &Rope) -> Position {
let line = rope.char_to_line(index);
let char = index - rope.line_to_char(line);
Position { line: line as u32, character: char as u32 }
}

fn lsp_range_to_rope_range(range: Range, rope: &Rope) -> std::ops::Range<usize> {
let start = position_to_index(range.start, rope);
let end = position_to_index(range.end, rope);
start..end
}

fn rope_range_to_lsp_range(range: std::ops::Range<usize>, rope: &Rope) -> Range {
let start = index_to_position(range.start, rope);
let end = index_to_position(range.end, rope);
Range { start, end }
}

impl Backend {
async fn update_diagnostics(&self, uri: Url, rope: &Rope) {
fn create_cache<'a>(&self, uri: &'a Url, rope: &Rope) -> ModuleCache<'a> {
// Urls always contain ablsoute canonical paths, so there's no need to canonicalize them.
let filename = Path::new(uri.path());
let cache_root = filename.parent().unwrap();

let (paths, contents) =
self.document_map.iter().fold((Vec::new(), Vec::new()), |(mut paths, mut contents), item| {
paths.push(PathBuf::from(item.key().path()));
contents.push(item.value().to_string());
(paths, contents)
});
let file_cache = paths.iter().zip(contents.into_iter()).map(|(p, c)| (p.as_path(), c)).collect();
let file_cache =
self.document_map.iter().map(|item| (PathBuf::from(item.key().path()), item.value().to_string())).collect();
let mut cache = ModuleCache::new(cache_root, file_cache);

let _ = frontend::check(filename, rope.to_string(), &mut cache, frontend::FrontendPhase::TypeCheck, false);

cache
}

async fn update_diagnostics(&self, uri: Url, rope: &Rope) {
let cache = self.create_cache(&uri, rope);

// Diagnostics for a document get cleared only when an empty list is sent for it's Uri.
// This presents an issue, as when we have files A and B, where file A imports the file B,
// and we provide a diagnostic for file A about incorrect usage of a function in file B,
Expand Down
1 change: 0 additions & 1 deletion ante-ls/test.an

This file was deleted.

12 changes: 6 additions & 6 deletions src/cache/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,10 @@ pub struct ModuleCache<'a> {
/// The number of errors emitted by the program
pub error_count: usize,

pub file_cache: FileCache<'a>,
pub file_cache: FileCache,
}

pub type FileCache<'a> = HashMap<&'a Path, String>;
pub type FileCache = HashMap<PathBuf, String>;

#[derive(Debug)]
pub struct MutualRecursionSet {
Expand Down Expand Up @@ -379,7 +379,7 @@ impl<'a> Locatable<'a> for EffectInfo<'a> {
}
}

pub fn cached_read<'a>(file_cache: &'a FileCache<'_>, path: &Path) -> Option<Cow<'a, str>> {
pub fn cached_read<'a>(file_cache: &'a FileCache, path: &Path) -> Option<Cow<'a, str>> {
match file_cache.get(path) {
Some(contents) => Some(Cow::Borrowed(contents)),
None => {
Expand All @@ -388,14 +388,14 @@ pub fn cached_read<'a>(file_cache: &'a FileCache<'_>, path: &Path) -> Option<Cow
let mut contents = String::new();
reader.read_to_string(&mut contents).ok()?;
Some(Cow::Owned(contents))
}
},
}
}

impl<'a> ModuleCache<'a> {
/// For consistency, all paths should be absolute and canonical.
/// They can be converted to relative paths for displaying errors later.
pub fn new(project_directory: &Path, file_cache: FileCache<'a>) -> ModuleCache<'a> {
pub fn new(project_directory: &Path, file_cache: FileCache) -> ModuleCache<'a> {
ModuleCache {
relative_roots: vec![project_directory.to_owned(), stdlib_dir()],
// Really wish you could do ..Default::default() for the remaining fields
Expand Down Expand Up @@ -451,7 +451,7 @@ impl<'a> ModuleCache<'a> {

if !contains_path {
let contents = contents.into_owned();
self.file_cache.insert(path, contents);
self.file_cache.insert(path.to_path_buf(), contents);
}

self.file_cache.get(path).map(|s| s.as_str())
Expand Down
5 changes: 5 additions & 0 deletions src/error/location.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,11 @@ impl<'c> Location<'c> {

Location { filename: self.filename, start, end }
}

#[allow(dead_code)]
pub fn contains_index(&self, idx: &usize) -> bool {
(self.start.index..self.end.index).contains(idx)
}
}

/// A trait representing anything that has a Location
Expand Down
Loading

0 comments on commit 7f6b458

Please sign in to comment.