dotrain_lsp/
lib.rs

1//! The Rain language server protocol ([LSP](https://microsoft.github.io/language-server-protocol/)) implementation (language services)
2//! written in rust and made available for NodeJs and broswers through [wasm-bindgen](https://rustwasm.github.io/docs/wasm-bindgen/)
3//! in Typescript/Javascript which makes it well suited for editors and IDEs (as it is used in Rainlang vscode and codemirror language extension).
4//! This includes all [LSP](https://microsoft.github.io/language-server-protocol/) (language services) related implementation that provide methods
5//! and functionalities for getting language server protocol based services for given text document and/or [RainDocument]
6//!
7//! - Dotrain lsp services are used for vscode and codemirror, see [rainlang-vscode](https://github.com/rainprotocol/rainlang-vscode) and [rainlang-codemirror](https://github.com/rainprotocol/rainlang-codemirror) repositories for more details.
8//! - Dotrain vscode extension can be found [here](https://marketplace.visualstudio.com/items?itemName=rainprotocol.rainlang-vscode).
9
10use std::sync::{Arc, RwLock};
11use dotrain::{RainDocument, Store, Rebind};
12use lsp_types::{
13    Hover, Position, Diagnostic, MarkupKind, CompletionItem, TextDocumentItem,
14    SemanticTokensPartialResult, Url,
15};
16
17#[cfg(feature = "js-api")]
18use wasm_bindgen::prelude::*;
19
20pub use dotrain;
21pub use lsp_types;
22pub use hover::get_hover;
23pub use completion::get_completion;
24pub use diagnostic::get_diagnostics;
25pub use semantic_token::get_semantic_token;
26
27mod hover;
28mod completion;
29mod diagnostic;
30mod semantic_token;
31
32#[cfg(feature = "js-api")]
33pub mod js_api;
34
35/// Parameters for initiating Language Services
36#[derive(Debug, Clone)]
37pub struct LanguageServiceParams {
38    /// The meta Store (CAS) instance used for all parsings of the RainLanguageServices
39    pub meta_store: Option<Arc<RwLock<Store>>>,
40}
41
42#[cfg_attr(
43    not(target_family = "wasm"),
44    doc = r#"Provides methods for getting language services (such as diagnostics, completion, etc)
45for a given TextDocumentItem or a RainDocument. Each instance is linked to a shared locked
46[Store] instance `Arc<RwLock<Store>>` that holds all the required metadata/functionalities that 
47are required during parsing a text.
48
49Position encodings provided by the client are irrevelant as RainDocument/Rainlang supports
50only ASCII characters (parsing will stop at very first encountered non-ASCII character), so any
51position encodings will result in the same LSP provided Position value which is 1 for each char.
52
53## Example
54
55```rust
56use std::sync::{Arc, RwLock};
57use dotrain_lsp::{
58    RainLanguageServices, 
59    LanguageServiceParams, 
60    dotrain::Store, 
61    lsp_types::{TextDocumentItem, MarkupKind, Position, Url}
62};
63
64// instaniate a shared locked Store
65let meta_store = Arc::new(RwLock::new(Store::default()));
66
67// create instatiation params
68let params = LanguageServiceParams {
69    meta_store: Some(meta_store)
70};
71
72// create a new instane with a shared locked Store that is used for all
73// parsings that are triggered through available methods of this instance
74let lang_services = RainLanguageServices::new(&params);
75
76let text_document = TextDocumentItem {
77    uri: Url::parse("file:///example.rain").unwrap(),
78    text: "some .rain text content".to_string(),
79    version: 0,
80    language_id: "rainlang".to_string()
81};
82
83// create a new RainDocument instance
84let rain_document = lang_services.new_rain_document(&text_document, None);
85
86// get LSP Diagnostics for a given TextDocumentItem
87let diagnostics_related_information = true;
88let diagnostics = lang_services.do_validate(&text_document, diagnostics_related_information, None);
89
90let position = Position {
91    line: 0,
92    character: 10
93};
94let content_format = Some(MarkupKind::PlainText);
95let hover = lang_services.do_hover(&text_document, position, content_format, None);
96```
97"#
98)]
99#[cfg_attr(
100    target_family = "wasm",
101    doc = " Provides methods for getting language services (such as diagnostics, completion, etc)
102 for a given TextDocumentItem or a RainDocument. Each instance is linked to a shared locked
103 MetaStore instance that holds all the required metadata/functionalities that are required during 
104 parsing a text.
105
106 Position encodings provided by the client are irrevelant as RainDocument/Rainlang supports
107 only ASCII characters (parsing will stop at very first encountered non-ASCII character), so any
108 position encodings will result in the same LSP provided Position value which is 1 for each char.
109 
110 @example
111 ```javascript
112 // create new MetaStore instance
113 let metaStore = new MetaStore();
114
115 // crate new instance
116 let langServices = new RainLanguageServices(metaStore);
117
118 let textDocument = {
119    text: \"some .rain text\",
120    uri:  \"file:///name.rain\",
121    version: 0,
122    languageId: \"rainlang\"
123 };
124
125 // creat new RainDocument
126 let rainDocument = langServices.newRainDocument(textdocument);
127
128 // get LSP Diagnostics
129 let diagnosticsRelatedInformation = true;
130 let diagnostics = langServices.doValidate(textDocument, diagnosticsRelatedInformation);
131 ```
132"
133)]
134#[cfg_attr(feature = "js-api", wasm_bindgen)]
135pub struct RainLanguageServices {
136    pub(crate) meta_store: Arc<RwLock<Store>>,
137}
138
139impl Default for RainLanguageServices {
140    fn default() -> Self {
141        let meta_store = Arc::new(RwLock::new(Store::default()));
142        RainLanguageServices { meta_store }
143    }
144}
145
146impl RainLanguageServices {
147    /// The meta Store associated with this RainLanguageServices instance
148    pub fn meta_store(&self) -> Arc<RwLock<Store>> {
149        self.meta_store.clone()
150    }
151    /// Instantiates from the given params
152    pub fn new(language_params: &LanguageServiceParams) -> RainLanguageServices {
153        RainLanguageServices {
154            meta_store: language_params
155                .meta_store
156                .as_ref()
157                .map_or(Arc::new(RwLock::new(Store::default())), |s| s.clone()),
158        }
159    }
160
161    /// Instantiates a RainDocument with remote meta search disabled when parsing from the given TextDocumentItem
162    pub fn new_rain_document(
163        &self,
164        text_document: &TextDocumentItem,
165        rebinds: Option<Vec<Rebind>>,
166    ) -> RainDocument {
167        RainDocument::create(
168            text_document.text.clone(),
169            Some(self.meta_store.clone()),
170            None,
171            rebinds,
172        )
173    }
174    /// Instantiates a RainDocument with remote meta search enabled when parsing from the given TextDocumentItem
175    pub async fn new_rain_document_async(
176        &self,
177        text_document: &TextDocumentItem,
178        rebinds: Option<Vec<Rebind>>,
179    ) -> RainDocument {
180        RainDocument::create_async(
181            text_document.text.clone(),
182            Some(self.meta_store.clone()),
183            None,
184            rebinds,
185        )
186        .await
187    }
188
189    /// Validates the document with remote meta search disabled when parsing and reports LSP diagnostics
190    pub fn do_validate(
191        &self,
192        text_document: &TextDocumentItem,
193        related_information: bool,
194        rebinds: Option<Vec<Rebind>>,
195    ) -> Vec<Diagnostic> {
196        let rain_document = RainDocument::create(
197            text_document.text.clone(),
198            Some(self.meta_store.clone()),
199            None,
200            rebinds,
201        );
202        diagnostic::get_diagnostics(&rain_document, &text_document.uri, related_information)
203    }
204    /// Validates the document with remote meta search enabled when parsing and reports LSP diagnostics
205    pub async fn do_validate_async(
206        &self,
207        text_document: &TextDocumentItem,
208        related_information: bool,
209        rebinds: Option<Vec<Rebind>>,
210    ) -> Vec<Diagnostic> {
211        let rain_document = RainDocument::create_async(
212            text_document.text.clone(),
213            Some(self.meta_store.clone()),
214            None,
215            rebinds,
216        )
217        .await;
218        diagnostic::get_diagnostics(&rain_document, &text_document.uri, related_information)
219    }
220    /// Reports LSP diagnostics from RainDocument's all problems
221    pub fn do_validate_rain_document(
222        &self,
223        rain_document: &RainDocument,
224        uri: &Url,
225        related_information: bool,
226    ) -> Vec<Diagnostic> {
227        diagnostic::get_diagnostics(rain_document, uri, related_information)
228    }
229
230    /// Provides completion items at the given position
231    pub fn do_complete(
232        &self,
233        text_document: &TextDocumentItem,
234        position: Position,
235        documentation_format: Option<MarkupKind>,
236        rebinds: Option<Vec<Rebind>>,
237    ) -> Option<Vec<CompletionItem>> {
238        let rain_document = RainDocument::create(
239            text_document.text.clone(),
240            Some(self.meta_store.clone()),
241            None,
242            rebinds,
243        );
244        completion::get_completion(
245            &rain_document,
246            &text_document.uri,
247            position,
248            documentation_format.unwrap_or(MarkupKind::PlainText),
249        )
250    }
251    /// Provides completion items at the given position
252    pub fn do_complete_rain_document(
253        &self,
254        rain_document: &RainDocument,
255        uri: &Url,
256        position: Position,
257        documentation_format: Option<MarkupKind>,
258    ) -> Option<Vec<CompletionItem>> {
259        completion::get_completion(
260            rain_document,
261            uri,
262            position,
263            documentation_format.unwrap_or(MarkupKind::PlainText),
264        )
265    }
266
267    /// Provides hover for a fragment at the given position
268    pub fn do_hover(
269        &self,
270        text_document: &TextDocumentItem,
271        position: Position,
272        content_format: Option<MarkupKind>,
273        rebinds: Option<Vec<Rebind>>,
274    ) -> Option<Hover> {
275        let rain_document = RainDocument::create(
276            text_document.text.clone(),
277            Some(self.meta_store.clone()),
278            None,
279            rebinds,
280        );
281        hover::get_hover(
282            &rain_document,
283            position,
284            content_format.unwrap_or(MarkupKind::PlainText),
285        )
286    }
287    /// Provides hover for a RainDocument fragment at the given position
288    pub fn do_hover_rain_document(
289        &self,
290        rain_document: &RainDocument,
291        position: Position,
292        content_format: Option<MarkupKind>,
293    ) -> Option<Hover> {
294        hover::get_hover(
295            rain_document,
296            position,
297            content_format.unwrap_or(MarkupKind::PlainText),
298        )
299    }
300
301    /// Provides semantic tokens for elided fragments
302    pub fn semantic_tokens(
303        &self,
304        text_document: &TextDocumentItem,
305        semantic_token_types_index: u32,
306        semantic_token_modifiers_len: usize,
307        rebinds: Option<Vec<Rebind>>,
308    ) -> SemanticTokensPartialResult {
309        let rain_document = RainDocument::create(
310            text_document.text.clone(),
311            Some(self.meta_store.clone()),
312            None,
313            rebinds,
314        );
315        get_semantic_token(
316            &rain_document,
317            semantic_token_types_index,
318            semantic_token_modifiers_len,
319        )
320    }
321    /// Provides semantic tokens for RainDocument's elided fragments
322    pub fn rain_document_semantic_tokens(
323        &self,
324        rain_document: &RainDocument,
325        semantic_token_types_index: u32,
326        semantic_token_modifiers_len: usize,
327    ) -> SemanticTokensPartialResult {
328        get_semantic_token(
329            rain_document,
330            semantic_token_types_index,
331            semantic_token_modifiers_len,
332        )
333    }
334}
335
336/// Trait for converting offset to lsp position (implemented for `&str` and `String`)
337pub trait PositionAt {
338    fn position_at(&self, offset: usize) -> Position;
339}
340
341/// Trait for converting lsp position to offset (implemented for `&str` and `String`)
342pub trait OffsetAt {
343    fn offset_at(&self, position: &Position) -> usize;
344}
345
346impl PositionAt for &str {
347    fn position_at(&self, offset: usize) -> Position {
348        let effective_offset = 0.max(offset.min(self.len()));
349        let mut line_offsets = vec![];
350        let mut acc = 0;
351        self.split_inclusive('\n').for_each(|v| {
352            line_offsets.push(acc);
353            acc += v.len();
354        });
355        let mut low = 0;
356        let mut high = line_offsets.len();
357        if high == 0 {
358            return Position {
359                line: 0,
360                character: effective_offset as u32,
361            };
362        }
363        while low < high {
364            let mid = (low + high) / 2;
365            if line_offsets[mid] > effective_offset {
366                high = mid;
367            } else {
368                low = mid + 1;
369            }
370        }
371        // low is the least x for which the line offset is larger than the current offset
372        // or array.length if no line offset is larger than the current offset
373        let line = low - 1;
374        Position {
375            line: line as u32,
376            character: (effective_offset - line_offsets[line]) as u32,
377        }
378    }
379}
380
381impl OffsetAt for &str {
382    fn offset_at(&self, position: &Position) -> usize {
383        let mut line_offsets = vec![];
384        let mut acc = 0;
385        self.split_inclusive('\n').for_each(|v| {
386            line_offsets.push(acc);
387            acc += v.len();
388        });
389        if position.line >= line_offsets.len() as u32 {
390            return self.len();
391        }
392        let line_offset = line_offsets[position.line as usize];
393        let next_line_offset = if position.line + 1 < line_offsets.len() as u32 {
394            line_offsets[position.line as usize + 1]
395        } else {
396            self.len()
397        };
398        line_offset.max((line_offset + position.character as usize).min(next_line_offset))
399    }
400}
401
402impl PositionAt for String {
403    fn position_at(&self, offset: usize) -> Position {
404        let effective_offset = 0.max(offset.min(self.len()));
405        let mut line_offsets = vec![];
406        let mut acc = 0;
407        self.split_inclusive('\n').for_each(|v| {
408            line_offsets.push(acc);
409            acc += v.len();
410        });
411        let mut low = 0;
412        let mut high = line_offsets.len();
413        if high == 0 {
414            return Position {
415                line: 0,
416                character: effective_offset as u32,
417            };
418        }
419        while low < high {
420            let mid = (low + high) / 2;
421            if line_offsets[mid] > effective_offset {
422                high = mid;
423            } else {
424                low = mid + 1;
425            }
426        }
427        // low is the least x for which the line offset is larger than the current offset
428        // or array.length if no line offset is larger than the current offset
429        let line = low - 1;
430        Position {
431            line: line as u32,
432            character: (effective_offset - line_offsets[line]) as u32,
433        }
434    }
435}
436
437impl OffsetAt for String {
438    fn offset_at(&self, position: &Position) -> usize {
439        let mut line_offsets = vec![];
440        let mut acc = 0;
441        self.split_inclusive('\n').for_each(|v| {
442            line_offsets.push(acc);
443            acc += v.len();
444        });
445        if position.line >= line_offsets.len() as u32 {
446            return self.len();
447        }
448        let line_offset = line_offsets[position.line as usize];
449        let next_line_offset = if position.line + 1 < line_offsets.len() as u32 {
450            line_offsets[position.line as usize + 1]
451        } else {
452            self.len()
453        };
454        line_offset.max((line_offset + position.character as usize).min(next_line_offset))
455    }
456}
457
458#[cfg(test)]
459mod tests {
460    use super::*;
461
462    #[test]
463    fn test_position_at() {
464        let text = r"abcd
465        efgh
466        ijkl";
467
468        let pos1 = text.position_at(14);
469        let pos1_expected = Position {
470            line: 1,
471            character: 9,
472        };
473        assert_eq!(pos1, pos1_expected);
474
475        let pos2 = text.position_at(28);
476        let pos2_expected = Position {
477            line: 2,
478            character: 10,
479        };
480        assert_eq!(pos2, pos2_expected);
481    }
482
483    #[test]
484    fn test_offset_at() {
485        let text = r"abcd
486        efgh
487        ijkl";
488
489        let offset1 = text.offset_at(&Position {
490            line: 1,
491            character: 9,
492        });
493        let expected_offset1 = 14;
494        assert_eq!(offset1, expected_offset1);
495
496        let offset2 = text.offset_at(&Position {
497            line: 2,
498            character: 10,
499        });
500        let expected_offset2 = 28;
501        assert_eq!(offset2, expected_offset2);
502    }
503}