Skip to main content

shuck_server/edit/
text_document.rs

1use lsp_types::TextDocumentContentChangeEvent;
2use shuck_indexer::LineIndex;
3
4use crate::PositionEncoding;
5
6use super::RangeExt;
7
8pub(crate) type DocumentVersion = i32;
9
10/// In-memory representation of an open LSP text document.
11#[derive(Debug, Clone)]
12pub struct TextDocument {
13    contents: String,
14    index: LineIndex,
15    version: DocumentVersion,
16    language_id: Option<LanguageId>,
17}
18
19#[derive(Debug, Copy, Clone, PartialEq, Eq)]
20pub enum LanguageId {
21    Bash,
22    ShellScript,
23    Sh,
24    Zsh,
25    Ksh,
26    Other,
27}
28
29impl From<&str> for LanguageId {
30    fn from(language_id: &str) -> Self {
31        match language_id {
32            "bash" => Self::Bash,
33            "shellscript" => Self::ShellScript,
34            "sh" => Self::Sh,
35            "zsh" => Self::Zsh,
36            "ksh" => Self::Ksh,
37            _ => Self::Other,
38        }
39    }
40}
41
42impl TextDocument {
43    /// Create a document with initial contents and version.
44    pub fn new(contents: String, version: DocumentVersion) -> Self {
45        let index = LineIndex::new(&contents);
46        Self {
47            contents,
48            index,
49            version,
50            language_id: None,
51        }
52    }
53
54    /// Return a copy with the LSP language identifier attached.
55    #[must_use]
56    pub fn with_language_id(mut self, language_id: &str) -> Self {
57        self.language_id = Some(LanguageId::from(language_id));
58        self
59    }
60
61    /// Return the current document contents.
62    pub fn contents(&self) -> &str {
63        &self.contents
64    }
65
66    /// Return the current line index for the document.
67    pub fn index(&self) -> &LineIndex {
68        &self.index
69    }
70
71    /// Return the current LSP document version.
72    pub fn version(&self) -> DocumentVersion {
73        self.version
74    }
75
76    /// Return the parsed language identifier, if one was supplied by the client.
77    pub fn language_id(&self) -> Option<LanguageId> {
78        self.language_id
79    }
80
81    /// Apply LSP content changes and update the document version.
82    pub fn apply_changes(
83        &mut self,
84        changes: Vec<TextDocumentContentChangeEvent>,
85        new_version: DocumentVersion,
86        encoding: PositionEncoding,
87    ) {
88        if let [
89            TextDocumentContentChangeEvent {
90                range: None, text, ..
91            },
92        ] = changes.as_slice()
93        {
94            self.contents.clone_from(text);
95            self.index = LineIndex::new(&self.contents);
96            self.version = new_version;
97            return;
98        }
99
100        let mut new_contents = self.contents.clone();
101        let mut active_index = self.index.clone();
102
103        for change in changes {
104            if let Some(range) = change.range {
105                let range = range.to_text_range(&new_contents, &active_index, encoding);
106                new_contents.replace_range(
107                    usize::from(range.start())..usize::from(range.end()),
108                    &change.text,
109                );
110            } else {
111                new_contents = change.text;
112            }
113            active_index = LineIndex::new(&new_contents);
114        }
115
116        self.contents = new_contents;
117        self.index = active_index;
118        self.version = new_version;
119    }
120
121    /// Update the document version without changing contents.
122    pub fn update_version(&mut self, new_version: DocumentVersion) {
123        debug_assert!(new_version >= self.version);
124        self.version = new_version;
125    }
126}