diff --git a/Cargo.lock b/Cargo.lock index c307a79..ffce2df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,15 +17,6 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" -[[package]] -name = "analysis" -version = "0.1.0" -dependencies = [ - "codespan", - "indexmap", - "parser", -] - [[package]] name = "anyhow" version = "1.0.97" @@ -81,11 +72,11 @@ checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" name = "ca65-lsp" version = "0.1.0" dependencies = [ - "analysis", "anyhow", "codespan", "lazy_static", "parser", + "path-clean", "serde", "serde_json", "streaming-iterator", @@ -570,6 +561,12 @@ dependencies = [ "codespan", ] +[[package]] +name = "path-clean" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17359afc20d7ab31fdb42bb844c8b3bb1dabd7dcf7e68428492da7f16966fcef" + [[package]] name = "percent-encoding" version = "2.3.1" diff --git a/Cargo.toml b/Cargo.toml index ce7d39a..676dba1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,10 @@ [workspace] -members = ["analysis", "codespan", "lsp", "parser"] +members = ["codespan", "lsp", "parser"] resolver = "2" +[workspace.package] +edition = "2024" + [workspace.dependencies] lsp-types = { version = "^0.97.0" } tower-lsp-server = "0.21.0" \ No newline at end of file diff --git a/analysis/Cargo.toml b/analysis/Cargo.toml deleted file mode 100644 index b5bf1a2..0000000 --- a/analysis/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "analysis" -version = "0.1.0" -edition = "2024" - -[dependencies] -indexmap = "2.9.0" -parser = { path = "../parser" } -codespan = { path = "../codespan" } \ No newline at end of file diff --git a/analysis/src/arena.rs b/analysis/src/arena.rs deleted file mode 100644 index c17cbc9..0000000 --- a/analysis/src/arena.rs +++ /dev/null @@ -1,62 +0,0 @@ -use std::ops::{Index, IndexMut}; - -pub struct Arena -where - T: PartialEq, -{ - data: Vec, -} - -impl Arena -where - T: PartialEq, -{ - pub fn new() -> Arena { - Arena { data: Vec::new() } - } - - pub fn alloc(&mut self, item: T) -> usize { - let idx = self.data.len(); - self.data.push(item); - idx - } -} - -impl Index for Arena -where - T: PartialEq, -{ - type Output = T; - - fn index(&self, index: usize) -> &T { - &self.data[index] - } -} - -impl IndexMut for Arena -where - T: PartialEq, -{ - fn index_mut(&mut self, index: usize) -> &mut T { - &mut self.data[index] - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_arena() { - let mut arena = Arena::new(); - let a = arena.alloc(1); - let b = arena.alloc(2); - let c = arena.alloc(3); - let d = arena.alloc(4); - - assert_eq!(arena.data[a], 1); - assert_eq!(arena.data[b], 2); - assert_eq!(arena.data[c], 3); - assert_eq!(arena.data[d], 4); - } -} diff --git a/analysis/src/def_analyzer.rs b/analysis/src/def_analyzer.rs deleted file mode 100644 index 23d8316..0000000 --- a/analysis/src/def_analyzer.rs +++ /dev/null @@ -1,130 +0,0 @@ -use codespan::Span; -use parser::{Statement, StatementKind, Token}; -use std::collections::HashMap; -use std::fmt::Write; - -pub struct DefAnalyzer { - statements: Vec, - symtab: HashMap, -} - -// TODO: these definitions are murky, this is more of a symbol analyzer -pub enum ScopeKind { - Label, - Macro, - Constant, - Parameter, -} - -pub struct SymDef { - pub title: String, - pub description: String, - pub span: Span, - pub kind: ScopeKind, -} - -impl DefAnalyzer { - pub fn new(statements: Vec) -> DefAnalyzer { - DefAnalyzer { - statements, - symtab: HashMap::new(), - } - } - - pub fn parse(mut self) -> HashMap { - for line in self.statements.iter() { - self.symtab.extend(Self::parse_statement(line)); - } - - self.symtab - } - - fn parse_statement(line: &Statement) -> Vec<(String, SymDef)> { - match &line.kind { - StatementKind::ConstantAssign(assign) => { - vec![( - assign.name.lexeme.clone(), - SymDef { - title: assign.name.lexeme.clone(), - description: assign.name.lexeme.clone(), // TODO: Add expression flattening/preview? Might need method on ast nodes to print formatted output - span: assign.name.span, - kind: ScopeKind::Constant, - }, - )] - } - StatementKind::Label(label) => { - vec![( - label.lexeme.clone(), - SymDef { - title: label.lexeme.clone(), - description: format!("{}:", label.lexeme.clone()), - span: label.span, - kind: ScopeKind::Label, - }, - )] - } - StatementKind::Procedure(name, _far, instructions) => { - let mut symbols = vec![( - name.lexeme.clone(), - SymDef { - title: name.lexeme.clone(), - description: format!("{}:", name.lexeme.clone()), - span: name.span, - kind: ScopeKind::Label, - }, - )]; - - for line in instructions.iter() { - symbols.extend(Self::parse_statement(line)); - } - - symbols - } - StatementKind::MacroDefinition(name, parameters, _) => { - let mut symbols = vec![( - name.lexeme.clone(), - SymDef { - title: name.lexeme.clone(), - description: format_parameters(name, parameters), - span: name.span, - kind: ScopeKind::Macro, - }, - )]; - - for line in parameters.iter() { - symbols.push(( - line.lexeme.clone(), - SymDef { - title: line.lexeme.clone(), - description: line.lexeme.clone(), - span: line.span, - kind: ScopeKind::Parameter, - }, - )); - } - - symbols - } - StatementKind::Instruction(_instruction) => { - vec![] - } - _ => vec![], - } - } -} - -fn format_parameters(name: &Token, parameters: &[Token]) -> String { - let mut output = String::new(); - - write!(&mut output, ".macro {} ", name.lexeme).unwrap(); - - for (i, token) in parameters.iter().enumerate() { - match i { - 0 => write!(output, "{}", token.lexeme), - _ => write!(output, ", {}", token.lexeme), - } - .unwrap() - } - - output -} diff --git a/analysis/src/lib.rs b/analysis/src/lib.rs deleted file mode 100644 index c7a7b29..0000000 --- a/analysis/src/lib.rs +++ /dev/null @@ -1,9 +0,0 @@ -pub mod arena; -pub mod def_analyzer; -pub mod scope_analyzer; -pub mod symbol_resolver; -pub mod visitor; - -pub use def_analyzer::*; -pub use scope_analyzer::*; -pub use symbol_resolver::*; \ No newline at end of file diff --git a/codespan/src/file.rs b/codespan/src/file.rs index 787eb31..2a01bcf 100644 --- a/codespan/src/file.rs +++ b/codespan/src/file.rs @@ -4,6 +4,7 @@ use core::panic; use std::fmt::{Display, Formatter}; #[allow(dead_code)] +#[derive(Debug, Clone)] pub struct File { pub name: String, pub source: String, diff --git a/codespan/src/file_id.rs b/codespan/src/file_id.rs index 0135b8a..660ac55 100644 --- a/codespan/src/file_id.rs +++ b/codespan/src/file_id.rs @@ -1,10 +1,11 @@ +use std::fmt::Display; #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct FileId(u32); impl FileId { const OFFSET: u32 = 1; - pub const NONE: Self = FileId(0); + pub const NONE: Self = FileId(0); pub fn new(index: usize) -> FileId { FileId(index as u32 + Self::OFFSET) @@ -13,4 +14,10 @@ impl FileId { pub fn get(self) -> usize { (self.0 - Self::OFFSET) as usize } -} \ No newline at end of file +} + +impl Display for FileId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} diff --git a/lsp/Cargo.toml b/lsp/Cargo.toml index 92ac06a..0594030 100644 --- a/lsp/Cargo.toml +++ b/lsp/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ca65-lsp" version = "0.1.0" -edition = "2021" +edition = "2024" [dependencies] anyhow = "1.0.93" @@ -15,9 +15,9 @@ toml = "0.8.19" tempfile = "3.14.0" streaming-iterator = "0.1.9" parser = { path = "../parser" } -analysis = { path = "../analysis" } codespan = { path = "../codespan", features = ["lsp"] } lazy_static = "1.5.0" walkdir = "2.5.0" uuid = { version = "1.17.0", features = ["v4"] } -url = "2.5.4" \ No newline at end of file +url = "2.5.4" +path-clean = "1.0.1" \ No newline at end of file diff --git a/lsp/src/analysis/mod.rs b/lsp/src/analysis/mod.rs new file mode 100644 index 0000000..c55e233 --- /dev/null +++ b/lsp/src/analysis/mod.rs @@ -0,0 +1,3 @@ +pub mod scope_analyzer; +pub mod symbol_resolver; +pub mod visitor; diff --git a/analysis/src/scope_analyzer.rs b/lsp/src/analysis/scope_analyzer.rs similarity index 94% rename from analysis/src/scope_analyzer.rs rename to lsp/src/analysis/scope_analyzer.rs index 224eb29..c255f38 100644 --- a/analysis/src/scope_analyzer.rs +++ b/lsp/src/analysis/scope_analyzer.rs @@ -1,5 +1,6 @@ -use crate::visitor::ASTVisitor; -use codespan::{FileId, Span}; +use crate::analysis::visitor::ASTVisitor; +use crate::cache_file::Include; +use codespan::Span; use parser::{ Ast, ConstantAssign, EnumMember, Expression, ImportExport, Statement, StructMember, Token, }; @@ -75,12 +76,6 @@ pub struct Scope { pub children: Vec, } -#[derive(Clone, Debug)] -pub struct Include { - pub file: FileId, - pub scope: Vec, -} - impl Scope { fn find_inner_scope(&self, index: usize) -> Option> { if index < self.span.start || index >= self.span.end { @@ -97,6 +92,12 @@ impl Scope { } } +impl PartialEq for Scope { + fn eq(&self, other: &Self) -> bool { + self.name == other.name + } +} + pub struct ScopeAnalyzer { pub ast: Vec, pub stack: Vec, @@ -354,9 +355,20 @@ impl ASTVisitor for ScopeAnalyzer { } } - fn visit_include(&mut self, _path: &Token, _span: Span) { + fn visit_global(&mut self, identifiers: &[Token], _zero_page: &bool, _span: Span) { + for identifier in identifiers { + self.insert_symbol( + identifier, + Symbol::Constant { + name: identifier.clone(), + }, + ); + } + } + + fn visit_include(&mut self, path: &Token, _span: Span) { self.includes.push(Include { - file: FileId::new(0), + path: path.clone(), scope: self.stack.clone(), }) } diff --git a/analysis/src/symbol_resolver.rs b/lsp/src/analysis/symbol_resolver.rs similarity index 98% rename from analysis/src/symbol_resolver.rs rename to lsp/src/analysis/symbol_resolver.rs index 7e41745..d8fd9c1 100644 --- a/analysis/src/symbol_resolver.rs +++ b/lsp/src/analysis/symbol_resolver.rs @@ -1,4 +1,4 @@ -use crate::visitor::ASTVisitor; +use crate::analysis::visitor::ASTVisitor; use codespan::Span; use parser::{Ast, EnumMember, Expression, ImportExport, Statement, StructMember, Token}; diff --git a/analysis/src/visitor.rs b/lsp/src/analysis/visitor.rs similarity index 100% rename from analysis/src/visitor.rs rename to lsp/src/analysis/visitor.rs diff --git a/lsp/src/asm_server.rs b/lsp/src/asm_server.rs index cf3c2a2..7e374e8 100644 --- a/lsp/src/asm_server.rs +++ b/lsp/src/asm_server.rs @@ -1,44 +1,43 @@ +use crate::analysis::scope_analyzer::Scope; use crate::cache_file::CacheFile; -use crate::codespan::Files; use crate::completion::{ Ca65DotOperatorCompletionProvider, Ca65KeywordCompletionProvider, CompletionProvider, FeatureCompletionProvider, InstructionCompletionProvider, MacpackCompletionProvider, SymbolCompletionProvider, }; use crate::data::configuration::Configuration; +use crate::data::files::Files; use crate::definition::Definition; use crate::documentation::DOCUMENTATION_COLLECTION; use crate::error::file_error_to_lsp; use crate::index_engine::IndexEngine; use crate::state::State; -use analysis::Scope; use codespan::FileId; use codespan::{File, Span}; -use std::collections::HashMap; use std::path::Path; use std::process::Output; use std::str::FromStr; use std::sync::Arc; use tokio::sync::Mutex; use tower_lsp_server::lsp_types::{ - ClientCapabilities, CodeActionParams, CodeActionProviderCapability, CodeActionResponse, - CompletionItem, CompletionOptions, CompletionParams, CompletionResponse, Diagnostic, - DiagnosticSeverity, DidChangeWatchedFilesParams, DidChangeWorkspaceFoldersParams, - DocumentSymbol, DocumentSymbolParams, DocumentSymbolResponse, FileOperationRegistrationOptions, - FoldingRange, FoldingRangeParams, FoldingRangeProviderCapability, HoverContents, - HoverProviderCapability, InitializedParams, InlayHint, InlayHintLabel, InlayHintParams, - LocationLink, MarkupContent, MarkupKind, MessageType, OneOf, Registration, SymbolKind, + CodeActionParams, CodeActionProviderCapability, CodeActionResponse, CompletionItem, + CompletionOptions, CompletionParams, CompletionResponse, Diagnostic, DiagnosticSeverity, + DidChangeWatchedFilesParams, DidChangeWorkspaceFoldersParams, DocumentSymbol, + DocumentSymbolParams, DocumentSymbolResponse, FileOperationRegistrationOptions, FoldingRange, + FoldingRangeParams, FoldingRangeProviderCapability, HoverContents, HoverProviderCapability, + InitializedParams, InlayHint, InlayHintLabel, InlayHintParams, LocationLink, MarkupContent, + MarkupKind, MessageType, OneOf, Registration, SymbolKind, WorkspaceFileOperationsServerCapabilities, WorkspaceFoldersServerCapabilities, WorkspaceServerCapabilities, }; use tower_lsp_server::{ + Client, LanguageServer, jsonrpc::Result, lsp_types::{ DidChangeTextDocumentParams, DidOpenTextDocumentParams, GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverParams, InitializeParams, InitializeResult, MarkedString, ServerCapabilities, TextDocumentSyncCapability, TextDocumentSyncKind, Uri, }, - Client, LanguageServer, }; #[allow(dead_code)] @@ -53,13 +52,7 @@ pub struct Asm { impl Asm { pub fn new(client: Client) -> Self { - let state = Arc::new(Mutex::new(State { - sources: HashMap::new(), - files: Files::new(), - workspace_folder: None, - client: client.clone(), - client_capabilities: ClientCapabilities::default(), - })); + let state = Arc::new(Mutex::new(State::new(client.clone()))); Asm { client, state: state.clone(), @@ -77,11 +70,39 @@ impl Asm { } } - async fn index(&self, file_id: &FileId) { + async fn index(&self, file_id: FileId) { let mut state = self.state.lock().await; - let file = state.files.get_mut(*file_id); - let diagnostics = [file.parse_labels().await, file.lint().await].concat(); - state.publish_diagnostics(*file_id, diagnostics).await; + let indexing_state = state.files.index(file_id).await; + let units = state.units.find_related(file_id); + + if indexing_state.includes_changed { + for unit in units.iter() { + // TODO: handle diagnostics + let (deps, _diagnostics) = IndexEngine::calculate_deps(&mut state.files, *unit); + state.units.insert(file_id, deps); + } + } + + for unit in units.iter() { + let symbols = IndexEngine::get_symbol_tree(&mut state.files, *unit); + state.units[*unit].symbols = symbols; + } + + // diagnostics.extend(IndexEngine::invalidate(&mut state, file_id).await); + + // eprintln!( + // "Affected Files: {:#?}", + // state + // .units + // .find_related(file_id) + // .iter() + // .map(|id| { state.files.get_uri(*id).as_str().to_owned() }) + // .collect::>() + // ); + + state + .publish_diagnostics(file_id, indexing_state.diagnostics) + .await; } async fn load_config(&self, path: &Path) -> Result<()> { @@ -222,13 +243,6 @@ impl LanguageServer for Asm { .await .unwrap(); } - // _ = self - // .client - // .progress(ProgressToken::String("load".to_string()), "Loading") - // .with_message("Indexing") - // .with_percentage(50) - // .begin() - // .await; } async fn shutdown(&self) -> Result<()> { @@ -240,7 +254,7 @@ impl LanguageServer for Asm { let id = state.get_or_insert_source(params.text_document.uri, params.text_document.text); drop(state); - self.index(&id).await; + self.index(id).await; } async fn did_change(&self, params: DidChangeTextDocumentParams) { @@ -248,7 +262,7 @@ impl LanguageServer for Asm { let id = state.reload_source(¶ms.text_document, params.content_changes); drop(state); - self.index(&id).await; + self.index(id).await; } async fn goto_definition( @@ -258,6 +272,7 @@ impl LanguageServer for Asm { let state = self.state.lock().await; if let Some(id) = state + .files .sources .get(¶ms.text_document_position_params.text_document.uri) { @@ -308,6 +323,7 @@ impl LanguageServer for Asm { let state = self.state.lock().await; if let Some(id) = state + .files .sources .get(¶ms.text_document_position_params.text_document.uri) { @@ -363,37 +379,14 @@ impl LanguageServer for Asm { ) -> Result> { let state = self.state.lock().await; - if let Some(id) = state.sources.get(¶ms.text_document.uri) { + if let Some(id) = state.files.sources.get(¶ms.text_document.uri) { let mut symbols = vec![]; let file = state.files.get(*id); - fn scope_to_symbol(scope: &Scope, file: &CacheFile) -> DocumentSymbol { - let range = file.file.byte_span_to_range(scope.span).unwrap().into(); - DocumentSymbol { - name: scope.name.clone(), - detail: None, - kind: SymbolKind::NAMESPACE, - tags: None, - deprecated: None, - range, - selection_range: range, - children: { - let children: Vec = scope - .children - .iter() - .map(|child| scope_to_symbol(child, file)) - .collect(); - if children.is_empty() { - None - } else { - Some(children) - } - }, - } - } - for symbol in file.scopes.iter() { - symbols.push(scope_to_symbol(symbol, file)); + if let Some(symbol) = scope_to_symbol(symbol, file) { + symbols.push(symbol); + } } return Ok(Some(DocumentSymbolResponse::Nested(symbols))); } @@ -404,6 +397,7 @@ impl LanguageServer for Asm { let state = self.state.lock().await; if let Some(id) = state + .files .sources .get(¶ms.text_document_position.text_document.uri) { @@ -494,7 +488,7 @@ impl LanguageServer for Asm { async fn folding_range(&self, params: FoldingRangeParams) -> Result>> { let state = self.state.lock().await; - if let Some(id) = state.sources.get(¶ms.text_document.uri) { + if let Some(id) = state.files.sources.get(¶ms.text_document.uri) { let file = &state.files.get(*id); Ok(Some( file.scopes @@ -509,7 +503,7 @@ impl LanguageServer for Asm { async fn inlay_hint(&self, params: InlayHintParams) -> Result>> { let state = self.state.lock().await; - if let Some(id) = state.sources.get(¶ms.text_document.uri) { + if let Some(id) = state.files.sources.get(¶ms.text_document.uri) { let file = &state.files.get(*id); Ok(Some( file.scopes @@ -524,45 +518,80 @@ impl LanguageServer for Asm { } fn scope_to_folding_range(file: &File, scope: &Scope) -> Vec { - let range = file.byte_span_to_range(scope.span).unwrap(); - let mut results = vec![FoldingRange { - start_line: range.start.line as u32, - start_character: None, - end_line: (range.end.line - 1) as u32, - end_character: None, - kind: None, - collapsed_text: None, - }]; - - results.extend( - scope - .children - .iter() - .flat_map(|scope| scope_to_folding_range(file, scope)), - ); - - results + if let Ok(range) = file.byte_span_to_range(scope.span) { + let mut results = vec![FoldingRange { + start_line: range.start.line as u32, + start_character: None, + end_line: (range.end.line - 1) as u32, + end_character: None, + kind: None, + collapsed_text: None, + }]; + + results.extend( + scope + .children + .iter() + .flat_map(|scope| scope_to_folding_range(file, scope)), + ); + + results + } else { + Vec::new() + } } fn scope_to_inlay_hint(file: &File, scope: &Scope) -> Vec { - let range = file.byte_span_to_range(scope.span).unwrap(); - let mut results = vec![InlayHint { - position: range.end.into(), - label: InlayHintLabel::String(scope.name.clone()), - kind: None, - text_edits: None, - tooltip: None, - padding_left: Some(true), - padding_right: None, - data: None, - }]; - - results.extend( - scope - .children - .iter() - .flat_map(|scope| scope_to_inlay_hint(file, scope)), - ); - - results + if let Ok(range) = file.byte_span_to_range(scope.span) { + let mut results = vec![InlayHint { + position: range.end.into(), + label: InlayHintLabel::String(scope.name.clone()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: Some(true), + padding_right: None, + data: None, + }]; + + results.extend( + scope + .children + .iter() + .flat_map(|scope| scope_to_inlay_hint(file, scope)), + ); + + results + } else { + Vec::new() + } +} + +fn scope_to_symbol(scope: &Scope, file: &CacheFile) -> Option { + if let Ok(range) = file.file.byte_span_to_range(scope.span) { + let range = range.into(); + Some(DocumentSymbol { + name: scope.name.clone(), + detail: None, + kind: SymbolKind::NAMESPACE, + tags: None, + deprecated: None, + range, + selection_range: range, + children: { + let children: Vec = scope + .children + .iter() + .filter_map(|child| scope_to_symbol(child, file)) + .collect(); + if children.is_empty() { + None + } else { + Some(children) + } + }, + }) + } else { + None + } } diff --git a/lsp/src/cache_file.rs b/lsp/src/cache_file.rs index 609b735..be99617 100644 --- a/lsp/src/cache_file.rs +++ b/lsp/src/cache_file.rs @@ -1,14 +1,19 @@ -use crate::{ - codespan::IndexError, - data::symbol::{Symbol, SymbolType}, -}; -use analysis::{Include, Scope, ScopeAnalyzer, SymbolResolver}; +use crate::analysis::scope_analyzer::Scope; +use crate::analysis::symbol_resolver::SymbolResolver; +use crate::data::files::IndexError; +use crate::data::symbol::Symbol; use codespan::{File, FileId}; -use parser::{Ast, ParseError, Token}; +use lazy_static::lazy_static; +use parser::{Ast, Instructions, ParseError, Token}; use tower_lsp_server::lsp_types::{Diagnostic, DiagnosticSeverity, Range}; +lazy_static! { + pub static ref INSTRUCTIONS: Instructions = Instructions::load(); +} + type IndexResult = Result; +#[derive(Debug, Clone)] pub struct CacheFile { pub id: FileId, pub file: File, @@ -16,9 +21,39 @@ pub struct CacheFile { pub ast: Ast, pub scopes: Vec, pub includes: Vec, + pub resolved_includes: Vec, pub symbols: Vec, } +#[derive(Clone, Debug)] +pub struct Include { + pub path: Token, + pub scope: Vec, +} + +impl PartialEq for Include { + fn eq(&self, other: &Self) -> bool { + self.path == other.path && self.scope.iter().eq(other.scope.iter()) + } +} + +#[derive(Clone, Debug)] +pub struct ResolvedInclude { + pub token: Token, + pub file: FileId, + pub scope: Vec, +} + +impl PartialEq for ResolvedInclude { + fn eq(&self, other: &Self) -> bool { + if self.file != other.file { + false + } else { + self.scope.iter().eq(other.scope.iter()) + } + } +} + impl CacheFile { pub fn new(file: File, id: FileId) -> CacheFile { CacheFile { @@ -28,86 +63,13 @@ impl CacheFile { ast: Ast::new(), scopes: vec![], includes: vec![], + resolved_includes: vec![], symbols: vec![], } } - pub async fn parse_labels(&mut self) -> Vec { - let mut diagnostics = vec![]; - - match self.index() { - Ok(parse_errors) => { - self.symbols.clear(); - let mut analyzer = ScopeAnalyzer::new(self.ast.clone()); - let (scopes, symtab, includes) = analyzer.analyze(); - self.scopes = scopes; - self.includes = includes; - // let symbols = analysis::DefAnalyzer::new(state.files.ast(id).clone()).parse(); - - for (symbol, scope) in symtab.iter() { - self.symbols.push(Symbol { - fqn: symbol.clone(), - label: symbol.clone(), - span: scope.get_span(), - file_id: self.id, - comment: scope.get_description(), - sym_type: match &scope { - analysis::Symbol::Macro { .. } => SymbolType::Macro, - analysis::Symbol::Label { .. } => SymbolType::Label, - analysis::Symbol::Constant { .. } => SymbolType::Constant, - analysis::Symbol::Parameter { .. } => SymbolType::Constant, - analysis::Symbol::Scope { .. } => SymbolType::Scope, - }, - }); - } - - for err in parse_errors.iter() { - match err { - ParseError::UnexpectedToken(token) => { - diagnostics.push(Diagnostic::new_simple( - self.file.byte_span_to_range(token.span).unwrap().into(), - format!("Unexpected Token {:?}", token.token_type), - )); - } - ParseError::Expected { expected, received } => { - diagnostics.push(Diagnostic::new_simple( - self.file.byte_span_to_range(received.span).unwrap().into(), - format!( - "Expected {:?} but received {:?}", - expected, received.token_type - ), - )); - } - ParseError::EOF => { - let pos = self - .file - .byte_index_to_position(self.file.source.len() - 1) - .unwrap(); - diagnostics.push(Diagnostic::new_simple( - Range::new(pos.into(), pos.into()), - "Unexpected EOF".to_string(), - )); - } - } - } - } - Err(err) => match err { - IndexError::TokenizerError(err) => { - let pos = self.file.byte_index_to_position(err.offset).unwrap(); - diagnostics.push(Diagnostic::new_simple( - Range::new(pos.into(), pos.into()), - "Unexpected character".to_string(), - )); - } - _ => {} - }, - } - - diagnostics - } - - pub fn index(&mut self) -> IndexResult> { - match parser::Tokenizer::new(&self.file.source, &crate::codespan::INSTRUCTIONS).parse() { + pub fn parse(&mut self) -> IndexResult> { + match parser::Tokenizer::new(&self.file.source, &INSTRUCTIONS).parse() { Ok(tokens) => { self.tokens = tokens; @@ -183,4 +145,40 @@ impl CacheFile { diagnostics } + + pub fn format_parse_errors(&self, errors: Vec) -> Vec { + let mut diagnostics = vec![]; + + for err in errors.iter() { + match err { + ParseError::UnexpectedToken(token) => { + diagnostics.push(Diagnostic::new_simple( + self.file.byte_span_to_range(token.span).unwrap().into(), + format!("Unexpected Token {:?}", token.token_type), + )); + } + ParseError::Expected { expected, received } => { + diagnostics.push(Diagnostic::new_simple( + self.file.byte_span_to_range(received.span).unwrap().into(), + format!( + "Expected {:?} but received {:?}", + expected, received.token_type + ), + )); + } + ParseError::EOF => { + let pos = self + .file + .byte_index_to_position(self.file.source.len() - 1) + .unwrap(); + diagnostics.push(Diagnostic::new_simple( + Range::new(pos.into(), pos.into()), + "Unexpected EOF".to_string(), + )); + } + } + } + + diagnostics + } } diff --git a/lsp/src/codespan.rs b/lsp/src/codespan.rs deleted file mode 100644 index b725aa1..0000000 --- a/lsp/src/codespan.rs +++ /dev/null @@ -1,86 +0,0 @@ -use crate::cache_file::CacheFile; -use crate::data::path::diff_paths; -use codespan::{File, FileId, Position}; -use lazy_static::lazy_static; -use parser::{Ast, Instructions, ParseError, Token, TokenizerError}; -use std::path::Path; -use std::str::FromStr; -use tower_lsp_server::lsp_types::Uri; - -pub struct Files { - files: Vec, -} - -impl Files { - pub fn get_uri_relative(&self, id: FileId, root: FileId) -> Option { - let target_uri = self.get_uri(id); - let relative_uri = self.get_uri(root); - - let path = diff_paths( - Path::new(&target_uri.path().to_string()), - Path::new(&relative_uri.path().to_string()).parent()?, - )?; - - Some(path.to_string_lossy().to_string()) - } -} - -pub enum IndexError { - TokenizerError(TokenizerError), - ParseError(ParseError), -} - -lazy_static! { - pub static ref INSTRUCTIONS: Instructions = Instructions::load(); -} - -impl Files { - pub fn new() -> Self { - Self { files: vec![] } - } - - pub fn add(&mut self, uri: Uri, contents: String) -> FileId { - let file_id = FileId::new(self.files.len()); - self.files - .push(CacheFile::new(File::new(uri.as_str(), contents), file_id)); - file_id - } - - pub fn get(&self, id: FileId) -> &CacheFile { - &self.files[id.get()] - } - - pub fn get_mut(&mut self, id: FileId) -> &mut CacheFile { - &mut self.files[id.get()] - } - - pub fn get_uri(&self, id: FileId) -> Uri { - Uri::from_str(self.get(id).file.name.as_str()).unwrap() - } - - pub fn source(&self, id: FileId) -> &String { - &self.get(id).file.source - } - - pub fn line_tokens(&self, id: FileId, position: Position) -> Vec { - let line_span = self.get(id).file.get_line(position.line).unwrap(); - let tokens = &self.files[id.get()].tokens; - - tokens - .iter() - .filter(|token| token.span.start >= line_span.start && token.span.end <= line_span.end) - .cloned() - .collect::>() - } - - pub fn update(&mut self, id: FileId, source: String) { - // tracing::info!("{}", source); - self.get_mut(id).file.update(source) - } - - pub fn show_instructions(&self, id: FileId, position: Position) -> bool { - let tokens = self.line_tokens(id, position); - let offset = self.get(id).file.position_to_byte_index(position).unwrap(); - tokens.is_empty() || tokens[0].span.end >= offset // Makes a naive guess at whether the current line contains an instruction. Doesn't work on lines with labels - } -} diff --git a/lsp/src/completion.rs b/lsp/src/completion.rs index 3d21220..ba5f38a 100644 --- a/lsp/src/completion.rs +++ b/lsp/src/completion.rs @@ -1,17 +1,17 @@ -use crate::documentation::{DocumentationKind, COMPLETION_ITEMS_COLLECTION}; -use crate::{ - data::symbol::SymbolType, - state::State, -}; -use analysis::ScopeAnalyzer; +use crate::analysis::scope_analyzer::ScopeAnalyzer; +use crate::documentation::{COMPLETION_ITEMS_COLLECTION, DocumentationKind}; +use crate::{data::symbol::SymbolType, state::State}; use codespan::FileId; use codespan::Position; use parser::TokenType; -use tower_lsp_server::lsp_types::{CompletionItem, CompletionItemKind, CompletionItemLabelDetails, CompletionTextEdit, Range, InsertReplaceEdit}; +use tower_lsp_server::lsp_types::{ + CompletionItem, CompletionItemKind, CompletionItemLabelDetails, CompletionTextEdit, + InsertReplaceEdit, Range, +}; pub trait CompletionProvider { fn completions_for(&self, state: &State, id: FileId, position: Position) - -> Vec; + -> Vec; } pub struct InstructionCompletionProvider; @@ -46,6 +46,12 @@ impl CompletionProvider for SymbolCompletionProvider { position: Position, ) -> Vec { let file = &state.files.get(id); + let units = state.units.find_related(id); + if units.len() == 0 { + // TODO: Not included + return Vec::new(); + } + let show_instructions = state.files.show_instructions(id, position); // Makes a naive guess at whether the current line contains an instruction. Doesn't work on lines with labels let byte_position = file.file.position_to_byte_index(position).unwrap_or(0); let scope = ScopeAnalyzer::search(&file.scopes, byte_position); @@ -53,7 +59,8 @@ impl CompletionProvider for SymbolCompletionProvider { let word_at_position = file.file.get_word_at_position(position).unwrap_or(""); let has_namespace = word_at_position.contains(":"); - file.symbols + state.units[units[0]] + .symbols .iter() .filter_map(|symbol| { if show_instructions @@ -119,14 +126,14 @@ impl CompletionProvider for Ca65DotOperatorCompletionProvider { let insert_range = Range { start: tower_lsp_server::lsp_types::Position { line: position.line as u32, - character: (position.character - curr_word.len()) as u32 + character: (position.character - curr_word.len()) as u32, }, end: tower_lsp_server::lsp_types::Position { line: position.line as u32, - character: position.character as u32 + character: position.character as u32, }, }; - + COMPLETION_ITEMS_COLLECTION .get() .expect("Could not get completion items collection for ca65 dot operators") @@ -135,11 +142,16 @@ impl CompletionProvider for Ca65DotOperatorCompletionProvider { .iter() .map(|item| { let mut new_item = item.clone(); - new_item.text_edit = Some(CompletionTextEdit::InsertAndReplace(InsertReplaceEdit { - new_text: item.insert_text.as_ref().expect("ca65 dot operator completion item did not have insert_text").clone(), - insert: insert_range, - replace: insert_range, - })); + new_item.text_edit = + Some(CompletionTextEdit::InsertAndReplace(InsertReplaceEdit { + new_text: item + .insert_text + .as_ref() + .expect("ca65 dot operator completion item did not have insert_text") + .clone(), + insert: insert_range, + replace: insert_range, + })); new_item }) .collect() @@ -165,11 +177,11 @@ impl CompletionProvider for Ca65KeywordCompletionProvider { let insert_range = Range { start: tower_lsp_server::lsp_types::Position { line: position.line as u32, - character: (position.character - curr_word.len()) as u32 + character: (position.character - curr_word.len()) as u32, }, end: tower_lsp_server::lsp_types::Position { line: position.line as u32, - character: position.character as u32 + character: position.character as u32, }, }; @@ -181,15 +193,19 @@ impl CompletionProvider for Ca65KeywordCompletionProvider { .iter() .map(|item| { let mut new_item = item.clone(); - new_item.text_edit = Some(CompletionTextEdit::InsertAndReplace(InsertReplaceEdit { - new_text: item.insert_text.as_ref().expect("ca65 keyword completion item did not have insert_text").clone(), - insert: insert_range, - replace: insert_range, - })); + new_item.text_edit = + Some(CompletionTextEdit::InsertAndReplace(InsertReplaceEdit { + new_text: item + .insert_text + .as_ref() + .expect("ca65 keyword completion item did not have insert_text") + .clone(), + insert: insert_range, + replace: insert_range, + })); new_item }) .collect() - } } diff --git a/lsp/src/data/files.rs b/lsp/src/data/files.rs new file mode 100644 index 0000000..5db42f8 --- /dev/null +++ b/lsp/src/data/files.rs @@ -0,0 +1,241 @@ +use crate::analysis::scope_analyzer; +use crate::analysis::scope_analyzer::ScopeAnalyzer; +use crate::cache_file::{CacheFile, Include, ResolvedInclude}; +use crate::data::indexing_state::IndexingState; +use crate::data::path::diff_paths; +use crate::data::symbol::{Symbol, SymbolType}; +use codespan::{File, FileId, Position}; +use parser::{ParseError, Token, TokenizerError}; +use path_clean::PathClean; +use std::collections::{HashMap, HashSet}; +use std::path::{Path, PathBuf}; +use std::str::FromStr; +use tower_lsp_server::lsp_types::{Diagnostic, Range, Uri}; + +pub enum IndexError { + TokenizerError(TokenizerError), + ParseError(ParseError), +} + +pub struct Files { + files: Vec, + pub sources: HashMap, +} + +impl Files { + pub fn new() -> Self { + Self { + files: vec![], + sources: HashMap::new(), + } + } + + pub fn get_uri_relative(&self, id: FileId, root: FileId) -> Option { + let target_uri = self.get_uri(id); + let relative_uri = self.get_uri(root); + + let path = diff_paths( + Path::new(&target_uri.path().to_string()), + Path::new(&relative_uri.path().to_string()).parent()?, + )?; + + Some(path.to_string_lossy().to_string()) + } + + pub fn add(&mut self, uri: Uri, contents: String) -> FileId { + let file_id = FileId::new(self.files.len()); + self.files + .push(CacheFile::new(File::new(uri.as_str(), contents), file_id)); + file_id + } + + pub fn get(&self, id: FileId) -> &CacheFile { + &self.files[id.get()] + } + + pub fn get_mut(&mut self, id: FileId) -> &mut CacheFile { + &mut self.files[id.get()] + } + + pub fn get_uri(&self, id: FileId) -> Uri { + Uri::from_str(self.get(id).file.name.as_str()).unwrap() + } + + pub fn source(&self, id: FileId) -> &String { + &self.get(id).file.source + } + + pub fn line_tokens(&self, id: FileId, position: Position) -> Vec { + let line_span = self.get(id).file.get_line(position.line).unwrap(); + let tokens = &self.files[id.get()].tokens; + + tokens + .iter() + .filter(|token| token.span.start >= line_span.start && token.span.end <= line_span.end) + .cloned() + .collect::>() + } + + pub fn update(&mut self, id: FileId, source: String) { + // tracing::info!("{}", source); + self.get_mut(id).file.update(source) + } + + pub fn show_instructions(&self, id: FileId, position: Position) -> bool { + let tokens = self.line_tokens(id, position); + let offset = self.get(id).file.position_to_byte_index(position).unwrap(); + tokens.is_empty() || tokens[0].span.end >= offset // Makes a naive guess at whether the current line contains an instruction. Doesn't work on lines with labels + } + + pub fn resolve_import(&self, parent: FileId, path: &str) -> anyhow::Result> { + let parent_uri = self.get_uri(parent); + + if !path.ends_with(".asm") && !path.ends_with(".s") && !path.ends_with(".inc") { + return Ok(None); + } + + let parent = PathBuf::from_str(parent_uri.path().as_str())? + .parent() + .ok_or_else(|| anyhow::anyhow!("parent folder not found"))? + .join(path) + .clean(); + let parent = Uri::from_str(url::Url::from_file_path(parent).unwrap().as_ref())?; + + let id = self + .sources + .iter() + .find_map(|(uri, id)| if *uri == parent { Some(*id) } else { None }); + + Ok(Some(id.ok_or_else(|| anyhow::anyhow!("file not found"))?)) + } + + pub fn resolve_import_paths( + &mut self, + parent: FileId, + ) -> (Vec, Vec) { + let mut results = vec![]; + let mut diagnostics = vec![]; + let parent_file = self.get(parent); + + for include in parent_file.includes.iter() { + match self.resolve_import( + parent, + &include.path.lexeme[1..include.path.lexeme.len() - 1], + ) { + Ok(Some(resolved)) => results.push(ResolvedInclude { + file: resolved, + scope: include.scope.clone(), + token: include.path.clone(), + }), + Ok(None) => {} + Err(e) => diagnostics.push(Diagnostic::new_simple( + parent_file + .file + .byte_span_to_range(include.path.span.into()) + .unwrap() + .into(), + e.to_string(), + )), + } + } + + (results, diagnostics) + } + + pub fn resolve_imports_for_file(&self, parent: FileId) -> HashSet { + // eprintln!("Crawling {:?}", parent); + let mut all_files = HashSet::new(); + for include in self.get(parent).resolved_includes.iter() { + // eprintln!("Including {:?}", include); + // eprintln!( + // "Including {:?} from {:?}", + // state.files.get_uri(include.file).as_str(), + // state.files.get_uri(parent).as_str() + // ); + if !all_files.contains(&include.file) && include.file != parent { + all_files.extend(self.resolve_imports_for_file(include.file)); + } + } + + all_files + } + + pub fn iter(&self) -> impl Iterator { + self.files.iter() + } + + pub fn iter_mut(&mut self) -> impl Iterator { + self.files.iter_mut() + } + + pub async fn index(&mut self, file_id: FileId) -> IndexingState { + let mut diagnostics = vec![]; + let mut includes_changed = false; + let parse_result = { + let mut file = self.get_mut(file_id); + file.parse() + }; + + let file = self.get_mut(file_id); + + if let Ok(parse_errors) = parse_result { + diagnostics.extend_from_slice(&file.format_parse_errors(parse_errors)); + + file.symbols.clear(); + let mut analyzer = ScopeAnalyzer::new(file.ast.clone()); + let (scopes, symtab, includes) = analyzer.analyze(); + file.scopes = scopes; + + for (symbol, scope) in symtab.iter() { + file.symbols.push(Symbol { + fqn: symbol.clone(), + label: symbol.clone(), + span: scope.get_span(), + file_id: file.id, + comment: scope.get_description(), + sym_type: match &scope { + scope_analyzer::Symbol::Macro { .. } => SymbolType::Macro, + scope_analyzer::Symbol::Label { .. } => SymbolType::Label, + scope_analyzer::Symbol::Constant { .. } => SymbolType::Constant, + scope_analyzer::Symbol::Parameter { .. } => SymbolType::Constant, + scope_analyzer::Symbol::Scope { .. } => SymbolType::Scope, + }, + }); + } + file.includes = includes; + + if !is_includes_same(&file.includes, &file.resolved_includes) { + let (resolved_imports, import_diagnostics) = self.resolve_import_paths(file_id); + let file = self.get_mut(file_id); + diagnostics.extend(import_diagnostics); + file.resolved_includes = resolved_imports; + includes_changed = true; + } + } else if let Err(IndexError::TokenizerError(err)) = parse_result { + let pos = file.file.byte_index_to_position(err.offset).unwrap(); + diagnostics.push(Diagnostic::new_simple( + Range::new(pos.into(), pos.into()), + "Unexpected character".to_string(), + )); + } + + IndexingState { + diagnostics, + includes_changed, + } + } +} + +fn is_includes_same(includes: &[Include], resolved_includes: &[ResolvedInclude]) -> bool { + if includes.len() != resolved_includes.len() { + return false; + } + + for (include, resolved) in includes.iter().zip(resolved_includes.iter()) { + if include.path.lexeme != resolved.token.lexeme { + return false; + } + } + + true +} \ No newline at end of file diff --git a/lsp/src/data/indexing_state.rs b/lsp/src/data/indexing_state.rs new file mode 100644 index 0000000..cd7453d --- /dev/null +++ b/lsp/src/data/indexing_state.rs @@ -0,0 +1,6 @@ +use tower_lsp_server::lsp_types::Diagnostic; + +pub struct IndexingState { + pub includes_changed: bool, + pub diagnostics: Vec, +} diff --git a/lsp/src/data/mod.rs b/lsp/src/data/mod.rs index 8b83de8..208a010 100644 --- a/lsp/src/data/mod.rs +++ b/lsp/src/data/mod.rs @@ -1,4 +1,7 @@ pub mod configuration; +pub mod indexing_state; pub mod instructions; pub mod path; pub mod symbol; +pub mod units; +pub mod files; diff --git a/lsp/src/data/units.rs b/lsp/src/data/units.rs new file mode 100644 index 0000000..68d1c05 --- /dev/null +++ b/lsp/src/data/units.rs @@ -0,0 +1,56 @@ +use crate::data::symbol::Symbol; +use codespan::FileId; +use std::collections::{HashMap, HashSet}; +use std::ops::{Index, IndexMut}; + +#[derive(Debug)] +pub struct Unit { + pub deps: Vec, + pub symbols: Vec, +} + +#[derive(Debug, Default)] +pub struct Units(pub HashMap); + +impl Units { + pub fn insert(&mut self, file: FileId, ids: Vec) { + self.0.insert( + file, + Unit { + deps: ids, + symbols: vec![], + }, + ); + } + pub fn get(&self, file_id: &FileId) -> Option<&Unit> { + self.0.get(file_id) + } + pub fn find_related(&self, file_id: FileId) -> Vec { + // TODO: Make sure this is a hashset, and don't include self + self.0 + .iter() + .filter_map(|(k, v)| { + if *k == file_id || v.deps.contains(&file_id) { + Some(*k) + } else { + None + } + }) + .collect::>() + .into_iter() + .collect() + } +} + +impl Index for Units { + type Output = Unit; + fn index(&self, file_id: FileId) -> &Self::Output { + &self.0[&file_id] + } +} + +impl IndexMut for Units { + fn index_mut(&mut self, file_id: FileId) -> &mut Unit { + self.0.get_mut(&file_id).unwrap() + } +} diff --git a/lsp/src/definition.rs b/lsp/src/definition.rs index b488654..c5d0fa5 100644 --- a/lsp/src/definition.rs +++ b/lsp/src/definition.rs @@ -1,8 +1,5 @@ -use crate::{ - state::State, - data::symbol::Symbol -}; -use analysis::ScopeAnalyzer; +use crate::analysis::scope_analyzer::ScopeAnalyzer; +use crate::{data::symbol::Symbol, state::State}; use codespan::{FileError, FileId, Position, Span}; use std::cmp::Ordering; @@ -46,6 +43,12 @@ impl Definition { position: Position, ) -> Result, Span)>, FileError> { let file = &state.files.get(id); + let units = state.units.find_related(id); + if units.len() == 0 { + // TODO: Not included + return Ok(None); + } + let (word, span) = file.file.get_word_span_at_position(position)?; let index = file.file.position_to_byte_index(position)?; let scopes = &file.scopes; @@ -55,9 +58,11 @@ impl Definition { let slice = &word[0..new_span.end]; let mut definitions = vec![]; + + let symbols = &state.units[units[0]].symbols; if slice.starts_with("::") { - if let Some(m) = file.symbols.iter().find(|Symbol { fqn, .. }| fqn == slice) { + if let Some(m) = symbols.iter().find(|Symbol { fqn, .. }| fqn == slice) { definitions.push(m.clone()); } } else { @@ -65,10 +70,9 @@ impl Definition { let target_fqn = [¤t_scopes[0..=idx], &[slice.to_string()]] .concat() .join("::"); - if let Some(m) = file - .symbols + if let Some(m) = symbols .iter() - .find(|Symbol { fqn, .. }| fqn == &target_fqn) + .find(|Symbol { fqn, .. }| fqn.as_str() == &target_fqn) { definitions.push(m.clone()); break; diff --git a/lsp/src/index_engine.rs b/lsp/src/index_engine.rs index 3151863..0535511 100644 --- a/lsp/src/index_engine.rs +++ b/lsp/src/index_engine.rs @@ -1,15 +1,20 @@ +use crate::analysis::scope_analyzer::Scope; +use crate::data::files::Files; +use crate::data::symbol::Symbol; use crate::state::State; -use std::collections::HashMap; +use codespan::FileId; +use std::collections::{HashMap, HashSet}; use std::ffi::OsStr; +use std::path::PathBuf; use std::str::FromStr; use std::sync::Arc; use tokio::sync::Mutex; +use tower_lsp_server::Client; use tower_lsp_server::lsp_types::request::WorkDoneProgressCreate; use tower_lsp_server::lsp_types::{ - InlayHintWorkspaceClientCapabilities, ProgressToken, Uri, WorkDoneProgressCreateParams, - WorkspaceClientCapabilities, + Diagnostic, InlayHintWorkspaceClientCapabilities, ProgressToken, Uri, + WorkDoneProgressCreateParams, WorkspaceClientCapabilities, }; -use tower_lsp_server::Client; use uuid::Uuid; pub struct IndexEngine { @@ -22,7 +27,7 @@ impl IndexEngine { } pub async fn crawl_fs(slf: Arc>, root_uri: Uri, client: Client) { - let slf = slf.lock().await; + let data = slf.lock().await; let token = ProgressToken::String(Uuid::new_v4().to_string()); client .send_request::(WorkDoneProgressCreateParams { @@ -56,7 +61,7 @@ impl IndexEngine { } } - let mut state = slf.state.lock().await; + let mut state = data.state.lock().await; let mut diagnostics = HashMap::new(); let mut parsed_files = vec![]; @@ -71,16 +76,11 @@ impl IndexEngine { let uri = Uri::from_str(url::Url::from_file_path(file).unwrap().as_ref()).unwrap(); let contents = std::fs::read_to_string(file).unwrap(); let id = state.get_or_insert_source(uri, contents); - diagnostics.insert(id, state.files.get_mut(id).parse_labels().await); + let file = state.files.index(id).await; + diagnostics.insert(id, file.diagnostics); parsed_files.push(id); } - for id in parsed_files.iter() { - let mut diags = diagnostics.get(id).unwrap().clone(); - diags.extend(state.files.get_mut(*id).lint().await); - state.publish_diagnostics(*id, diags).await; - } - if matches!( &state.client_capabilities.workspace, Some(WorkspaceClientCapabilities { @@ -94,6 +94,125 @@ impl IndexEngine { state.client.inlay_hint_refresh().await.unwrap(); } + for id in parsed_files.iter() { + let uri = state.files.get_uri(*id); + let path = PathBuf::from_str(uri.path().as_str()).unwrap(); + if let Some(ext) = path.extension() + && ext.to_str() == Some("s") + { + let (deps, dep_diagnostics) = IndexEngine::calculate_deps(&mut state.files, *id); + diagnostics.insert(*id, dep_diagnostics); + state.units.insert(*id, deps); + } + } + + let units = state.units.0.keys().cloned().collect::>(); + for unit in units { + let symbols = IndexEngine::get_symbol_tree(&mut state.files, unit); + state.units[unit].symbols = symbols; + } + + for id in parsed_files.iter() { + // let diags = IndexEngine::invalidate(&mut state, *id).await; + state + .publish_diagnostics( + *id, + diagnostics + .get(id) + .and_then(|d| Some(d.clone())) + .unwrap_or_default(), + ) + .await; + } + progress.finish().await; } + + pub async fn invalidate(state: &mut State, file: FileId) -> Vec { + let mut diagnostics = vec![]; + + let (resolved_imports, import_diagnostics) = state.files.resolve_import_paths(file); + diagnostics.extend(import_diagnostics); + + diagnostics.extend(state.files.get_mut(file).lint().await); + + let file = state.files.get_mut(file); + if resolved_imports.iter().ne(&file.resolved_includes) { + // eprintln!("Changed {:#?}", file.resolved_includes); + file.resolved_includes = resolved_imports; + } else { + // eprintln!("No changes"); + } + + diagnostics + } + + pub fn calculate_deps(files: &mut Files, file: FileId) -> (Vec, Vec) { + let mut deps = HashSet::new(); + let mut diagnostics = vec![]; + IndexEngine::flatten_dependencies(files, file, &mut deps, &mut diagnostics); + if deps.contains(&file) { + eprintln!("Circular dependency"); + } + + (deps.into_iter().collect(), diagnostics) + } + + fn flatten_dependencies( + files: &mut Files, + file: FileId, + dependencies: &mut HashSet, + diagnostics: &mut Vec, + ) { + let (resolved_imports, import_diagnostics) = files.resolve_import_paths(file); + + diagnostics.extend(import_diagnostics); + + for include in resolved_imports.iter() { + if !dependencies.contains(&include.file) { + dependencies.insert(include.file); + Self::flatten_dependencies(files, include.file, dependencies, diagnostics); + } + } + } + + pub fn get_symbol_tree(files: &mut Files, file_id: FileId) -> Vec { + let mut stack = vec!["".to_owned()]; + let mut symbols = Vec::new(); + Self::get_symbols_for_file(files, file_id, &mut symbols, &mut stack); + + symbols + } + + fn get_symbols_for_file( + files: &mut Files, + file_id: FileId, + symbols: &mut Vec, + stack: &mut Vec, + ) { + let file = &files.get(file_id); + let resolved_includes = file.resolved_includes.clone(); + let file_symbols = file.symbols.clone(); + + for include in resolved_includes { + let backup = stack.clone(); + stack.extend_from_slice( + &include.scope[1..] + .iter() + .map(|s| s.name.to_owned()) + .collect::>(), + ); + Self::get_symbols_for_file(files, include.file, symbols, stack); + *stack = backup; + } + + for symbol in file_symbols { + let mut symbol = symbol.clone(); + let fqn = [stack.clone(), vec![symbol.fqn[2..].to_string()]] + .concat() + .join("::"); + symbol.fqn = fqn; + symbols.push(symbol); + } + } } diff --git a/lsp/src/logger.rs b/lsp/src/logger.rs deleted file mode 100644 index 793b7ba..0000000 --- a/lsp/src/logger.rs +++ /dev/null @@ -1,45 +0,0 @@ -use std::io; - -use tower_lsp_server::Client; -use tracing_subscriber::fmt::MakeWriter; - -pub struct LspLogger { - client: Client, -} - -impl LspLogger { - fn new(client: Client) -> Self { - Self { client } - } -} - -impl io::Write for LspLogger { - fn write(&mut self, buf: &[u8]) -> io::Result { - let client = self.client.clone(); - let message = buf.to_vec(); - tokio::spawn(async move { - client - .log_message( - tower_lsp_server::lsp_types::MessageType::LOG, - String::from_utf8(message).unwrap(), - ) - .await; - }); - Ok(buf.len()) - } - - fn flush(&mut self) -> io::Result<()> { - Ok(()) - } -} - -pub struct LogWriter { - client: Client, -} - -impl<'a> MakeWriter<'a> for LogWriter { - type Writer = LspLogger; - fn make_writer(&'a self) -> Self::Writer { - LspLogger::new(self.client.clone()) - } -} diff --git a/lsp/src/main.rs b/lsp/src/main.rs index cf7e73f..d3bc56a 100644 --- a/lsp/src/main.rs +++ b/lsp/src/main.rs @@ -1,13 +1,12 @@ +mod analysis; mod asm_server; mod cache_file; -mod codespan; mod completion; mod data; mod definition; mod documentation; mod error; mod index_engine; -mod logger; mod state; use asm_server::Asm; diff --git a/lsp/src/state.rs b/lsp/src/state.rs index 85360b4..0f68d5e 100644 --- a/lsp/src/state.rs +++ b/lsp/src/state.rs @@ -1,28 +1,36 @@ -use crate::codespan::Files; +use crate::{data::files::Files, data::units::Units}; use codespan::FileId; -use std::collections::HashMap; use std::str::FromStr; +use tower_lsp_server::Client; use tower_lsp_server::lsp_types::{ ClientCapabilities, Diagnostic, TextDocumentContentChangeEvent, Uri, VersionedTextDocumentIdentifier, }; -use tower_lsp_server::Client; pub struct State { - pub sources: HashMap, pub files: Files, pub workspace_folder: Option, pub client: Client, pub client_capabilities: ClientCapabilities, + pub units: Units, } impl State { + pub fn new(client: Client) -> Self { + Self { + files: Files::new(), + workspace_folder: None, + client, + client_capabilities: ClientCapabilities::default(), + units: Units::default(), + } + } pub fn get_or_insert_source(&mut self, uri: Uri, text: String) -> FileId { - if let Some(id) = self.sources.get(&uri) { + if let Some(id) = self.files.sources.get(&uri) { *id } else { let id = self.files.add(uri.clone(), text); - self.sources.insert(uri.clone(), id); + self.files.sources.insert(uri.clone(), id); id } } @@ -32,25 +40,22 @@ impl State { document: &VersionedTextDocumentIdentifier, changes: Vec, ) -> FileId { - if let Some(id) = self.sources.get(&document.uri) { - let file = &self.files.get(*id); - let mut source = file.file.source.to_owned(); - for change in changes { - if let (None, None) = (change.range, change.range_length) { - source = change.text; - } else if let Some(range) = change.range { - let span = file - .file - .range_to_byte_span(&range.into()) - .unwrap_or_default(); - source.replace_range(span, &change.text); - } + let id = *self.files.sources.get(&document.uri).unwrap(); + let file = &self.files.get(id); + let mut source = file.file.source.to_owned(); + for change in changes { + if let (None, None) = (change.range, change.range_length) { + source = change.text; + } else if let Some(range) = change.range { + let span = file + .file + .range_to_byte_span(&range.into()) + .unwrap_or_default(); + source.replace_range(span, &change.text); } - self.files.update(*id, source); - *id - } else { - panic!(); } + self.files.update(id, source); + id } pub async fn publish_diagnostics(&mut self, id: FileId, diagnostics: Vec) { diff --git a/parser/Cargo.toml b/parser/Cargo.toml index 62a2fa6..e961b1b 100644 --- a/parser/Cargo.toml +++ b/parser/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "parser" version = "0.1.0" -edition = "2021" +edition = "2024" [lib] name = "parser" diff --git a/parser/src/data/mod.rs b/parser/src/data/mod.rs new file mode 100644 index 0000000..24523c5 --- /dev/null +++ b/parser/src/data/mod.rs @@ -0,0 +1,5 @@ +mod token; +mod token_type; + +pub use token::*; +pub use token_type::*; diff --git a/parser/src/data/token.rs b/parser/src/data/token.rs new file mode 100644 index 0000000..896225d --- /dev/null +++ b/parser/src/data/token.rs @@ -0,0 +1,27 @@ +use crate::TokenType; +use codespan::Span; +use std::fmt::{Display, Formatter}; + +#[derive(Debug, Clone, PartialEq)] +pub struct Token { + pub token_type: TokenType, + pub lexeme: String, + pub span: Span, +} + +impl Token { + pub fn new(token_type: TokenType, lexeme: String, index: usize) -> Token { + let span = Span::new(index, index + lexeme.len()); + Token { + token_type, + lexeme, + span, + } + } +} + +impl Display for Token { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.lexeme.clone()) + } +} diff --git a/parser/src/data/token_type.rs b/parser/src/data/token_type.rs new file mode 100644 index 0000000..417097c --- /dev/null +++ b/parser/src/data/token_type.rs @@ -0,0 +1,54 @@ + +#[derive(Debug, Clone, PartialEq)] +#[repr(u32)] +pub enum TokenType { + Label, + Instruction, + Identifier, + Number, + Hash, + Plus, + Minus, + LeftParen, + RightParen, + Comma, + Dot, + Colon, + Equal, + EOF, + EOL, + String, + Macro, + BitwiseOr, + BitwiseAnd, + BitwiseNot, + Not, + LessThan, + GreaterThan, + Caret, + And, + Multiply, + Divide, + ScopeSeparator, + Mod, + BitwiseXor, + ShiftLeft, + ShiftRight, + Or, + Xor, + NotEqual, + LessThanEq, + GreaterThanEq, + ConstAssign, + LeftBrace, + RightBrace, + Bank, + SizeOf, + Match, + Def, + UnnamedLabelReference, + Extract, + WordOp, + LeftBracket, + RightBracket, +} \ No newline at end of file diff --git a/parser/src/lib.rs b/parser/src/lib.rs index e95ecea..455751b 100644 --- a/parser/src/lib.rs +++ b/parser/src/lib.rs @@ -1,8 +1,10 @@ +pub mod data; pub mod instructions; pub mod parser; pub mod stream; pub mod tokenizer; +pub use data::*; pub use instructions::*; pub use parser::*; pub use tokenizer::*; diff --git a/parser/src/main.rs b/parser/src/main.rs index 3dbbb09..6ff78c1 100644 --- a/parser/src/main.rs +++ b/parser/src/main.rs @@ -1,19 +1,17 @@ use parser::{Instructions, ParseError, Tokenizer, TokenizerError}; -use std::fs::File; -use std::io::Read; fn main() { let mut args = std::env::args(); let instructions = Instructions::load(); - + // if args.len() < 2 { // eprintln!("Usage: parser "); // std::process::exit(1); // } - + let buf = std::fs::read_to_string("test.s").unwrap(); let cs_file = codespan::File::new("test", buf); - + let mut tokenizer = Tokenizer::new(&cs_file.source, &instructions); match tokenizer.parse() { Ok(tokens) => { diff --git a/parser/src/parser.rs b/parser/src/parser.rs index 7c0cabf..a98aa37 100644 --- a/parser/src/parser.rs +++ b/parser/src/parser.rs @@ -1,4 +1,4 @@ -use crate::tokenizer::{Token, TokenType}; +use crate::{Token, TokenType}; use codespan::Span; use std::fmt::{Display, Formatter}; diff --git a/parser/src/tokenizer.rs b/parser/src/tokenizer.rs index fbf7ae8..673cf11 100644 --- a/parser/src/tokenizer.rs +++ b/parser/src/tokenizer.rs @@ -1,86 +1,9 @@ use crate::instructions::Instructions; use crate::stream::Stream; +use crate::{Token, TokenType}; use codespan::Span; use std::fmt::{Display, Formatter}; -#[derive(Debug, Clone, PartialEq)] -#[repr(u32)] -pub enum TokenType { - Label, - Instruction, - Identifier, - Number, - Hash, - Plus, - Minus, - LeftParen, - RightParen, - Comma, - Dot, - Colon, - Equal, - EOF, - EOL, - String, - Macro, - BitwiseOr, - BitwiseAnd, - BitwiseNot, - Not, - LessThan, - GreaterThan, - Caret, - And, - Multiply, - Divide, - ScopeSeparator, - Mod, - BitwiseXor, - ShiftLeft, - ShiftRight, - Or, - Xor, - NotEqual, - LessThanEq, - GreaterThanEq, - ConstAssign, - LeftBrace, - RightBrace, - Bank, - SizeOf, - Match, - Def, - UnnamedLabelReference, - Extract, - WordOp, - LeftBracket, - RightBracket, -} - -#[derive(Debug, Clone, PartialEq)] -pub struct Token { - pub token_type: TokenType, - pub lexeme: String, - pub span: Span, -} - -impl Token { - pub fn new(token_type: TokenType, lexeme: String, index: usize) -> Token { - let span = Span::new(index, index + lexeme.len()); - Token { - token_type, - lexeme, - span, - } - } -} - -impl Display for Token { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.lexeme.clone()) - } -} - #[derive(Debug)] pub enum TokenizerErrorKind { UnexpectedToken,