use async_lsp::lsp_types::{self as lt, TextDocumentPositionParams};
use std::path::PathBuf;
use strop_core::id::LineIndex;
use super::queue::WireEnv;
use crate::protocol::{
LocKind, LspEvent, ReplyContext, ServerColumn, ServerEdit, ServerLocation, ServerPosition,
WireVersion,
};
pub(super) fn is_content_modified<T>(resp: &Result<T, async_lsp::Error>) -> bool {
matches!(resp, Err(async_lsp::Error::Response(e))
if e.code == async_lsp::ErrorCode::from(-32801))
}
pub(super) fn first_location(response: lt::GotoDefinitionResponse) -> Option<lt::Location> {
use lt::GotoDefinitionResponse as R;
match response {
R::Scalar(l) => Some(l),
R::Array(v) => v.into_iter().next(),
R::Link(v) => v.into_iter().next().map(|l| lt::Location {
uri: l.target_uri,
range: l.target_selection_range,
}),
}
}
pub(super) fn server_edit(edit: lt::TextEdit) -> ServerEdit {
let position = |p: lt::Position| ServerPosition {
line: LineIndex::new(p.line as usize),
column: ServerColumn::new(p.character as usize),
};
ServerEdit {
start: position(edit.range.start),
end: position(edit.range.end),
new_text: edit.new_text,
}
}
pub(super) enum WorkspaceEditRefusal {
ResourceOperations,
VersionMismatch(String),
Undecodable(String),
}
impl WorkspaceEditRefusal {
pub(super) fn note_text(&self, kind_label: &str, workspace_label: &str) -> String {
match self {
Self::ResourceOperations => format!(
"{kind_label} edit carries file operations (create/rename/delete) — refusing the whole edit"
),
Self::VersionMismatch(uri) => format!(
"{kind_label} targets a version of {uri} this connection never sent — refusing the whole edit"
),
Self::Undecodable(uri) => {
format!("{kind_label} target is not a file on {workspace_label}: {uri} — refusing the whole edit")
}
}
}
}
pub(super) fn workspace_edits(
env: &WireEnv,
edit: lt::WorkspaceEdit,
) -> Result<Vec<(strop_workspace::ResourceLocation, Vec<ServerEdit>)>, WorkspaceEditRefusal> {
let location = |uri: <::Url| {
env.workspace
.decode(uri)
.map(|path| strop_workspace::ResourceLocation {
filesystem: env.workspace.target(),
path,
})
};
match edit.document_changes {
Some(lt::DocumentChanges::Operations(_)) => Err(WorkspaceEditRefusal::ResourceOperations),
Some(lt::DocumentChanges::Edits(documents)) => {
let mut out = Vec::with_capacity(documents.len());
for document in documents {
let uri = document.text_document.uri;
let Some(doc) = location(&uri) else {
return Err(WorkspaceEditRefusal::Undecodable(uri.to_string()));
};
if let Some(version) = document.text_document.version {
if super::sync::sent_version(env, &doc.path) != Some(WireVersion::new(version))
{
return Err(WorkspaceEditRefusal::VersionMismatch(uri.to_string()));
}
}
let edits = document
.edits
.into_iter()
.map(|e| match e {
lt::OneOf::Left(edit) => server_edit(edit),
lt::OneOf::Right(annotated) => server_edit(annotated.text_edit),
})
.collect();
out.push((doc, edits));
}
Ok(out)
}
None => {
let Some(changes) = edit.changes else {
return Ok(Vec::new());
};
let mut out = Vec::with_capacity(changes.len());
for (uri, edits) in changes {
let Some(doc) = location(&uri) else {
return Err(WorkspaceEditRefusal::Undecodable(uri.to_string()));
};
out.push((doc, edits.into_iter().map(server_edit).collect()));
}
out.sort_by(|a, b| a.0.path.cmp(&b.0.path));
Ok(out)
}
}
}
pub(super) fn to_server_location(
l: lt::Location,
workspace: &crate::target::Workspace,
) -> Result<ServerLocation, String> {
match workspace.decode(&l.uri) {
Some(path) => Ok(ServerLocation {
doc: strop_workspace::ResourceLocation {
filesystem: workspace.target(),
path,
},
position: ServerPosition {
line: LineIndex::new(l.range.start.line as usize),
column: ServerColumn::new(l.range.start.character as usize),
},
}),
None => Err(l.uri.to_string()),
}
}
fn locations(
items: Vec<lt::Location>,
workspace: &crate::target::Workspace,
) -> (Vec<ServerLocation>, usize) {
let mut dropped = 0;
let items = items
.into_iter()
.filter_map(|l| match to_server_location(l, workspace) {
Ok(location) => Some(location),
Err(_) => {
dropped += 1;
None
}
})
.collect();
(items, dropped)
}
pub(super) async fn request_locations(
env: &WireEnv,
kind: LocKind,
tdp: TextDocumentPositionParams,
context: ReplyContext,
path: PathBuf,
) {
use async_lsp::lsp_types::request as req;
let socket = env.socket.clone();
let _ = path;
macro_rules! goto_shaped {
($request:ident, $params:expr) => {
match socket.request::<req::$request>($params).await {
Ok(Some(response)) => {
let locs = match response {
lt::GotoDefinitionResponse::Scalar(l) => vec![l],
lt::GotoDefinitionResponse::Array(v) => v,
lt::GotoDefinitionResponse::Link(v) => v
.into_iter()
.map(|l| lt::Location {
uri: l.target_uri,
range: l.target_selection_range,
})
.collect(),
};
Some(locs)
}
Ok(None) => Some(Vec::new()),
Err(error) => {
let _ = env.tx.send(LspEvent::Note {
context,
text: format!("{} failed: {error}", kind.label()),
});
return;
}
}
};
}
let items: Option<Vec<lt::Location>> = match kind {
LocKind::References => {
let params = lt::ReferenceParams {
text_document_position: tdp,
context: lt::ReferenceContext {
include_declaration: true,
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
match socket.request::<req::References>(params).await {
Ok(items) => items,
Err(error) => {
let _ = env.tx.send(LspEvent::Note {
context,
text: format!("{} failed: {error}", kind.label()),
});
return;
}
}
}
LocKind::Implementation => {
let params = lt::request::GotoImplementationParams {
text_document_position_params: tdp,
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
goto_shaped!(GotoImplementation, params)
}
LocKind::TypeDefinition => {
let params = lt::request::GotoTypeDefinitionParams {
text_document_position_params: tdp,
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
goto_shaped!(GotoTypeDefinition, params)
}
LocKind::Declaration => {
let params = lt::request::GotoDeclarationParams {
text_document_position_params: tdp,
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
goto_shaped!(GotoDeclaration, params)
}
};
match items {
Some(locs) => {
let (items, dropped) = locations(locs, &env.workspace);
if items.is_empty() && dropped > 0 {
let _ = env.tx.send(LspEvent::Note {
context,
text: format!("no file on {} {}", env.workspace.label(), kind.label()),
});
} else {
let _ = env.tx.send(LspEvent::Locations {
context,
kind,
items,
});
}
}
None => {
let _ = env.tx.send(LspEvent::Locations {
context,
kind,
items: Vec::new(),
});
}
}
}