sway_lsp/capabilities/
on_enter.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
use crate::{
    config::OnEnterConfig,
    core::document::{Documents, TextDocument},
    lsp_ext::OnEnterParams,
};
use tower_lsp::lsp_types::{
    DocumentChanges, OneOf, OptionalVersionedTextDocumentIdentifier, Position, Range,
    TextDocumentEdit, TextEdit, Url, WorkspaceEdit,
};

const NEWLINE: &str = "\n";
const COMMENT_START: &str = "//";
const DOC_COMMENT_START: &str = "///";

/// If the change was an enter keypress or pasting multiple lines in a comment, it prefixes the line(s)
/// with the appropriate comment start pattern (// or ///).
pub fn on_enter(
    config: &OnEnterConfig,
    documents: &Documents,
    temp_uri: &Url,
    params: &OnEnterParams,
) -> Option<WorkspaceEdit> {
    if !(params.content_changes[0].text.contains(NEWLINE)) {
        return None;
    }

    let mut workspace_edit = None;
    let text_document = documents
        .get_text_document(temp_uri)
        .expect("could not get text document");

    if config.continue_doc_comments.unwrap_or(false) {
        workspace_edit = get_comment_workspace_edit(DOC_COMMENT_START, params, &text_document);
    }

    if config.continue_comments.unwrap_or(false) && workspace_edit.is_none() {
        workspace_edit = get_comment_workspace_edit(COMMENT_START, params, &text_document);
    }

    workspace_edit
}

fn get_comment_workspace_edit(
    start_pattern: &str,
    change_params: &OnEnterParams,
    text_document: &TextDocument,
) -> Option<WorkspaceEdit> {
    let range = change_params.content_changes[0]
        .range
        .expect("change is missing range");
    let line = text_document.get_line(range.start.line as usize);

    // If the previous line doesn't start with a comment, return early.
    if !line.trim().starts_with(start_pattern) {
        return None;
    }

    let uri = change_params.text_document.uri.clone();
    let text = change_params.content_changes[0].text.clone();

    let indentation = &line[..line.find(start_pattern).unwrap_or(0)];
    let mut edits = vec![];

    // To support pasting multiple lines in a comment, we need to add the comment start pattern after each newline,
    // except the last one.
    let lines: Vec<_> = text.split(NEWLINE).collect();
    lines.iter().enumerate().for_each(|(i, _)| {
        if i < lines.len() - 1 {
            let position =
                Position::new(range.start.line + (i as u32) + 1, indentation.len() as u32);
            edits.push(OneOf::Left(TextEdit {
                new_text: format!("{start_pattern} "),
                range: Range::new(position, position),
            }));
        }
    });

    let edit = TextDocumentEdit {
        text_document: OptionalVersionedTextDocumentIdentifier {
            // Use the original uri to make updates, not the temporary one from the session.
            uri,
            version: None,
        },
        edits,
    };

    Some(WorkspaceEdit {
        document_changes: Some(DocumentChanges::Edits(vec![edit])),
        ..Default::default()
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use lsp_types::{AnnotatedTextEdit, TextDocumentContentChangeEvent, TextDocumentIdentifier};
    use sway_lsp_test_utils::get_absolute_path;

    fn assert_text_edit(
        actual: &OneOf<TextEdit, AnnotatedTextEdit>,
        new_text: String,
        line: u32,
        character: u32,
    ) {
        match actual {
            OneOf::Left(edit) => {
                let position = Position { line, character };
                let expected = TextEdit {
                    new_text,
                    range: Range {
                        start: position,
                        end: position,
                    },
                };
                assert_eq!(*edit, expected);
            }
            OneOf::Right(_) => panic!("expected left"),
        }
    }

    #[tokio::test]
    async fn get_comment_workspace_edit_double_slash_indented() {
        let path = get_absolute_path("sway-lsp/tests/fixtures/diagnostics/dead_code/src/main.sw");
        let uri = Url::from_file_path(path.clone()).unwrap();
        let text_document = TextDocument::build_from_path(path.as_str())
            .await
            .expect("failed to build document");
        let params = OnEnterParams {
            text_document: TextDocumentIdentifier { uri },
            content_changes: vec![TextDocumentContentChangeEvent {
                range: Some(Range {
                    start: Position {
                        line: 47,
                        character: 34,
                    },
                    end: Position {
                        line: 47,
                        character: 34,
                    },
                }),
                range_length: Some(0),
                text: "\n    ".to_string(),
            }],
        };

        let result = get_comment_workspace_edit(COMMENT_START, &params, &text_document)
            .expect("workspace edit");
        let changes = result.document_changes.expect("document changes");
        let edits = match changes {
            DocumentChanges::Edits(edits) => edits,
            DocumentChanges::Operations(_) => panic!("expected edits"),
        };

        assert_eq!(edits.len(), 1);
        assert_eq!(edits[0].edits.len(), 1);
        assert_text_edit(&edits[0].edits[0], "// ".to_string(), 48, 4);
    }

    #[tokio::test]
    async fn get_comment_workspace_edit_triple_slash_paste() {
        let path = get_absolute_path("sway-lsp/tests/fixtures/diagnostics/dead_code/src/main.sw");
        let uri = Url::from_file_path(path.clone()).unwrap();
        let text_document = TextDocument::build_from_path(path.as_str())
            .await
            .expect("failed to build document");
        let params = OnEnterParams {
            text_document: TextDocumentIdentifier { uri },
            content_changes: vec![TextDocumentContentChangeEvent {
                range: Some(Range {
                    start: Position {
                        line: 41,
                        character: 4,
                    },
                    end: Position {
                        line: 41,
                        character: 34,
                    },
                }),
                range_length: Some(30),
                text: "fn not_used2(input: u64) -> u64 {\n    return input + 1;\n}".to_string(),
            }],
        };

        let result = get_comment_workspace_edit(DOC_COMMENT_START, &params, &text_document)
            .expect("workspace edit");
        let changes = result.document_changes.expect("document changes");
        let edits = match changes {
            DocumentChanges::Edits(edits) => edits,
            DocumentChanges::Operations(_) => panic!("expected edits"),
        };

        assert_eq!(edits.len(), 1);
        assert_eq!(edits[0].edits.len(), 2);
        assert_text_edit(&edits[0].edits[0], "/// ".to_string(), 42, 0);
        assert_text_edit(&edits[0].edits[1], "/// ".to_string(), 43, 0);
    }
}