pub mod documents;
pub mod framing;
pub mod session;
pub mod text;
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::{Duration, Instant};
use serde::Serialize;
use serde_json::{Value, json};
use tokio::sync::{Mutex, broadcast, mpsc};
use wsb::omnicode::locate::{self, Omni, Unavailable};
use wsb::omnicode::roots::{AnalysisRoot, Fleet};
use documents::{ClientId, Outgoing, Registry, SyncError};
use session::{Inbound, RequestFailure, Session, State};
pub const IDLE: Duration = Duration::from_secs(10 * 60);
pub const CLIENT_GRACE: Duration = Duration::from_secs(30);
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum CodeEvent {
Status { root: String, status: RootStatus },
Diagnostics { path: String, version: i32, digest: String, diagnostics: Vec<Value> },
AnalysisDiagnostics { root: String, files: BTreeMap<String, Vec<Value>> },
Message { level: String, text: String },
}
#[derive(Debug, Clone, Serialize)]
pub struct RootStatus {
pub root: String,
#[serde(flatten)]
pub state: State,
}
#[derive(Debug, Clone, PartialEq)]
pub enum AskError {
Stale(String),
Resync(String),
NotServed(String),
Unavailable(String),
Failed(String),
}
impl std::fmt::Display for AskError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Stale(m) | Self::Resync(m) | Self::NotServed(m) | Self::Unavailable(m) | Self::Failed(m) => {
f.write_str(m)
}
}
}
}
impl From<SyncError> for AskError {
fn from(e: SyncError) -> Self {
match e {
SyncError::Stale { .. } => Self::Stale(e.to_string()),
_ => Self::Resync(e.to_string()),
}
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "served", rename_all = "snake_case")]
pub enum Opened {
Yes { root: String, language: String, features: Vec<String> },
No { reason: String },
}
struct Root {
root: AnalysisRoot,
session: Option<Arc<Session>>,
ended: Option<State>,
registry: Registry,
live: BTreeMap<String, (i32, String, Vec<Value>)>,
analysis: BTreeMap<String, Vec<Value>>,
idle_since: Option<Instant>,
}
#[derive(Default)]
struct Inner {
roots: BTreeMap<PathBuf, Root>,
streams: BTreeMap<ClientId, usize>,
external: BTreeMap<String, PathBuf>,
}
pub struct CodeIntel {
worktree: PathBuf,
inner: Mutex<Inner>,
events: broadcast::Sender<CodeEvent>,
inbound: mpsc::UnboundedSender<(PathBuf, Inbound)>,
}
impl std::fmt::Debug for CodeIntel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CodeIntel").field("worktree", &self.worktree).finish_non_exhaustive()
}
}
fn read_text(path: &Path) -> Result<String, String> {
std::fs::read_to_string(path).map_err(|e| format!("{}: {e}", path.display()))
}
impl CodeIntel {
pub fn new(worktree: &Path) -> Result<Arc<Self>, String> {
let worktree = wsb::platform::canonicalize(worktree).map_err(|e| format!("{}: {e}", worktree.display()))?;
let (events, _) = broadcast::channel(512);
let (inbound, mut rx) = mpsc::unbounded_channel::<(PathBuf, Inbound)>();
let intel = Arc::new(Self { worktree, inner: Mutex::new(Inner::default()), events, inbound });
{
let weak = Arc::downgrade(&intel);
tokio::spawn(async move {
while let Some((dir, pushed)) = rx.recv().await {
let Some(intel) = weak.upgrade() else { break };
intel.on_inbound(&dir, pushed).await;
}
});
}
{
let weak = Arc::downgrade(&intel);
tokio::spawn(async move {
let mut every = tokio::time::interval(Duration::from_secs(60));
loop {
every.tick().await;
let Some(intel) = weak.upgrade() else { break };
intel.stop_idle().await;
}
});
}
Ok(intel)
}
pub fn subscribe(&self) -> broadcast::Receiver<CodeEvent> {
self.events.subscribe()
}
fn label(&self, dir: &Path) -> String {
match dir.strip_prefix(&self.worktree) {
Ok(rel) if rel.as_os_str().is_empty() => ".".to_owned(),
Ok(rel) => rel.to_string_lossy().replace('\\', "/"),
Err(_) => dir.display().to_string(),
}
}
fn absolute(&self, path: &str) -> Result<PathBuf, String> {
let rel = Path::new(path);
if rel.is_absolute() || rel.components().any(|c| !matches!(c, std::path::Component::Normal(_))) {
return Err(format!("{path} is not a path inside the worktree"));
}
Ok(self.worktree.join(rel))
}
fn uri_of(&self, path: &str) -> Result<String, String> {
session::file_uri(&self.absolute(path)?)
}
fn path_of(&self, uri: &str) -> Option<String> {
let path = url::Url::parse(uri).ok()?.to_file_path().ok()?;
let rel = path.strip_prefix(&self.worktree).ok()?;
Some(rel.to_string_lossy().replace('\\', "/"))
}
pub fn omni(&self) -> Result<Omni, Unavailable> {
locate::find()
}
pub async fn status(&self) -> Vec<RootStatus> {
let inner = self.inner.lock().await;
inner.roots.iter().map(|(dir, r)| RootStatus { root: self.label(dir), state: root_state(r) }).collect()
}
fn emit(&self, event: CodeEvent) {
let _ = self.events.send(event);
}
fn send(&self, session: &Session, outgoing: Vec<Outgoing>) -> Result<(), String> {
for o in outgoing {
match o {
Outgoing::Open { path, language_id, version, text } => session.notify(
"textDocument/didOpen",
json!({ "textDocument": { "uri": self.uri_of(&path)?, "languageId": language_id, "version": version, "text": text } }),
)?,
Outgoing::Edit { path, version, changes } => session.notify(
"textDocument/didChange",
json!({ "textDocument": { "uri": self.uri_of(&path)?, "version": version }, "contentChanges": changes }),
)?,
Outgoing::Replace { path, version, text } => session.notify(
"textDocument/didChange",
json!({ "textDocument": { "uri": self.uri_of(&path)?, "version": version }, "contentChanges": [{ "text": text }] }),
)?,
Outgoing::Close { path } => session.notify(
"textDocument/didClose",
json!({ "textDocument": { "uri": self.uri_of(&path)? } }),
)?,
}
}
Ok(())
}
pub async fn open(&self, client: &str, path: &str, version: i64, text: &str) -> Result<Opened, String> {
let omni = match self.omni() {
Ok(o) => o,
Err(why) => return Ok(Opened::No { reason: why.to_string() }),
};
let file = self.absolute(path)?;
let Some((language, served)) = omni.language_of(&file) else {
return Ok(Opened::No {
reason: format!("omnicode does not analyze {path}: no language it answers for takes this file"),
});
};
let (language, features) = (language.to_owned(), served.features.clone());
let located = match wsb::platform::canonicalize(&file) {
Ok(f) => f,
Err(_) => wsb::platform::canonicalize(file.parent().unwrap_or(&self.worktree))
.map_err(|e| format!("{path}: {e}"))?
.join(file.file_name().unwrap_or_default()),
};
let root = Fleet::discover(&self.worktree)?.analysis_root(&located)?;
let session = self.session_for(&omni, root.clone()).await?;
let mut inner = self.inner.lock().await;
let r = inner.roots.get_mut(&root.dir).ok_or("the root vanished while it was started")?;
r.idle_since = None;
let outgoing = r.registry.open(client, path, &language, version, text);
self.send(&session, outgoing)?;
Ok(Opened::Yes { root: self.label(&root.dir), language, features })
}
async fn session_for(&self, omni: &Omni, root: AnalysisRoot) -> Result<Arc<Session>, String> {
{
let mut inner = self.inner.lock().await;
let r = inner.roots.entry(root.dir.clone()).or_insert_with(|| Root {
root: root.clone(),
session: None,
ended: None,
registry: Registry::default(),
live: BTreeMap::new(),
analysis: BTreeMap::new(),
idle_since: None,
});
if let Some(s) = r.session.as_ref().filter(|s| s.running()) {
return Ok(s.clone());
}
if let Some(ended @ State::Crashed { .. }) = &r.ended {
return Err(format!(
"the language server of {} ended ({}); start it again to go on",
self.label(&root.dir),
serde_json::to_string(ended).unwrap_or_default()
));
}
}
self.emit(CodeEvent::Status {
root: self.label(&root.dir),
status: RootStatus { root: self.label(&root.dir), state: State::Starting },
});
let started = Session::start(omni, root.clone(), self.inbound.clone()).await;
let mut inner = self.inner.lock().await;
let r = inner.roots.get_mut(&root.dir).ok_or("the root vanished while it was started")?;
match started {
Ok(s) => {
r.session = Some(s.clone());
r.ended = None;
let replay = r.registry.replay();
self.send(&s, replay)?;
Ok(s)
}
Err(e) => {
let failed = State::Failed { message: e.clone() };
r.ended = Some(failed.clone());
drop(inner);
self.emit(CodeEvent::Status {
root: self.label(&root.dir),
status: RootStatus { root: self.label(&root.dir), state: failed },
});
Err(e)
}
}
}
pub async fn restart(&self, root: &str) -> Result<(), String> {
let omni = self.omni().map_err(|e| e.to_string())?;
let dir = if root == "." { self.worktree.clone() } else { self.absolute(root)? };
let dir = wsb::platform::canonicalize(&dir).map_err(|e| format!("{root}: {e}"))?;
let (old, analysis_root) = {
let mut inner = self.inner.lock().await;
let r = inner.roots.get_mut(&dir).ok_or_else(|| format!("{root} is not a root of this worktree's editor"))?;
r.ended = None;
(r.session.take(), r.root.clone())
};
if let Some(old) = old {
old.shutdown().await;
}
self.session_for(&omni, analysis_root).await.map(|_| ())
}
fn root_of<'a>(inner: &'a mut Inner, worktree: &Path, path: &str) -> Option<&'a mut Root> {
let file = worktree.join(path);
inner
.roots
.values_mut()
.filter(|r| file.starts_with(&r.root.dir))
.max_by_key(|r| r.root.dir.components().count())
}
pub async fn change(
&self,
client: &str,
path: &str,
from: i64,
to: i64,
changes: &[text::Change],
) -> Result<(), AskError> {
let mut inner = self.inner.lock().await;
let worktree = self.worktree.clone();
let r = Self::root_of(&mut inner, &worktree, path)
.ok_or_else(|| AskError::Resync(format!("{path} is not open")))?;
let outgoing = r.registry.change(client, path, from, to, changes)?;
let session = r.session.clone().filter(|s| s.running());
if let Some(s) = session {
self.send(&s, outgoing).map_err(AskError::Unavailable)?;
}
Ok(())
}
pub async fn close(&self, client: &str, path: &str) {
let mut inner = self.inner.lock().await;
let worktree = self.worktree.clone();
let Some(r) = Self::root_of(&mut inner, &worktree, path) else { return };
let outgoing = r.registry.close(client, path);
if outgoing.iter().any(|o| matches!(o, Outgoing::Close { .. })) {
r.live.remove(path);
}
if r.registry.is_empty() {
r.idle_since = Some(Instant::now());
}
if let Some(s) = r.session.clone().filter(|s| s.running()) {
let _ = self.send(&s, outgoing);
}
}
pub async fn stream_opened(&self, client: &str) {
*self.inner.lock().await.streams.entry(client.to_owned()).or_default() += 1;
}
pub fn stream_closed(self: &Arc<Self>, client: &str) {
let intel = Arc::downgrade(self);
let client = client.to_owned();
tokio::spawn(async move {
{
let Some(intel) = intel.upgrade() else { return };
let mut inner = intel.inner.lock().await;
let left = inner.streams.entry(client.clone()).or_default();
*left = left.saturating_sub(1);
}
tokio::time::sleep(CLIENT_GRACE).await;
let Some(intel) = intel.upgrade() else { return };
let mut inner = intel.inner.lock().await;
if inner.streams.get(&client).copied().unwrap_or(0) > 0 {
return;
}
inner.streams.remove(&client);
for r in inner.roots.values_mut() {
let outgoing = r.registry.close_client(&client);
for o in &outgoing {
if let Outgoing::Close { path } = o {
r.live.remove(path);
}
}
if r.registry.is_empty() && r.idle_since.is_none() {
r.idle_since = Some(Instant::now());
}
if let Some(s) = r.session.clone().filter(|s| s.running()) {
let _ = intel.send(&s, outgoing);
}
}
});
}
pub async fn ask(
&self,
client: &str,
path: &str,
version: i64,
feature: &str,
params: Value,
) -> Result<Value, AskError> {
let omni = self.omni().map_err(|e| AskError::NotServed(e.to_string()))?;
let file = self.absolute(path).map_err(AskError::NotServed)?;
let (_, served) = omni
.language_of(&file)
.ok_or_else(|| AskError::NotServed(format!("omnicode does not analyze {path}")))?;
if !served.features.iter().any(|f| f == feature) {
return Err(AskError::NotServed(format!("{feature} is not served for {path}")));
}
let method = method_of(feature).ok_or_else(|| AskError::NotServed(format!("{feature} is not a feature")))?;
let session = {
let mut inner = self.inner.lock().await;
let worktree = self.worktree.clone();
let r = Self::root_of(&mut inner, &worktree, path)
.ok_or_else(|| AskError::Resync(format!("{path} is not open")))?;
let session = match (&r.session, &r.ended) {
(Some(s), _) if s.running() => s.clone(),
(_, Some(ended)) => {
return Err(AskError::Unavailable(format!(
"the language server is not running: {}",
serde_json::to_string(ended).unwrap_or_default()
)));
}
_ => return Err(AskError::Unavailable("the language server is not running".into())),
};
let outgoing = r.registry.ensure(client, path, version)?;
self.send(&session, outgoing).map_err(AskError::Unavailable)?;
session
};
let mut params = params;
params["textDocument"] = json!({ "uri": self.uri_of(path).map_err(AskError::NotServed)? });
match session.request(method, params).await {
Ok(result) => Ok(self.rewrite(result).await),
Err(RequestFailure::Rpc { code, message })
if code == session::CONTENT_MODIFIED || code == session::REQUEST_CANCELLED =>
{
Err(AskError::Stale(message))
}
Err(RequestFailure::Rpc { message, .. }) => Err(AskError::Failed(message)),
Err(other) => Err(AskError::Unavailable(other.to_string())),
}
}
async fn rewrite(&self, mut value: Value) -> Value {
let mut external = BTreeMap::new();
rewrite_uris(&mut value, &mut |uri: &str| match self.path_of(uri) {
Some(path) => json!({ "path": path }),
None => {
let file = url::Url::parse(uri).ok().and_then(|u| u.to_file_path().ok());
match file {
Some(f) => {
let token = text::digest(&f.display().to_string())[..32].to_owned();
external.insert(token.clone(), f.clone());
json!({ "external": token, "name": f.display().to_string() })
}
None => json!({ "unreadable": uri }),
}
}
});
if !external.is_empty() {
self.inner.lock().await.external.extend(external);
}
value
}
pub async fn external(&self, token: &str) -> Result<(String, String), String> {
let path = self
.inner
.lock()
.await
.external
.get(token)
.cloned()
.ok_or_else(|| "no language server named a file by this token".to_owned())?;
Ok((path.display().to_string(), read_text(&path)?))
}
pub async fn saved(&self, path: &str) {
let mut inner = self.inner.lock().await;
let worktree = self.worktree.clone();
let Some(r) = Self::root_of(&mut inner, &worktree, path) else { return };
if r.registry.server_copy(path).is_none() {
return;
}
if let (Some(s), Ok(uri)) = (r.session.clone().filter(|s| s.running()), self.uri_of(path)) {
let _ = s.notify("textDocument/didSave", json!({ "textDocument": { "uri": uri } }));
}
}
pub async fn disk_changed(&self, changes: &[(String, bool)]) {
let inner = self.inner.lock().await;
for r in inner.roots.values() {
let Some(s) = r.session.clone().filter(|s| s.running()) else { continue };
let events: Vec<Value> = changes
.iter()
.filter(|(p, _)| self.worktree.join(p).starts_with(&r.root.dir))
.filter_map(|(p, deleted)| Some(json!({ "uri": self.uri_of(p).ok()?, "type": if *deleted { 3 } else { 2 } })))
.collect();
if !events.is_empty() {
let _ = s.notify("workspace/didChangeWatchedFiles", json!({ "changes": events }));
}
}
}
pub async fn diagnostics(&self) -> Vec<CodeEvent> {
let inner = self.inner.lock().await;
let mut out = Vec::new();
for (dir, r) in &inner.roots {
out.push(CodeEvent::AnalysisDiagnostics { root: self.label(dir), files: r.analysis.clone() });
for (path, (version, digest, diagnostics)) in &r.live {
out.push(CodeEvent::Diagnostics {
path: path.clone(),
version: *version,
digest: digest.clone(),
diagnostics: diagnostics.clone(),
});
}
}
out
}
async fn on_inbound(self: &Arc<Self>, dir: &Path, pushed: Inbound) {
match pushed {
Inbound::Diagnostics(params) => {
let Some(path) = params["uri"].as_str().and_then(|u| self.path_of(u)) else { return };
let mut inner = self.inner.lock().await;
let Some(r) = inner.roots.get_mut(dir) else { return };
let Some((version, text)) = r.registry.server_copy(&path) else {
return;
};
if params["version"].as_i64().is_some_and(|v| v != i64::from(version)) {
return;
}
let digest = text::digest(text);
let diagnostics = params["diagnostics"].as_array().cloned().unwrap_or_default();
r.live.insert(path.clone(), (version, digest.clone(), diagnostics.clone()));
drop(inner);
self.emit(CodeEvent::Diagnostics { path, version, digest, diagnostics });
}
Inbound::Message { level, text } => self.emit(CodeEvent::Message { level: level.to_owned(), text }),
Inbound::State(state) => {
let root = self.label(dir);
let ready = matches!(state, State::Ready { .. });
if matches!(state, State::Crashed { .. } | State::Stopped) {
let mut inner = self.inner.lock().await;
if let Some(r) = inner.roots.get_mut(dir) {
r.ended = Some(state.clone());
r.session = None;
r.live.clear();
}
}
self.emit(CodeEvent::Status { root: root.clone(), status: RootStatus { root, state } });
if ready {
self.refresh_analysis(dir).await;
}
}
Inbound::Reanalyzed => self.refresh_analysis(dir).await,
}
}
async fn refresh_analysis(self: &Arc<Self>, dir: &Path) {
let Ok(omni) = self.omni() else { return };
let worktree = self.worktree.clone();
let root_dir = dir.to_owned();
let read = tokio::task::spawn_blocking(move || analysis_diagnostics(&omni, &worktree, &root_dir)).await;
let files = match read {
Ok(Ok(files)) => files,
Ok(Err(e)) => {
self.emit(CodeEvent::Message {
level: "warning".into(),
text: format!("the analysis diagnostics of {} cannot be read: {e}", self.label(dir)),
});
return;
}
Err(e) => {
tracing::warn!("reading analysis diagnostics panicked: {e}");
return;
}
};
{
let mut inner = self.inner.lock().await;
if let Some(r) = inner.roots.get_mut(dir) {
r.analysis = files.clone();
}
}
self.emit(CodeEvent::AnalysisDiagnostics { root: self.label(dir), files });
}
async fn stop_idle(&self) {
let idle: Vec<Arc<Session>> = {
let mut inner = self.inner.lock().await;
inner
.roots
.values_mut()
.filter(|r| r.registry.is_empty() && r.idle_since.is_some_and(|t| t.elapsed() >= IDLE))
.filter_map(|r| {
r.idle_since = None;
r.session.take()
})
.collect()
};
for s in idle {
s.shutdown().await;
}
}
}
fn root_state(r: &Root) -> State {
match (&r.session, &r.ended) {
(Some(s), _) => s.state(),
(None, Some(ended)) => ended.clone(),
(None, None) => State::Stopped,
}
}
pub fn method_of(feature: &str) -> Option<&'static str> {
Some(match feature {
"hover" => "textDocument/hover",
"definition" => "textDocument/definition",
"references" => "textDocument/references",
"document_symbols" => "textDocument/documentSymbol",
"semantic_tokens" => "textDocument/semanticTokens/full",
"completion" => "textDocument/completion",
"signature_help" => "textDocument/signatureHelp",
"inlay_hints" => "textDocument/inlayHint",
_ => return None,
})
}
fn rewrite_uris(value: &mut Value, name: &mut dyn FnMut(&str) -> Value) {
match value {
Value::Object(map) => {
for (key, v) in map.iter_mut() {
match (key.as_str(), &*v) {
("uri" | "targetUri", Value::String(uri)) => {
let uri = uri.clone();
*v = name(&uri);
}
_ => rewrite_uris(v, name),
}
}
}
Value::Array(items) => {
for v in items {
rewrite_uris(v, name);
}
}
_ => {}
}
}
fn analysis_diagnostics(
omni: &Omni,
worktree: &Path,
dir: &Path,
) -> Result<BTreeMap<String, Vec<Value>>, String> {
let out = wsb::git_backend::command_without_repo_scope(&omni.path)
.args(["--json", "diagnostics", "-C"])
.arg(dir)
.output()
.map_err(|e| format!("cannot run {}: {e}", omni.path.display()))?;
let v: Value = serde_json::from_slice(&out.stdout).map_err(|e| format!("not JSON: {e}"))?;
if !out.status.success() {
return Err(v["message"].as_str().unwrap_or("omni diagnostics failed").to_owned());
}
let items = v.as_array().ok_or("omni diagnostics answered with no list")?;
let mut files: BTreeMap<String, Vec<Value>> = BTreeMap::new();
let mut texts: BTreeMap<String, Option<String>> = BTreeMap::new();
for d in items {
let primary = &d["primary"];
let Some(rel) = primary["path"].as_str() else { continue };
let abs = dir.join(rel);
let text = texts.entry(rel.to_owned()).or_insert_with(|| {
let bytes = std::fs::read(&abs).ok()?;
let hex = format!("{:x}", <sha2::Sha256 as sha2::Digest>::digest(&bytes));
let wanted = primary["content_hash"].as_str().unwrap_or("").trim_start_matches("sha256:");
(hex == wanted).then(|| String::from_utf8(bytes).ok()).flatten()
});
let Some(text) = text.as_deref() else { continue };
let (Some(a), Some(b)) = (primary["range_utf8"][0].as_u64(), primary["range_utf8"][1].as_u64()) else { continue };
let (Ok(start), Ok(end)) = (text::position(text, a as usize), text::position(text, b as usize)) else { continue };
let Ok(path) = abs.strip_prefix(worktree).map(|p| p.to_string_lossy().replace('\\', "/")) else { continue };
files.entry(path).or_default().push(json!({
"range": { "start": start, "end": end },
"severity": match d["severity"].as_str() { Some("error") => 1, Some("warning") => 2, Some("info") => 3, _ => 4 },
"code": d["code"],
"source": format!("omnicode/{}", d["source"].as_str().unwrap_or("")),
"message": d["message"],
}));
}
Ok(files)
}
#[cfg(test)]
#[path = "mod_tests.rs"]
mod tests;