You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
helix/helix-lsp/src/lib.rs

323 lines
10 KiB
Rust

mod client;
mod transport;
pub use client::Client;
pub use futures_executor::block_on;
pub use jsonrpc::Call;
pub use jsonrpc_core as jsonrpc;
pub use lsp::{Position, Url};
pub use lsp_types as lsp;
use futures_util::stream::select_all::SelectAll;
use helix_core::syntax::LanguageConfiguration;
use std::{
collections::{hash_map::Entry, HashMap},
sync::Arc,
};
4 years ago
use serde::{Deserialize, Serialize};
use thiserror::Error;
3 years ago
use tokio_stream::wrappers::UnboundedReceiverStream;
pub type Result<T> = core::result::Result<T, Error>;
type LanguageId = String;
#[derive(Error, Debug)]
pub enum Error {
#[error("protocol error: {0}")]
Rpc(#[from] jsonrpc::Error),
#[error("failed to parse: {0}")]
Parse(#[from] serde_json::Error),
#[error("IO Error: {0}")]
IO(#[from] std::io::Error),
#[error("request timed out")]
Timeout,
#[error("server closed the stream")]
StreamClosed,
#[error("LSP not defined")]
LspNotDefined,
#[error(transparent)]
Other(#[from] anyhow::Error),
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub enum OffsetEncoding {
/// UTF-8 code units aka bytes
#[serde(rename = "utf-8")]
Utf8,
/// UTF-16 code units
#[serde(rename = "utf-16")]
Utf16,
}
pub mod util {
use super::*;
use helix_core::{Range, Rope, Transaction};
/// Converts [`lsp::Position`] to a position in the document.
///
/// Returns `None` if position exceeds document length or an operation overflows.
pub fn lsp_pos_to_pos(
doc: &Rope,
pos: lsp::Position,
offset_encoding: OffsetEncoding,
) -> Option<usize> {
let max_line = doc.lines().count().saturating_sub(1);
let pos_line = pos.line as usize;
let pos_line = if pos_line > max_line {
return None;
} else {
pos_line
};
match offset_encoding {
OffsetEncoding::Utf8 => {
let max_char = doc
.line_to_char(max_line)
.checked_add(doc.line(max_line).len_chars())?;
let line = doc.line_to_char(pos_line);
let pos = line.checked_add(pos.character as usize)?;
if pos <= max_char {
Some(pos)
} else {
None
}
}
OffsetEncoding::Utf16 => {
let max_char = doc
.line_to_char(max_line)
.checked_add(doc.line(max_line).len_chars())?;
let max_cu = doc.char_to_utf16_cu(max_char);
let line = doc.line_to_char(pos_line);
let line_start = doc.char_to_utf16_cu(line);
let pos = line_start.checked_add(pos.character as usize)?;
if pos <= max_cu {
Some(doc.utf16_cu_to_char(pos))
} else {
None
}
}
}
}
/// Converts position in the document to [`lsp::Position`].
///
/// Panics when `pos` is out of `doc` bounds or operation overflows.
pub fn pos_to_lsp_pos(
doc: &Rope,
pos: usize,
offset_encoding: OffsetEncoding,
) -> lsp::Position {
match offset_encoding {
OffsetEncoding::Utf8 => {
let line = doc.char_to_line(pos);
let line_start = doc.line_to_char(line);
let col = pos - line_start;
lsp::Position::new(line as u32, col as u32)
}
OffsetEncoding::Utf16 => {
let line = doc.char_to_line(pos);
let line_start = doc.char_to_utf16_cu(doc.line_to_char(line));
let col = doc.char_to_utf16_cu(pos) - line_start;
lsp::Position::new(line as u32, col as u32)
}
}
}
/// Converts a range in the document to [`lsp::Range`].
pub fn range_to_lsp_range(
doc: &Rope,
range: Range,
offset_encoding: OffsetEncoding,
) -> lsp::Range {
let start = pos_to_lsp_pos(doc, range.from(), offset_encoding);
let end = pos_to_lsp_pos(doc, range.to(), offset_encoding);
lsp::Range::new(start, end)
}
pub fn generate_transaction_from_edits(
doc: &Rope,
edits: Vec<lsp::TextEdit>,
offset_encoding: OffsetEncoding,
) -> Transaction {
Transaction::change(
doc,
edits.into_iter().map(|edit| {
// simplify "" into None for cleaner changesets
let replacement = if !edit.new_text.is_empty() {
Some(edit.new_text.into())
} else {
None
};
let start =
if let Some(start) = lsp_pos_to_pos(doc, edit.range.start, offset_encoding) {
start
} else {
return (0, 0, None);
};
let end = if let Some(end) = lsp_pos_to_pos(doc, edit.range.end, offset_encoding) {
end
} else {
return (0, 0, None);
};
(start, end, replacement)
}),
)
}
}
#[derive(Debug, PartialEq, Clone)]
pub enum Notification {
PublishDiagnostics(lsp::PublishDiagnosticsParams),
ShowMessage(lsp::ShowMessageParams),
LogMessage(lsp::LogMessageParams),
ProgressMessage(lsp::ProgressParams),
}
impl Notification {
pub fn parse(method: &str, params: jsonrpc::Params) -> Option<Notification> {
use lsp::notification::Notification as _;
let notification = match method {
lsp::notification::PublishDiagnostics::METHOD => {
let params: lsp::PublishDiagnosticsParams = params
.parse()
.expect("Failed to parse PublishDiagnostics params");
// TODO: need to loop over diagnostics and distinguish them by URI
3 years ago
Self::PublishDiagnostics(params)
}
lsp::notification::ShowMessage::METHOD => {
let params: lsp::ShowMessageParams = params.parse().ok()?;
3 years ago
Self::ShowMessage(params)
}
lsp::notification::LogMessage::METHOD => {
let params: lsp::LogMessageParams = params.parse().ok()?;
3 years ago
Self::LogMessage(params)
}
lsp::notification::Progress::METHOD => {
let params: lsp::ProgressParams = params.parse().ok()?;
Self::ProgressMessage(params)
}
_ => {
log::error!("unhandled LSP notification: {}", method);
return None;
}
};
Some(notification)
}
}
#[derive(Debug)]
4 years ago
pub struct Registry {
inner: HashMap<LanguageId, Arc<Client>>,
3 years ago
pub incoming: SelectAll<UnboundedReceiverStream<Call>>,
4 years ago
}
impl Default for Registry {
fn default() -> Self {
Self::new()
}
}
4 years ago
impl Registry {
pub fn new() -> Self {
4 years ago
Self {
inner: HashMap::new(),
incoming: SelectAll::new(),
4 years ago
}
}
pub fn get(&mut self, language_config: &LanguageConfiguration) -> Result<Arc<Client>> {
if let Some(config) = &language_config.language_server {
// avoid borrow issues
let inner = &mut self.inner;
let s_incoming = &mut self.incoming;
match inner.entry(language_config.scope.clone()) {
Entry::Occupied(language_server) => Ok(language_server.get().clone()),
Entry::Vacant(entry) => {
4 years ago
// initialize a new client
let (mut client, incoming) = Client::start(&config.command, &config.args)?;
// TODO: run this async without blocking
futures_executor::block_on(client.initialize())?;
3 years ago
s_incoming.push(UnboundedReceiverStream::new(incoming));
let client = Arc::new(client);
entry.insert(client.clone());
Ok(client)
}
}
} else {
Err(Error::LspNotDefined)
}
4 years ago
}
}
// REGISTRY = HashMap<LanguageId, Lazy/OnceCell<Arc<RwLock<Client>>>
// spawn one server per language type, need to spawn one per workspace if server doesn't support
// workspaces
//
// could also be a client per root dir
//
// storing a copy of Option<Arc<RwLock<Client>>> on Document would make the LSP client easily
// accessible during edit/save callbacks
//
// the event loop needs to process all incoming streams, maybe we can just have that be a separate
// task that's continually running and store the state on the client, then use read lock to
// retrieve data during render
// -> PROBLEM: how do you trigger an update on the editor side when data updates?
//
// -> The data updates should pull all events until we run out so we don't frequently re-render
//
//
// v2:
//
// there should be a registry of lsp clients, one per language type (or workspace).
// the clients should lazy init on first access
// the client.initialize() should be called async and we buffer any requests until that completes
// there needs to be a way to process incoming lsp messages from all clients.
// -> notifications need to be dispatched to wherever
// -> requests need to generate a reply and travel back to the same lsp!
#[cfg(test)]
mod tests {
use super::{lsp, util::*, OffsetEncoding};
use helix_core::Rope;
#[test]
fn converts_lsp_pos_to_pos() {
macro_rules! test_case {
($doc:expr, ($x:expr, $y:expr) => $want:expr) => {
let doc = Rope::from($doc);
let pos = lsp::Position::new($x, $y);
assert_eq!($want, lsp_pos_to_pos(&doc, pos, OffsetEncoding::Utf16));
assert_eq!($want, lsp_pos_to_pos(&doc, pos, OffsetEncoding::Utf8))
};
}
test_case!("", (0, 0) => Some(0));
test_case!("", (0, 1) => None);
test_case!("", (1, 0) => None);
test_case!("\n\n", (0, 0) => Some(0));
test_case!("\n\n", (1, 0) => Some(1));
test_case!("\n\n", (1, 1) => Some(2));
test_case!("\n\n", (2, 0) => Some(2));
test_case!("\n\n", (3, 0) => None);
test_case!("test\n\n\n\ncase", (4, 3) => Some(11));
test_case!("test\n\n\n\ncase", (4, 4) => Some(12));
test_case!("test\n\n\n\ncase", (4, 5) => None);
test_case!("", (u32::MAX, u32::MAX) => None);
}
}