#![allow(clippy::result_large_err)]
use std::sync::Arc;
use {
reovim_kernel::api::v1::BufferId,
reovim_protocol::v2::{
GetLanguageInfoRequest, GetLanguageInfoResponse, GetTokensRequest, GetTokensResponse,
StreamTokensRequest, TokenSpan, TokenUpdate, syntax_service_server::SyntaxService,
},
tokio::sync::mpsc,
tokio_stream::wrappers::ReceiverStream,
tonic::{Request, Response, Status},
};
use crate::session::{Session, SessionId, SessionRegistry, SyntaxSessionState, SyntaxStreamState};
#[cfg_attr(coverage_nightly, coverage(off))]
async fn forward_token_updates(
tx: mpsc::Sender<Result<TokenUpdate, Status>>,
initial_update: TokenUpdate,
mut syntax_rx: mpsc::Receiver<TokenUpdate>,
buffer_id: BufferId,
_session: Arc<Session>,
) {
if tx.send(Ok(initial_update)).await.is_err() {
return; }
while let Some(update) = syntax_rx.recv().await {
if update.buffer_id == buffer_id.as_usize() as u64 && tx.send(Ok(update)).await.is_err() {
break; }
}
tracing::debug!(buffer_id = buffer_id.as_usize(), "StreamTokens stream ended");
}
pub struct SyntaxServiceImpl {
sessions: Arc<SessionRegistry>,
default_session_id: SessionId,
}
impl SyntaxServiceImpl {
#[must_use]
pub const fn new(sessions: Arc<SessionRegistry>, default_session_id: SessionId) -> Self {
Self {
sessions,
default_session_id,
}
}
fn get_session(&self) -> Result<Arc<crate::session::Session>, Status> {
self.sessions
.get(&self.default_session_id)
.ok_or_else(|| Status::not_found("No active session"))
}
}
fn detect_language_from_path(path: Option<&str>) -> (&'static str, &'static str) {
let Some(path) = path else {
return ("text", "Plain Text");
};
let ext = path.rsplit('.').next().unwrap_or("");
match ext.to_lowercase().as_str() {
"rs" => ("rust", "Rust"),
"py" | "pyi" => ("python", "Python"),
"js" | "mjs" | "cjs" => ("javascript", "JavaScript"),
"ts" | "mts" | "cts" => ("typescript", "TypeScript"),
"tsx" => ("typescriptreact", "TypeScript React"),
"jsx" => ("javascriptreact", "JavaScript React"),
"c" => ("c", "C"),
"cpp" | "cc" | "cxx" | "hpp" | "hxx" => ("cpp", "C++"),
"h" => ("c", "C Header"),
"go" => ("go", "Go"),
"java" => ("java", "Java"),
"rb" => ("ruby", "Ruby"),
"php" => ("php", "PHP"),
"swift" => ("swift", "Swift"),
"kt" | "kts" => ("kotlin", "Kotlin"),
"scala" => ("scala", "Scala"),
"hs" => ("haskell", "Haskell"),
"ml" | "mli" => ("ocaml", "OCaml"),
"lua" => ("lua", "Lua"),
"sh" | "bash" => ("shellscript", "Shell Script"),
"zsh" => ("zsh", "Zsh"),
"fish" => ("fish", "Fish"),
"css" => ("css", "CSS"),
"scss" => ("scss", "SCSS"),
"less" => ("less", "Less"),
"html" | "htm" => ("html", "HTML"),
"xml" => ("xml", "XML"),
"json" => ("json", "JSON"),
"yaml" | "yml" => ("yaml", "YAML"),
"toml" => ("toml", "TOML"),
"md" | "markdown" => ("markdown", "Markdown"),
"sql" => ("sql", "SQL"),
"vim" => ("vim", "Vimscript"),
"el" | "lisp" => ("lisp", "Lisp"),
"clj" | "cljs" => ("clojure", "Clojure"),
"ex" | "exs" => ("elixir", "Elixir"),
"erl" => ("erlang", "Erlang"),
"zig" => ("zig", "Zig"),
"nim" => ("nim", "Nim"),
"cr" => ("crystal", "Crystal"),
"dart" => ("dart", "Dart"),
"r" => ("r", "R"),
"jl" => ("julia", "Julia"),
"proto" => ("protobuf", "Protocol Buffers"),
"graphql" | "gql" => ("graphql", "GraphQL"),
"dockerfile" => ("dockerfile", "Dockerfile"),
"make" | "makefile" => ("makefile", "Makefile"),
"cmake" => ("cmake", "CMake"),
"txt" => ("plaintext", "Plain Text"),
_ => ("text", "Plain Text"),
}
}
fn extensions_for_language(language_id: &str) -> Vec<String> {
match language_id {
"rust" => vec![".rs".to_string()],
"python" => vec![".py".to_string(), ".pyi".to_string()],
"javascript" => vec![".js".to_string(), ".mjs".to_string(), ".cjs".to_string()],
"typescript" => vec![".ts".to_string(), ".mts".to_string(), ".cts".to_string()],
"typescriptreact" => vec![".tsx".to_string()],
"javascriptreact" => vec![".jsx".to_string()],
"c" => vec![".c".to_string(), ".h".to_string()],
"cpp" => vec![
".cpp".to_string(),
".cc".to_string(),
".cxx".to_string(),
".hpp".to_string(),
],
"go" => vec![".go".to_string()],
"java" => vec![".java".to_string()],
"ruby" => vec![".rb".to_string()],
"php" => vec![".php".to_string()],
"swift" => vec![".swift".to_string()],
"kotlin" => vec![".kt".to_string(), ".kts".to_string()],
"scala" => vec![".scala".to_string()],
"haskell" => vec![".hs".to_string()],
"ocaml" => vec![".ml".to_string(), ".mli".to_string()],
"lua" => vec![".lua".to_string()],
"shellscript" => vec![".sh".to_string(), ".bash".to_string()],
"zsh" => vec![".zsh".to_string()],
"fish" => vec![".fish".to_string()],
"css" => vec![".css".to_string()],
"scss" => vec![".scss".to_string()],
"less" => vec![".less".to_string()],
"html" => vec![".html".to_string(), ".htm".to_string()],
"xml" => vec![".xml".to_string()],
"json" => vec![".json".to_string()],
"yaml" => vec![".yaml".to_string(), ".yml".to_string()],
"toml" => vec![".toml".to_string()],
"markdown" => vec![".md".to_string(), ".markdown".to_string()],
"sql" => vec![".sql".to_string()],
"vim" => vec![".vim".to_string()],
"lisp" => vec![".el".to_string(), ".lisp".to_string()],
"clojure" => vec![".clj".to_string(), ".cljs".to_string()],
"elixir" => vec![".ex".to_string(), ".exs".to_string()],
"erlang" => vec![".erl".to_string()],
"zig" => vec![".zig".to_string()],
"nim" => vec![".nim".to_string()],
"crystal" => vec![".cr".to_string()],
"dart" => vec![".dart".to_string()],
"r" => vec![".r".to_string()],
"julia" => vec![".jl".to_string()],
"protobuf" => vec![".proto".to_string()],
"graphql" => vec![".graphql".to_string(), ".gql".to_string()],
"dockerfile" => vec!["Dockerfile".to_string()],
"makefile" => vec!["Makefile".to_string(), ".make".to_string()],
"cmake" => vec!["CMakeLists.txt".to_string(), ".cmake".to_string()],
_ => vec![],
}
}
#[tonic::async_trait]
impl SyntaxService for SyntaxServiceImpl {
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::significant_drop_tightening)]
async fn get_tokens(
&self,
request: Request<GetTokensRequest>,
) -> Result<Response<GetTokensResponse>, Status> {
let req = request.into_inner();
let session = self.get_session()?;
let requested_buffer_id = if req.buffer_id == 0 {
None
} else {
Some(BufferId::from_raw(req.buffer_id as usize))
};
session
.with_state_mut(|state| {
let buffer_id = requested_buffer_id
.or_else(|| state.app.kernel.buffers.list().first().copied())
.ok_or_else(|| Status::not_found("No active buffer"))?;
let buffer_arc = state.buffer(buffer_id).ok_or_else(|| {
Status::not_found(format!("Buffer {} not found", buffer_id.as_usize()))
})?;
let buffer = buffer_arc.read();
let total_lines = buffer.line_count();
let content = buffer.content();
let file_path = buffer.file_path().map(String::from);
let (language_id, _language_name) = detect_language_from_path(file_path.as_deref());
let start_line = req.start_line.unwrap_or(0) as usize;
let end_line = match req.end_line {
Some(e) if (e as usize) < total_lines => e as usize,
_ => total_lines.saturating_sub(1),
};
let start_byte =
buffer.position_to_byte(reovim_kernel::api::v1::Position::new(start_line, 0));
let end_byte = if end_line < total_lines {
buffer.position_to_byte(reovim_kernel::api::v1::Position::new(end_line + 1, 0))
} else {
content.len()
};
let byte_range = start_byte..end_byte;
drop(buffer);
drop(buffer_arc);
let syntax_state = state.app.extensions.get_or_insert::<SyntaxSessionState>();
if let Some(path) = &file_path {
syntax_state.ensure_driver_from_path(buffer_id, path, &content);
}
if !syntax_state.has_driver(buffer_id) {
syntax_state.ensure_driver(buffer_id, language_id, &content);
}
let tokens = if syntax_state.has_driver(buffer_id) {
syntax_state.get(buffer_id).map_or_else(Vec::new, |driver| {
let mut annotations = driver.highlights(byte_range.clone());
annotations.extend(driver.decorations(byte_range));
annotations
.into_iter()
.map(|span| TokenSpan {
start_byte: span.start_byte as u32,
end_byte: span.end_byte as u32,
category: span.category.to_string(),
})
.collect()
})
} else {
Vec::new()
};
Ok(Response::new(GetTokensResponse {
buffer_id: buffer_id.as_usize() as u64,
tokens,
language_id: language_id.to_string(),
total_lines: total_lines as u64,
}))
})
.await
}
type StreamTokensStream = ReceiverStream<Result<TokenUpdate, Status>>;
#[allow(clippy::cast_possible_truncation)]
async fn stream_tokens(
&self,
request: Request<StreamTokensRequest>,
) -> Result<Response<Self::StreamTokensStream>, Status> {
let req = request.into_inner();
let session = self.get_session()?;
let session_clone = Arc::clone(&session);
let buffer_id = BufferId::from_raw(req.buffer_id as usize);
let (initial_update, syntax_rx) = session
.with_state_mut(|state| {
let buffer_arc = state.buffer(buffer_id).ok_or_else(|| {
Status::not_found(format!("Buffer {} not found", buffer_id.as_usize()))
})?;
let buffer = buffer_arc.read();
let total_lines = buffer.line_count() as u64;
let content = buffer.content();
let file_path = buffer.file_path().map(String::from);
let (language_id, _) = detect_language_from_path(file_path.as_deref());
drop(buffer);
drop(buffer_arc);
let syntax_state = state.app.extensions.get_or_insert::<SyntaxSessionState>();
if let Some(path) = &file_path {
syntax_state.ensure_driver_from_path(buffer_id, path, &content);
}
if !syntax_state.has_driver(buffer_id) {
syntax_state.ensure_driver(buffer_id, language_id, &content);
}
let tokens = syntax_state.get(buffer_id).map_or_else(Vec::new, |driver| {
let len = content.len();
let mut annotations = driver.highlights(0..len);
annotations.extend(driver.decorations(0..len));
annotations
.into_iter()
.map(|span| TokenSpan {
start_byte: span.start_byte as u32,
end_byte: span.end_byte as u32,
category: span.category.to_string(),
})
.collect()
});
let stream_state = state.app.extensions.get_or_insert::<SyntaxStreamState>();
let rx = stream_state.subscribe();
let initial = TokenUpdate {
buffer_id: buffer_id.as_usize() as u64,
tokens,
start_line: 0,
end_line: total_lines.saturating_sub(1),
full_refresh: true,
layer: "syntax".into(),
priority: 0,
};
Ok::<_, Status>((initial, rx))
})
.await?;
let (tx, rx) = mpsc::channel(32);
tokio::spawn(forward_token_updates(
tx,
initial_update,
syntax_rx,
buffer_id,
session_clone,
));
Ok(Response::new(ReceiverStream::new(rx)))
}
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::significant_drop_tightening)]
async fn get_language_info(
&self,
request: Request<GetLanguageInfoRequest>,
) -> Result<Response<GetLanguageInfoResponse>, Status> {
let req = request.into_inner();
let session = self.get_session()?;
session
.with_state_mut(|state| {
let buffer_id = BufferId::from_raw(req.buffer_id as usize);
let buffer_arc = state.buffer(buffer_id).ok_or_else(|| {
Status::not_found(format!("Buffer {} not found", buffer_id.as_usize()))
})?;
let buffer = buffer_arc.read();
let file_path = buffer.file_path().map(String::from);
drop(buffer);
drop(buffer_arc);
let syntax_state = state.app.extensions.get_or_insert::<SyntaxSessionState>();
if let Some(ref path) = file_path
&& let Some(lang_id) = syntax_state.detect_language(path)
&& let Some(registry) = syntax_state.registry()
&& let Some(info) = registry.get_info(&lang_id)
{
let has_parser = syntax_state.factory().is_some_and(|f| f.supports(&lang_id));
let extensions = info.extensions.iter().map(|e| format!(".{e}")).collect();
return Ok(Response::new(GetLanguageInfoResponse {
language_id: lang_id,
language_name: info.name.clone(),
extensions,
has_parser,
}));
}
let (language_id, language_name) = detect_language_from_path(file_path.as_deref());
let extensions = extensions_for_language(language_id);
let has_parser = syntax_state
.factory()
.is_some_and(|f| f.supports(language_id));
Ok(Response::new(GetLanguageInfoResponse {
language_id: language_id.to_string(),
language_name: language_name.to_string(),
extensions,
has_parser,
}))
})
.await
}
}
#[cfg(test)]
#[path = "syntax_tests.rs"]
mod tests;