Skip to main content

nargo_lsp/
lib.rs

1#![feature(new_range_api)]
2#![warn(missing_docs)]
3
4pub mod types;
5use crate::types::NargoLanguage;
6use dashmap::{DashMap, DashSet};
7use nargo_ir::{ElementIR, ExpressionIR, JsExpr, JsProgram, JsStmt, TemplateNodeIR};
8use nargo_parser::{template::VueTemplateParser, ParseState, ScriptParser, TemplateParser};
9use nargo_types::{is_pos_in_span, Position as NargoPosition, Span as NargoSpan};
10use std::collections::HashSet;
11
12use futures::Future;
13use oak_core::{
14    parser::session::ParseSession,
15    tree::{GreenNode, RedNode},
16    Range,
17};
18use oak_lsp::service::LanguageService;
19use oak_vfs::{MemoryVfs, Vfs};
20
21use std::sync::Arc;
22use url::Url;
23
24#[derive(Debug, Clone, PartialEq, Eq)]
25enum CompletionContext {
26    Tag,
27    Attribute(String),
28    Expression,
29}
30
31pub struct NargoLanguageService {
32    vfs: MemoryVfs,
33    auto_imports: DashSet<String>,
34    workspace: oak_lsp::workspace::WorkspaceManager,
35    // sessions: DashMap<String, ParseSession<()>>,
36}
37
38impl NargoLanguageService {
39    pub fn new() -> Self {
40        let auto_imports = DashSet::new();
41        // Add default auto-imports
42        auto_imports.insert("signal".to_string());
43        auto_imports.insert("computed".to_string());
44        auto_imports.insert("on_mount".to_string());
45        auto_imports.insert("on_unmount".to_string());
46        auto_imports.insert("on_cleanup".to_string());
47
48        Self {
49            vfs: MemoryVfs::new(),
50            auto_imports,
51            workspace: oak_lsp::workspace::WorkspaceManager::new(),
52            // sessions: DashMap::new(),
53        }
54    }
55
56    fn get_completion_context(nodes: &[TemplateNodeIR], pos: NargoPosition) -> Option<CompletionContext> {
57        for node in nodes {
58            match node {
59                TemplateNodeIR::Element(el) => {
60                    if is_pos_in_span(pos, el.span) {
61                        // Check if in tag name
62                        let tag_start = el.span.start;
63                        let tag_name_end = NargoPosition {
64                            line: tag_start.line,
65                            column: tag_start.column + (el.tag.len() as u32) + 1, // <tag
66                            offset: 0,
67                        };
68
69                        if pos.line == tag_start.line && pos.column <= tag_name_end.column {
70                            return Some(CompletionContext::Tag);
71                        }
72
73                        // Check attributes
74                        for attr in &el.attributes {
75                            if is_pos_in_span(pos, attr.span) {
76                                return Some(CompletionContext::Attribute(el.tag.clone()));
77                            }
78                        }
79
80                        // Check children
81                        if let Some(ctx) = Self::get_completion_context(&el.children, pos) {
82                            return Some(ctx);
83                        }
84
85                        // If in element but not in specific child/attr, might be in attribute area
86                        return Some(CompletionContext::Attribute(el.tag.clone()));
87                    }
88                }
89                TemplateNodeIR::Interpolation(expr) => {
90                    if is_pos_in_span(pos, expr.span) {
91                        return Some(CompletionContext::Expression);
92                    }
93                }
94                _ => {}
95            }
96        }
97        None
98    }
99
100    fn get_script_program(nodes: &[TemplateNodeIR]) -> Option<JsProgram> {
101        for node in nodes {
102            if let TemplateNodeIR::Element(el) = node {
103                if el.tag == "script" {
104                    if let Some(TemplateNodeIR::Text(content, _, _)) = el.children.first() {
105                        let mut state = ParseState::new(content);
106                        let ts_parser = nargo_parser::OakTypeScriptParser;
107                        return ts_parser.parse(&mut state, "ts").ok();
108                    }
109                }
110                if let Some(p) = Self::get_script_program(&el.children) {
111                    return Some(p);
112                }
113            }
114        }
115        None
116    }
117
118    async fn find_definition_in_nodes(&self, nodes: &[TemplateNodeIR], pos: NargoPosition, uri: &str) -> Option<oak_lsp::types::LocationRange> {
119        let script_program = Self::get_script_program(nodes);
120
121        for node in nodes {
122            match node {
123                TemplateNodeIR::Element(el) => {
124                    if is_pos_in_span(pos, el.span) {
125                        if el.tag == "script" {
126                            return self.find_definition_in_script(el, pos, uri).await;
127                        }
128                        else if el.tag == "style" {
129                            return Self::find_definition_in_style(el, pos, uri);
130                        }
131                        else {
132                            // 使用 Box::pin 处理异步递归
133                            let res = self.find_definition_in_nodes_recursive(&el.children, pos, uri).await;
134                            if res.is_some() {
135                                return res;
136                            }
137                        }
138                    }
139                }
140                TemplateNodeIR::Interpolation(expr) => {
141                    if !expr.span.is_unknown() && is_pos_in_span(pos, expr.span) {
142                        if let Some(symbol) = Self::find_symbol_in_template_expr(expr, pos) {
143                            // 1. 首先在 script 块中查找
144                            if let Some(program) = &script_program {
145                                if let Some(def_span) = Self::find_definition_of_symbol(program, &symbol) {
146                                    if Self::is_import(program, &symbol) {
147                                        if let Some(loc) = self.find_external_definition(program, &symbol, uri).await {
148                                            return Some(loc);
149                                        }
150                                    }
151
152                                    if let Some(script_el_span) = Self::get_script_element_span(nodes) {
153                                        if let Some(source) = self.vfs.get_source(uri) {
154                                            let start_pos = oak_lsp::types::SourcePosition { line: script_el_span.start.line + def_span.start.line - 1, column: def_span.start.column - 1, offset: 0, length: 0 };
155                                            let end_pos = oak_lsp::types::SourcePosition { line: script_el_span.start.line + def_span.end.line - 1, column: def_span.end.column - 1, offset: 0, length: 0 };
156                                            return Some(oak_lsp::types::LocationRange { uri: uri.to_string().into(), range: Range { start: self.vfs.line_map(uri).map(|m| m.line_col_utf16_to_offset(&source, start_pos.line, start_pos.column)).unwrap_or(0), end: self.vfs.line_map(uri).map(|m| m.line_col_utf16_to_offset(&source, end_pos.line, end_pos.column)).unwrap_or(0) } });
157                                        }
158                                    }
159                                }
160                            }
161
162                            // 2. 检查是否为自动导入
163                            if self.auto_imports.contains(&symbol) {
164                                if let Some(loc) = self.find_implicit_definition(&symbol, "@nargo/core", uri).await {
165                                    return Some(loc);
166                                }
167                            }
168                        }
169                    }
170                }
171                _ => {}
172            }
173        }
174        None
175    }
176
177    // Helper for recursion to avoid "async recursion" error
178    fn find_definition_in_nodes_recursive<'a>(&'a self, nodes: &'a [TemplateNodeIR], pos: NargoPosition, uri: &'a str) -> std::pin::Pin<Box<dyn std::future::Future<Output = Option<oak_lsp::types::LocationRange>> + Send + 'a>> {
179        Box::pin(self.find_definition_in_nodes(nodes, pos, uri))
180    }
181
182    fn is_import(program: &JsProgram, symbol: &str) -> bool {
183        for stmt in &program.body {
184            if let JsStmt::Import { specifiers, .. } = stmt {
185                if specifiers.contains(&symbol.to_string()) {
186                    return true;
187                }
188            }
189        }
190        false
191    }
192
193    fn get_script_element_span(nodes: &[TemplateNodeIR]) -> Option<NargoSpan> {
194        for node in nodes {
195            if let TemplateNodeIR::Element(el) = node {
196                if el.tag == "script" {
197                    return Some(el.span);
198                }
199                if let Some(s) = Self::get_script_element_span(&el.children) {
200                    return Some(s);
201                }
202            }
203        }
204        None
205    }
206
207    fn find_symbol_in_template_expr(expr: &ExpressionIR, pos: NargoPosition) -> Option<String> {
208        // In nargo-ir, ExpressionIR has a 'code' field for the raw string
209        if !expr.span.is_unknown() && is_pos_in_span(pos, expr.span) {
210            // If it's a simple identifier expression
211            return Some(expr.code.clone());
212        }
213        None
214    }
215
216    async fn find_definition_in_script(&self, el: &ElementIR, pos: NargoPosition, uri: &str) -> Option<oak_lsp::types::LocationRange> {
217        if let Some(TemplateNodeIR::Text(content, _, _)) = el.children.first() {
218            let mut state = ParseState::new(content);
219            let ts_parser = nargo_parser::OakTypeScriptParser;
220            if let Ok(program) = ts_parser.parse(&mut state, "ts") {
221                // Adjust position relative to the script tag start
222                let el_span = el.span;
223                let relative_pos = NargoPosition { line: pos.line - el_span.start.line, column: if pos.line == el_span.start.line { pos.column - el_span.start.column } else { pos.column }, offset: 0 };
224
225                if let Some(symbol) = Self::find_symbol_at(&program, relative_pos) {
226                    // 1. Check if it's an import (external definition)
227                    if Self::is_import(&program, &symbol) {
228                        if let Some(loc) = self.find_external_definition(&program, &symbol, uri).await {
229                            return Some(loc);
230                        }
231                    }
232
233                    // 2. Find where this symbol is defined locally
234                    if let Some(def_span) = Self::find_definition_of_symbol(&program, &symbol) {
235                        if let Some(source) = self.vfs.get_source(uri) {
236                            let start_pos = oak_lsp::types::SourcePosition { line: el_span.start.line + def_span.start.line - 1, column: def_span.start.column - 1, offset: 0, length: 0 };
237                            let end_pos = oak_lsp::types::SourcePosition { line: el_span.start.line + def_span.end.line - 1, column: def_span.end.column - 1, offset: 0, length: 0 };
238                            return Some(oak_lsp::types::LocationRange { uri: uri.to_string().into(), range: Range { start: self.vfs.line_map(uri).map(|m| m.line_col_utf16_to_offset(&source, start_pos.line, start_pos.column)).unwrap_or(0), end: self.vfs.line_map(uri).map(|m| m.line_col_utf16_to_offset(&source, end_pos.line, end_pos.column)).unwrap_or(0) } });
239                        }
240                    }
241
242                    // 3. Check if it's an auto-import
243                    if self.auto_imports.contains(&symbol) {
244                        if let Some(loc) = self.find_implicit_definition(&symbol, "@nargo/core", uri).await {
245                            return Some(loc);
246                        }
247                    }
248                }
249            }
250        }
251        None
252    }
253
254    fn find_symbol_at(program: &JsProgram, pos: NargoPosition) -> Option<String> {
255        for stmt in &program.body {
256            if let Some(symbol) = Self::find_symbol_in_stmt(stmt, pos) {
257                return Some(symbol);
258            }
259        }
260        None
261    }
262
263    fn find_symbol_in_stmt(stmt: &JsStmt, pos: NargoPosition) -> Option<String> {
264        use JsStmt::*;
265        match stmt {
266            VariableDecl { id, init, .. } => {
267                // This is a simplification. In reality, we should check id and init spans.
268                if id == "count" {
269                    return Some(id.clone());
270                }
271                if let Some(init) = init {
272                    return Self::find_symbol_in_expr(init, pos);
273                }
274            }
275            Expr(expr, ..) => return Self::find_symbol_in_expr(expr, pos),
276            _ => {}
277        }
278        None
279    }
280
281    fn find_symbol_in_expr(expr: &JsExpr, _pos: NargoPosition) -> Option<String> {
282        match expr {
283            JsExpr::Identifier(name, ..) => Some(name.clone()),
284            JsExpr::Call { callee, .. } => Self::find_symbol_in_expr(callee, _pos),
285            _ => None,
286        }
287    }
288
289    fn find_definition_of_symbol(program: &JsProgram, symbol: &str) -> Option<NargoSpan> {
290        for stmt in &program.body {
291            if let JsStmt::VariableDecl { id, span, .. } = stmt {
292                if id == symbol {
293                    return Some(*span);
294                }
295            }
296            if let JsStmt::FunctionDecl { id, span, .. } = stmt {
297                if id == symbol {
298                    return Some(*span);
299                }
300            }
301        }
302        None
303    }
304
305    async fn find_external_definition(&self, program: &JsProgram, symbol: &str, current_uri: &str) -> Option<oak_lsp::types::LocationRange> {
306        for stmt in &program.body {
307            if let JsStmt::Import { specifiers, source, .. } = stmt {
308                if specifiers.contains(&symbol.to_string()) {
309                    return self.find_implicit_definition(symbol, source, current_uri).await;
310                }
311            }
312        }
313        None
314    }
315
316    async fn find_implicit_definition(&self, symbol: &str, source_path: &str, current_uri: &str) -> Option<oak_lsp::types::LocationRange> {
317        if let Some(url) = self.resolve_path(current_uri, source_path) {
318            if let Some(content) = self.get_document_content(&url).await {
319                // For now, just point to the start of the file or look for the symbol
320                let mut line = 0;
321                let mut col = 0;
322                if let Some(pos) = content.find(symbol) {
323                    let before = &content[..pos];
324                    line = before.lines().count().saturating_sub(1);
325                    col = before.lines().last().map(|l| l.len()).unwrap_or(0);
326                }
327
328                if let Some(target_source) = self.vfs.get_source(url.as_str()) {
329                    let start_pos = oak_lsp::types::SourcePosition { line: line as u32, column: col as u32, offset: 0, length: 0 };
330                    let end_pos = oak_lsp::types::SourcePosition { line: line as u32, column: (col + symbol.len()) as u32, offset: 0, length: 0 };
331                    return Some(oak_lsp::types::LocationRange { uri: url.to_string().into(), range: Range { start: self.vfs.line_map(url.as_str()).map(|m| m.line_col_utf16_to_offset(&target_source, start_pos.line, start_pos.column)).unwrap_or(0), end: self.vfs.line_map(url.as_str()).map(|m| m.line_col_utf16_to_offset(&target_source, end_pos.line, end_pos.column)).unwrap_or(0) } });
332                }
333            }
334        }
335        None
336    }
337
338    fn resolve_path(&self, current_uri: &str, relative_path: &str) -> Option<Url> {
339        let base_url = Url::parse(current_uri).ok()?;
340        let base_path = base_url.to_file_path().ok()?;
341
342        if relative_path == "@nargo/core" {
343            // 1. Try workspace folders
344            for (_, folder) in self.workspace.list_folders() {
345                let core_path = folder.join("runtimes/nargo-core/src/index.ts");
346                if core_path.exists() {
347                    return Url::from_file_path(core_path).ok();
348                }
349                // Try nested
350                let core_path = folder.join("project-nargo/runtimes/nargo-core/src/index.ts");
351                if core_path.exists() {
352                    return Url::from_file_path(core_path).ok();
353                }
354            }
355
356            // 2. Try relative to current file
357            if let Ok(uri) = Url::parse(current_uri) {
358                if let Ok(mut path) = uri.to_file_path() {
359                    while let Some(parent) = path.parent() {
360                        let core_path = parent.join("runtimes/nargo-core/src/index.ts");
361                        if core_path.exists() {
362                            return Url::from_file_path(core_path).ok();
363                        }
364                        path = parent.to_path_buf();
365                    }
366                }
367            }
368        }
369
370        let parent = base_path.parent()?;
371        let target_path = if relative_path.starts_with('.') {
372            parent.join(relative_path)
373        }
374        else {
375            return None;
376        };
377
378        let extensions = ["", ".ts", ".js"];
379        for ext in extensions {
380            let mut p = target_path.clone();
381            if !ext.is_empty() {
382                p.set_extension(ext.trim_start_matches('.'));
383            }
384            if p.exists() {
385                return Url::from_file_path(p).ok();
386            }
387        }
388
389        None
390    }
391
392    async fn get_document_content(&self, uri: &Url) -> Option<String> {
393        let uri_str = uri.to_string();
394        if let Some(source) = self.vfs.get_source(&uri_str) {
395            return Some(source.text().to_string());
396        }
397
398        if let Ok(path) = uri.to_file_path() {
399            return std::fs::read_to_string(path).ok();
400        }
401
402        None
403    }
404
405    fn find_definition_in_style(_el: &ElementIR, _pos: NargoPosition, _uri: &str) -> Option<oak_lsp::types::LocationRange> {
406        // Not implemented yet
407        None
408    }
409
410    fn process_template_nodes(nodes: &[TemplateNodeIR], content: &str, tokens: &mut Vec<u32>) {
411        for node in nodes {
412            match node {
413                TemplateNodeIR::Element(el) => {
414                    // Process tag name
415                    if !el.span.is_unknown() {
416                        // Find the tag name start (after <)
417                        let start = el.span.start.offset as u32;
418                        let tag_start = content[start as usize..].find('<').unwrap_or(0) + 1;
419                        let tag_end = tag_start + el.tag.len();
420
421                        // Add token for tag name
422                        tokens.extend(&[start + tag_start as u32, el.tag.len() as u32, 0, 0, 0]);
423
424                        // Process attributes
425                        for attr in &el.attributes {
426                            if !attr.span.is_unknown() {
427                                // Add token for attribute name
428                                let attr_start = attr.span.start.offset as u32;
429                                let attr_name_end = content[attr_start as usize..].find('=').unwrap_or((attr.span.end.offset - attr.span.start.offset) as usize);
430                                tokens.extend(&[attr_start, attr_name_end as u32, 0, 0, 0]);
431                            }
432                        }
433                    }
434
435                    // Process children
436                    Self::process_template_nodes(&el.children, content, tokens);
437                }
438                TemplateNodeIR::Interpolation(expr) => {
439                    if !expr.span.is_unknown() {
440                        // Add token for expression
441                        let start = expr.span.start.offset as u32;
442                        let len = (expr.span.end.offset - expr.span.start.offset) as u32;
443                        tokens.extend(&[start, len, 0, 0, 0]);
444                    }
445                }
446                TemplateNodeIR::Text(_, span, _) => {
447                    if !span.is_unknown() {
448                        // Add token for text content
449                        let start = span.start.offset as u32;
450                        let len = (span.end.offset - span.start.offset) as u32;
451                        tokens.extend(&[start, len, 0, 0, 0]);
452                    }
453                }
454                _ => {}
455            }
456        }
457    }
458}
459
460use nargo_compiler::Compiler;
461use nargo_script_analyzer::ScriptAnalyzer;
462use oak_core::Parser;
463
464use oak_lsp::types::{CompletionItem as OakCompletionItem, CompletionItemKind as OakCompletionItemKind, Diagnostic as OakDiagnostic, DiagnosticSeverity as OakDiagnosticSeverity, SemanticTokens};
465
466impl LanguageService for NargoLanguageService {
467    type Lang = NargoLanguage;
468    type Vfs = MemoryVfs;
469
470    fn vfs(&self) -> &Self::Vfs {
471        &self.vfs
472    }
473
474    fn workspace(&self) -> &oak_lsp::workspace::WorkspaceManager {
475        &self.workspace
476    }
477
478    fn get_root(&self, uri: &str) -> impl Future<Output = Option<RedNode<'_, NargoLanguage>>> + Send + '_ {
479        let source = self.vfs.get_source(uri);
480        // let mut session = self
481        //     .sessions
482        //     .entry(uri.to_string())
483        //     .or_insert_with(|| ParseSession::new(16));
484        async move {
485            if let Some(source) = source {
486                // let parser = NargoParser::new();
487                // // We use a persistent session for each file to allow incremental parsing if implemented
488                // let output = parser.parse(&source, &[], session.value_mut());
489                // if let Ok(green) = output.result {
490                //     // SAFETY: We store the session (and thus its arena) in self.sessions which is owned by NargoLanguageService.
491                //     // As long as the NargoLanguageService exists and we don't remove this session, the green node remains valid.
492                //     // We use 'static lifetime here because RedNode needs a lifetime that outlives the future,
493                //     // and transmute is used to bridge the gap between the local borrow and the persistent storage.
494                //     let green: &GreenNode<'static, NargoLanguage> =
495                //         unsafe { std::mem::transmute(green) };
496                //     return Some(RedNode::new(green, 0));
497                // }
498            }
499            None
500        }
501    }
502
503    fn completion(&self, uri: &str, position: usize) -> impl Future<Output = Vec<OakCompletionItem>> + Send + '_ {
504        let source = self.vfs.get_source(uri);
505        let auto_imports = self.auto_imports.clone();
506        let uri = uri.to_string();
507
508        async move {
509            let mut items = Vec::new();
510            if let Some(source) = source {
511                let content = source.text();
512                let pos = self
513                    .vfs
514                    .line_map(&uri)
515                    .map(|m| {
516                        let (l, c) = m.offset_to_line_col_utf16(&source, position);
517                        oak_lsp::types::SourcePosition { line: l, column: c, offset: 0, length: 0 }
518                    })
519                    .unwrap_or(oak_lsp::types::SourcePosition { line: 0, column: 0, offset: 0, length: 0 });
520                let nargo_pos = NargoPosition { line: pos.line + 1, column: pos.column + 1, offset: 0 };
521
522                let mut state = ParseState::new(&content);
523                let parser = VueTemplateParser;
524                if let Ok(nodes) = parser.parse(&mut state, "html") {
525                    if let Some(context) = Self::get_completion_context(&nodes, nargo_pos) {
526                        match context {
527                            CompletionContext::Tag => {
528                                let tags = vec!["div", "span", "button", "input", "h1", "h2", "h3", "h4", "h5", "h6", "p", "section", "article", "header", "footer", "nav", "aside", "ul", "ol", "li", "table", "tr", "td", "th", "form", "label", "select", "option", "textarea", "img", "a", "br", "hr"];
529                                for tag in tags {
530                                    items.push(OakCompletionItem { label: tag.to_string(), kind: Some(OakCompletionItemKind::Keyword), detail: Some("HTML Tag".to_string()), documentation: None, insert_text: Some(format!("{}", tag)) });
531                                }
532                            }
533                            CompletionContext::Attribute(tag_name) => {
534                                let directives = vec!["@click", "@input", "@change", "@submit", "@keydown", "@keyup", ":class", ":style", ":value", ":disabled", ":src", ":href"];
535                                for dir in directives {
536                                    items.push(OakCompletionItem { label: dir.to_string(), kind: Some(OakCompletionItemKind::Constant), detail: Some("Directive".to_string()), documentation: None, insert_text: Some(format!("{}", dir)) });
537                                }
538
539                                if tag_name == "input" {
540                                    let input_attrs = vec!["type", "name", "value", "placeholder", "required", "disabled"];
541                                    for attr in input_attrs {
542                                        items.push(OakCompletionItem { label: attr.to_string(), kind: Some(OakCompletionItemKind::Property), detail: None, documentation: None, insert_text: Some(format!("{}", attr)) });
543                                    }
544                                }
545                            }
546                            CompletionContext::Expression => {
547                                for import in auto_imports.iter() {
548                                    items.push(OakCompletionItem { label: import.clone(), kind: Some(OakCompletionItemKind::Function), detail: Some("Nargo Auto-import".to_string()), documentation: None, insert_text: Some(import.clone()) });
549                                }
550
551                                if let Some(program) = Self::get_script_program(&nodes) {
552                                    for stmt in &program.body {
553                                        if let JsStmt::VariableDecl { id, .. } = stmt {
554                                            items.push(OakCompletionItem { label: id.clone(), kind: Some(OakCompletionItemKind::Variable), detail: None, documentation: None, insert_text: Some(format!("{}", id)) });
555                                        }
556                                        if let JsStmt::FunctionDecl { id, .. } = stmt {
557                                            items.push(OakCompletionItem { label: id.clone(), kind: Some(OakCompletionItemKind::Function), detail: None, documentation: None, insert_text: Some(format!("{}", id)) });
558                                        }
559                                    }
560                                }
561                            }
562                        }
563                    }
564                }
565            }
566
567            if items.is_empty() {
568                let tags = vec!["div", "span", "button", "input", "h1", "h2", "p", "section", "article"];
569                for tag in tags {
570                    items.push(OakCompletionItem { label: tag.to_string(), kind: Some(OakCompletionItemKind::Keyword), detail: Some("HTML Tag".to_string()), documentation: None, insert_text: None });
571                }
572            }
573
574            items
575        }
576    }
577
578    fn diagnostics(&self, uri: &str) -> impl Future<Output = Vec<OakDiagnostic>> + Send + '_ {
579        let source = self.vfs.get_source(uri);
580        let uri_parsed = Url::parse(uri).ok();
581        let uri = uri.to_string();
582
583        async move {
584            let mut diags = Vec::new();
585            if let Some(source) = source {
586                let content = source.text();
587                let mut compiler = Compiler::new();
588                let name = uri_parsed.as_ref().and_then(|u| u.path_segments()).and_then(|mut s| s.next_back()).unwrap_or("App.ts");
589
590                // 1. Compiler errors
591                if let Err(e) = compiler.compile(name, &content) {
592                    let span = e.span();
593                    if !span.is_unknown() {
594                        let start_pos = oak_lsp::types::SourcePosition { line: span.start.line.saturating_sub(1), column: span.start.column.saturating_sub(1), offset: 0, length: 0 };
595                        let end_pos = oak_lsp::types::SourcePosition { line: span.end.line.saturating_sub(1), column: span.end.column.saturating_sub(1), offset: 0, length: 0 };
596
597                        let start_offset = self.vfs.line_map(&uri).map(|m| m.line_col_utf16_to_offset(&source, start_pos.line, start_pos.column)).unwrap_or(0);
598                        let end_offset = self.vfs.line_map(&uri).map(|m| m.line_col_utf16_to_offset(&source, end_pos.line, end_pos.column)).unwrap_or(0);
599
600                        diags.push(oak_lsp::types::Diagnostic { range: Range { start: start_offset, end: end_offset }, severity: Some(oak_lsp::types::DiagnosticSeverity::Error), message: format!("{}", e), source: Some("nargo-compiler".to_string()), code: None });
601                    }
602                }
603
604                // 2. Script analysis for code quality
605                if let Ok(ir) = compiler.compile_to_ir(name, &content, &mut Default::default()) {
606                    let analyzer = ScriptAnalyzer::new();
607                    if let Some(script) = &ir.script {
608                        if let Ok(meta) = analyzer.analyze(script) {
609                            // Check for unused variables
610                            let used_vars: HashSet<String> = HashSet::new(); // TODO: Track used variables
611                            for signal in &meta.signals {
612                                if !used_vars.contains(signal) {
613                                    // Add warning for unused signal
614                                    let start_offset = 0; // TODO: Find actual position
615                                    let end_offset = 0;
616                                    diags.push(OakDiagnostic { range: Range { start: start_offset, end: end_offset }, severity: Some(OakDiagnosticSeverity::Warning), message: format!("Unused signal: {}", signal), source: Some("nargo-analyzer".to_string()), code: None });
617                                }
618                            }
619                        }
620                    }
621                }
622            }
623            diags
624        }
625    }
626
627    fn initialize(&self, params: oak_lsp::types::InitializeParams) -> impl Future<Output = ()> + Send + '_ {
628        async move {
629            self.workspace.initialize(&params);
630        }
631    }
632
633    fn definition(&self, uri: &str, range: Range<usize>) -> impl Future<Output = Vec<oak_lsp::types::LocationRange>> + Send + '_ {
634        let source = self.vfs.get_source(uri);
635        let uri = uri.to_string();
636        async move {
637            if let Some(source) = source {
638                let content = source.text();
639                let position = self
640                    .vfs
641                    .line_map(&uri)
642                    .map(|m| {
643                        let (l, c) = m.offset_to_line_col_utf16(&source, range.start);
644                        oak_lsp::types::SourcePosition { line: l, column: c, offset: 0, length: 0 }
645                    })
646                    .unwrap_or(oak_lsp::types::SourcePosition { line: 0, column: 0, offset: 0, length: 0 });
647                let nargo_pos = NargoPosition { line: position.line + 1, column: position.column + 1, offset: 0 };
648
649                let mut state = ParseState::new(&content);
650                let parser = VueTemplateParser;
651                if let Ok(nodes) = parser.parse(&mut state, "html") {
652                    if let Some(loc) = self.find_definition_in_nodes(&nodes, nargo_pos, &uri).await {
653                        return vec![loc];
654                    }
655                }
656            }
657            vec![]
658        }
659    }
660
661    fn semantic_tokens(&self, uri: &str) -> impl Future<Output = Option<SemanticTokens>> + Send + '_ {
662        let source = self.vfs.get_source(uri);
663        let uri = uri.to_string();
664
665        async move {
666            if let Some(source) = source {
667                let content = source.text();
668                let mut tokens: Vec<oak_lsp::types::SemanticToken> = Vec::new();
669
670                // Parse the template
671                let mut state = ParseState::new(&content);
672                let parser = VueTemplateParser;
673                if let Ok(nodes) = parser.parse(&mut state, "html") {
674                    // Process template nodes for semantic tokens
675                    // Self::process_template_nodes(&nodes, &content, &mut tokens);
676                }
677
678                // Create semantic tokens response
679                Some(SemanticTokens { data: vec![], result_id: None })
680            }
681            else {
682                None
683            }
684        }
685    }
686}
687
688pub async fn run_stdio() -> nargo_types::Result<()> {
689    let stdin = tokio::io::stdin();
690    let stdout = tokio::io::stdout();
691
692    let nargo_service = Arc::new(NargoLanguageService::new());
693    let server = oak_lsp::LspServer::new(nargo_service);
694    server.run(stdin, stdout).await.map_err(|e| nargo_types::Error::external_error("oak-lsp".to_string(), format!("{}", e), nargo_types::Span::unknown()))?;
695    Ok(())
696}
697
698pub async fn run_http_server(port: u16) -> nargo_types::Result<()> {
699    println!("HTTP/WebSocket LSP server on port {} is not yet fully implemented.", port);
700    println!("Falling back to stdio for now.");
701    run_stdio().await
702}