mirror of
https://github.com/roc-lang/roc.git
synced 2025-08-02 19:32:17 +00:00
Go-to-definition basic support
This commit is contained in:
parent
e954e074fb
commit
886a367026
10 changed files with 465 additions and 313 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -2638,6 +2638,7 @@ dependencies = [
|
|||
"bumpalo",
|
||||
"parking_lot",
|
||||
"roc_can",
|
||||
"roc_collections",
|
||||
"roc_load",
|
||||
"roc_module",
|
||||
"roc_packaging",
|
||||
|
|
|
@ -727,6 +727,17 @@ pub fn find_closest_type_at(
|
|||
visitor.region_typ
|
||||
}
|
||||
|
||||
/// Given an ability Foo has foo : ..., returns (T, foo1) if the symbol at the given region is a
|
||||
/// symbol foo1 that specializes foo for T. Otherwise if the symbol is foo but the specialization
|
||||
/// is unknown, (Foo, foo) is returned. Otherwise [None] is returned.
|
||||
pub fn find_closest_symbol_at(
|
||||
position: Position,
|
||||
decls: &Declarations,
|
||||
abilities_store: &AbilitiesStore,
|
||||
) -> Option<FoundSymbol> {
|
||||
find_symbol_at_impl(Region::from_pos(position), decls, abilities_store, true)
|
||||
}
|
||||
|
||||
/// Given an ability Foo has foo : ..., returns (T, foo1) if the symbol at the given region is a
|
||||
/// symbol foo1 that specializes foo for T. Otherwise if the symbol is foo but the specialization
|
||||
/// is unknown, (Foo, foo) is returned. Otherwise [None] is returned.
|
||||
|
@ -734,11 +745,21 @@ pub fn find_symbol_at(
|
|||
region: Region,
|
||||
decls: &Declarations,
|
||||
abilities_store: &AbilitiesStore,
|
||||
) -> Option<FoundSymbol> {
|
||||
find_symbol_at_impl(region, decls, abilities_store, false)
|
||||
}
|
||||
|
||||
pub fn find_symbol_at_impl(
|
||||
region: Region,
|
||||
decls: &Declarations,
|
||||
abilities_store: &AbilitiesStore,
|
||||
allow_subregion: bool,
|
||||
) -> Option<FoundSymbol> {
|
||||
let mut visitor = Finder {
|
||||
region,
|
||||
found: None,
|
||||
abilities_store,
|
||||
allow_subregion,
|
||||
};
|
||||
visitor.visit_decls(decls);
|
||||
return visitor.found;
|
||||
|
@ -747,6 +768,17 @@ pub fn find_symbol_at(
|
|||
region: Region,
|
||||
abilities_store: &'a AbilitiesStore,
|
||||
found: Option<FoundSymbol>,
|
||||
allow_subregion: bool,
|
||||
}
|
||||
|
||||
impl<'a> Finder<'a> {
|
||||
fn is_at_wanted_region(&self, region: Region) -> bool {
|
||||
if self.allow_subregion {
|
||||
region.contains(&self.region)
|
||||
} else {
|
||||
region == self.region
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Visitor for Finder<'_> {
|
||||
|
@ -755,7 +787,7 @@ pub fn find_symbol_at(
|
|||
}
|
||||
|
||||
fn visit_pattern(&mut self, pattern: &Pattern, region: Region, _opt_var: Option<Variable>) {
|
||||
if region == self.region {
|
||||
if self.is_at_wanted_region(region) {
|
||||
match pattern {
|
||||
Pattern::AbilityMemberSpecialization {
|
||||
ident: spec_symbol,
|
||||
|
@ -776,7 +808,7 @@ pub fn find_symbol_at(
|
|||
}
|
||||
|
||||
fn visit_expr(&mut self, expr: &Expr, region: Region, var: Variable) {
|
||||
if region == self.region {
|
||||
if self.is_at_wanted_region(region) {
|
||||
match expr {
|
||||
&Expr::AbilityMember(member_symbol, specialization_id, _var) => {
|
||||
debug_assert!(self.found.is_none());
|
||||
|
|
|
@ -22,7 +22,7 @@ pub use roc_load_internal::file::{
|
|||
Threading,
|
||||
};
|
||||
pub use roc_load_internal::module::{
|
||||
EntryPoint, Expectations, ExposedToHost, LoadedModule, MonomorphizedModule,
|
||||
CheckedModule, EntryPoint, Expectations, ExposedToHost, LoadedModule, MonomorphizedModule,
|
||||
};
|
||||
pub use roc_solve::FunctionKind;
|
||||
|
||||
|
|
|
@ -1116,32 +1116,7 @@ impl<'a> LoadStart<'a> {
|
|||
);
|
||||
|
||||
match res_loaded {
|
||||
Ok(header_output) => {
|
||||
if let Msg::Header(ModuleHeader {
|
||||
module_id: header_id,
|
||||
header_type,
|
||||
is_root_module,
|
||||
..
|
||||
}) = &header_output.msg
|
||||
{
|
||||
debug_assert_eq!(*header_id, header_output.module_id);
|
||||
debug_assert!(is_root_module);
|
||||
|
||||
if let HeaderType::Interface { name, .. } = header_type {
|
||||
// Interface modules can have names like Foo.Bar.Baz,
|
||||
// in which case we need to adjust the src_dir to
|
||||
// remove the "Bar/Baz" directories in order to correctly
|
||||
// resolve this interface module's imports!
|
||||
let dirs_to_pop = name.as_str().matches('.').count();
|
||||
|
||||
for _ in 0..dirs_to_pop {
|
||||
src_dir.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
header_output
|
||||
}
|
||||
Ok(header_output) => adjust_header_paths(header_output, &mut src_dir),
|
||||
|
||||
Err(problem) => {
|
||||
let module_ids = Arc::try_unwrap(arc_modules)
|
||||
|
@ -1175,7 +1150,7 @@ impl<'a> LoadStart<'a> {
|
|||
filename: PathBuf,
|
||||
src: &'a str,
|
||||
roc_cache_dir: RocCacheDir<'_>,
|
||||
src_dir: PathBuf,
|
||||
mut src_dir: PathBuf,
|
||||
) -> Result<Self, LoadingProblem<'a>> {
|
||||
let arc_modules = Arc::new(Mutex::new(PackageModuleIds::default()));
|
||||
let root_exposed_ident_ids = IdentIds::exposed_builtins(0);
|
||||
|
@ -1189,7 +1164,7 @@ impl<'a> LoadStart<'a> {
|
|||
} = {
|
||||
let root_start_time = Instant::now();
|
||||
|
||||
load_from_str(
|
||||
let header_output = load_from_str(
|
||||
arena,
|
||||
filename,
|
||||
src,
|
||||
|
@ -1197,7 +1172,9 @@ impl<'a> LoadStart<'a> {
|
|||
Arc::clone(&ident_ids_by_module),
|
||||
roc_cache_dir,
|
||||
root_start_time,
|
||||
)?
|
||||
)?;
|
||||
|
||||
adjust_header_paths(header_output, &mut src_dir)
|
||||
};
|
||||
|
||||
Ok(LoadStart {
|
||||
|
@ -1211,6 +1188,34 @@ impl<'a> LoadStart<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
fn adjust_header_paths<'a>(
|
||||
header_output: HeaderOutput<'a>,
|
||||
src_dir: &mut PathBuf,
|
||||
) -> HeaderOutput<'a> {
|
||||
if let Msg::Header(ModuleHeader {
|
||||
module_id: header_id,
|
||||
header_type,
|
||||
..
|
||||
}) = &header_output.msg
|
||||
{
|
||||
debug_assert_eq!(*header_id, header_output.module_id);
|
||||
|
||||
if let HeaderType::Interface { name, .. } = header_type {
|
||||
// Interface modules can have names like Foo.Bar.Baz,
|
||||
// in which case we need to adjust the src_dir to
|
||||
// remove the "Bar/Baz" directories in order to correctly
|
||||
// resolve this interface module's imports!
|
||||
let dirs_to_pop = name.as_str().matches('.').count();
|
||||
|
||||
for _ in 0..dirs_to_pop {
|
||||
src_dir.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
header_output
|
||||
}
|
||||
|
||||
pub enum LoadResult<'a> {
|
||||
TypeChecked(LoadedModule),
|
||||
Monomorphized(MonomorphizedModule<'a>),
|
||||
|
|
|
@ -9,6 +9,7 @@ path = "src/server.rs"
|
|||
|
||||
[dependencies]
|
||||
roc_can = { path = "../compiler/can" }
|
||||
roc_collections = { path = "../compiler/collections" }
|
||||
roc_load = { path = "../compiler/load" }
|
||||
roc_module = { path = "../compiler/module" }
|
||||
roc_problem = { path = "../compiler/problem" }
|
||||
|
@ -22,5 +23,5 @@ roc_packaging = {path = "../packaging"}
|
|||
bumpalo = { version = "3.8.0", features = ["collections"] }
|
||||
|
||||
tower-lsp = "0.17.0"
|
||||
tokio = { version = "1.20.1", features = [ "rt", "rt-multi-thread", "macros", "io-std", ] }
|
||||
tokio = { version = "1.20.1", features = [ "rt", "rt-multi-thread", "macros", "io-std" ] }
|
||||
parking_lot = "0.12.1"
|
||||
|
|
4
crates/lang_srv/debug_server.sh
Executable file
4
crates/lang_srv/debug_server.sh
Executable file
|
@ -0,0 +1,4 @@
|
|||
#!/bin/bash
|
||||
|
||||
SCRIPT_DIR=$(cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd)
|
||||
${SCRIPT_DIR}/../../target/debug/roc_ls "$@" 2> /tmp/rocls.err
|
332
crates/lang_srv/src/analysis.rs
Normal file
332
crates/lang_srv/src/analysis.rs
Normal 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)))
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
);
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue