use std::path::Path;
use lsp_types::{
ClientCapabilities, DocumentFormattingParams, DocumentRangeFormattingParams, OneOf, Position,
Range, ServerCapabilities, TextEdit,
};
use tokio_util::sync::CancellationToken;
use super::RegisterCapabilities;
use crate::context::{RequestOutcome, ServerContextSnapshot, snapshot_query};
use crate::util::uri_to_file_path;
pub async fn on_formatting(
context: ServerContextSnapshot,
params: DocumentFormattingParams,
cancel_token: CancellationToken,
) -> RequestOutcome<Vec<TextEdit>> {
format_request(context, params.text_document.uri, None, cancel_token).await
}
pub async fn on_range_formatting(
context: ServerContextSnapshot,
params: DocumentRangeFormattingParams,
cancel_token: CancellationToken,
) -> RequestOutcome<Vec<TextEdit>> {
format_request(
context,
params.text_document.uri,
Some(params.range),
cancel_token,
)
.await
}
pub async fn on_will_save_wait_until(
context: ServerContextSnapshot,
params: lsp_types::WillSaveTextDocumentParams,
cancel_token: CancellationToken,
) -> RequestOutcome<Vec<TextEdit>> {
format_request(context, params.text_document.uri, None, cancel_token).await
}
async fn format_request(
context: ServerContextSnapshot,
uri: lsp_types::Uri,
only: Option<Range>,
cancel_token: CancellationToken,
) -> RequestOutcome<Vec<TextEdit>> {
if let Some(path) = uri_to_file_path(&uri) {
context.analysis().prepare(&path).await;
}
snapshot_query(context.analysis(), cancel_token, move |session| {
let path = uri_to_file_path(&uri)?;
let text = read_text(session, &path)?;
let style = cpp_formatter::config::style_for_file(&path);
let formatted = match only {
Some(range) => cpp_formatter::format_range(
&text,
&style,
range.start.line as usize,
range.end.line as usize,
),
None => cpp_formatter::format_file(&path, &text, &style),
};
Some(edits_between(&text, &formatted))
})
.await
}
pub fn edits_between(old: &str, formatted: &str) -> Vec<TextEdit> {
if old == formatted {
return Vec::new();
}
let old_lines: Vec<&str> = old.split_inclusive('\n').collect();
let new_lines: Vec<&str> = formatted.split_inclusive('\n').collect();
let mut prefix = 0usize;
while prefix < old_lines.len()
&& prefix < new_lines.len()
&& old_lines[prefix] == new_lines[prefix]
{
prefix += 1;
}
let mut suffix = 0usize;
while suffix < old_lines.len() - prefix
&& suffix < new_lines.len() - prefix
&& old_lines[old_lines.len() - 1 - suffix] == new_lines[new_lines.len() - 1 - suffix]
{
suffix += 1;
}
let old_middle = &old_lines[prefix..old_lines.len() - suffix];
let new_middle: String = new_lines[prefix..new_lines.len() - suffix].concat();
vec![TextEdit {
range: Range {
start: Position {
line: prefix as u32,
character: 0,
},
end: Position {
line: (prefix + old_middle.len()) as u32,
character: 0,
},
},
new_text: new_middle,
}]
}
fn read_text(
session: &cpp_code_analysis::Session<cpp_code_analysis::DiskFiles>,
path: &Path,
) -> Option<String> {
if let Some(held) = session.files().held(path) {
return Some(held.text.to_string());
}
std::fs::read_to_string(path).ok()
}
pub struct FormattingCapabilities;
impl RegisterCapabilities for FormattingCapabilities {
fn register_capabilities(server_capabilities: &mut ServerCapabilities, _: &ClientCapabilities) {
server_capabilities.document_formatting_provider = Some(OneOf::Left(true));
server_capabilities.document_range_formatting_provider = Some(OneOf::Left(true));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn nothing_changes_so_there_are_no_edits() {
assert!(edits_between("int x = 1;\n", "int x = 1;\n").is_empty());
}
#[test]
fn one_changed_line_is_one_edit_covering_only_that_line() {
let edits = edits_between("int x = 1;\nint y = 2;\n", "int x = 1;\nint y = 2;\n");
assert_eq!(edits.len(), 1);
assert_eq!(edits[0].range.start.line, 0);
assert_eq!(edits[0].range.end.line, 1);
assert_eq!(edits[0].new_text, "int x = 1;\n");
}
}