mod client;
mod diagnostics;
mod manager;
mod queries;
mod stdio;
mod sync;
pub(crate) use manager::{EditDiagnosticsRequest, LspManager};
pub(crate) use diagnostics::{DiagnosticsStore, LspDiagnostic};
pub(crate) use queries::ReferenceLocation;
pub(crate) use sync::DocumentVersions;
use anyhow::{Context, bail};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
pub(crate) const MAX_INJECTED_DIAGNOSTICS: usize = 20;
pub(crate) const MAX_INJECTED_DIAGNOSTICS_BYTES: usize = 2 * 1024;
pub(crate) const MAX_TOOL_DIAGNOSTICS: usize = 200;
pub(crate) const MAX_REFERENCES: usize = 100;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub(crate) struct DocumentUri(String);
impl DocumentUri {
pub(crate) fn from_path(path: &Path) -> anyhow::Result<Self> {
let absolute = if path.is_absolute() {
path.to_path_buf()
} else {
std::env::current_dir()?.join(path)
};
let path = absolute
.canonicalize()
.with_context(|| format!("canonicalizing {}", absolute.display()))?;
Ok(Self(format!("file://{}", percent_encode_path(&path))))
}
pub(crate) fn to_file_path(&self) -> anyhow::Result<PathBuf> {
let Some(rest) = self.0.strip_prefix("file://") else {
bail!("unsupported LSP URI scheme");
};
Ok(PathBuf::from(percent_decode(rest)?))
}
pub(crate) fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
#[repr(u8)]
pub(crate) enum DiagnosticSeverity {
Error = 1,
Warning = 2,
Information = 3,
Hint = 4,
}
impl<'de> Deserialize<'de> for DiagnosticSeverity {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
match u8::deserialize(deserializer)? {
1 => Ok(Self::Error),
2 => Ok(Self::Warning),
3 => Ok(Self::Information),
4 => Ok(Self::Hint),
other => Err(serde::de::Error::custom(format!(
"unknown diagnostic severity {other}"
))),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct LspRange {
pub(crate) start: LspPosition,
pub(crate) end: LspPosition,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct LspPosition {
pub(crate) line: u32,
pub(crate) character: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) struct DocumentVersion(pub(crate) i32);
fn percent_encode_path(path: &Path) -> String {
path.to_string_lossy()
.bytes()
.flat_map(|byte| match byte {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'/' | b'-' | b'_' | b'.' | b'~' => {
vec![byte as char]
}
_ => format!("%{byte:02X}").chars().collect(),
})
.collect()
}
fn percent_decode(value: &str) -> anyhow::Result<String> {
let bytes = value.as_bytes();
let mut decoded = Vec::with_capacity(bytes.len());
let mut index = 0;
while index < bytes.len() {
if bytes[index] == b'%' {
if index + 2 >= bytes.len() {
bail!("invalid percent-encoded URI");
}
let hex = std::str::from_utf8(&bytes[index + 1..index + 3])?;
decoded.push(u8::from_str_radix(hex, 16).context("invalid percent-encoded URI")?);
index += 3;
} else {
decoded.push(bytes[index]);
index += 1;
}
}
Ok(String::from_utf8(decoded)?)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn document_uri_round_trips_path_with_spaces() {
let temp = tempfile::TempDir::new().unwrap();
let path = temp.path().join("file with spaces.rs");
std::fs::write(&path, "fn main() {}\n").unwrap();
let uri = DocumentUri::from_path(&path).unwrap();
assert!(uri.as_str().starts_with("file://"));
assert!(uri.as_str().contains("%20"));
assert_eq!(uri.to_file_path().unwrap(), path.canonicalize().unwrap());
}
}