//! Helpers for building `WorkspaceEdit`s. //! //! Phase 13+ (rename, list/table edits, link/TOC generation) all return //! `WorkspaceEdit`. The builder centralises the byte-offset → LSP-position //! conversion (UTF-8 or UTF-16, matching whatever was negotiated in //! `initialize`) so each call site doesn't have to redo it. //! //! The builder emits `documentChanges` (with `Operation` for file ops) //! whenever any file op is queued, and falls back to the simpler //! `changes` field for pure text-edit batches — the latter is what older //! LSP 3.15- clients understand. use std::collections::HashMap; use tower_lsp::lsp_types::{ CreateFile, DeleteFile, DocumentChangeOperation, DocumentChanges, OneOf, OptionalVersionedTextDocumentIdentifier, Position, Range, RenameFile, ResourceOp, TextDocumentEdit, TextEdit, Url, WorkspaceEdit, }; use nuwiki_core::ast::Span; use crate::semantic_tokens::LineIndex; /// Replace the source covered by `span` with `replacement`. `text` and /// `utf8` are needed to translate the byte-offset span into the LSP /// `Range` the client expects. pub fn text_edit_replace(span: Span, replacement: String, text: &str, utf8: bool) -> TextEdit { TextEdit { range: span_to_range(span, text, utf8), new_text: replacement, } } /// Insert `content` at `pos`. The caller has already converted to LSP /// coordinates — no further conversion is performed here. pub fn text_edit_insert(pos: Position, content: String) -> TextEdit { TextEdit { range: Range { start: pos, end: pos, }, new_text: content, } } /// Delete the source covered by `span`. pub fn text_edit_delete(span: Span, text: &str, utf8: bool) -> TextEdit { text_edit_replace(span, String::new(), text, utf8) } pub fn op_rename(old: Url, new: Url) -> DocumentChangeOperation { DocumentChangeOperation::Op(ResourceOp::Rename(RenameFile { old_uri: old, new_uri: new, options: None, annotation_id: None, })) } pub fn op_delete(uri: Url) -> DocumentChangeOperation { // lsp-types 0.94.x ships `DeleteFile` without `annotation_id`; later // versions added it. Stick with the 0.94 shape until we move tower-lsp. DocumentChangeOperation::Op(ResourceOp::Delete(DeleteFile { uri, options: None })) } pub fn op_create(uri: Url) -> DocumentChangeOperation { DocumentChangeOperation::Op(ResourceOp::Create(CreateFile { uri, options: None, annotation_id: None, })) } #[derive(Default)] pub struct WorkspaceEditBuilder { changes: HashMap>, ops: Vec, } impl WorkspaceEditBuilder { pub fn new() -> Self { Self::default() } pub fn edit(&mut self, uri: Url, edit: TextEdit) -> &mut Self { self.changes.entry(uri).or_default().push(edit); self } pub fn edits(&mut self, uri: Url, edits: I) -> &mut Self where I: IntoIterator, { self.changes.entry(uri).or_default().extend(edits); self } pub fn file_op(&mut self, op: DocumentChangeOperation) -> &mut Self { self.ops.push(op); self } pub fn is_empty(&self) -> bool { self.changes.is_empty() && self.ops.is_empty() } pub fn build(self) -> WorkspaceEdit { // documentChanges is required whenever there are file ops (LSP // doesn't represent renames/deletes/creates in the flat `changes` // map). For pure text edits the older `changes` form keeps // compatibility with LSP 3.15-clients. if self.ops.is_empty() { return WorkspaceEdit { changes: Some(self.changes), document_changes: None, change_annotations: None, }; } let mut document_changes: Vec = self.ops; // Edits land after file ops so the file exists when the text edit // applies (e.g. create + initial content). for (uri, edits) in self.changes { document_changes.push(DocumentChangeOperation::Edit(TextDocumentEdit { text_document: OptionalVersionedTextDocumentIdentifier { uri, version: None }, edits: edits.into_iter().map(OneOf::Left).collect(), })); } WorkspaceEdit { changes: None, document_changes: Some(DocumentChanges::Operations(document_changes)), change_annotations: None, } } } fn span_to_range(span: Span, text: &str, utf8: bool) -> Range { let idx = LineIndex::new(text); Range { start: Position { line: span.start.line, character: char_at(span.start.line, span.start.column, text, &idx, utf8), }, end: Position { line: span.end.line, character: char_at(span.end.line, span.end.column, text, &idx, utf8), }, } } fn char_at(line: u32, byte_col: u32, text: &str, idx: &LineIndex, utf8: bool) -> u32 { if utf8 { return byte_col; } let line_str = idx.line_str(line, text); let upto = (byte_col as usize).min(line_str.len()); line_str[..upto].chars().map(char::len_utf16).sum::() as u32 }