use lsp_types::*;
use reflexo_typst::Bytes;
use serde::{Deserialize, Serialize};
use tinymist_query::ty::PathKind;
use tinymist_query::{is_untitled_path, to_typst_range, PositionEncoding};
use tinymist_std::error::prelude::*;
use tinymist_std::ImmutPath;
use typst::ecow::EcoString;
use typst::{
diag::{FileError, FileResult},
syntax::Source,
};
use crate::project::Interrupt;
use crate::world::vfs::{notify::MemoryEvent, FileChangeSet, FilesystemEvent};
use crate::world::TaskInputs;
use crate::*;
mod watch;
pub use watch::WatchAccessModel;
#[cfg(feature = "lock")]
use crate::project::ProjectResolutionKind;
#[cfg(feature = "lock")]
use crate::route::ProjectResolution;
impl ServerState {
fn update_sources(&mut self, files: FileChangeSet) -> Result<()> {
log::trace!("update source: {files:?}");
let intr = Interrupt::Memory(MemoryEvent::Update(files.clone()));
self.project.interrupt(intr);
Ok(())
}
pub fn create_source(&mut self, path: ImmutPath, content: String) -> Result<()> {
let _scope = typst_timing::TimingScope::new("create_source");
log::trace!("create source: {path:?}");
self.memory_changes
.insert(path.clone(), Source::detached(content.clone()));
let content = Bytes::from_string(content);
let files = FileChangeSet::new_inserts(vec![(path, FileResult::Ok(content).into())]);
self.update_sources(files)
}
pub fn remove_source(&mut self, path: ImmutPath) -> Result<()> {
let _scope = typst_timing::TimingScope::new("remove_source");
self.memory_changes.remove(&path);
log::trace!("remove source: {path:?}");
let files = FileChangeSet::new_removes(vec![path]);
self.update_sources(files)
}
pub fn edit_source(
&mut self,
path: ImmutPath,
content: Vec<TextDocumentContentChangeEvent>,
position_encoding: PositionEncoding,
) -> Result<()> {
let _scope = typst_timing::TimingScope::new("edit_source");
let source = self
.memory_changes
.get_mut(&path)
.ok_or_else(|| error_once!("file missing", path: path.display()))?;
for change in content {
let replacement = change.text;
match change.range {
Some(lsp_range) => {
let range = to_typst_range(lsp_range, position_encoding, source)
.expect("invalid range");
source.edit(range, &replacement);
}
None => {
source.replace(&replacement);
}
}
}
let snapshot = FileResult::Ok(Bytes::from_string(source.text().to_owned())).into();
let files = FileChangeSet::new_inserts(vec![(path.clone(), snapshot)]);
self.update_sources(files)
}
pub fn save_source(&mut self, path: ImmutPath) -> Result<()> {
self.project.interrupt(Interrupt::Save(path));
Ok(())
}
pub fn query_source<T>(
&self,
path: ImmutPath,
f: impl FnOnce(Source) -> LspResult<T>,
) -> LspResult<T> {
let snapshot = self.memory_changes.get(&path);
let snapshot = snapshot.ok_or_else(|| internal_error(format!("file missing {path:?}")))?;
let source = snapshot.clone();
f(source)
}
pub fn fs_change(&mut self, params: FsChangeParams) -> ScheduleResult {
use base64::Engine;
let _scope = typst_timing::TimingScope::new("fs_change");
let mut inserts = vec![];
let mut removes = vec![];
for change in params.inserts {
let path: ImmutPath =
tinymist_query::url_to_path(&Url::parse(&change.uri).unwrap()).into();
let content: FileResult<Bytes> = match change.content {
FileChangeResult::Ok { content } => base64::engine::general_purpose::STANDARD
.decode(content)
.map(Bytes::new)
.map_err(|e| FileError::Other(Some(_eco_format!("base64 decode error: {e}")))),
FileChangeResult::Err { error } => {
log::info!("file content not available: {path:?} {error}");
Err(FileError::Other(Some(error)))
}
};
inserts.push((path, content.into()));
}
for path in params.removes {
removes.push(tinymist_query::url_to_path(&Url::parse(&path).unwrap()).into());
}
let update = FilesystemEvent::Update(FileChangeSet { inserts, removes }, params.is_sync);
log::info!("fs_change: {update:?}");
self.project.interrupt(Interrupt::Fs(update));
self.schedule_async();
just_ok(serde_json::Value::Null)
}
}
impl ServerState {
pub fn set_pin_by_preview(&mut self, pin: bool, browsing: bool) {
self.pinning_by_preview = pin;
self.pinning_by_browsing_preview = browsing;
}
pub fn change_main_file(&mut self, path: Option<ImmutPath>) -> Result<bool> {
if path.as_deref().is_some_and(|p| {
let is_absolute = if cfg!(target_arch = "wasm32") {
p.has_root()
} else {
p.is_absolute()
};
!is_absolute && !p.starts_with("/untitled")
}) {
return Err(error_once!("entry file must be absolute", path: path.unwrap().display()));
}
let task = self.resolve_task_or(path);
log::info!("the task of the primary is changing to {task:?}");
let id = self.project.primary_id().clone();
self.project.interrupt(Interrupt::ChangeTask(id, task));
Ok(true)
}
pub fn pin_main_file(&mut self, new_entry: Option<ImmutPath>) -> Result<()> {
self.pinning_by_user = new_entry.is_some();
let entry = new_entry
.or_else(|| self.entry_resolver().resolve_default())
.or_else(|| self.focusing.clone());
self.change_main_file(entry).map(|_| ())
}
pub fn focus_main_file(&mut self, new_entry: Option<ImmutPath>) -> Result<bool> {
if new_entry.as_deref() != self.focusing.as_deref() {
self.implicit_position = None;
}
self.focusing = new_entry.clone();
if self.pinning_by_user
|| (self.pinning_by_preview && !self.pinning_by_browsing_preview)
|| self.config.has_default_entry_path
{
return Ok(false);
}
self.change_main_file(new_entry)
}
pub fn implicit_focus_entry(
&mut self,
new_entry: impl FnOnce() -> Option<ImmutPath>,
site: char,
) {
if self.ever_manual_focusing {
return;
}
match site {
'f' | 'h' | 't' => {
self.ever_focusing_by_activities = true;
}
_ => {
if self.ever_focusing_by_activities {
return;
}
}
}
let new_entry = new_entry();
if new_entry.as_ref().is_some_and(|entry| {
!is_untitled_path(entry)
&& !PathKind::Source {
allow_package: true,
}
.is_match(entry)
}) {
return;
}
let update_result = self.focus_main_file(new_entry.clone());
match update_result {
Ok(true) => {
log::info!("file focused[implicit,{site}]: {new_entry:?}");
}
Err(err) => {
log::warn!("could not focus file: {err}");
}
Ok(false) => {}
}
}
}
impl ServerState {
fn resolve_task_without_lock(&self, path: Option<ImmutPath>) -> TaskInputs {
TaskInputs {
entry: Some(self.entry_resolver().resolve(path)),
..TaskInputs::default()
}
}
pub(crate) fn resolve_task_or(&mut self, path: Option<ImmutPath>) -> TaskInputs {
path.clone()
.map(|path| self.resolve_task(path))
.unwrap_or_else(|| self.resolve_task_without_lock(path))
}
#[cfg(not(feature = "lock"))]
pub(crate) fn resolve_task(&mut self, path: ImmutPath) -> TaskInputs {
self.resolve_task_without_lock(Some(path))
}
#[cfg(feature = "lock")]
pub(crate) fn resolve_task(&mut self, path: ImmutPath) -> TaskInputs {
let proj_input = matches!(
self.entry_resolver().project_resolution,
ProjectResolutionKind::LockDatabase
)
.then(|| {
let resolution = self.route.resolve(&path)?;
let lock = self.route.locate(&resolution)?;
let ProjectResolution {
lock_dir,
project_id,
} = &resolution;
let input = lock.get_document(project_id)?;
let root = input
.root
.as_ref()
.and_then(|res| Some(res.to_abs_path(lock_dir)?.as_path().into()))
.unwrap_or_else(|| lock_dir.clone());
let main = input
.main
.to_abs_path(lock_dir)
.map(|path| path.as_path().into())
.unwrap_or_else(|| path.clone());
let entry = self
.entry_resolver()
.resolve_with_root(Some(root), Some(main));
log::info!("resolved task with state: {path:?} -> {project_id:?} -> {entry:?}");
Some(TaskInputs {
entry: Some(entry),
..TaskInputs::default()
})
});
proj_input
.flatten()
.unwrap_or_else(|| self.resolve_task_without_lock(Some(path)))
}
}
pub enum FsChange {}
impl lsp_types::request::Request for FsChange {
type Params = FsChangeParams;
type Result = ();
const METHOD: &'static str = "tinymist/fsChange";
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FsChangeParams {
pub inserts: Vec<FileChange>,
pub removes: Vec<String>,
pub is_sync: bool,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct FileChange {
pub uri: String,
pub content: FileChangeResult,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "type")]
pub enum FileChangeResult {
Ok {
content: String,
},
Err {
error: EcoString,
},
}