Go-to-definition basic support

This commit is contained in:
Ayaz Hafiz 2023-10-22 12:45:11 -04:00
parent e954e074fb
commit 886a367026
No known key found for this signature in database
GPG key ID: 0E2A37416A25EF58
10 changed files with 465 additions and 313 deletions

View file

@ -0,0 +1,332 @@
use std::path::{Path, PathBuf};
use bumpalo::Bump;
use roc_can::{abilities::AbilitiesStore, expr::Declarations};
use roc_collections::MutMap;
use roc_load::{CheckedModule, LoadedModule};
use roc_module::symbol::{Interns, ModuleId, Symbol};
use roc_packaging::cache::{self, RocCacheDir};
use roc_region::all::{LineInfo, Region};
use roc_reporting::report::RocDocAllocator;
use roc_solve_problem::TypeError;
use roc_types::subs::Subs;
use tower_lsp::lsp_types::{
Diagnostic, GotoDefinitionResponse, Hover, HoverContents, Location, MarkedString, Position,
Range, Url,
};
use crate::convert::{
diag::{IntoLspDiagnostic, ProblemFmt},
ToRange, ToRocPosition,
};
pub(crate) struct GlobalAnalysis {
pub documents: Vec<AnalyzedDocument>,
}
impl GlobalAnalysis {
pub fn new(source_url: Url, source: String) -> GlobalAnalysis {
let arena = Bump::new();
let fi = source_url.to_file_path().unwrap();
let src_dir = find_src_dir(&fi).to_path_buf();
let line_info = LineInfo::new(&source);
let loaded = roc_load::load_and_typecheck_str(
&arena,
fi,
&source,
src_dir,
roc_target::TargetInfo::default_x86_64(),
roc_load::FunctionKind::LambdaSet,
roc_reporting::report::RenderTarget::Generic,
RocCacheDir::Persistent(cache::roc_cache_dir().as_path()),
roc_reporting::report::DEFAULT_PALETTE,
);
let module = match loaded {
Ok(module) => module,
Err(problem) => {
let all_problems = problem
.into_lsp_diagnostic(&())
.into_iter()
.collect::<Vec<_>>();
let analyzed_document = AnalyzedDocument {
url: source_url,
line_info,
module: None,
diagnostics: all_problems,
};
return GlobalAnalysis {
documents: vec![analyzed_document],
};
}
};
let mut documents = vec![];
let LoadedModule {
module_id,
interns,
mut can_problems,
mut type_problems,
mut declarations_by_id,
sources,
mut typechecked,
solved,
abilities_store,
..
} = module;
let mut root_module = Some(RootModule {
module_id,
subs: solved.into_inner(),
abilities_store,
});
let mut builder = AnalyzedDocumentBuilder {
interns: &interns,
can_problems: &mut can_problems,
type_problems: &mut type_problems,
declarations_by_id: &mut declarations_by_id,
typechecked: &mut typechecked,
root_module: &mut root_module,
};
for (module_id, (path, source)) in sources {
documents.push(builder.build_document(path, source, module_id));
}
GlobalAnalysis { documents }
}
}
fn find_src_dir(path: &Path) -> &Path {
path.parent().unwrap_or(path)
}
fn _find_parent_git_repo(path: &Path) -> Option<&Path> {
let mut path = path;
loop {
if path.join(".git").exists() {
return Some(path);
}
path = path.parent()?;
}
}
struct RootModule {
module_id: ModuleId,
subs: Subs,
abilities_store: AbilitiesStore,
}
struct AnalyzedDocumentBuilder<'a> {
interns: &'a Interns,
can_problems: &'a mut MutMap<ModuleId, Vec<roc_problem::can::Problem>>,
type_problems: &'a mut MutMap<ModuleId, Vec<TypeError>>,
declarations_by_id: &'a mut MutMap<ModuleId, Declarations>,
typechecked: &'a mut MutMap<ModuleId, CheckedModule>,
root_module: &'a mut Option<RootModule>,
}
impl<'a> AnalyzedDocumentBuilder<'a> {
fn build_document(
&mut self,
path: PathBuf,
source: Box<str>,
module_id: ModuleId,
) -> AnalyzedDocument {
let subs;
let abilities;
let declarations;
if let Some(m) = self.typechecked.remove(&module_id) {
subs = m.solved_subs.into_inner();
abilities = m.abilities_store;
declarations = m.decls;
} else {
let rm = self.root_module.take().unwrap();
assert!(rm.module_id == module_id);
subs = rm.subs;
abilities = rm.abilities_store;
declarations = self.declarations_by_id.remove(&module_id).unwrap();
}
let analyzed_module = AnalyzedModule {
subs,
abilities,
declarations,
module_id,
interns: self.interns.clone(),
};
let line_info = LineInfo::new(&source);
let diagnostics = self.build_diagnostics(&path, &source, &line_info, module_id);
let path = if path.is_relative() {
// Make it <tmpdir>/path
let tmpdir = std::env::temp_dir();
tmpdir.join(path)
} else {
path
};
AnalyzedDocument {
url: Url::from_file_path(path).unwrap(),
line_info,
module: Some(analyzed_module),
diagnostics,
}
}
fn build_diagnostics(
&mut self,
source_path: &Path,
source: &str,
line_info: &LineInfo,
module_id: ModuleId,
) -> Vec<Diagnostic> {
let lines: Vec<_> = source.lines().collect();
let alloc = RocDocAllocator::new(&lines, module_id, self.interns);
let mut all_problems = Vec::new();
let fmt = ProblemFmt {
alloc: &alloc,
line_info,
path: source_path,
};
let can_problems = self.can_problems.remove(&module_id).unwrap_or_default();
let type_problems = self.type_problems.remove(&module_id).unwrap_or_default();
for can_problem in can_problems {
if let Some(diag) = can_problem.into_lsp_diagnostic(&fmt) {
all_problems.push(diag);
}
}
for type_problem in type_problems {
if let Some(diag) = type_problem.into_lsp_diagnostic(&fmt) {
all_problems.push(diag);
}
}
all_problems
}
}
#[derive(Debug)]
struct AnalyzedModule {
module_id: ModuleId,
interns: Interns,
subs: Subs,
abilities: AbilitiesStore,
declarations: Declarations,
}
#[derive(Debug)]
pub(crate) struct AnalyzedDocument {
url: Url,
line_info: LineInfo,
module: Option<AnalyzedModule>,
diagnostics: Vec<Diagnostic>,
}
impl AnalyzedDocument {
pub fn url(&self) -> &Url {
&self.url
}
pub fn module_id(&self) -> Option<ModuleId> {
self.module.as_ref().map(|m| m.module_id)
}
fn line_info(&self) -> &LineInfo {
&self.line_info
}
fn module_mut(&mut self) -> Option<&mut AnalyzedModule> {
self.module.as_mut()
}
fn module(&self) -> Option<&AnalyzedModule> {
self.module.as_ref()
}
fn location(&self, range: Range) -> Location {
Location {
uri: self.url.clone(),
range,
}
}
pub fn diagnostics(&mut self) -> Vec<Diagnostic> {
self.diagnostics.clone()
}
pub fn symbol_at(&self, position: Position) -> Option<Symbol> {
let line_info = self.line_info();
let position = position.to_roc_position(line_info);
let AnalyzedModule {
declarations,
abilities,
..
} = self.module()?;
let found_symbol =
roc_can::traverse::find_closest_symbol_at(position, declarations, abilities)?;
Some(found_symbol.implementation_symbol())
}
pub fn hover(&mut self, position: Position) -> Option<Hover> {
let line_info = self.line_info();
let pos = position.to_roc_position(line_info);
let AnalyzedModule {
subs,
declarations,
module_id,
interns,
..
} = self.module_mut()?;
let (region, var) = roc_can::traverse::find_closest_type_at(pos, declarations)?;
let snapshot = subs.snapshot();
let type_str = roc_types::pretty_print::name_and_print_var(
var,
subs,
*module_id,
interns,
roc_types::pretty_print::DebugPrint::NOTHING,
);
subs.rollback_to(snapshot);
let range = region.to_range(self.line_info());
Some(Hover {
contents: HoverContents::Scalar(MarkedString::String(type_str)),
range: Some(range),
})
}
pub fn goto_definition(&self, symbol: Symbol) -> Option<GotoDefinitionResponse> {
let AnalyzedModule { declarations, .. } = self.module()?;
let found_declaration = roc_can::traverse::find_declaration(symbol, declarations)?;
let range = found_declaration.region().to_range(self.line_info());
Some(GotoDefinitionResponse::Scalar(self.location(range)))
}
}

