편집 기능 볶음짬뽕
요약
이전 글에서 소개했던 아무말 생성기의 문제 중 하나는 생성물이 단순한 텍스트 형식이기 때문에 줄넘김 같은게 전혀 없이 한줄로 쭉 생성되고, 편하게 보려면 그걸 다시 재정렬(이를테면 vim에서 gq)해 줘야 한다는 점이었다.
생성된 결과물에 formatter를 돌린 결과물을 받아볼 수 있을까?
데모
pub trait EditAndFormat {
fn edit_and_format(&self, edit: TextEdit) -> TextEdit;
}
fn apply_edits<T: IncrementalSync + LspAdapter>(
init: T,
edits: impl AsRef<[TextEdit]>,
) -> T {
edits.as_ref().iter().fold(init, |acc, edit| {
let start_byte = acc.position_to_offset(&edit.range.start).unwrap();
let end_byte = acc.position_to_offset(&edit.range.end).unwrap();
acc.with_change(start_byte..end_byte, &edit.new_text)
})
}
impl EditAndFormat for TreesittingDocument {
fn edit_and_format(&self, edit: TextEdit) -> TextEdit {
let start_byte = self.position_to_offset(&edit.range.start).unwrap();
let end_byte = self.position_to_offset(&edit.range.end).unwrap();
let rest_size = self.rope().len_bytes() - end_byte;
let new_end_byte = start_byte + edit.new_text.len();
let updated = self.with_change(start_byte..end_byte, &edit.new_text);
let Some(edits) = updated.format(start_byte..new_end_byte) else {
// No further changes from formatting, just return the edit
return edit;
};
let updated2 = apply_edits(updated, edits);
let diff_from_source = updated2
.rope()
.byte_slice(start_byte..(updated2.rope().len_bytes() - rest_size))
.to_string();
TextEdit { range: edit.range, new_text: diff_from_source }
}
}
내부적으로 정리를 많이 해야 했는데, 정리하고 나니 좀 간단한 것 같기도… 하여튼 설명하자면 기존 코드는 변경을 적용할 때 기존 객체가 없어지는 방식으로 구현되어 있었다.
pub trait IncrementalSync
where
Self: Sized,
{
/// Update self with the given change applied.
fn apply_change<R: RangeBounds<usize>, S: AsRef<str>>(
self, // self get consumed
byte_range: R,
text: S,
) -> Self;
}
근데, 내가 하려는 건 생성된 결과물에 대해 format 기능을 실행해서 그 결과물을
얻어야 하는 것이라, 원본 문서와 중간 문서를 모두 가지고 있어야 한다. 따라서 기존
객체를 그대로 두고 새 복제본을 생성해야 하는건데… 쉽게 말하면 위에서 self
를
&self
로 바꿔야 한다.
pub trait IncrementalSync2
where
Self: Sized,
{
/// Creates a new Self having the change applied.
fn with_change<R: RangeBounds<usize>, S: AsRef<str>>(
&self, // self get referenced
byte_range: R,
text: S,
) -> Self;
}
다행히 내부에서 쓰는 데이터 타입들이 Clone을 큰 비용 없이 Copy-on-write 비스무레하게 지원해서 큰 성능 낭비 없이 구현할 수 있었다.
그 뒷부분은 간단하다. 그냥 as-if로 format 적용하고, 최초 문서에서 달라지는 부분을 떼어내서 최종 변경사항을 만들어내는 방식.
결론
덕지덕지 요구사항에 맞춰 몸비틀기하는 프로젝트지만 어쨌든 쓸만하다.