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
//! LSP Server [initialization](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize) implementation.

use crate::utils;

/// Implements LSP server initialization.
///
/// Ref: <https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize>.
pub fn initialize(
    connection: lsp_server::Connection,
) -> anyhow::Result<(lsp_server::Connection, lsp_types::InitializeParams)> {
    // Starts initialization (blocks and waits for initialize request from the client).
    let (initialize_id, initialize_params_json) = connection.initialize_start()?;
    let initialize_params: lsp_types::InitializeParams =
        serde_json::from_value(initialize_params_json).map_err(|error| {
            anyhow::format_err!("Failed to deserialize initialize parameters: {error}")
        })?;

    // Composes initialization result.
    let initialize_result = serde_json::to_value(lsp_types::InitializeResult {
        // Sets server capabilities based on client's capabilities.
        capabilities: server_capabilities(&initialize_params.capabilities),
        server_info: Some(lsp_types::ServerInfo {
            name: "ink-analyzer".to_owned(),
            version: Some(env!("CARGO_PKG_VERSION").to_owned()),
        }),
        offset_encoding: None,
    })
    .map_err(|error| anyhow::format_err!("Failed to serialize initialize result: {error}"))?;

    // Finishes initialization (sends `InitializeResult` back to the client).
    connection.initialize_finish(initialize_id, initialize_result)?;

    Ok((connection, initialize_params))
}

/// Returns the capabilities of the language server based on the given client capabilities.
///
/// Ref: <https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#serverCapabilities>.
pub fn server_capabilities(
    client_capabilities: &lsp_types::ClientCapabilities,
) -> lsp_types::ServerCapabilities {
    lsp_types::ServerCapabilities {
        position_encoding: Some(utils::position_encoding(client_capabilities)),
        text_document_sync: Some(lsp_types::TextDocumentSyncCapability::Options(
            lsp_types::TextDocumentSyncOptions {
                open_close: Some(true),
                // ink! projects are currently single file and tend to be pretty small,
                // so full document sync is fine (for now).
                change: Some(lsp_types::TextDocumentSyncKind::FULL),
                will_save: None,
                will_save_wait_until: None,
                save: Some(lsp_types::SaveOptions::default().into()),
            },
        )),
        hover_provider: Some(lsp_types::HoverProviderCapability::Simple(true)),
        completion_provider: Some(lsp_types::CompletionOptions {
            resolve_provider: None,
            // ink! completions are all attribute based.
            trigger_characters: Some(vec![
                "[".to_owned(),
                "(".to_owned(),
                ",".to_owned(),
                ":".to_owned(),
            ]),
            all_commit_characters: None,
            work_done_progress_options: Default::default(),
            completion_item: Default::default(),
        }),
        code_action_provider: Some(
            utils::code_actions_kinds(client_capabilities)
                .map(|code_action_kinds| {
                    lsp_types::CodeActionProviderCapability::Options(lsp_types::CodeActionOptions {
                        code_action_kinds: Some(code_action_kinds),
                        work_done_progress_options: Default::default(),
                        resolve_provider: None,
                    })
                })
                .unwrap_or_else(|| lsp_types::CodeActionProviderCapability::Simple(true)),
        ),
        inlay_hint_provider: Some(lsp_types::OneOf::Right(
            lsp_types::InlayHintServerCapabilities::Options(lsp_types::InlayHintOptions {
                work_done_progress_options: Default::default(),
                resolve_provider: None,
            }),
        )),
        signature_help_provider: Some(lsp_types::SignatureHelpOptions {
            trigger_characters: Some(vec!["(".to_owned(), ",".to_owned()]),
            retrigger_characters: None,
            work_done_progress_options: Default::default(),
        }),
        execute_command_provider: Some(lsp_types::ExecuteCommandOptions {
            commands: vec!["createProject".to_owned()],
            work_done_progress_options: Default::default(),
        }),
        ..Default::default()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::thread;

    #[test]
    fn initialize_works() {
        // Creates pair of in-memory connections to simulate an LSP client and server.
        let (server_connection, client_connection) = lsp_server::Connection::memory();

        // Starts server initialization on a separate thread (because `initialize` function is blocking).
        thread::spawn(|| initialize(server_connection));

        // Verifies that an initialization request (from client to server) gets an `InitializeResult` response (from server to client).
        // Creates initialization request.
        use lsp_types::request::Request;
        let init_req_id = lsp_server::RequestId::from(1);
        let init_req = lsp_server::Request {
            id: init_req_id.clone(),
            method: lsp_types::request::Initialize::METHOD.to_owned(),
            params: serde_json::to_value(lsp_types::InitializeParams::default()).unwrap(),
        };
        // Sends initialization request from client to server.
        client_connection.sender.send(init_req.into()).unwrap();
        // Confirms receipt of `InitializeResult` response by the client.
        let message = client_connection.receiver.recv().unwrap();
        let init_result_resp = match message {
            lsp_server::Message::Response(it) => Some(it),
            _ => None,
        }
        .unwrap();
        assert_eq!(init_result_resp.id, init_req_id);
        // Verifies that an initialization result is created with the expected server name.
        let init_result: lsp_types::InitializeResult =
            serde_json::from_value(init_result_resp.result.unwrap()).unwrap();
        assert_eq!(init_result.server_info.unwrap().name, "ink-analyzer");
    }

    #[test]
    fn server_capabilities_works() {
        // Creates server capabilities based on client capabilities.
        let server_capabilities = server_capabilities(&Default::default());

        // Verifies the expected default server capabilities.
        // NOTE: See the `utils` module for utilities for "reactive" server capabilities (i.e. change based on the client capabilities)
        // and their unit tests
        // (e.g. position_encoding, code action kinds, snippet support, signature information active parameter and label offset support e.t.c).
        assert_eq!(
            server_capabilities.position_encoding,
            Some(lsp_types::PositionEncodingKind::UTF16)
        );
        assert_eq!(
            server_capabilities.code_action_provider,
            Some(lsp_types::CodeActionProviderCapability::Simple(true))
        );
    }
}