View file

@ -116,44 +116,40 @@ pub(crate) mod diag {
},
};
let msg;
match self {
let msg = match self {
LoadingProblem::FileProblem { filename, error } => {
msg = format!(
format!(
"Failed to load {} due to an I/O error: {}",
filename.display(),
error
);
)
}
LoadingProblem::ParsingFailed(_) => {
unreachable!("should be formatted before sent back")
LoadingProblem::ParsingFailed(fe) => {
let problem = &fe.problem.problem;
format!("Failed to parse Roc source file: {problem:?}")
}
LoadingProblem::UnexpectedHeader(header) => {
msg = format!("Unexpected header: {}", header);
format!("Unexpected header: {}", header)
}
LoadingProblem::ChannelProblem(_) => {
msg = "Internal error: message channel died".to_string();
"Internal error: message channel died".to_string()
}
LoadingProblem::ErrJoiningWorkerThreads => {
msg = "Internal error: analysis worker threads died".to_string();
"Internal error: analysis worker threads died".to_string()
}
LoadingProblem::TriedToImportAppModule => {
msg = "Attempted to import app module".to_string();
}
LoadingProblem::FormattedReport(report) => {
msg = report.clone();
"Attempted to import app module".to_string()
}
LoadingProblem::FormattedReport(report) => report.clone(),
LoadingProblem::ImportCycle(_, _) => {
msg = "Circular dependency between modules".to_string();
}
LoadingProblem::IncorrectModuleName(_) => {
msg = "Incorrect module name".to_string();
"Circular dependency between modules".to_string()
}
LoadingProblem::IncorrectModuleName(_) => "Incorrect module name".to_string(),
LoadingProblem::CouldNotFindCacheDir => {
msg = format!(
format!(
"Could not find Roc cache directory {}",
roc_packaging::cache::roc_cache_dir().display()
);
)
}
};

View file

@ -1,292 +1,72 @@
use std::collections::HashMap;
use bumpalo::Bump;
use roc_can::{abilities::AbilitiesStore, expr::Declarations};
use roc_load::LoadedModule;
use roc_module::symbol::{Interns, ModuleId, Symbol};
use roc_packaging::cache::{self, RocCacheDir};
use roc_region::all::{LineInfo, Region};
use roc_reporting::report::RocDocAllocator;
use roc_types::subs::Subs;
use tower_lsp::lsp_types::{
Diagnostic, GotoDefinitionResponse, Hover, HoverContents, MarkedString, Position, Range, Url,
};
use roc_module::symbol::ModuleId;
use tower_lsp::lsp_types::{Diagnostic, GotoDefinitionResponse, Hover, Position, Url};
use crate::convert::{
diag::{IntoLspDiagnostic, ProblemFmt},
ToRange, ToRocPosition,
};
use crate::analysis::{AnalyzedDocument, GlobalAnalysis};
pub(crate) enum DocumentChange {
Modified(Url, String),
Closed(Url),
}
#[derive(Debug)]
struct Analysis {
line_info: LineInfo,
module: Option<LoadedModule>,
diagnostics: Vec<Diagnostic>,
}
impl Analysis {
fn new(url: &Url, source: &str, arena: &Bump) -> Self {
let fi = url.to_file_path().unwrap();
let src_dir = fi.parent().unwrap().to_path_buf();
let mut loaded = roc_load::load_and_typecheck_str(
arena,
fi,
source,
src_dir,
roc_target::TargetInfo::default_x86_64(),
roc_load::FunctionKind::LambdaSet,
roc_reporting::report::RenderTarget::Generic,
RocCacheDir::Persistent(cache::roc_cache_dir().as_path()),
roc_reporting::report::DEFAULT_PALETTE,
);
let line_info = LineInfo::new(source);
let diagnostics = match loaded.as_mut() {
Ok(module) => {
let lines: Vec<_> = source.lines().collect();
let alloc = RocDocAllocator::new(&lines, module.module_id, &module.interns);
let mut all_problems = Vec::new();
let module_path = url.to_file_path().unwrap();
let fmt = ProblemFmt {
alloc: &alloc,
line_info: &line_info,
path: &module_path,
};
for can_problem in module
.can_problems
.remove(&module.module_id)
.unwrap_or_default()
{
if let Some(diag) = can_problem.into_lsp_diagnostic(&fmt) {
all_problems.push(diag);
}
}
for type_problem in module
.type_problems
.remove(&module.module_id)
.unwrap_or_default()
{
if let Some(diag) = type_problem.into_lsp_diagnostic(&fmt) {
all_problems.push(diag);
}
}
all_problems
}
Err(problem) => {
let mut all_problems = vec![];
all_problems.extend(problem.into_lsp_diagnostic(&()));
all_problems
}
};
Self {
line_info: LineInfo::new(source),
module: loaded.ok(),
diagnostics,
}
}
}
#[derive(Debug)]
struct Document {
url: Url,
source: String,
arena: Bump,
analysis: Analysis,
}
impl Document {
fn new(url: Url, source: String) -> Self {
let arena = Bump::new();
let analysis = Analysis::new(&url, &source, &arena);
Self {
url,
source,
arena,
analysis,
}
}
fn refresh(&mut self, source: String) {
self.source = source;
self.analysis = Analysis::new(&self.url, &self.source, &self.arena);
}
fn line_info(&self) -> &LineInfo {
&self.analysis.line_info
}
fn module(&mut self) -> Option<&mut LoadedModule> {
self.analysis.module.as_mut()
}
fn diagnostics(&mut self) -> Vec<Diagnostic> {
self.analysis.diagnostics.clone()
}
fn split_module(&mut self) -> Option<SplitModule<'_>> {
self.module()?.try_into().ok()
}
fn symbol_at(&mut self, position: Position) -> Option<Symbol> {
let line_info = self.line_info();
let region = Region::from_pos(position.to_roc_position(line_info));
let SplitModule {
decls,
abilities_store,
..
} = self.split_module()?;
let found_symbol = roc_can::traverse::find_symbol_at(region, decls, abilities_store)?;
Some(found_symbol.implementation_symbol())
}
fn hover(&mut self, position: Position) -> Option<Hover> {
let line_info = self.line_info();
let pos = position.to_roc_position(line_info);
let SplitModule {
subs,
decls,
module_id,
interns,
..
} = self.split_module()?;
let (region, var) = roc_can::traverse::find_closest_type_at(pos, decls)?;
let snapshot = subs.snapshot();
let type_str = roc_types::pretty_print::name_and_print_var(
var,
subs,
module_id,
interns,
roc_types::pretty_print::DebugPrint::NOTHING,
);
subs.rollback_to(snapshot);
let range = region.to_range(self.line_info());
Some(Hover {
contents: HoverContents::Scalar(MarkedString::String(type_str)),
range: Some(range),
})
}
}
struct SplitModule<'a> {
subs: &'a mut Subs,
abilities_store: &'a AbilitiesStore,
decls: &'a Declarations,
module_id: ModuleId,
interns: &'a Interns,
}
impl<'a> TryFrom<&'a mut LoadedModule> for SplitModule<'a> {
type Error = ();
fn try_from(module: &'a mut LoadedModule) -> Result<Self, Self::Error> {
let module_id = module.module_id;
let interns = &module.interns;
let subs;
let abilities_store;
let decls;
if let Some(m) = module.typechecked.get_mut(&module.module_id) {
subs = &mut m.solved_subs;
abilities_store = &m.abilities_store;
decls = &m.decls;
} else if let Some(d) = module.declarations_by_id.get(&module.module_id) {
subs = &mut module.solved;
abilities_store = &module.abilities_store;
decls = d;
} else {
return Err(());
}
Ok(Self {
subs: subs.inner_mut(),
abilities_store,
decls,
module_id,
interns,
})
}
}
fn missing_hover(module: &mut LoadedModule, position: Position) -> Option<Hover> {
Some(Hover {
contents: HoverContents::Scalar(MarkedString::String(format!(
"{:?}",
(module.typechecked.keys().collect::<Vec<_>>())
))),
range: Some(Range::new(
position,
Position {
line: position.line,
character: position.character + 1,
},
)),
})
}
#[derive(Debug, Default)]
pub(crate) struct Registry {
documents: HashMap<Url, Document>,
documents: HashMap<Url, AnalyzedDocument>,
module_id_to_url: HashMap<ModuleId, Url>,
}
impl Registry {
pub fn apply_change(&mut self, change: DocumentChange) {
match change {
DocumentChange::Modified(url, source) => match self.documents.get_mut(&url) {
Some(document) => document.refresh(source),
None => {
self.documents
.insert(url.clone(), Document::new(url, source));
DocumentChange::Modified(url, source) => {
let GlobalAnalysis { documents } = GlobalAnalysis::new(url, source);
// Only replace the set of documents and all dependencies that were re-analyzed.
// Note that this is actually the opposite of what we want - in truth we want to
// re-evaluate all dependents!
for document in documents {
let url = document.url().clone();
let module_id = document.module_id();
self.documents.insert(url.clone(), document);
if let Some(module_id) = module_id {
self.module_id_to_url.insert(module_id, url);
}
}
},
DocumentChange::Closed(url) => {
self.documents.remove(&url);
}
DocumentChange::Closed(_url) => {
// Do nothing.
}
}
}
pub fn diagnostics(&mut self, document: &Url) -> Vec<Diagnostic> {
self.documents.get_mut(document).unwrap().diagnostics()
fn document_by_url(&mut self, url: &Url) -> Option<&mut AnalyzedDocument> {
self.documents.get_mut(url)
}
pub fn hover(&mut self, document: &Url, position: Position) -> Option<Hover> {
self.documents.get_mut(document).unwrap().hover(position)
fn document_by_module_id(&mut self, module_id: ModuleId) -> Option<&mut AnalyzedDocument> {
let url = self.module_id_to_url.get(&module_id)?;
self.documents.get_mut(url)
}
pub fn diagnostics(&mut self, url: &Url) -> Vec<Diagnostic> {
let Some(document) = self.document_by_url(url) else {
return vec![];
};
document.diagnostics()
}
pub fn hover(&mut self, url: &Url, position: Position) -> Option<Hover> {
self.document_by_url(url)?.hover(position)
}
pub fn goto_definition(
&mut self,
document: &Url,
url: &Url,
position: Position,
) -> Option<GotoDefinitionResponse> {
let symbol = self
.documents
.get_mut(document)
.unwrap()
.symbol_at(position)?;
None
let symbol = self.document_by_url(url)?.symbol_at(position)?;
let def_document = self.document_by_module_id(symbol.module_id())?;
def_document.goto_definition(symbol)
}
}

View file

@ -4,6 +4,7 @@ use tower_lsp::jsonrpc::Result;
use tower_lsp::lsp_types::*;
use tower_lsp::{Client, LanguageServer, LspService, Server};
mod analysis;
mod convert;
mod registry;