use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use super::super::refactor_core::{EditSpan, IdentifierSpan};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct Position {
pub line: u32,
pub character: u32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct Range {
pub start: Position,
pub end: Position,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct TextEdit {
pub range: Range,
pub new_text: String,
}
#[derive(Debug, Deserialize)]
pub(crate) struct VersionedDocument {
pub uri: String,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub(crate) enum DocumentChange {
Edit {
#[serde(rename = "textDocument")]
text_document: VersionedDocument,
edits: Vec<TextEdit>,
},
Resource { kind: String },
}
#[derive(Debug, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct WorkspaceEdit {
#[serde(default)]
pub changes: Option<BTreeMap<String, Vec<TextEdit>>>,
#[serde(default)]
pub document_changes: Option<Vec<DocumentChange>>,
}
impl WorkspaceEdit {
pub(crate) fn into_file_edits(self) -> Result<BTreeMap<PathBuf, Vec<TextEdit>>, String> {
let mut out: BTreeMap<PathBuf, Vec<TextEdit>> = BTreeMap::new();
if let Some(changes) = self.document_changes {
for change in changes {
match change {
DocumentChange::Edit {
text_document,
edits,
} => out
.entry(uri_to_path(&text_document.uri)?)
.or_default()
.extend(edits),
DocumentChange::Resource { kind } => {
return Err(format!("the server's edit needs a `{kind}` file operation"));
}
}
}
} else if let Some(changes) = self.changes {
for (uri, edits) in changes {
out.entry(uri_to_path(&uri)?).or_default().extend(edits);
}
}
Ok(out)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Encoding {
Utf8,
Utf16,
}
impl Encoding {
pub(crate) fn from_wire(raw: Option<&str>) -> Self {
match raw {
Some("utf-8") => Self::Utf8,
_ => Self::Utf16,
}
}
}
pub(crate) struct LineIndex<'a> {
source: &'a str,
starts: Vec<usize>,
}
impl<'a> LineIndex<'a> {
pub(crate) fn new(source: &'a str) -> Self {
let mut starts = vec![0];
starts.extend(source.match_indices('\n').map(|(at, _)| at + 1));
Self { source, starts }
}
fn line_text(&self, line: usize) -> Option<&'a str> {
let start = *self.starts.get(line)?;
let end = self
.starts
.get(line + 1)
.map_or(self.source.len(), |next| next - 1);
self.source.get(start..end)
}
pub(crate) fn offset(&self, position: Position, encoding: Encoding) -> Option<usize> {
let line = position.line as usize;
let text = self.line_text(line)?;
let wanted = position.character as usize;
let mut units = 0;
for (at, ch) in text.char_indices() {
if units >= wanted {
return (units == wanted).then_some(self.starts[line] + at);
}
units += match encoding {
Encoding::Utf8 => ch.len_utf8(),
Encoding::Utf16 => ch.len_utf16(),
};
}
(units == wanted).then_some(self.starts[line] + text.len())
}
pub(crate) fn position(&self, offset: usize, encoding: Encoding) -> Position {
let line = self.starts.partition_point(|start| *start <= offset) - 1;
let prefix = self
.source
.get(self.starts[line]..offset)
.unwrap_or_default();
let character = match encoding {
Encoding::Utf8 => prefix.len(),
Encoding::Utf16 => prefix.encode_utf16().count(),
};
Position {
line: line as u32,
character: character as u32,
}
}
fn span(&self, start: usize, end: usize) -> IdentifierSpan {
let start_pos = self.position(start, Encoding::Utf8);
let end_pos = self.position(end, Encoding::Utf8);
IdentifierSpan {
start_byte: start,
end_byte: end,
start_row: start_pos.line as usize,
start_col: start_pos.character as usize,
end_row: end_pos.line as usize,
end_col: end_pos.character as usize,
}
}
}
pub(crate) fn lower_edits(
source: &str,
edits: &[TextEdit],
encoding: Encoding,
) -> Result<Vec<EditSpan>, String> {
let index = LineIndex::new(source);
let mut spans = Vec::with_capacity(edits.len());
for edit in edits {
let (Some(start), Some(end)) = (
index.offset(edit.range.start, encoding),
index.offset(edit.range.end, encoding),
) else {
return Err(format!(
"edit range {}:{}-{}:{} is outside the document",
edit.range.start.line,
edit.range.start.character,
edit.range.end.line,
edit.range.end.character
));
};
if end < start {
return Err("edit range ends before it starts".into());
}
let before = source.get(start..end).unwrap_or_default().to_string();
spans.push(EditSpan {
span: index.span(start, end),
before,
after: edit.new_text.clone(),
});
}
spans.sort_by_key(|edit| edit.span.start_byte);
if spans
.windows(2)
.any(|pair| pair[1].span.start_byte < pair[0].span.end_byte)
{
return Err("the server returned overlapping edits".into());
}
Ok(spans)
}
pub(crate) fn path_to_uri(path: &Path) -> String {
let raw = path.to_string_lossy().replace('\\', "/");
let mut out = String::from("file://");
if !raw.starts_with('/') {
out.push('/');
}
for byte in raw.bytes() {
if byte.is_ascii_alphanumeric() || b"-._~/:".contains(&byte) {
out.push(byte as char);
} else {
out.push_str(&format!("%{byte:02X}"));
}
}
out
}
pub(crate) fn uri_to_path(uri: &str) -> Result<PathBuf, String> {
let rest = uri
.strip_prefix("file://")
.ok_or_else(|| format!("unsupported document URI `{uri}`"))?;
let rest = rest.strip_prefix("localhost").unwrap_or(rest);
let bytes = rest.as_bytes();
let mut decoded = Vec::with_capacity(bytes.len());
let mut at = 0;
while at < bytes.len() {
if bytes[at] == b'%' && at + 2 < bytes.len() {
let hex = std::str::from_utf8(&bytes[at + 1..at + 3]).unwrap_or("");
if let Ok(value) = u8::from_str_radix(hex, 16) {
decoded.push(value);
at += 3;
continue;
}
}
decoded.push(bytes[at]);
at += 1;
}
let text = String::from_utf8(decoded).map_err(|_| format!("non-UTF-8 URI `{uri}`"))?;
let windows_drive = text.len() > 2 && text.as_bytes()[2] == b':' && text.starts_with('/');
Ok(PathBuf::from(if cfg!(windows) && windows_drive {
text.strip_prefix('/').unwrap_or(&text)
} else {
text.as_str()
}))
}
#[cfg(test)]
#[path = "edits_tests.rs"]
mod tests;