use cairo_lang_filesystem::db::{
AsFilesGroupMut, FilesGroup, FilesGroupEx, PrivRawFileContentQuery,
};
use lsp_types::notification::{
DidChangeConfiguration, DidChangeTextDocument, DidChangeWatchedFiles, DidCloseTextDocument,
DidOpenTextDocument, DidSaveTextDocument, Notification,
};
use lsp_types::request::{
CodeActionRequest, Completion, ExecuteCommand, Formatting, GotoDefinition, HoverRequest,
Request, SemanticTokensFullRequest,
};
use lsp_types::{
CodeActionParams, CodeActionResponse, CompletionParams, CompletionResponse,
DidChangeConfigurationParams, DidChangeTextDocumentParams, DidChangeWatchedFilesParams,
DidCloseTextDocumentParams, DidOpenTextDocumentParams, DidSaveTextDocumentParams,
DocumentFormattingParams, ExecuteCommandParams, GotoDefinitionParams, GotoDefinitionResponse,
Hover, HoverParams, SemanticTokensParams, SemanticTokensResult, TextDocumentContentChangeEvent,
TextDocumentPositionParams, TextEdit, Url,
};
use serde_json::Value;
use tracing::error;
use crate::lang::lsp::LsProtoGroup;
use crate::lsp::ext::{
ExpandMacro, ProvideVirtualFile, ProvideVirtualFileRequest, ProvideVirtualFileResponse,
ViewAnalyzedCrates,
};
use crate::lsp::result::{LSPError, LSPResult};
use crate::server::client::{Notifier, Requester};
use crate::server::commands::ServerCommands;
use crate::state::{State, StateSnapshot};
use crate::{Backend, ide, lang};
pub trait SyncRequestHandler: Request {
fn run(
state: &mut State,
notifier: Notifier,
requester: &mut Requester<'_>,
params: <Self as Request>::Params,
) -> LSPResult<<Self as Request>::Result>;
}
pub trait BackgroundDocumentRequestHandler: Request {
fn run_with_snapshot(
snapshot: StateSnapshot,
notifier: Notifier,
params: <Self as Request>::Params,
) -> LSPResult<<Self as Request>::Result>;
}
pub trait SyncNotificationHandler: Notification {
fn run(
state: &mut State,
notifier: Notifier,
requester: &mut Requester<'_>,
params: <Self as Notification>::Params,
) -> LSPResult<()>;
}
impl BackgroundDocumentRequestHandler for CodeActionRequest {
#[tracing::instrument(name = "textDocument/codeAction", skip_all)]
fn run_with_snapshot(
snapshot: StateSnapshot,
_notifier: Notifier,
params: CodeActionParams,
) -> Result<Option<CodeActionResponse>, LSPError> {
Ok(ide::code_actions::code_actions(params, &snapshot.db))
}
}
impl SyncRequestHandler for ExecuteCommand {
#[tracing::instrument(
name = "workspace/executeCommand",
skip_all,
fields(command = params.command)
)]
fn run(
state: &mut State,
_notifier: Notifier,
requester: &mut Requester<'_>,
params: ExecuteCommandParams,
) -> LSPResult<Option<Value>> {
let command = ServerCommands::try_from(params.command);
if let Ok(cmd) = command {
match cmd {
ServerCommands::Reload => {
Backend::reload(state, requester)?;
}
}
}
Ok(None)
}
}
impl BackgroundDocumentRequestHandler for HoverRequest {
#[tracing::instrument(name = "textDocument/hover", skip_all)]
fn run_with_snapshot(
snapshot: StateSnapshot,
_notifier: Notifier,
params: HoverParams,
) -> LSPResult<Option<Hover>> {
Ok(ide::hover::hover(params, &snapshot.db))
}
}
impl BackgroundDocumentRequestHandler for Formatting {
#[tracing::instrument(name = "textDocument/formatting", skip_all)]
fn run_with_snapshot(
snapshot: StateSnapshot,
_notifier: Notifier,
params: DocumentFormattingParams,
) -> LSPResult<Option<Vec<TextEdit>>> {
Ok(ide::formatter::format(params, &snapshot.db))
}
}
impl SyncNotificationHandler for DidChangeTextDocument {
#[tracing::instrument(
name = "textDocument/didChange",
skip_all,
fields(uri = %params.text_document.uri)
)]
fn run(
state: &mut State,
_notifier: Notifier,
_requester: &mut Requester<'_>,
params: DidChangeTextDocumentParams,
) -> LSPResult<()> {
let text = if let Ok([TextDocumentContentChangeEvent { text, .. }]) =
TryInto::<[_; 1]>::try_into(params.content_changes)
{
text
} else {
error!("unexpected format of document change");
return Ok(());
};
if let Some(file) = state.db.file_for_url(¶ms.text_document.uri) {
state.db.override_file_content(file, Some(text.into()));
};
Ok(())
}
}
impl SyncNotificationHandler for DidChangeConfiguration {
#[tracing::instrument(name = "workspace/didChangeConfiguration", skip_all)]
fn run(
state: &mut State,
_notifier: Notifier,
requester: &mut Requester<'_>,
_params: DidChangeConfigurationParams,
) -> LSPResult<()> {
state.config.reload(requester, &state.client_capabilities)
}
}
impl SyncNotificationHandler for DidChangeWatchedFiles {
#[tracing::instrument(name = "workspace/didChangeWatchedFiles", skip_all)]
fn run(
state: &mut State,
_notifier: Notifier,
requester: &mut Requester<'_>,
params: DidChangeWatchedFilesParams,
) -> LSPResult<()> {
for change in ¶ms.changes {
if is_cairo_file_path(&change.uri) {
let Some(file) = state.db.file_for_url(&change.uri) else { continue };
PrivRawFileContentQuery.in_db_mut(state.db.as_files_group_mut()).invalidate(&file);
}
}
for change in params.changes {
let changed_file_path = change.uri.to_file_path().unwrap_or_default();
let changed_file_name = changed_file_path.file_name().unwrap_or_default();
if ["Scarb.toml", "cairo_project.toml"].map(Some).contains(&changed_file_name.to_str())
{
Backend::reload(state, requester)?;
}
}
Ok(())
}
}
impl SyncNotificationHandler for DidCloseTextDocument {
#[tracing::instrument(
name = "textDocument/didClose",
skip_all,
fields(uri = %params.text_document.uri)
)]
fn run(
state: &mut State,
_notifier: Notifier,
_requester: &mut Requester<'_>,
params: DidCloseTextDocumentParams,
) -> LSPResult<()> {
state.open_files.remove(¶ms.text_document.uri);
if let Some(file) = state.db.file_for_url(¶ms.text_document.uri) {
state.db.override_file_content(file, None);
}
Ok(())
}
}
impl SyncNotificationHandler for DidOpenTextDocument {
#[tracing::instrument(name = "textDocument/didOpen",
skip_all,
fields(uri = %params.text_document.uri)
)]
fn run(
state: &mut State,
_notifier: Notifier,
_requester: &mut Requester<'_>,
params: DidOpenTextDocumentParams,
) -> LSPResult<()> {
let uri = params.text_document.uri;
if uri.scheme() == "file" {
let Ok(path) = uri.to_file_path() else { return Ok(()) };
state.project_controller.update_project_for_file(&path);
}
if let Some(file_id) = state.db.file_for_url(&uri) {
state.open_files.insert(uri);
state.db.override_file_content(file_id, Some(params.text_document.text.into()));
}
Ok(())
}
}
impl SyncNotificationHandler for DidSaveTextDocument {
#[tracing::instrument(
name = "textDocument/didSave",
skip_all,
fields(uri = %params.text_document.uri)
)]
fn run(
state: &mut State,
_notifier: Notifier,
_requester: &mut Requester<'_>,
params: DidSaveTextDocumentParams,
) -> LSPResult<()> {
if let Some(file) = state.db.file_for_url(¶ms.text_document.uri) {
PrivRawFileContentQuery.in_db_mut(state.db.as_files_group_mut()).invalidate(&file);
state.db.override_file_content(file, None);
}
Ok(())
}
}
impl BackgroundDocumentRequestHandler for GotoDefinition {
#[tracing::instrument(name = "textDocument/definition", skip_all)]
fn run_with_snapshot(
snapshot: StateSnapshot,
_notifier: Notifier,
params: GotoDefinitionParams,
) -> LSPResult<Option<GotoDefinitionResponse>> {
Ok(ide::navigation::goto_definition::goto_definition(params, &snapshot.db))
}
}
impl BackgroundDocumentRequestHandler for Completion {
#[tracing::instrument(name = "textDocument/completion", skip_all)]
fn run_with_snapshot(
snapshot: StateSnapshot,
_notifier: Notifier,
params: CompletionParams,
) -> LSPResult<Option<CompletionResponse>> {
Ok(ide::completion::complete(params, &snapshot.db))
}
}
impl BackgroundDocumentRequestHandler for SemanticTokensFullRequest {
#[tracing::instrument(name = "textDocument/semanticTokens/full", skip_all)]
fn run_with_snapshot(
snapshot: StateSnapshot,
_notifier: Notifier,
params: SemanticTokensParams,
) -> LSPResult<Option<SemanticTokensResult>> {
Ok(ide::semantic_highlighting::semantic_highlight_full(params, &snapshot.db))
}
}
impl BackgroundDocumentRequestHandler for ProvideVirtualFile {
#[tracing::instrument(name = "vfs/provide", skip_all)]
fn run_with_snapshot(
snapshot: StateSnapshot,
_notifier: Notifier,
params: ProvideVirtualFileRequest,
) -> LSPResult<ProvideVirtualFileResponse> {
let content = snapshot
.db
.file_for_url(¶ms.uri)
.and_then(|file_id| snapshot.db.file_content(file_id))
.map(|content| content.to_string());
Ok(ProvideVirtualFileResponse { content })
}
}
impl BackgroundDocumentRequestHandler for ViewAnalyzedCrates {
#[tracing::instrument(name = "cairo/viewAnalyzedCrates", skip_all)]
fn run_with_snapshot(
snapshot: StateSnapshot,
_notifier: Notifier,
_params: (),
) -> LSPResult<String> {
Ok(lang::inspect::crates::inspect_analyzed_crates(&snapshot.db))
}
}
impl BackgroundDocumentRequestHandler for ExpandMacro {
#[tracing::instrument(name = "cairo/expandMacro", skip_all)]
fn run_with_snapshot(
snapshot: StateSnapshot,
_notifier: Notifier,
params: TextDocumentPositionParams,
) -> LSPResult<Option<String>> {
Ok(ide::macros::expand::expand_macro(&snapshot.db, ¶ms))
}
}
fn is_cairo_file_path(file_path: &Url) -> bool {
file_path.path().ends_with(".cairo")
}