Skip to main content

tokio_lsp/types/
lsp.rs

1//! Core LSP types as defined by the Language Server Protocol specification.
2//!
3//! This module contains the basic data structures used throughout the LSP,
4//! such as positions, ranges, locations, and other fundamental types.
5
6use crate::types::{DocumentUri, Uri};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10/// Position in a text document expressed as zero-based line and character offset.
11/// The offsets are based on a UTF-16 string representation.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
13pub struct Position {
14    /// Line position in a document (zero-based).
15    pub line: u32,
16    /// Character offset on a line in a document (zero-based).
17    /// Assuming that the line is represented as a string, the `character` value
18    /// represents the gap between the `character` and `character + 1`.
19    pub character: u32,
20}
21
22impl Position {
23    /// Create a new position.
24    pub fn new(line: u32, character: u32) -> Self {
25        Self { line, character }
26    }
27
28    /// Create a position at the start of a document.
29    pub fn start() -> Self {
30        Self::new(0, 0)
31    }
32}
33
34/// A range in a text document expressed as (zero-based) start and end positions.
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
36pub struct Range {
37    /// The range's start position.
38    pub start: Position,
39    /// The range's end position.
40    pub end: Position,
41}
42
43impl Range {
44    /// Create a new range.
45    pub fn new(start: Position, end: Position) -> Self {
46        Self { start, end }
47    }
48
49    /// Create a range from line/character coordinates.
50    pub fn from_coords(start_line: u32, start_char: u32, end_line: u32, end_char: u32) -> Self {
51        Self::new(
52            Position::new(start_line, start_char),
53            Position::new(end_line, end_char),
54        )
55    }
56
57    /// Create a single-character range at the given position.
58    pub fn single_char(position: Position) -> Self {
59        Self::new(
60            position,
61            Position::new(position.line, position.character + 1),
62        )
63    }
64
65    /// Check if this range contains the given position.
66    pub fn contains(&self, position: Position) -> bool {
67        position >= self.start && position < self.end
68    }
69
70    /// Check if this range is empty (start equals end).
71    pub fn is_empty(&self) -> bool {
72        self.start == self.end
73    }
74}
75
76/// Represents a location inside a resource, such as a line inside a text file.
77#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
78pub struct Location {
79    /// The resource's URI.
80    pub uri: DocumentUri,
81    /// The range in the document.
82    pub range: Range,
83}
84
85impl Location {
86    /// Create a new location.
87    pub fn new(uri: impl Into<DocumentUri>, range: Range) -> Self {
88        Self {
89            uri: uri.into(),
90            range,
91        }
92    }
93}
94
95/// Represents a link between a source and a target location.
96#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
97pub struct LocationLink {
98    /// Span of the origin of this link.
99    /// Used as the underlined span for mouse interaction. Defaults to the word range at
100    /// the mouse position.
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub origin_selection_range: Option<Range>,
103
104    /// The target resource identifier of this link.
105    pub target_uri: DocumentUri,
106
107    /// The full target range of this link. If the target for example is a symbol then
108    /// target range is the range enclosing this symbol not including leading/trailing
109    /// whitespace but everything else like comments.
110    pub target_range: Range,
111
112    /// The range that should be selected and revealed when this link is being followed,
113    /// e.g. the name of a function. Must be contained by the `target_range`.
114    pub target_selection_range: Range,
115}
116
117/// Defines a diagnostic, such as a compiler error or warning.
118#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
119pub struct Diagnostic {
120    /// The range at which the message applies.
121    pub range: Range,
122
123    /// The diagnostic's severity. Can be omitted. If omitted it is up to the
124    /// client to interpret diagnostics as error, warning, info or hint.
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub severity: Option<DiagnosticSeverity>,
127
128    /// The diagnostic's code, which usually appear in the user interface.
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub code: Option<DiagnosticCode>,
131
132    /// An optional property to describe the error code.
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub code_description: Option<CodeDescription>,
135
136    /// A human-readable string describing the source of this diagnostic.
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub source: Option<String>,
139
140    /// The diagnostic's message.
141    pub message: String,
142
143    /// Additional metadata about the diagnostic.
144    #[serde(skip_serializing_if = "Option::is_none")]
145    pub tags: Option<Vec<DiagnosticTag>>,
146
147    /// An array of related diagnostic information.
148    #[serde(skip_serializing_if = "Option::is_none")]
149    pub related_information: Option<Vec<DiagnosticRelatedInformation>>,
150
151    /// A data entry field that is preserved between a textDocument/publishDiagnostics
152    /// notification and textDocument/codeAction request.
153    #[serde(skip_serializing_if = "Option::is_none")]
154    pub data: Option<serde_json::Value>,
155}
156
157/// The diagnostic's severity.
158#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
159#[repr(u8)]
160pub enum DiagnosticSeverity {
161    /// Reports an error.
162    Error = 1,
163    /// Reports a warning.
164    Warning = 2,
165    /// Reports an information.
166    Information = 3,
167    /// Reports a hint.
168    Hint = 4,
169}
170
171impl Serialize for DiagnosticSeverity {
172    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
173    where
174        S: serde::Serializer,
175    {
176        serializer.serialize_u8(*self as u8)
177    }
178}
179
180impl<'de> Deserialize<'de> for DiagnosticSeverity {
181    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
182    where
183        D: serde::Deserializer<'de>,
184    {
185        let value = u8::deserialize(deserializer)?;
186        match value {
187            1 => Ok(DiagnosticSeverity::Error),
188            2 => Ok(DiagnosticSeverity::Warning),
189            3 => Ok(DiagnosticSeverity::Information),
190            4 => Ok(DiagnosticSeverity::Hint),
191            _ => Err(serde::de::Error::custom(format!(
192                "Invalid diagnostic severity: {}",
193                value
194            ))),
195        }
196    }
197}
198
199/// The diagnostic's code.
200#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
201#[serde(untagged)]
202pub enum DiagnosticCode {
203    Number(i32),
204    String(String),
205}
206
207/// Structure to capture a description for an error code.
208#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
209pub struct CodeDescription {
210    /// A URI to open with more information about the diagnostic error.
211    pub href: Uri,
212}
213
214/// The diagnostic tags.
215#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
216#[repr(u8)]
217pub enum DiagnosticTag {
218    /// Unused or unnecessary code.
219    Unnecessary = 1,
220    /// Deprecated or obsolete code.
221    Deprecated = 2,
222}
223
224impl Serialize for DiagnosticTag {
225    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
226    where
227        S: serde::Serializer,
228    {
229        serializer.serialize_u8(*self as u8)
230    }
231}
232
233impl<'de> Deserialize<'de> for DiagnosticTag {
234    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
235    where
236        D: serde::Deserializer<'de>,
237    {
238        let value = u8::deserialize(deserializer)?;
239        match value {
240            1 => Ok(DiagnosticTag::Unnecessary),
241            2 => Ok(DiagnosticTag::Deprecated),
242            _ => Err(serde::de::Error::custom(format!(
243                "Invalid diagnostic tag: {}",
244                value
245            ))),
246        }
247    }
248}
249
250/// Represents a related message and source code location for a diagnostic.
251#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
252pub struct DiagnosticRelatedInformation {
253    /// The location of this related diagnostic information.
254    pub location: Location,
255    /// The message of this related diagnostic information.
256    pub message: String,
257}
258
259/// A command is returned from the server to the client.
260#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
261pub struct Command {
262    /// Title of the command, like 'save'.
263    pub title: String,
264    /// The identifier of the actual command handler.
265    pub command: String,
266    /// Arguments that the command handler should be invoked with.
267    #[serde(skip_serializing_if = "Option::is_none")]
268    pub arguments: Option<Vec<serde_json::Value>>,
269}
270
271/// A text edit applicable to a text document.
272#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
273pub struct TextEdit {
274    /// The range of the text document to be manipulated.
275    pub range: Range,
276    /// The string to be inserted. For delete operations use an empty string.
277    pub new_text: String,
278}
279
280impl TextEdit {
281    /// Create a new text edit.
282    pub fn new(range: Range, new_text: impl Into<String>) -> Self {
283        Self {
284            range,
285            new_text: new_text.into(),
286        }
287    }
288
289    /// Create a text edit that inserts text at a position.
290    pub fn insert(position: Position, text: impl Into<String>) -> Self {
291        Self::new(Range::new(position, position), text)
292    }
293
294    /// Create a text edit that deletes a range.
295    pub fn delete(range: Range) -> Self {
296        Self::new(range, "")
297    }
298
299    /// Create a text edit that replaces a range with new text.
300    pub fn replace(range: Range, new_text: impl Into<String>) -> Self {
301        Self::new(range, new_text)
302    }
303}
304
305/// Additional information that describes document changes.
306#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
307pub struct ChangeAnnotation {
308    /// A human-readable string describing the actual change.
309    pub label: String,
310
311    /// A flag which indicates that user confirmation is needed before applying the change.
312    #[serde(skip_serializing_if = "Option::is_none")]
313    pub needs_confirmation: Option<bool>,
314
315    /// A human-readable string which is rendered less prominent in the user interface.
316    #[serde(skip_serializing_if = "Option::is_none")]
317    pub description: Option<String>,
318}
319
320/// An identifier referring to a change annotation managed by a workspace edit.
321pub type ChangeAnnotationIdentifier = String;
322
323/// A special text edit with an additional change annotation.
324#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
325pub struct AnnotatedTextEdit {
326    /// The range of the text document to be manipulated.
327    pub range: Range,
328    /// The string to be inserted.
329    pub new_text: String,
330    /// The actual identifier of the change annotation.
331    pub annotation_id: ChangeAnnotationIdentifier,
332}
333
334/// Describes textual changes on a text document.
335#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
336pub struct TextDocumentEdit {
337    /// The text document to change.
338    pub text_document: OptionalVersionedTextDocumentIdentifier,
339    /// The edits to be applied.
340    pub edits: Vec<OneOf<TextEdit, AnnotatedTextEdit>>,
341}
342
343/// A generic resource operation.
344#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
345#[serde(tag = "kind")]
346pub enum ResourceOperation {
347    #[serde(rename = "create")]
348    Create(CreateFile),
349    #[serde(rename = "rename")]
350    Rename(RenameFile),
351    #[serde(rename = "delete")]
352    Delete(DeleteFile),
353}
354
355/// Create file operation.
356#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
357pub struct CreateFile {
358    /// A create file operation.
359    pub kind: String, // "create"
360    /// The resource to create.
361    pub uri: DocumentUri,
362    /// Additional options.
363    #[serde(skip_serializing_if = "Option::is_none")]
364    pub options: Option<CreateFileOptions>,
365    /// An optional annotation identifier describing the operation.
366    #[serde(skip_serializing_if = "Option::is_none")]
367    pub annotation_id: Option<ChangeAnnotationIdentifier>,
368}
369
370/// Options to create a file.
371#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
372pub struct CreateFileOptions {
373    /// Overwrite existing file. Overwrite wins over `ignore_if_exists`.
374    #[serde(skip_serializing_if = "Option::is_none")]
375    pub overwrite: Option<bool>,
376    /// Ignore if exists.
377    #[serde(skip_serializing_if = "Option::is_none")]
378    pub ignore_if_exists: Option<bool>,
379}
380
381/// Rename file operation
382#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
383pub struct RenameFile {
384    /// A rename file operation
385    pub kind: String, // "rename"
386    /// The old (existing) location.
387    pub old_uri: DocumentUri,
388    /// The new location.
389    pub new_uri: DocumentUri,
390    /// Rename options.
391    #[serde(skip_serializing_if = "Option::is_none")]
392    pub options: Option<RenameFileOptions>,
393    /// An optional annotation identifier describing the operation.
394    #[serde(skip_serializing_if = "Option::is_none")]
395    pub annotation_id: Option<ChangeAnnotationIdentifier>,
396}
397
398/// Rename file options
399#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
400pub struct RenameFileOptions {
401    /// Overwrite target if existing. Overwrite wins over `ignore_if_exists`.
402    #[serde(skip_serializing_if = "Option::is_none")]
403    pub overwrite: Option<bool>,
404    /// Ignores if target exists.
405    #[serde(skip_serializing_if = "Option::is_none")]
406    pub ignore_if_exists: Option<bool>,
407}
408
409/// Delete file operation
410#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
411pub struct DeleteFile {
412    /// A delete file operation
413    pub kind: String, // "delete"
414    /// The file to delete.
415    pub uri: DocumentUri,
416    /// Delete options.
417    #[serde(skip_serializing_if = "Option::is_none")]
418    pub options: Option<DeleteFileOptions>,
419    /// An optional annotation identifier describing the operation.
420    #[serde(skip_serializing_if = "Option::is_none")]
421    pub annotation_id: Option<ChangeAnnotationIdentifier>,
422}
423
424/// Delete file options
425#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
426pub struct DeleteFileOptions {
427    /// Delete the content recursively if a folder is denoted.
428    #[serde(skip_serializing_if = "Option::is_none")]
429    pub recursive: Option<bool>,
430    /// Ignore the operation if the file doesn't exist.
431    #[serde(skip_serializing_if = "Option::is_none")]
432    pub ignore_if_not_exists: Option<bool>,
433}
434
435/// A workspace edit represents changes to many resources managed in the workspace.
436#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
437pub struct WorkspaceEdit {
438    /// Holds changes to existing resources.
439    #[serde(skip_serializing_if = "Option::is_none")]
440    pub changes: Option<HashMap<DocumentUri, Vec<TextEdit>>>,
441
442    /// Depending on the client capability `workspace.workspaceEdit.resourceOperations` document changes
443    /// are either an array of `TextDocumentEdit`s to express changes to n different text documents
444    /// where each text document edit addresses a specific version of a text document.
445    #[serde(skip_serializing_if = "Option::is_none")]
446    pub document_changes: Option<Vec<DocumentChange>>,
447
448    /// A map of change annotations that can be referenced in `AnnotatedTextEdit`s or create, rename and
449    /// delete file / folder operations.
450    #[serde(skip_serializing_if = "Option::is_none")]
451    pub change_annotations: Option<HashMap<ChangeAnnotationIdentifier, ChangeAnnotation>>,
452}
453
454/// Document change type for workspace edits.
455#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
456#[serde(untagged)]
457pub enum DocumentChange {
458    TextDocumentEdit(TextDocumentEdit),
459    ResourceOperation(ResourceOperation),
460}
461
462/// Text documents are identified using a URI.
463#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
464pub struct TextDocumentIdentifier {
465    /// The text document's URI.
466    pub uri: DocumentUri,
467}
468
469impl TextDocumentIdentifier {
470    /// Create a new text document identifier.
471    pub fn new(uri: impl Into<DocumentUri>) -> Self {
472        Self { uri: uri.into() }
473    }
474}
475
476/// A versioned text document identifier.
477#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
478pub struct VersionedTextDocumentIdentifier {
479    /// The text document's URI.
480    pub uri: DocumentUri,
481    /// The version number of this document.
482    pub version: i32,
483}
484
485impl VersionedTextDocumentIdentifier {
486    /// Create a new versioned text document identifier.
487    pub fn new(uri: impl Into<DocumentUri>, version: i32) -> Self {
488        Self {
489            uri: uri.into(),
490            version,
491        }
492    }
493}
494
495/// A text document identifier where the version is optional.
496#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
497pub struct OptionalVersionedTextDocumentIdentifier {
498    /// The text document's URI.
499    pub uri: DocumentUri,
500    /// The version number of this document. If a versioned text document identifier
501    /// is sent from the server to the client and the file is not open in the editor
502    /// (the server has not received an open notification before) the server can send
503    /// `null` to indicate that the version is unknown and the content on disk is the
504    /// truth (as specified with document content ownership).
505    pub version: Option<i32>,
506}
507
508impl OptionalVersionedTextDocumentIdentifier {
509    /// Create a new optional versioned text document identifier.
510    pub fn new(uri: impl Into<DocumentUri>, version: Option<i32>) -> Self {
511        Self {
512            uri: uri.into(),
513            version,
514        }
515    }
516}
517
518/// A helper type to represent either one of two types.
519#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
520#[serde(untagged)]
521pub enum OneOf<A, B> {
522    Left(A),
523    Right(B),
524}
525
526impl<A, B> OneOf<A, B> {
527    pub fn is_left(&self) -> bool {
528        matches!(self, OneOf::Left(_))
529    }
530
531    pub fn is_right(&self) -> bool {
532        matches!(self, OneOf::Right(_))
533    }
534
535    pub fn left(self) -> Option<A> {
536        match self {
537            OneOf::Left(a) => Some(a),
538            OneOf::Right(_) => None,
539        }
540    }
541
542    pub fn right(self) -> Option<B> {
543        match self {
544            OneOf::Right(b) => Some(b),
545            OneOf::Left(_) => None,
546        }
547    }
548}
549
550impl<A> From<A> for OneOf<A, ()> {
551    fn from(a: A) -> Self {
552        OneOf::Left(a)
553    }
554}
555
556#[cfg(test)]
557mod tests {
558    use super::*;
559
560    #[test]
561    fn test_position_ordering() {
562        let pos1 = Position::new(1, 5);
563        let pos2 = Position::new(1, 10);
564        let pos3 = Position::new(2, 0);
565
566        assert!(pos1 < pos2);
567        assert!(pos2 < pos3);
568        assert!(pos1 < pos3);
569    }
570
571    #[test]
572    fn test_range_contains() {
573        let range = Range::new(Position::new(1, 5), Position::new(1, 10));
574
575        assert!(range.contains(Position::new(1, 7)));
576        assert!(!range.contains(Position::new(1, 4)));
577        assert!(!range.contains(Position::new(1, 10))); // end is exclusive
578        assert!(!range.contains(Position::new(2, 0)));
579    }
580
581    #[test]
582    fn test_range_is_empty() {
583        let pos = Position::new(1, 5);
584        let empty_range = Range::new(pos, pos);
585        let non_empty_range = Range::new(pos, Position::new(1, 6));
586
587        assert!(empty_range.is_empty());
588        assert!(!non_empty_range.is_empty());
589    }
590
591    #[test]
592    fn test_text_edit_operations() {
593        let pos = Position::new(1, 5);
594        let range = Range::new(pos, Position::new(1, 10));
595
596        let insert = TextEdit::insert(pos, "text");
597        assert_eq!(insert.range.start, insert.range.end);
598
599        let delete = TextEdit::delete(range);
600        assert_eq!(delete.new_text, "");
601
602        let replace = TextEdit::replace(range, "new");
603        assert_eq!(replace.new_text, "new");
604        assert_eq!(replace.range, range);
605    }
606
607    #[test]
608    fn test_diagnostic_severity_values() {
609        // Test that the enum values match the LSP specification
610        assert_eq!(DiagnosticSeverity::Error as u8, 1);
611        assert_eq!(DiagnosticSeverity::Warning as u8, 2);
612        assert_eq!(DiagnosticSeverity::Information as u8, 3);
613        assert_eq!(DiagnosticSeverity::Hint as u8, 4);
614    }
615
616    #[test]
617    fn test_one_of_type() {
618        let left: OneOf<i32, String> = OneOf::Left(42);
619        let right: OneOf<i32, String> = OneOf::Right("test".to_string());
620
621        assert!(left.is_left());
622        assert!(!left.is_right());
623        assert!(!right.is_left());
624        assert!(right.is_right());
625
626        assert_eq!(left.left(), Some(42));
627        assert_eq!(right.right(), Some("test".to_string()));
628    }
629}