Files
galaxy/crates/lsp/src/service.rs
T

993 lines
33 KiB
Rust

use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use anyhow::Result;
use galaxy_util::on_cancel::OnCancelFutureExt;
use globset::{Glob, GlobMatcher};
use jsonrpc::{JsonRpcService, RequestId, ServerNotificationEvent};
use lsp_types::notification::{self, Notification};
use lsp_types::request::{self, Request};
use lsp_types::{
CancelParams, CodeActionContext, CodeActionParams, CompletionContext, CompletionItem,
CompletionParams, CompletionTriggerKind, DidChangeTextDocumentParams,
DidChangeWatchedFilesParams, DidChangeWatchedFilesRegistrationOptions,
DidCloseTextDocumentParams, DidOpenTextDocumentParams, DocumentFormattingParams,
FileChangeType, FileSystemWatcher, FormattingOptions, GlobPattern, GotoDefinitionParams,
GotoDefinitionResponse, HoverParams, InitializeParams, InitializedParams, NumberOrString,
OneOf, Position, Range as LspRange, ReferenceParams, RegistrationParams, RelativePattern,
RenameParams, SignatureHelpParams, TextDocumentIdentifier, TextDocumentItem,
TextDocumentPositionParams, UnregistrationParams, VersionedTextDocumentIdentifier, WatchKind,
};
use serde_json::Value;
#[cfg(not(target_arch = "wasm32"))]
use simple_logger::SimpleLogger;
use crate::config::{lsp_uri_to_path, path_to_lsp_uri, LanguageId};
use crate::types::{
CodeActionData, CompletionResult, FileEdits, HoverResult, Location, LspDefinitionLocation,
PrepareRenameResult, Range, ReferenceLocation, RenameResult, SignatureHelpResult,
TextDocumentContentChangeEvent, TextEdit, WatchedFileChangeEvent,
};
use crate::LspServerLogLevel;
/// Tracks the sync state for an open document.
#[derive(Debug, Clone)]
pub struct DocumentSyncState {
/// The last buffer version that was successfully synced with the LSP server.
/// None means the document was opened but no subsequent changes have been synced yet.
pub last_synced_version: Option<usize>,
}
pub struct LspService {
jsonrpc_service: JsonRpcService,
server_capabilities: Option<lsp_types::ServerCapabilities>,
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
open_documents: Arc<Mutex<HashMap<PathBuf, DocumentSyncState>>>,
watched_files_registry: Arc<Mutex<WatchedFilesRegistry>>,
notify_tx: async_channel::Sender<ServerNotificationEvent>,
#[cfg(not(target_arch = "wasm32"))]
logger: Option<SimpleLogger>,
}
struct LspServerRequestHandler {
watched_files_registry: Arc<Mutex<WatchedFilesRegistry>>,
}
impl LspServerRequestHandler {
fn handle_request(&self, method: &str, params: Value, id: RequestId) -> Result<()> {
match method {
"client/registerCapability" => {
let params = match serde_json::from_value::<RegistrationParams>(params) {
Ok(params) => params,
Err(e) => {
log::warn!(
"Failed to parse client/registerCapability params (id: {id:?}): {e}"
);
return Ok(());
}
};
for registration in params.registrations {
if registration.method != notification::DidChangeWatchedFiles::METHOD {
continue;
}
let Some(register_options) = registration.register_options else {
log::debug!(
"Ignoring didChangeWatchedFiles registration without options (id: {})",
registration.id
);
continue;
};
let options = match serde_json::from_value::<
DidChangeWatchedFilesRegistrationOptions,
>(register_options)
{
Ok(options) => options,
Err(e) => {
log::warn!(
"Failed to parse didChangeWatchedFiles registration options (id: {}): {e}",
registration.id
);
continue;
}
};
let mut registry = self
.watched_files_registry
.lock()
.unwrap_or_else(|e| e.into_inner());
registry.register(registration.id, options);
}
}
"client/unregisterCapability" => {
let params = match serde_json::from_value::<UnregistrationParams>(params) {
Ok(params) => params,
Err(e) => {
log::warn!(
"Failed to parse client/unregisterCapability params (id: {id:?}): {e}"
);
return Ok(());
}
};
let mut registry = self
.watched_files_registry
.lock()
.unwrap_or_else(|e| e.into_inner());
for unregistration in params.unregisterations {
if unregistration.method == notification::DidChangeWatchedFiles::METHOD {
registry.unregister(&unregistration.id);
}
}
}
_ => {}
}
Ok(())
}
}
impl LspService {
/// Creates a new LspService with the given JsonRpcService.
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
pub(crate) fn new(
jsonrpc_service: JsonRpcService,
notify_tx: async_channel::Sender<ServerNotificationEvent>,
workspace_root: PathBuf,
#[cfg(not(target_arch = "wasm32"))] logger: Option<SimpleLogger>,
) -> Result<Self> {
let watched_files_registry =
Arc::new(Mutex::new(WatchedFilesRegistry::new(workspace_root)));
let server_request_handler = Arc::new(LspServerRequestHandler {
watched_files_registry: watched_files_registry.clone(),
});
let server_request_handler_for_closure = server_request_handler.clone();
jsonrpc_service.set_server_request_handler(move |method, params, id| {
server_request_handler_for_closure.handle_request(&method, params, id)
});
let service = Self {
jsonrpc_service,
server_capabilities: None,
open_documents: Arc::new(Mutex::new(HashMap::new())),
watched_files_registry,
notify_tx,
#[cfg(not(target_arch = "wasm32"))]
logger,
};
Ok(service)
}
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
pub(crate) async fn initialize(&mut self, params: InitializeParams) -> Result<()> {
let response = self.send_request::<request::Initialize>(params).await?;
self.server_capabilities = Some(response.capabilities);
self.send_notification::<notification::Initialized>(InitializedParams {})?;
self.subscribe::<notification::Progress>().await;
self.subscribe::<notification::PublishDiagnostics>().await;
log::info!("LSP initialized successfully and will now run startup tasks");
Ok(())
}
pub(crate) async fn subscribe<N: notification::Notification>(&self) {
self.jsonrpc_service
.subscribe(N::METHOD.to_string(), self.notify_tx.clone())
.await;
}
pub fn server_capabilities(&self) -> &Option<lsp_types::ServerCapabilities> {
&self.server_capabilities
}
pub fn log_to_server_log(&self, level: LspServerLogLevel, message: impl Into<String>) {
#[cfg(not(target_arch = "wasm32"))]
{
if let Some(logger) = &self.logger {
logger.log(format!("[{level}] {}", message.into()));
}
}
#[cfg(target_arch = "wasm32")]
{
let _ = (level, message.into());
}
}
pub async fn shutdown(&self) -> anyhow::Result<()> {
// Send LSP shutdown request first
log::debug!("Sending LSP shutdown request");
match self.send_request::<request::Shutdown>(()).await {
Ok(_) => log::debug!("LSP shutdown request completed successfully"),
Err(e) => {
log::warn!("LSP shutdown request failed: {e}");
// Continue with exit notification even if shutdown request fails
}
}
// Send exit notification
log::debug!("Sending LSP exit notification");
if let Err(e) = self.send_notification::<notification::Exit>(()) {
log::warn!("Failed to send LSP exit notification: {e}");
}
// Finally, shutdown the transport (kill process if needed)
self.jsonrpc_service
.shutdown(std::time::Duration::from_secs(5))
.await?;
log::debug!("LSP shutdown sequence completed");
Ok(())
}
/// Get a handle to text-document related operations and requests
pub fn text_document(&self) -> TextDocumentService<'_> {
TextDocumentService {
service: self,
open_documents: self.open_documents.clone(),
}
}
pub fn workspace_watched_files_changed(
&self,
events: Vec<WatchedFileChangeEvent>,
) -> Result<()> {
if events.is_empty() {
return Ok(());
}
let watched_files_registry = self
.watched_files_registry
.lock()
.map_err(|_| anyhow::anyhow!("Failed to acquire lock on watched files registry"))?;
// If the server hasn't registered any watchers yet, don't send notifications.
if watched_files_registry.is_empty() {
return Ok(());
}
let mut changes = Vec::new();
for event in events {
if !watched_files_registry.matches(&event) {
continue;
}
match event.into_lsp() {
Ok(event) => changes.push(event),
Err(e) => log::warn!("Failed to convert file event: {e}"),
}
}
drop(watched_files_registry);
if changes.is_empty() {
return Ok(());
}
self.send_notification::<notification::DidChangeWatchedFiles>(DidChangeWatchedFilesParams {
changes,
})
}
async fn send_request_internal(&self, method: String, params: Value) -> Result<Value> {
let request_id = self.jsonrpc_service.next_id();
let request = self
.jsonrpc_service
.send_request(request_id, method, params);
request
.on_cancel(move || {
self.cancel_request(request_id);
})
.await
}
fn send_notification<N: Notification>(&self, params: N::Params) -> Result<()> {
let params = serde_json::to_value(params)?;
self.jsonrpc_service
.send_notification(N::METHOD.to_string(), params)
}
async fn send_request<R: Request>(&self, params: R::Params) -> Result<R::Result> {
let params = serde_json::to_value(params)?;
let request = self.send_request_internal(R::METHOD.to_string(), params);
let response = request.await?;
serde_json::from_value::<R::Result>(response)
.map_err(|e| anyhow::anyhow!("Failed to parse response: {e}"))
}
fn cancel_request(&self, request_id: RequestId) {
let cancel = serde_json::to_value(CancelParams {
id: NumberOrString::Number(request_id),
})
.expect("Failed to serialize cancel params");
if let Err(e) = self
.jsonrpc_service
.send_notification(notification::Cancel::METHOD.to_string(), cancel)
{
log::error!("Failed to send cancel notification: {e}");
}
}
}
struct WatchedFilesRegistry {
workspace_root: PathBuf,
registrations: HashMap<String, Vec<GlobFileMatcher>>,
}
impl WatchedFilesRegistry {
fn new(workspace_root: PathBuf) -> Self {
Self {
workspace_root,
registrations: HashMap::new(),
}
}
fn is_empty(&self) -> bool {
self.registrations.is_empty()
}
fn register(
&mut self,
registration_id: String,
options: DidChangeWatchedFilesRegistrationOptions,
) {
let watchers = options
.watchers
.into_iter()
.filter_map(|watcher| self.compile_watcher(watcher))
.collect();
self.registrations.insert(registration_id, watchers);
}
fn unregister(&mut self, registration_id: &str) {
self.registrations.remove(registration_id);
}
fn matches(&self, event: &WatchedFileChangeEvent) -> bool {
let Ok(path_relative) = event.path.strip_prefix(&self.workspace_root) else {
return false;
};
let path_relative = galaxy_util::path::normalize_relative_path_for_glob(path_relative);
self.registrations
.values()
.flatten()
.any(|watcher| watcher.matches(&path_relative, event.typ))
}
fn compile_watcher(&self, watcher: FileSystemWatcher) -> Option<GlobFileMatcher> {
let FileSystemWatcher { glob_pattern, kind } = watcher;
let pattern = self.pattern_for_glob_pattern(glob_pattern)?;
let matcher = match Glob::new(&pattern) {
Ok(glob) => {
let matcher: GlobMatcher = glob.compile_matcher();
matcher
}
Err(e) => {
log::warn!("Failed to compile watched-files glob pattern {pattern:?}: {e}");
return None;
}
};
Some(GlobFileMatcher { matcher, kind })
}
fn pattern_for_glob_pattern(&self, glob_pattern: GlobPattern) -> Option<String> {
match glob_pattern {
GlobPattern::String(pattern) => Some(pattern),
GlobPattern::Relative(relative_pattern) => {
self.pattern_for_relative_pattern(relative_pattern)
}
}
}
fn pattern_for_relative_pattern(&self, relative_pattern: RelativePattern) -> Option<String> {
let RelativePattern { base_uri, pattern } = relative_pattern;
let base_path = match base_uri {
OneOf::Left(folder) => folder.uri,
OneOf::Right(uri) => uri,
};
let base_path = match lsp_uri_to_path(&base_path) {
Ok(path) => path,
Err(e) => {
let base_uri = base_path.as_str();
log::warn!("Failed to resolve relativePattern baseUri {base_uri}: {e}");
return None;
}
};
let Ok(base_relative) = base_path.strip_prefix(&self.workspace_root) else {
return None;
};
// Normalize to forward slashes so glob patterns and event paths are comparable across platforms (esp. Windows).
let prefix = galaxy_util::path::normalize_relative_path_for_glob(base_relative);
if prefix.is_empty() {
Some(pattern)
} else {
Some(format!("{prefix}/{pattern}"))
}
}
}
#[derive(Debug)]
struct GlobFileMatcher {
matcher: GlobMatcher,
kind: Option<WatchKind>,
}
impl GlobFileMatcher {
fn matches(&self, path_relative: &str, change_type: FileChangeType) -> bool {
if let Some(kind) = self.kind {
if let Some(required) = watch_kind_for_change_type(change_type) {
if !kind.contains(required) {
return false;
}
}
}
self.matcher.is_match(path_relative)
}
}
fn watch_kind_for_change_type(change_type: FileChangeType) -> Option<WatchKind> {
match change_type {
FileChangeType::CREATED => Some(WatchKind::Create),
FileChangeType::CHANGED => Some(WatchKind::Change),
FileChangeType::DELETED => Some(WatchKind::Delete),
_ => None,
}
}
/// Encapsulates text-document related operations and requests. This exists only for bookkeeping
/// and discoverability.
pub struct TextDocumentService<'a> {
service: &'a LspService,
open_documents: Arc<Mutex<HashMap<PathBuf, DocumentSyncState>>>,
}
impl<'a> TextDocumentService<'a> {
pub fn document_is_open(&self, path: &PathBuf) -> Result<bool> {
let open_documents = self
.open_documents
.lock()
.map_err(|_| anyhow::anyhow!("Failed to acquire lock on open documents"))?;
Ok(open_documents.contains_key(path))
}
/// Returns the last synced buffer version for the document, if it is open.
/// Returns None if the document is not open.
pub fn last_synced_version(&self, path: &PathBuf) -> Result<Option<usize>> {
let open_documents = self
.open_documents
.lock()
.map_err(|_| anyhow::anyhow!("Failed to acquire lock on open documents"))?;
Ok(open_documents
.get(path)
.and_then(|state| state.last_synced_version))
}
pub async fn did_open(
&self,
path: &Path,
content: String,
initial_version: usize,
) -> Result<()> {
{
// Drop the guard before await
let mut open_documents = self
.open_documents
.lock()
.map_err(|_| anyhow::anyhow!("Failed to acquire lock on open documents"))?;
// Use entry API to check if already present
if open_documents.contains_key(&path.to_path_buf()) {
return Ok(());
}
open_documents.insert(
path.to_path_buf(),
DocumentSyncState {
last_synced_version: Some(initial_version),
},
);
}
self.service.log_to_server_log(
LspServerLogLevel::Debug,
format!(
"didOpen -> server: file={} version={initial_version}",
path.display()
),
);
// Determine language ID from the file path
let language_id = LanguageId::from_path(path)
.ok_or_else(|| {
anyhow::anyhow!(
"Could not determine language ID for file: {}",
path.display()
)
})?
.lsp_language_identifier()
.to_owned();
let did_open_params = DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: path_to_lsp_uri(path)?,
language_id,
version: initial_version as i32,
text: content,
},
};
self.service
.send_notification::<notification::DidOpenTextDocument>(did_open_params)
}
pub async fn did_close(&self, path: &Path) -> Result<()> {
{
// Drop the guard before await
let mut open_documents = self
.open_documents
.lock()
.map_err(|_| anyhow::anyhow!("Failed to acquire lock on open documents"))?;
if open_documents.remove(&path.to_path_buf()).is_none() {
return Ok(());
}
}
let did_close_params = DidCloseTextDocumentParams {
text_document: TextDocumentIdentifier {
uri: path_to_lsp_uri(path)?,
},
};
self.service
.send_notification::<notification::DidCloseTextDocument>(did_close_params)
}
pub async fn did_change(
&self,
path: &Path,
version: i32,
deltas: Vec<TextDocumentContentChangeEvent>,
) -> Result<()> {
{
// Check if document is open
let mut open_documents = self
.open_documents
.lock()
.map_err(|_| anyhow::anyhow!("Failed to acquire lock on open documents"))?;
let Some(state) = open_documents.get_mut(path) else {
return Ok(());
};
state.last_synced_version = Some(version as usize);
}
let did_change_params = DidChangeTextDocumentParams {
text_document: VersionedTextDocumentIdentifier {
uri: path_to_lsp_uri(path)?,
version,
},
content_changes: deltas.into_iter().map(|delta| delta.into_lsp()).collect(),
};
self.service
.send_notification::<notification::DidChangeTextDocument>(did_change_params)
}
pub async fn definition(
&self,
path: &Path,
position: Position,
) -> anyhow::Result<Vec<LspDefinitionLocation>> {
let uri = path_to_lsp_uri(path)?;
let definition_params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position,
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let result = self
.service
.send_request::<request::GotoDefinition>(definition_params)
.await;
if let Err(e) = &result {
self.service.log_to_server_log(
LspServerLogLevel::Error,
format!("textDocument/definition failed: {e}"),
);
}
// The LSP spec says textDocument/definition can return null when no definition is found
// Handle this case explicitly since GotoDefinitionResponse doesn't deserialize null properly
let Some(response) = result? else {
return Err(anyhow::anyhow!("No definition found or LSP busy"));
};
match response {
GotoDefinitionResponse::Scalar(location) => Ok(vec![location.into()]),
GotoDefinitionResponse::Array(locations) => {
Ok(locations.into_iter().map(Into::into).collect())
}
GotoDefinitionResponse::Link(locations) => {
Ok(locations.into_iter().map(Into::into).collect())
}
}
}
pub async fn format(
&self,
path: &Path,
options: FormattingOptions,
) -> anyhow::Result<Option<Vec<TextEdit>>> {
let format_params = DocumentFormattingParams {
text_document: TextDocumentIdentifier {
uri: path_to_lsp_uri(path)?,
},
options,
work_done_progress_params: Default::default(),
};
let result = self
.service
.send_request::<request::Formatting>(format_params)
.await;
if let Err(e) = &result {
self.service.log_to_server_log(
LspServerLogLevel::Error,
format!("textDocument/formatting failed: {e}"),
);
}
// The LSP spec says textDocument/formatting can return null when formatting is not supported
result.map(|edits_option| {
edits_option.map(|text_edits| text_edits.into_iter().map(Into::into).collect())
})
}
pub async fn hover(
&self,
path: &Path,
position: Position,
) -> anyhow::Result<Option<HoverResult>> {
let uri = path_to_lsp_uri(path)?;
let hover_params = HoverParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position,
},
work_done_progress_params: Default::default(),
};
let result = self
.service
.send_request::<request::HoverRequest>(hover_params)
.await;
if let Err(e) = &result {
self.service.log_to_server_log(
LspServerLogLevel::Error,
format!("textDocument/hover failed: {e}"),
);
}
Ok(result?.map(Into::into))
}
pub async fn references(
&self,
path: &Path,
position: Position,
) -> anyhow::Result<Vec<ReferenceLocation>> {
let uri = path_to_lsp_uri(path)?;
let reference_params = ReferenceParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position,
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
context: lsp_types::ReferenceContext {
include_declaration: true,
},
};
let result = self
.service
.send_request::<request::References>(reference_params)
.await;
if let Err(e) = &result {
self.service.log_to_server_log(
LspServerLogLevel::Error,
format!("textDocument/references failed: {e}"),
);
}
// The LSP spec says textDocument/references can return null when no references are found
Ok(result?
.unwrap_or_default()
.into_iter()
.filter_map(|loc| ReferenceLocation::try_from(loc).ok())
.collect())
}
pub async fn completion(
&self,
path: &Path,
position: Position,
trigger: Option<CompletionTriggerKind>,
trigger_character: Option<String>,
) -> anyhow::Result<Option<CompletionResult>> {
let uri = path_to_lsp_uri(path)?;
let completion_params = CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position,
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
context: Some(CompletionContext {
trigger_kind: trigger.unwrap_or(CompletionTriggerKind::INVOKED),
trigger_character,
}),
};
let result = self
.service
.send_request::<request::Completion>(completion_params)
.await;
if let Err(e) = &result {
self.service.log_to_server_log(
LspServerLogLevel::Error,
format!("textDocument/completion failed: {e}"),
);
}
Ok(result?.map(Into::into))
}
pub async fn completion_resolve(&self, item: CompletionItem) -> anyhow::Result<CompletionItem> {
let result = self
.service
.send_request::<request::ResolveCompletionItem>(item)
.await;
if let Err(e) = &result {
self.service.log_to_server_log(
LspServerLogLevel::Error,
format!("completionItem/resolve failed: {e}"),
);
}
result
}
pub async fn signature_help(
&self,
path: &Path,
position: Position,
) -> anyhow::Result<Option<SignatureHelpResult>> {
let uri = path_to_lsp_uri(path)?;
let params = SignatureHelpParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position,
},
work_done_progress_params: Default::default(),
context: None,
};
let result = self
.service
.send_request::<request::SignatureHelpRequest>(params)
.await;
if let Err(e) = &result {
self.service.log_to_server_log(
LspServerLogLevel::Error,
format!("textDocument/signatureHelp failed: {e}"),
);
}
Ok(result?.map(Into::into))
}
pub async fn code_action(
&self,
path: &Path,
range: LspRange,
diagnostics: Vec<lsp_types::Diagnostic>,
) -> anyhow::Result<Vec<CodeActionData>> {
let uri = path_to_lsp_uri(path)?;
let params = CodeActionParams {
text_document: TextDocumentIdentifier { uri },
range,
context: CodeActionContext {
diagnostics,
only: None,
trigger_kind: None,
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let result = self
.service
.send_request::<request::CodeActionRequest>(params)
.await;
if let Err(e) = &result {
self.service.log_to_server_log(
LspServerLogLevel::Error,
format!("textDocument/codeAction failed: {e}"),
);
}
Ok(result?
.unwrap_or_default()
.into_iter()
.map(Into::into)
.collect())
}
pub async fn prepare_rename(
&self,
path: &Path,
position: Position,
) -> anyhow::Result<Option<PrepareRenameResult>> {
let uri = path_to_lsp_uri(path)?;
let params = TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position,
};
let result = self
.service
.send_request::<request::PrepareRenameRequest>(params)
.await;
if let Err(e) = &result {
self.service.log_to_server_log(
LspServerLogLevel::Error,
format!("textDocument/prepareRename failed: {e}"),
);
}
Ok(result?.map(|response| match response {
lsp_types::PrepareRenameResponse::Range(range) => PrepareRenameResult {
range: range.into(),
placeholder: None,
},
lsp_types::PrepareRenameResponse::RangeWithPlaceholder { range, placeholder } => {
PrepareRenameResult {
range: range.into(),
placeholder: Some(placeholder),
}
}
lsp_types::PrepareRenameResponse::DefaultBehavior { .. } => PrepareRenameResult {
range: Range {
start: Location { line: 0, column: 0 },
end: Location { line: 0, column: 0 },
},
placeholder: None,
},
}))
}
pub async fn rename(
&self,
path: &Path,
position: Position,
new_name: String,
) -> anyhow::Result<Option<RenameResult>> {
let uri = path_to_lsp_uri(path)?;
let params = RenameParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position,
},
new_name,
work_done_progress_params: Default::default(),
};
let result = self.service.send_request::<request::Rename>(params).await;
if let Err(e) = &result {
self.service.log_to_server_log(
LspServerLogLevel::Error,
format!("textDocument/rename failed: {e}"),
);
}
let workspace_edit = match result? {
Some(edit) => edit,
None => return Ok(None),
};
let file_edits = workspace_edit_to_file_edits(workspace_edit)?;
Ok(Some(RenameResult { edits: file_edits }))
}
}
fn workspace_edit_to_file_edits(
workspace_edit: lsp_types::WorkspaceEdit,
) -> anyhow::Result<Vec<FileEdits>> {
let mut result: Vec<FileEdits> = Vec::new();
if let Some(changes) = workspace_edit.changes {
for (uri, edits) in changes {
let path = lsp_uri_to_path(&uri)?;
result.push(FileEdits {
path,
edits: edits.into_iter().map(Into::into).collect(),
});
}
}
if let Some(document_changes) = workspace_edit.document_changes {
match document_changes {
lsp_types::DocumentChanges::Edits(edits) => {
for edit in edits {
let path = lsp_uri_to_path(&edit.text_document.uri)?;
result.push(FileEdits {
path,
edits: edit
.edits
.into_iter()
.map(|e| match e {
lsp_types::OneOf::Left(text_edit) => text_edit.into(),
lsp_types::OneOf::Right(annotated) => lsp_types::TextEdit {
range: annotated.text_edit.range,
new_text: annotated.text_edit.new_text,
}
.into(),
})
.collect(),
});
}
}
lsp_types::DocumentChanges::Operations(_) => {
// File create/rename/delete operations are not supported yet
log::warn!("workspace/rename returned file operations which are not yet supported");
}
}
}
Ok(result)
}