use crate::config::{FstarConfig, LspSettings};
use crate::document::{DocumentState, FragmentStatus, StatusUpdate};
use crate::error::Result;
use dashmap::DashMap;
use std::sync::Arc;
use tokio::sync::{mpsc, RwLock};
use tower_lsp::jsonrpc::Result as RpcResult;
use tower_lsp::lsp_types::{
CompletionOptions, CompletionParams, CompletionResponse, DidChangeConfigurationParams,
DidChangeTextDocumentParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams,
DidSaveTextDocumentParams, DocumentFormattingParams, DocumentRangeFormattingParams,
GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverContents, HoverParams,
HoverProviderCapability, InitializeParams, InitializeResult, InitializedParams, MarkupContent,
MarkupKind, MessageType, OneOf, Position, Range, SaveOptions, ServerCapabilities, ServerInfo,
TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions,
TextDocumentSyncSaveOptions, TextEdit, Url, WorkspaceFolder,
};
use tower_lsp::{Client, LanguageServer};
use tracing::{debug, error, info};
pub mod requests {
use serde::{Deserialize, Serialize};
use tower_lsp::lsp_types::{Range, TextDocumentIdentifier};
pub type GetDiagnosticsParams = TextDocumentIdentifier;
pub type GetFragmentsParams = TextDocumentIdentifier;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FragmentInfo {
pub range: Range,
pub status: String,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub stale: bool,
}
}
pub mod notifications {
use serde::{Deserialize, Serialize};
use tower_lsp::lsp_types::notification::Notification;
#[derive(Debug)]
pub enum StatusNotification {}
impl Notification for StatusNotification {
type Params = StatusParams;
const METHOD: &'static str = "$/fstar/status";
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StatusParams {
pub uri: String,
pub fragments: Vec<FragmentStatus>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FragmentStatus {
pub kind: String,
pub range: tower_lsp::lsp_types::Range,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub stale: bool,
}
#[allow(dead_code)]
#[derive(Debug)]
pub enum VerifyToPosition {}
impl Notification for VerifyToPosition {
type Params = VerifyToPositionParams;
const METHOD: &'static str = "$/fstar/verifyToPosition";
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VerifyToPositionParams {
pub uri: String,
pub position: tower_lsp::lsp_types::Position,
pub lax: bool,
}
#[allow(dead_code)]
#[derive(Debug)]
pub enum Restart {}
impl Notification for Restart {
type Params = RestartParams;
const METHOD: &'static str = "$/fstar/restart";
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RestartParams {
pub uri: String,
}
#[allow(dead_code)]
#[derive(Debug)]
pub enum KillAndRestartSolver {}
impl Notification for KillAndRestartSolver {
type Params = KillAndRestartSolverParams;
const METHOD: &'static str = "$/fstar/killAndRestartSolver";
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KillAndRestartSolverParams {
pub uri: String,
}
#[allow(dead_code)]
#[derive(Debug)]
pub enum KillAll {}
impl Notification for KillAll {
type Params = KillAllParams;
const METHOD: &'static str = "$/fstar/killAll";
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KillAllParams {}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetTranslatedFstParams {
pub uri: String,
pub position: tower_lsp::lsp_types::Position,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetTranslatedFstResponse {
pub uri: String,
pub position: tower_lsp::lsp_types::Position,
}
}
pub struct FstarServer {
client: Client,
documents: DashMap<String, Arc<DocumentState>>,
settings: RwLock<LspSettings>,
status_tx: mpsc::Sender<StatusUpdate>,
workspace_folders: RwLock<Vec<WorkspaceFolder>>,
flycheck_handles: DashMap<String, tokio::task::AbortHandle>,
}
impl FstarServer {
pub fn new(client: Client, cli_settings: LspSettings) -> Self {
let (status_tx, status_rx) = mpsc::channel(100);
let server = Self {
client: client.clone(),
documents: DashMap::new(),
settings: RwLock::new(cli_settings),
status_tx,
workspace_folders: RwLock::new(Vec::new()),
flycheck_handles: DashMap::new(),
};
tokio::spawn(Self::status_update_handler(client, status_rx));
server
}
async fn status_update_handler(client: Client, mut rx: mpsc::Receiver<StatusUpdate>) {
while let Some(update) = rx.recv().await {
let params = notifications::StatusParams {
uri: update.uri.to_string(),
fragments: update
.fragments
.iter()
.map(|f| notifications::FragmentStatus {
kind: match f.status {
FragmentStatus::Ok => "ok",
FragmentStatus::LaxOk => "lax-ok",
FragmentStatus::Started => "started",
FragmentStatus::Failed => "failed",
}
.to_string(),
range: f.range,
stale: f.stale,
})
.collect(),
};
client
.send_notification::<notifications::StatusNotification>(params)
.await;
client
.publish_diagnostics(update.uri, update.diagnostics, None)
.await;
}
}
async fn get_or_create_document(
&self,
uri: &Url,
text: Option<String>,
version: i32,
) -> Result<Arc<DocumentState>> {
let key = uri.to_string();
if let Some(doc) = self.documents.get(&key) {
return Ok(Arc::clone(&doc));
}
let file_path = uri
.to_file_path()
.map_err(|_| crate::error::FstarError::Config("Invalid URI".to_string()))?;
let ws_folders: Vec<std::path::PathBuf> = self
.workspace_folders
.read()
.await
.iter()
.filter_map(|f| Url::parse(&f.uri.to_string()).ok()?.to_file_path().ok())
.collect();
let mut config = match FstarConfig::find_and_load(&file_path, &ws_folders).await? {
Some((config, _)) => config,
None => FstarConfig::default(),
};
let settings = self.settings.read().await;
if config.fstar_exe.is_none() {
if let Some(ref exe) = settings.fstar_exe {
config.fstar_exe = Some(exe.clone());
}
}
drop(settings);
let settings = self.settings.read().await.clone();
let doc = Arc::new(
DocumentState::new(
uri.clone(),
text.unwrap_or_default(),
version,
config,
settings,
self.status_tx.clone(),
)
.await?,
);
if let Err(e) = doc.initialize().await {
error!("Failed to initialize F* for {}: {}", uri, e);
self.client
.show_message(MessageType::ERROR, format!("Failed to start F*: {}", e))
.await;
return Err(e);
}
self.documents.insert(key, Arc::clone(&doc));
Ok(doc)
}
fn get_document(&self, uri: &Url) -> Option<Arc<DocumentState>> {
self.documents.get(&uri.to_string()).map(|r| Arc::clone(&r))
}
async fn remove_document(&self, uri: &Url) {
if let Some((_, doc)) = self.documents.remove(&uri.to_string()) {
doc.dispose().await;
}
}
}
#[tower_lsp::async_trait]
impl LanguageServer for FstarServer {
async fn initialize(&self, params: InitializeParams) -> RpcResult<InitializeResult> {
info!("F* LSP server initializing");
if let Some(folders) = params.workspace_folders {
*self.workspace_folders.write().await = folders;
}
if let Some(options) = params.initialization_options {
if let Ok(client_settings) = serde_json::from_value::<LspSettings>(options) {
let mut settings = self.settings.write().await;
let cli_fstar_exe = settings.fstar_exe.clone();
settings.verify_on_open = client_settings.verify_on_open;
settings.verify_on_save = client_settings.verify_on_save;
settings.fly_check = client_settings.fly_check;
settings.timeout_ms = client_settings.timeout_ms;
settings.max_processes = client_settings.max_processes;
settings.debug = settings.debug || client_settings.debug;
if cli_fstar_exe.is_some() {
settings.fstar_exe = cli_fstar_exe;
} else {
settings.fstar_exe = client_settings.fstar_exe;
}
}
}
Ok(InitializeResult {
capabilities: ServerCapabilities {
text_document_sync: Some(TextDocumentSyncCapability::Options(
TextDocumentSyncOptions {
open_close: Some(true),
change: Some(TextDocumentSyncKind::INCREMENTAL),
save: Some(TextDocumentSyncSaveOptions::SaveOptions(SaveOptions {
include_text: Some(false),
})),
..Default::default()
},
)),
hover_provider: Some(HoverProviderCapability::Simple(true)),
completion_provider: Some(CompletionOptions {
trigger_characters: Some(vec![".".to_string()]),
..Default::default()
}),
definition_provider: Some(OneOf::Left(true)),
document_formatting_provider: Some(OneOf::Left(true)),
document_range_formatting_provider: Some(OneOf::Left(true)),
..Default::default()
},
server_info: Some(ServerInfo {
name: "fstar-lsp".to_string(),
version: Some(env!("CARGO_PKG_VERSION").to_string()),
}),
})
}
async fn initialized(&self, _: InitializedParams) {
info!("F* LSP server initialized");
}
async fn shutdown(&self) -> RpcResult<()> {
info!("F* LSP server shutting down");
let docs: Vec<Arc<DocumentState>> = self
.documents
.iter()
.map(|entry| Arc::clone(entry.value()))
.collect();
for doc in docs {
doc.dispose().await;
}
self.documents.clear();
Ok(())
}
async fn did_open(&self, params: DidOpenTextDocumentParams) {
let uri = params.text_document.uri;
let text = params.text_document.text;
let version = params.text_document.version;
debug!("Document opened: {}", uri);
match self.get_or_create_document(&uri, Some(text), version).await {
Ok(doc) => {
let settings = self.settings.read().await;
if settings.verify_on_open {
drop(settings);
if let Err(e) = doc.verify_full().await {
error!("Verification failed: {}", e);
}
} else if settings.fly_check {
drop(settings);
if let Err(e) = doc.verify_lax().await {
error!("Lax check failed: {}", e);
}
}
}
Err(e) => {
error!("Failed to open document: {}", e);
}
}
}
async fn did_change(&self, params: DidChangeTextDocumentParams) {
let uri = params.text_document.uri;
let version = params.text_document.version;
let key = uri.to_string();
if let Some(doc) = self.get_document(&uri) {
doc.update(params.content_changes, version).await;
let settings = self.settings.read().await;
if settings.fly_check {
drop(settings);
if let Some((_, prev)) = self.flycheck_handles.remove(&key) {
prev.abort();
}
let doc = Arc::clone(&doc);
let handle_key = key.clone();
let handles = self.flycheck_handles.clone();
let task = tokio::spawn(async move {
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
let _ = doc.verify_lax().await;
let _ = doc.verify_cache().await;
handles.remove(&handle_key);
});
self.flycheck_handles.insert(key, task.abort_handle());
}
}
}
async fn did_save(&self, params: DidSaveTextDocumentParams) {
let uri = params.text_document.uri;
if let Some(doc) = self.get_document(&uri) {
let settings = self.settings.read().await;
if settings.verify_on_save {
drop(settings);
if let Err(e) = doc.verify_full().await {
error!("Verification on save failed: {}", e);
}
}
}
}
async fn did_close(&self, params: DidCloseTextDocumentParams) {
let uri = params.text_document.uri;
let key = uri.to_string();
debug!("Document closed: {}", uri);
if let Some((_, handle)) = self.flycheck_handles.remove(&key) {
handle.abort();
}
self.remove_document(&uri).await;
}
async fn did_change_configuration(&self, params: DidChangeConfigurationParams) {
if let Ok(client_settings) = serde_json::from_value::<LspSettings>(params.settings) {
info!("Configuration updated");
let mut settings = self.settings.write().await;
let cli_fstar_exe = settings.fstar_exe.clone();
settings.verify_on_open = client_settings.verify_on_open;
settings.verify_on_save = client_settings.verify_on_save;
settings.fly_check = client_settings.fly_check;
settings.timeout_ms = client_settings.timeout_ms;
settings.max_processes = client_settings.max_processes;
settings.debug = settings.debug || client_settings.debug;
if cli_fstar_exe.is_some() {
settings.fstar_exe = cli_fstar_exe;
} else {
settings.fstar_exe = client_settings.fstar_exe;
}
}
}
async fn hover(&self, params: HoverParams) -> RpcResult<Option<Hover>> {
let uri = params.text_document_position_params.text_document.uri;
let position = params.text_document_position_params.position;
if let Some(doc) = self.get_document(&uri) {
if let Some((contents, range)) = doc.hover(position).await {
return Ok(Some(Hover {
contents: HoverContents::Markup(MarkupContent {
kind: MarkupKind::Markdown,
value: contents,
}),
range,
}));
}
}
Ok(None)
}
async fn goto_definition(
&self,
params: GotoDefinitionParams,
) -> RpcResult<Option<GotoDefinitionResponse>> {
let uri = params.text_document_position_params.text_document.uri;
let position = params.text_document_position_params.position;
if let Some(doc) = self.get_document(&uri) {
if let Some(location) = doc.definition(position).await {
return Ok(Some(GotoDefinitionResponse::Scalar(location)));
}
}
Ok(None)
}
async fn completion(&self, params: CompletionParams) -> RpcResult<Option<CompletionResponse>> {
let uri = params.text_document_position.text_document.uri;
let position = params.text_document_position.position;
if let Some(doc) = self.get_document(&uri) {
let items = doc.completions(position).await;
if !items.is_empty() {
return Ok(Some(CompletionResponse::Array(items)));
}
}
Ok(None)
}
async fn formatting(
&self,
params: DocumentFormattingParams,
) -> RpcResult<Option<Vec<TextEdit>>> {
let uri = params.text_document.uri;
if let Some(doc) = self.get_document(&uri) {
if let Some(formatted) = doc.format().await {
let end_line = doc.line_count().await;
return Ok(Some(vec![TextEdit {
range: Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: end_line,
character: 0,
},
},
new_text: formatted,
}]));
}
}
Ok(None)
}
async fn range_formatting(
&self,
params: DocumentRangeFormattingParams,
) -> RpcResult<Option<Vec<TextEdit>>> {
let uri = params.text_document.uri;
let range = params.range;
if let Some(doc) = self.get_document(&uri) {
if let Some(selected_text) = doc.get_text_in_range(range).await {
if let Some(formatted) = doc.format_text(&selected_text).await {
return Ok(Some(vec![TextEdit {
range,
new_text: formatted,
}]));
}
}
}
Ok(None)
}
}
impl FstarServer {
pub async fn handle_verify_to_position(&self, params: notifications::VerifyToPositionParams) {
if let Ok(uri) = Url::parse(¶ms.uri) {
if let Some(doc) = self.get_document(&uri) {
if let Err(e) = doc.verify_to_position(params.position, params.lax).await {
error!("Verify to position failed: {}", e);
}
}
}
}
pub async fn handle_restart(&self, params: notifications::RestartParams) {
if let Ok(uri) = Url::parse(¶ms.uri) {
if let Some(doc) = self.get_document(&uri) {
if let Err(e) = doc.restart().await {
error!("Restart failed: {}", e);
self.client
.show_message(MessageType::ERROR, format!("Failed to restart F*: {}", e))
.await;
}
}
}
}
pub async fn handle_kill_and_restart_solver(
&self,
params: notifications::KillAndRestartSolverParams,
) {
if let Ok(uri) = Url::parse(¶ms.uri) {
if let Some(doc) = self.get_document(&uri) {
if let Err(e) = doc.restart_solver().await {
error!("Restart solver failed: {}", e);
}
}
}
}
pub async fn handle_kill_all(&self, _params: notifications::KillAllParams) {
let docs: Vec<Arc<DocumentState>> = self
.documents
.iter()
.map(|entry| Arc::clone(entry.value()))
.collect();
for doc in docs {
doc.dispose().await;
}
self.documents.clear();
}
pub async fn handle_get_translated_fst(
&self,
_params: notifications::GetTranslatedFstParams,
) -> tower_lsp::jsonrpc::Result<Option<notifications::GetTranslatedFstResponse>> {
Ok(None)
}
pub async fn handle_get_diagnostics(
&self,
params: requests::GetDiagnosticsParams,
) -> RpcResult<Vec<tower_lsp::lsp_types::Diagnostic>> {
if let Some(doc) = self.get_document(¶ms.uri) {
Ok(doc.get_diagnostics().await)
} else {
Ok(vec![])
}
}
pub async fn handle_get_fragments(
&self,
params: requests::GetFragmentsParams,
) -> RpcResult<Vec<requests::FragmentInfo>> {
if let Some(doc) = self.get_document(¶ms.uri) {
let fragments = doc.get_fragments().await;
Ok(fragments
.into_iter()
.map(|f| requests::FragmentInfo {
range: f.range,
status: fragment_status_to_string(f.status),
stale: f.stale,
})
.collect())
} else {
Ok(vec![])
}
}
}
fn fragment_status_to_string(status: FragmentStatus) -> String {
match status {
FragmentStatus::Ok => "ok",
FragmentStatus::LaxOk => "lax-ok",
FragmentStatus::Started => "started",
FragmentStatus::Failed => "failed",
}
.to_string()
}