Skip to content

Commit

Permalink
Implement multi root workspaces
Browse files Browse the repository at this point in the history
  • Loading branch information
Arcticae committed Sep 23, 2024
1 parent 346ddae commit 3a2088d
Show file tree
Hide file tree
Showing 14 changed files with 338 additions and 84 deletions.
7 changes: 7 additions & 0 deletions crates/cairo-lang-language-server/src/env_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ pub const CAIRO_LS_LOG: &'_ str = "CAIRO_LS_LOG";
pub const CAIRO_LS_PROFILE: &'_ str = "CAIRO_LS_PROFILE";
pub const SCARB: &'_ str = "SCARB";

pub const CAIRO_LS_WORKSPACE_FOLDER: &'_ str = "CAIRO_LS_WORKSPACE_FOLDER";

/// Interval between compiler database regenerations (to free unused memory).
pub fn db_replace_interval() -> Duration {
const DEFAULT: u64 = 300;
Expand All @@ -28,6 +30,10 @@ pub fn db_replace_interval() -> Duration {
.unwrap_or_else(|| Duration::from_secs(DEFAULT))
}

pub fn current_workspace_scope() -> Option<PathBuf> {
env::var_os(CAIRO_LS_WORKSPACE_FOLDER).map(PathBuf::from)
}

/// LS tracing filter, see [`tracing_subscriber::EnvFilter`] for more.
pub fn log_env_filter() -> String {
env::var(CAIRO_LS_LOG).unwrap_or_default()
Expand All @@ -45,6 +51,7 @@ pub fn scarb_path() -> Option<PathBuf> {

/// Print all environment variables values (or defaults) as debug messages in logs.
pub fn report_to_logs() {
debug!("{CAIRO_LS_WORKSPACE_FOLDER}={:?}", current_workspace_scope());
debug!("{CAIRO_LS_DB_REPLACE_INTERVAL}={:?}", db_replace_interval());
debug!("{CAIRO_LS_LOG}={}", log_env_filter());
debug!("{CAIRO_LS_PROFILE}={}", tracing_profile());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::path::PathBuf;

use cairo_lang_filesystem::db::get_originating_location;
use cairo_lang_filesystem::ids::FileId;
use cairo_lang_filesystem::span::{TextPosition, TextSpan};
Expand All @@ -17,11 +19,12 @@ use crate::lang::lsp::{LsProtoGroup, ToCairo, ToLsp};
pub fn goto_definition(
params: GotoDefinitionParams,
db: &AnalysisDatabase,
workspace: Option<PathBuf>,
) -> Option<GotoDefinitionResponse> {
let file = db.file_for_url(&params.text_document_position_params.text_document.uri)?;
let position = params.text_document_position_params.position.to_cairo();
let (found_file, span) = get_definition_location(db, file, position)?;
let found_uri = db.url_for_file(found_file);
let found_uri = db.url_for_file(found_file, workspace);

let range = span.position_in_file(db.upcast(), found_file)?.to_lsp();
Some(GotoDefinitionResponse::Scalar(Location { uri: found_uri, range }))
Expand Down
10 changes: 8 additions & 2 deletions crates/cairo-lang-language-server/src/lang/diagnostics/lsp.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::path::PathBuf;

use cairo_lang_diagnostics::{DiagnosticEntry, DiagnosticLocation, Diagnostics, Severity};
use cairo_lang_filesystem::db::FilesGroup;
use cairo_lang_filesystem::ids::FileId;
Expand All @@ -13,6 +15,7 @@ use crate::lang::lsp::{LsProtoGroup, ToLsp};
#[tracing::instrument(level = "trace", skip_all)]
pub fn map_cairo_diagnostics_to_lsp<T: DiagnosticEntry>(
db: &T::DbType,
workspace: Option<PathBuf>,
diags: &mut Vec<Diagnostic>,
diagnostics: &Diagnostics<T>,
trace_macro_diagnostics: bool,
Expand All @@ -28,14 +31,15 @@ pub fn map_cairo_diagnostics_to_lsp<T: DiagnosticEntry>(
if let Some(location) = &note.location {
let Some((range, file_id)) = get_mapped_range_and_add_mapping_note(
db,
workspace.clone(),
location,
trace_macro_diagnostics.then_some(&mut related_information),
"Next note mapped from here.",
) else {
continue;
};
related_information.push(DiagnosticRelatedInformation {
location: Location { uri: db.url_for_file(file_id), range },
location: Location { uri: db.url_for_file(file_id, workspace.clone()), range },
message: note.text.clone(),
});
} else {
Expand All @@ -45,6 +49,7 @@ pub fn map_cairo_diagnostics_to_lsp<T: DiagnosticEntry>(

let Some((range, _)) = get_mapped_range_and_add_mapping_note(
db,
workspace.clone(),
&diagnostic.location(db),
trace_macro_diagnostics.then_some(&mut related_information),
"Diagnostic mapped from here.",
Expand All @@ -69,6 +74,7 @@ pub fn map_cairo_diagnostics_to_lsp<T: DiagnosticEntry>(
/// location.
fn get_mapped_range_and_add_mapping_note(
db: &(impl Upcast<dyn FilesGroup> + ?Sized),
workspace: Option<PathBuf>,
orig: &DiagnosticLocation,
related_info: Option<&mut Vec<DiagnosticRelatedInformation>>,
message: &str,
Expand All @@ -79,7 +85,7 @@ fn get_mapped_range_and_add_mapping_note(
if *orig != mapped {
if let Some(range) = get_lsp_range(db.upcast(), orig) {
related_info.push(DiagnosticRelatedInformation {
location: Location { uri: db.url_for_file(orig.file_id), range },
location: Location { uri: db.url_for_file(orig.file_id, workspace), range },
message: message.to_string(),
});
}
Expand Down
47 changes: 32 additions & 15 deletions crates/cairo-lang-language-server/src/lang/lsp/ls_proto_group.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::path::PathBuf;

use cairo_lang_filesystem::db::FilesGroup;
use cairo_lang_filesystem::ids::{FileId, FileLongId};
use cairo_lang_utils::Upcast;
Expand All @@ -20,19 +22,21 @@ pub trait LsProtoGroup: Upcast<dyn FilesGroup> {
.inspect_err(|()| error!("invalid file url: {uri}"))
.ok()
.map(|path| FileId::new(self.upcast(), path)),
"vfs" => uri
.host_str()
.or_else(|| {
error!("invalid vfs url, missing host string: {uri}");
None
})?
.parse::<usize>()
.inspect_err(|e| {
error!("invalid vfs url, host string is not a valid integer, {e}: {uri}")
})
.ok()
.map(Into::into)
.map(FileId::from_intern_id),
"vfs" =>
// For vfs://[workspace_folder]/1234.cairo -> 1234
// TODO: Test the case below (standalone project)
// For vfs://1234.cairo -> 1234
{
uri.host_str()
.expect("No host str")
.parse::<usize>()
.inspect_err(|e| {
error!("invalid vfs url, host string is not a valid integer, {e}: {uri}")
})
.ok()
.map(Into::into)
.map(FileId::from_intern_id)
}
_ => {
error!("invalid url, scheme is not supported by this language server: {uri}");
None
Expand All @@ -41,17 +45,30 @@ pub trait LsProtoGroup: Upcast<dyn FilesGroup> {
}

/// Get the canonical [`Url`] for a [`FileId`].
fn url_for_file(&self, file_id: FileId) -> Url {
fn url_for_file(&self, file_id: FileId, workspace_folder: Option<PathBuf>) -> Url {
let vf = match self.upcast().lookup_intern_file(file_id) {
FileLongId::OnDisk(path) => return Url::from_file_path(path).unwrap(),
FileLongId::Virtual(vf) => vf,
FileLongId::External(id) => self.upcast().ext_as_virtual(id),
};
// NOTE: The URL is constructed using setters and path segments in order to
// url-encode any funky characters in parts that LS is not controlling.
let workspace_scope = workspace_folder.map(|workspace_folder| {
let workspace_name = workspace_folder
.file_name()
.expect("No workspace name detected")
.to_str()
.expect("Workspace name is not correct utf-8");
format!("[{}]", workspace_name)
});
let mut url = Url::parse("vfs://").unwrap();
url.set_host(Some(&file_id.as_intern_id().to_string())).unwrap();
url.path_segments_mut().unwrap().push(&format!("{}.cairo", vf.name));
let mut path_segments = url.path_segments_mut().unwrap();
if let Some(workspace_scope) = workspace_scope {
path_segments.push(&workspace_scope);
}
path_segments.push(&format!("{}.cairo", vf.name));
drop(path_segments);
url
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ fn file_url() {
let expected_file = db.intern_file(expected_file_long);

assert_eq!(db.file_for_url(&expected_url), Some(expected_file));
assert_eq!(db.url_for_file(expected_file), expected_url);
assert_eq!(db.url_for_file(expected_file, None), expected_url);
};

check("file:///foo/bar", FileLongId::OnDisk("/foo/bar".into()));
Expand Down
23 changes: 19 additions & 4 deletions crates/cairo-lang-language-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,18 @@ use tokio::task::spawn_blocking;
use tower_lsp::jsonrpc::{Error as LSPError, Result as LSPResult};
use tower_lsp::lsp_types::request::Request;
use tower_lsp::lsp_types::{TextDocumentPositionParams, Url};
use tower_lsp::{Client, ClientSocket, LanguageServer, LspService, Server};
#[cfg(feature = "testing")]
use tower_lsp::LanguageServer;
use tower_lsp::{Client, ClientSocket, LspService, Server};
use tracing::{debug, error, info, trace_span, warn, Instrument};

use crate::config::Config;
use crate::lang::db::AnalysisDatabase;
use crate::lang::diagnostics::lsp::map_cairo_diagnostics_to_lsp;
use crate::lang::lsp::LsProtoGroup;
use crate::lsp::ext::{
CorelibVersionMismatch, ProvideVirtualFileRequest, ProvideVirtualFileResponse,
CorelibVersionMismatch, CorelibVersionMismatchParams, ProvideVirtualFileRequest,
ProvideVirtualFileResponse,
};
use crate::project::scarb::update_crate_roots;
use crate::project::unmanaged_core_crate::try_to_init_unmanaged_core;
Expand Down Expand Up @@ -254,6 +257,7 @@ struct Backend {
scarb_toolchain: ScarbToolchain,
last_replace: tokio::sync::Mutex<SystemTime>,
db_replace_interval: Duration,
workspace_folder: Option<PathBuf>, // Backend can be unscoped as well
}

/// TODO: Remove when we move to sync world.
Expand Down Expand Up @@ -291,6 +295,7 @@ impl Backend {
scarb_toolchain,
last_replace: tokio::sync::Mutex::new(SystemTime::now()),
db_replace_interval: env_config::db_replace_interval(),
workspace_folder: env_config::current_workspace_scope(),
}
}

Expand Down Expand Up @@ -466,7 +471,7 @@ impl Backend {
let state = self.state_snapshot().await;
let db = state.db;
let config = state.config;
let file_url = db.url_for_file(*file);
let file_url = db.url_for_file(*file, self.workspace_folder.clone());
let mut semantic_file_diagnostics: Vec<SemanticDiagnostic> = vec![];
let mut lowering_file_diagnostics: Vec<LoweringDiagnostic> = vec![];

Expand Down Expand Up @@ -532,18 +537,21 @@ impl Backend {
let trace_macro_diagnostics = config.trace_macro_diagnostics;
map_cairo_diagnostics_to_lsp(
(*db).upcast(),
self.workspace_folder.clone(),
&mut diags,
&new_file_diagnostics.parser,
trace_macro_diagnostics,
);
map_cairo_diagnostics_to_lsp(
(*db).upcast(),
self.workspace_folder.clone(),
&mut diags,
&new_file_diagnostics.semantic,
trace_macro_diagnostics,
);
map_cairo_diagnostics_to_lsp(
(*db).upcast(),
self.workspace_folder.clone(),
&mut diags,
&new_file_diagnostics.lowering,
trace_macro_diagnostics,
Expand Down Expand Up @@ -696,6 +704,7 @@ impl Backend {
) {
match ProjectManifestPath::discover(&file_path) {
Some(ProjectManifestPath::Scarb(manifest_path)) => {
let manifest_copy = manifest_path.clone();
let Ok(metadata) = spawn_blocking({
let scarb = self.scarb_toolchain.clone();
move || {
Expand Down Expand Up @@ -729,7 +738,13 @@ impl Backend {

if let Err(result) = validate_corelib(db) {
self.client
.send_notification::<CorelibVersionMismatch>(result.to_string())
.send_notification::<CorelibVersionMismatch>(CorelibVersionMismatchParams {
error_message: result.to_string(),
manifest_dir: manifest_copy
.to_str()
.expect("Invalid utf-8 as path")
.to_string(),
})
.await;
}
}
Expand Down
18 changes: 15 additions & 3 deletions crates/cairo-lang-language-server/src/lsp/capabilities/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
//! > capability.

use std::ops::Not;
use std::path::PathBuf;

use missing_lsp_types::{
CodeActionRegistrationOptions, DefinitionRegistrationOptions,
Expand Down Expand Up @@ -101,20 +102,31 @@ pub fn collect_server_capabilities(client_capabilities: &ClientCapabilities) ->
/// Returns registrations of capabilities the server wants to register dynamically.
pub fn collect_dynamic_registrations(
client_capabilities: &ClientCapabilities,
workspace_folder: Option<PathBuf>,
) -> Vec<Registration> {
let mut registrations = vec![];

let workspace_pattern = workspace_folder
.clone()
.map(|folder| format!("{}/**/*", folder.to_str().expect("Incorrect workspace path")));
let vfs_workspace_pattern = workspace_folder.map(|folder| {
let workspace_name = folder
.file_name()
.expect("No folder name for workspace")
.to_str()
.expect("Incorrect utf-8 as workspace folder name");
format!("/[{}]/*", workspace_name)
});
// Relevant files.
let document_selector = Some(vec![
DocumentFilter {
language: Some("cairo".to_string()),
scheme: Some("file".to_string()),
pattern: None,
pattern: workspace_pattern.clone(),
},
DocumentFilter {
language: Some("cairo".to_string()),
scheme: Some("vfs".to_string()),
pattern: None,
pattern: vfs_workspace_pattern,
},
]);
let text_document_registration_options =
Expand Down
10 changes: 7 additions & 3 deletions crates/cairo-lang-language-server/src/lsp/controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ impl LanguageServer for Backend {
// Dynamically register capabilities.
let client_capabilities = self.state_snapshot().await.client_capabilities;

let dynamic_registrations = collect_dynamic_registrations(&client_capabilities);
let dynamic_registrations =
collect_dynamic_registrations(&client_capabilities, self.workspace_folder.clone());
if !dynamic_registrations.is_empty() {
let result = self.client.register_capability(dynamic_registrations).await;
if let Err(err) = result {
Expand Down Expand Up @@ -243,8 +244,11 @@ impl LanguageServer for Backend {
params: GotoDefinitionParams,
) -> LSPResult<Option<GotoDefinitionResponse>> {
let db = self.db_snapshot().await;
self.catch_panics(move || ide::navigation::goto_definition::goto_definition(params, &db))
.await
let workspace_folder = self.workspace_folder.clone();
self.catch_panics(move || {
ide::navigation::goto_definition::goto_definition(params, &db, workspace_folder)
})
.await
}

#[tracing::instrument(level = "trace", skip_all)]
Expand Down
9 changes: 8 additions & 1 deletion crates/cairo-lang-language-server/src/lsp/ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,14 @@ impl Request for ExpandMacro {
#[derive(Debug)]
pub struct CorelibVersionMismatch;

#[derive(Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CorelibVersionMismatchParams {
pub error_message: String,
pub manifest_dir: String,
}

impl Notification for CorelibVersionMismatch {
type Params = String;
type Params = CorelibVersionMismatchParams;
const METHOD: &'static str = "cairo/corelib-version-mismatch";
}
Loading

0 comments on commit 3a2088d

Please sign in to comment.