use std::collections::HashMap;
use lsp_types::notification::{Notification, PublishDiagnostics as PublishDiagnosticsBase};
use lsp_types::{Diagnostic, Url};
use reflexo::path::unix_slash;
use reflexo_typst::typst::prelude::{eco_vec, EcoVec};
use serde::{Deserialize, Serialize};
use tinymist_project::CompileReport;
use tinymist_query::DiagnosticsMap;
use tokio::sync::mpsc;
use crate::project::ProjectInsId;
use crate::{tool::word_count::WordsCount, LspClient};
#[derive(Debug, Clone)]
pub struct EditorActorConfig {
pub notify_status: bool,
}
pub enum EditorRequest {
Config(EditorActorConfig),
Diag(ProjVersion, Option<DiagnosticsMap>),
Status(CompileReport),
WordCount(ProjectInsId, WordsCount),
}
pub struct EditorActor {
client: LspClient,
editor_rx: mpsc::UnboundedReceiver<EditorRequest>,
config: EditorActorConfig,
diagnostics: HashMap<Url, HashMap<ProjectInsId, EcoVec<Diagnostic>>>,
affect_map: HashMap<ProjectInsId, Vec<Url>>,
status: StatusAll,
}
impl EditorActor {
pub fn new(
client: LspClient,
editor_rx: mpsc::UnboundedReceiver<EditorRequest>,
notify_status: bool,
) -> Self {
Self {
client,
editor_rx,
diagnostics: HashMap::new(),
affect_map: HashMap::new(),
config: EditorActorConfig { notify_status },
status: StatusAll {
status: CompileStatusEnum::Compiling,
path: "".to_owned(),
page_count: 0,
words_count: None,
},
}
}
pub async fn run(mut self) {
while let Some(req) = self.editor_rx.recv().await {
self.handle(req);
}
log::info!("editor actor is stopped");
}
#[cfg(not(feature = "system"))]
pub fn step(&mut self) {
while let Ok(req) = self.editor_rx.try_recv() {
self.handle(req);
}
}
fn handle(&mut self, req: EditorRequest) {
match req {
EditorRequest::Config(config) => {
log::info!("received config request: {config:?}");
self.config = config;
}
EditorRequest::Diag(version, diagnostics) => {
log::debug!(
"received diagnostics from {version:?}: diag({:?})",
diagnostics.as_ref().map(|files| files.len())
);
self.publish(version.id, diagnostics);
}
EditorRequest::Status(compile_status) => {
log::trace!("received status request: {compile_status:?}");
if self.config.notify_status && compile_status.id == ProjectInsId::PRIMARY {
use tinymist_project::CompileStatusEnum::*;
self.status.path = compile_status
.compiling_id
.map(|fid| unix_slash(fid.vpath().as_rooted_path()))
.unwrap_or_default();
self.status.page_count = compile_status.page_count;
self.status.status = match &compile_status.status {
Compiling => CompileStatusEnum::Compiling,
Suspend | CompileSuccess { .. } => CompileStatusEnum::CompileSuccess,
ExportError { .. } | CompileError { .. } => CompileStatusEnum::CompileError,
};
self.client.send_notification::<StatusAll>(&self.status);
}
}
EditorRequest::WordCount(id, count) => {
log::trace!("received word count request");
if self.config.notify_status && id == ProjectInsId::PRIMARY {
self.status.words_count = Some(count);
self.client.send_notification::<StatusAll>(&self.status);
}
}
}
}
pub fn publish(&mut self, id: ProjectInsId, next_diag: Option<DiagnosticsMap>) {
let affected = match next_diag.as_ref() {
Some(next_diag) => self
.affect_map
.insert(id.clone(), next_diag.keys().cloned().collect()),
None => self.affect_map.remove(&id),
};
for uri in affected.into_iter().flatten() {
if !next_diag.as_ref().is_some_and(|e| e.contains_key(&uri)) {
self.publish_file(&id, uri, None)
}
}
for (uri, next) in next_diag.into_iter().flatten() {
self.publish_file(&id, uri, Some(next))
}
}
fn publish_file(&mut self, id: &ProjectInsId, uri: Url, next: Option<EcoVec<Diagnostic>>) {
let mut diagnostics = EcoVec::new();
let path_diags = self.diagnostics.entry(uri.clone()).or_default();
for (existing_id, diags) in path_diags.iter() {
if existing_id != id {
diagnostics.push(diags.clone());
}
}
if let Some(diags) = &next {
diagnostics.push(diags.clone())
}
match next {
Some(next) => path_diags.insert(id.clone(), next),
None => path_diags.remove(id),
};
self.client
.send_notification::<PublishDiagnostics>(&PublishDiagnosticsParams {
uri,
diagnostics: ScatterVec(diagnostics),
version: None,
});
}
}
#[derive(Debug, Clone)]
pub struct ProjVersion {
pub id: ProjectInsId,
pub revision: usize,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum CompileStatusEnum {
Compiling,
CompileSuccess,
CompileError,
}
impl From<&tinymist_project::CompileStatusEnum> for CompileStatusEnum {
fn from(value: &tinymist_project::CompileStatusEnum) -> Self {
use tinymist_project::CompileStatusEnum::*;
match value {
Compiling => CompileStatusEnum::Compiling,
Suspend | CompileSuccess { .. } => CompileStatusEnum::CompileSuccess,
ExportError { .. } | CompileError { .. } => CompileStatusEnum::CompileError,
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct StatusAll {
pub status: CompileStatusEnum,
pub path: String,
pub page_count: u32,
pub words_count: Option<WordsCount>,
}
impl lsp_types::notification::Notification for StatusAll {
type Params = Self;
const METHOD: &'static str = "tinymist/compileStatus";
}
#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)]
pub struct PublishDiagnosticsParams {
pub uri: Url,
pub diagnostics: ScatterVec<Diagnostic>,
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<i32>,
}
#[derive(Debug)]
pub enum PublishDiagnostics {}
impl Notification for PublishDiagnostics {
type Params = PublishDiagnosticsParams;
const METHOD: &'static str = PublishDiagnosticsBase::METHOD;
}
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct ScatterVec<T>(EcoVec<EcoVec<T>>);
impl serde::Serialize for ScatterVec<Diagnostic> {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut vec = Vec::new();
for e in &self.0 {
vec.extend(e.iter().cloned())
}
vec.serialize(serializer)
}
}
impl<'de> serde::Deserialize<'de> for ScatterVec<Diagnostic> {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let vec = EcoVec::<Diagnostic>::deserialize(deserializer)?;
Ok(ScatterVec(eco_vec![vec]))
}
}