solscript_lsp/
lib.rs

1//! SolScript Language Server Protocol Implementation
2//!
3//! Provides IDE features like diagnostics, go-to-definition, hover, and autocomplete.
4
5mod completion;
6mod definition;
7mod diagnostics;
8mod document;
9mod hover;
10
11use dashmap::DashMap;
12use tower_lsp::jsonrpc::Result;
13use tower_lsp::lsp_types::*;
14use tower_lsp::{Client, LanguageServer};
15
16pub use document::Document;
17
18/// The SolScript language server
19pub struct SolScriptLanguageServer {
20    /// LSP client for sending notifications
21    client: Client,
22    /// Open documents indexed by URI
23    documents: DashMap<Url, Document>,
24}
25
26impl SolScriptLanguageServer {
27    pub fn new(client: Client) -> Self {
28        Self {
29            client,
30            documents: DashMap::new(),
31        }
32    }
33
34    /// Get a document by URI
35    fn get_document(&self, uri: &Url) -> Option<dashmap::mapref::one::Ref<'_, Url, Document>> {
36        self.documents.get(uri)
37    }
38
39    /// Analyze a document and publish diagnostics
40    async fn analyze_document(&self, uri: &Url) {
41        if let Some(doc) = self.documents.get(uri) {
42            let diagnostics = diagnostics::get_diagnostics(&doc);
43            self.client
44                .publish_diagnostics(uri.clone(), diagnostics, Some(doc.version))
45                .await;
46        }
47    }
48}
49
50#[tower_lsp::async_trait]
51impl LanguageServer for SolScriptLanguageServer {
52    async fn initialize(&self, _: InitializeParams) -> Result<InitializeResult> {
53        Ok(InitializeResult {
54            capabilities: ServerCapabilities {
55                text_document_sync: Some(TextDocumentSyncCapability::Kind(
56                    TextDocumentSyncKind::FULL,
57                )),
58                completion_provider: Some(CompletionOptions {
59                    trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
60                    resolve_provider: Some(false),
61                    ..Default::default()
62                }),
63                hover_provider: Some(HoverProviderCapability::Simple(true)),
64                definition_provider: Some(OneOf::Left(true)),
65                document_formatting_provider: Some(OneOf::Left(true)),
66                ..Default::default()
67            },
68            server_info: Some(ServerInfo {
69                name: "solscript-lsp".to_string(),
70                version: Some(env!("CARGO_PKG_VERSION").to_string()),
71            }),
72        })
73    }
74
75    async fn initialized(&self, _: InitializedParams) {
76        self.client
77            .log_message(MessageType::INFO, "SolScript language server initialized")
78            .await;
79    }
80
81    async fn shutdown(&self) -> Result<()> {
82        Ok(())
83    }
84
85    async fn did_open(&self, params: DidOpenTextDocumentParams) {
86        let uri = params.text_document.uri;
87        let version = params.text_document.version;
88        let text = params.text_document.text;
89
90        let doc = Document::new(text, version);
91        self.documents.insert(uri.clone(), doc);
92        self.analyze_document(&uri).await;
93    }
94
95    async fn did_change(&self, params: DidChangeTextDocumentParams) {
96        let uri = params.text_document.uri;
97        let version = params.text_document.version;
98
99        if let Some(change) = params.content_changes.into_iter().last() {
100            if let Some(mut doc) = self.documents.get_mut(&uri) {
101                doc.update(change.text, version);
102            }
103        }
104
105        self.analyze_document(&uri).await;
106    }
107
108    async fn did_close(&self, params: DidCloseTextDocumentParams) {
109        self.documents.remove(&params.text_document.uri);
110    }
111
112    async fn did_save(&self, params: DidSaveTextDocumentParams) {
113        self.analyze_document(&params.text_document.uri).await;
114    }
115
116    async fn completion(&self, params: CompletionParams) -> Result<Option<CompletionResponse>> {
117        let uri = &params.text_document_position.text_document.uri;
118        let position = params.text_document_position.position;
119
120        if let Some(doc) = self.get_document(uri) {
121            let items = completion::get_completions(&doc, position);
122            return Ok(Some(CompletionResponse::Array(items)));
123        }
124
125        Ok(None)
126    }
127
128    async fn hover(&self, params: HoverParams) -> Result<Option<Hover>> {
129        let uri = &params.text_document_position_params.text_document.uri;
130        let position = params.text_document_position_params.position;
131
132        if let Some(doc) = self.get_document(uri) {
133            return Ok(hover::get_hover(&doc, position));
134        }
135
136        Ok(None)
137    }
138
139    async fn goto_definition(
140        &self,
141        params: GotoDefinitionParams,
142    ) -> Result<Option<GotoDefinitionResponse>> {
143        let uri = &params.text_document_position_params.text_document.uri;
144        let position = params.text_document_position_params.position;
145
146        if let Some(doc) = self.get_document(uri) {
147            if let Some(location) = definition::get_definition(&doc, position, uri) {
148                return Ok(Some(GotoDefinitionResponse::Scalar(location)));
149            }
150        }
151
152        Ok(None)
153    }
154
155    async fn formatting(&self, params: DocumentFormattingParams) -> Result<Option<Vec<TextEdit>>> {
156        let uri = &params.text_document.uri;
157
158        if let Some(doc) = self.get_document(uri) {
159            // Parse and format using the AST
160            if let Ok(program) = solscript_parser::parse(&doc.text) {
161                let formatted = format_program(&program);
162                let edit = TextEdit {
163                    range: Range {
164                        start: Position::new(0, 0),
165                        end: Position::new(u32::MAX, u32::MAX),
166                    },
167                    new_text: formatted,
168                };
169                return Ok(Some(vec![edit]));
170            }
171        }
172
173        Ok(None)
174    }
175}
176
177/// Format a program AST back to source code
178fn format_program(program: &solscript_ast::Program) -> String {
179    use solscript_ast::*;
180
181    let mut output = String::new();
182
183    for item in &program.items {
184        match item {
185            Item::Contract(c) => {
186                if c.is_abstract {
187                    output.push_str("abstract ");
188                }
189                output.push_str("contract ");
190                output.push_str(&c.name.name);
191
192                if !c.bases.is_empty() {
193                    output.push_str(" is ");
194                    let bases: Vec<_> = c.bases.iter().map(|b| b.name().to_string()).collect();
195                    output.push_str(&bases.join(", "));
196                }
197
198                output.push_str(" {\n");
199
200                for member in &c.members {
201                    match member {
202                        ContractMember::StateVar(v) => {
203                            output.push_str("    ");
204                            output.push_str(&format_type(&v.ty));
205                            if let Some(vis) = &v.visibility {
206                                output.push_str(&format!(" {}", format_visibility(vis)));
207                            }
208                            output.push_str(&format!(" {};\n", v.name.name));
209                        }
210                        ContractMember::Function(f) => {
211                            output.push_str(&format_function(f, 1));
212                        }
213                        ContractMember::Constructor(c) => {
214                            output.push_str("    constructor(");
215                            let params: Vec<_> = c
216                                .params
217                                .iter()
218                                .map(|p| format!("{} {}", format_type(&p.ty), p.name.name))
219                                .collect();
220                            output.push_str(&params.join(", "));
221                            output.push_str(") {\n");
222                            // TODO: format body
223                            output.push_str("    }\n\n");
224                        }
225                        _ => {}
226                    }
227                }
228
229                output.push_str("}\n\n");
230            }
231            Item::Event(e) => {
232                output.push_str(&format!("event {}(", e.name.name));
233                let params: Vec<_> = e
234                    .params
235                    .iter()
236                    .map(|p| {
237                        let indexed = if p.indexed { "indexed " } else { "" };
238                        format!("{} {}{}", format_type(&p.ty), indexed, p.name.name)
239                    })
240                    .collect();
241                output.push_str(&params.join(", "));
242                output.push_str(");\n\n");
243            }
244            Item::Error(e) => {
245                output.push_str(&format!("error {}(", e.name.name));
246                let params: Vec<_> = e
247                    .params
248                    .iter()
249                    .map(|p| format!("{} {}", format_type(&p.ty), p.name.name))
250                    .collect();
251                output.push_str(&params.join(", "));
252                output.push_str(");\n\n");
253            }
254            Item::Interface(i) => {
255                output.push_str(&format!("interface {} {{\n", i.name.name));
256                for sig in &i.members {
257                    output.push_str(&format_fn_sig(sig, 1));
258                }
259                output.push_str("}\n\n");
260            }
261            _ => {}
262        }
263    }
264
265    output
266}
267
268fn format_type(ty: &solscript_ast::TypeExpr) -> String {
269    ty.name().to_string()
270}
271
272fn format_visibility(vis: &solscript_ast::Visibility) -> &'static str {
273    match vis {
274        solscript_ast::Visibility::Public => "public",
275        solscript_ast::Visibility::Private => "private",
276        solscript_ast::Visibility::Internal => "internal",
277        solscript_ast::Visibility::External => "external",
278    }
279}
280
281fn format_fn_sig(sig: &solscript_ast::FnSig, indent: usize) -> String {
282    let ind = "    ".repeat(indent);
283    let mut output = String::new();
284
285    output.push_str(&format!("{}function {}(", ind, sig.name.name));
286
287    let params: Vec<_> = sig
288        .params
289        .iter()
290        .map(|p| format!("{} {}", p.ty.name(), p.name.name))
291        .collect();
292    output.push_str(&params.join(", "));
293    output.push(')');
294
295    if let Some(vis) = &sig.visibility {
296        output.push_str(&format!(" {}", format_visibility(vis)));
297    }
298
299    for m in &sig.state_mutability {
300        match m {
301            solscript_ast::StateMutability::View => output.push_str(" view"),
302            solscript_ast::StateMutability::Pure => output.push_str(" pure"),
303            solscript_ast::StateMutability::Payable => output.push_str(" payable"),
304        }
305    }
306
307    if !sig.return_params.is_empty() {
308        output.push_str(" returns (");
309        let returns: Vec<_> = sig.return_params.iter().map(|p| p.ty.name()).collect();
310        output.push_str(&returns.join(", "));
311        output.push(')');
312    }
313
314    output.push_str(";\n");
315    output
316}
317
318fn format_function(f: &solscript_ast::FnDef, indent: usize) -> String {
319    let ind = "    ".repeat(indent);
320    let mut output = String::new();
321
322    // Attributes
323    for attr in &f.attributes {
324        output.push_str(&format!("{}#[{}]\n", ind, attr.name.name));
325    }
326
327    output.push_str(&format!("{}function {}(", ind, f.name.name));
328
329    let params: Vec<_> = f
330        .params
331        .iter()
332        .map(|p| format!("{} {}", format_type(&p.ty), p.name.name))
333        .collect();
334    output.push_str(&params.join(", "));
335    output.push(')');
336
337    // Visibility
338    if let Some(vis) = &f.visibility {
339        output.push_str(&format!(" {}", format_visibility(vis)));
340    }
341
342    // State mutability
343    for m in &f.state_mutability {
344        match m {
345            solscript_ast::StateMutability::View => output.push_str(" view"),
346            solscript_ast::StateMutability::Pure => output.push_str(" pure"),
347            solscript_ast::StateMutability::Payable => output.push_str(" payable"),
348        }
349    }
350
351    // Returns
352    if !f.return_params.is_empty() {
353        output.push_str(" returns (");
354        let returns: Vec<_> = f.return_params.iter().map(|p| format_type(&p.ty)).collect();
355        output.push_str(&returns.join(", "));
356        output.push(')');
357    }
358
359    if f.body.is_some() {
360        output.push_str(" {\n");
361        // TODO: format body statements
362        output.push_str(&format!("{}    // ...\n", ind));
363        output.push_str(&format!("{}}}\n\n", ind));
364    } else {
365        output.push_str(";\n\n");
366    }
367
368    output
369}