diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..e69de29 diff --git a/src/lsp/interface.ts b/src/lsp/interface.ts new file mode 100644 index 0000000..6efe22d --- /dev/null +++ b/src/lsp/interface.ts @@ -0,0 +1,12 @@ +import {Transport,LSPClient } from "@codemirror/lsp-client" + + + +function WasmWebTransport(path: string): Promise { + let handlers: ((value: string) => void)[] = [] + let worker = new Worker(path); + worker.onmessage = e => { for (let h of handlers) h(e.data.toString()) } + return new Promise(resolve => { + worker.onopen = () => resolve() + }) +} diff --git a/src/lsp/lsp85/.gitignore b/src/lsp/lsp85/.gitignore new file mode 100644 index 0000000..96ef6c0 --- /dev/null +++ b/src/lsp/lsp85/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/src/lsp/lsp85/Cargo.toml b/src/lsp/lsp85/Cargo.toml new file mode 100644 index 0000000..399e300 --- /dev/null +++ b/src/lsp/lsp85/Cargo.toml @@ -0,0 +1,16 @@ +[lib] +crate-type = ["cdylib", "rlib"] +path = "src/lib.rs" + +[package] +name = "lsp85" +version = "0.1.0" +edition = "2024" + +[dependencies] +lsp-server = "0.7.9" +lsp-types = "0.97.0" +serde = { version = "1", features = ["derive"] } +serde-wasm-bindgen = "0.6.5" +serde_json = "1" +wasm-bindgen = "0.2.106" diff --git a/src/lsp/lsp85/README.md b/src/lsp/lsp85/README.md new file mode 100644 index 0000000..795bed4 --- /dev/null +++ b/src/lsp/lsp85/README.md @@ -0,0 +1,22 @@ +``` +[ CodeMirror Editor ] + │ + ▼ + LSPClient (browser) + │ JSON-RPC + ▼ postMessage +┌──────────────────────────┐ +│ WebWorker (JS) │ +│ - Handles LSP messages │ +│ - Calls WASM functions │ +└───────────┬──────────────┘ + │ + ▼ + [ WASM Module ] + - completion() + - hover() + - parsing logic +``` +(x) - Code completions +image +() Populate for all the instructions diff --git a/src/lsp/lsp85/src/frontend/lexer.rs b/src/lsp/lsp85/src/frontend/lexer.rs new file mode 100644 index 0000000..32c7977 --- /dev/null +++ b/src/lsp/lsp85/src/frontend/lexer.rs @@ -0,0 +1,216 @@ +use crate::frontend::token::{Location, Token, TokenType}; + +#[derive(Debug)] +pub struct Lexer { + pub source: String, // source string + pub ch: char, // current literal + pub curr_position: usize, // current position + pub read_position: usize, // next position + pub location: Location, // current location +} + +impl Lexer { + pub fn new(source: String, line_no: usize) -> Self { + Self { + ch: source.chars().nth(0).expect("source of size <1?"), + curr_position: 0, + read_position: 1, + location: Location { + row: line_no, + col: 0, + }, + source: source, + } + } +} + +impl Iterator for Lexer { + type Item = Token; + fn next(&mut self) -> Option { + match self.ch { + c if self.ch.is_alphabetic() => { + // identifier + return Some(self.read_identifier()); + } + c if self.ch.is_numeric() => { + return Some(self.read_immediate()); + } + ',' => { + self.consume(); + return Some(Token::new( + 1, + TokenType::COMMA_DELIM, + self.location, + String::from(','), + )); + } + ' ' => { + self.consume(); + return self.next(); + } + '\n' => { + self.consume(); + let buf_token = Some(Token::new( + 1, + TokenType::EOL, + self.location, + String::from('\n'), + )); + self.location.col = 0; + self.location.row += 1; + + return buf_token; + } + '\0' => { + return None; + } + _ => { + self.consume(); + return Some(Token::new( + 1, + TokenType::ILLEGAL, + self.location, + String::from('\0'), + )); + } + } + } +} +impl Lexer { + pub fn consume(&mut self) { + if self.read_position >= self.source.len() { + self.ch = '\0'; + } else { + self.ch = self.source.chars().nth(self.read_position).unwrap_or(' '); + } + self.curr_position = self.read_position; + self.read_position = self.curr_position + 1; + self.location.col += 1; + } + pub fn read_identifier(&mut self) -> Token { + let mut identifier_buf = String::from(""); + while self.ch.is_alphabetic() { + identifier_buf += &self.ch.to_string(); + self.consume(); + } + return Token::new( + identifier_buf.len(), + get_identifier_token(&identifier_buf), + self.location, + identifier_buf, + ); + } + pub fn read_immediate(&mut self) -> Token { + let mut immediate_buf = String::from(""); + + //Support for hex digits + while self.ch.is_ascii_hexdigit() { + immediate_buf += &self.ch.to_string(); + self.consume(); + } + + //H suffix handling Eg: 123AH + if self.ch == 'H' { + immediate_buf += &self.ch.to_string(); + self.consume(); + } + return Token::new( + immediate_buf.len(), + TokenType::IMM_VALUE, + self.location, + immediate_buf, + ); + } +} +fn get_identifier_token(identifier_lit: &String) -> TokenType { + match identifier_lit.as_str() { + "ADD" | "SUB" | "MOV" | "MVI" | "LXI" | "PUSH" | "POP" | "INR" | "DCR" | "DAD" | "LDAX" + | "STAX" => { + return TokenType::OPERATION; + } + "A" | "B" | "C" | "D" | "E" | "PSW" | "H" | "L" | "SP" => { + return TokenType::REGISTER; + } + _ => { + return TokenType::ILLEGAL; + } + } +} + +#[cfg(test)] +mod tests { + + use super::Lexer; + use crate::frontend::token::{Location, Token, TokenType}; + #[test] + fn imm_test() { + let source = String::from("MVI A,05H\n"); + let mut l = Lexer::new(source, 0); + let mut tokens: Vec = vec![]; + for token in l { + tokens.push(token); + } + + assert_eq!( + vec![ + Token::new( + 3, + TokenType::OPERATION, + Location::new(0, 3), + "MVI".to_string() + ), + Token::new(1, TokenType::REGISTER, Location::new(0, 5), "A".to_string()), + Token::new( + 1, + TokenType::COMMA_DELIM, + Location::new(0, 6), + ",".to_string() + ), + Token::new( + 3, + TokenType::IMM_VALUE, + Location::new(0, 9), + "05H".to_string() + ), + Token::new(1, TokenType::EOL, Location::new(0, 10), "\n".to_string()) + ], + tokens + ); + } + + #[test] + fn reg_pair() { + let source = String::from("MVI A,SP\n"); + let mut l = Lexer::new(source, 0); + let mut tokens: Vec = vec![]; + for token in l { + tokens.push(token); + } + + assert_eq!( + vec![ + Token::new( + 3, + TokenType::OPERATION, + Location::new(0, 3), + "MVI".to_string() + ), + Token::new(1, TokenType::REGISTER, Location::new(0, 5), "A".to_string()), + Token::new( + 1, + TokenType::COMMA_DELIM, + Location::new(0, 6), + ",".to_string() + ), + Token::new( + 2, + TokenType::REGISTER, + Location::new(0, 8), + "SP".to_string() + ), + Token::new(1, TokenType::EOL, Location::new(0, 9), "\n".to_string()) + ], + tokens + ); + } +} diff --git a/src/lsp/lsp85/src/frontend/mod.rs b/src/lsp/lsp85/src/frontend/mod.rs new file mode 100644 index 0000000..306e9fa --- /dev/null +++ b/src/lsp/lsp85/src/frontend/mod.rs @@ -0,0 +1,4 @@ +pub mod lexer; +pub mod parser; +pub mod token; +pub mod utils; diff --git a/src/lsp/lsp85/src/frontend/parser.rs b/src/lsp/lsp85/src/frontend/parser.rs new file mode 100644 index 0000000..f391155 --- /dev/null +++ b/src/lsp/lsp85/src/frontend/parser.rs @@ -0,0 +1,144 @@ +use crate::frontend::token::{Token, TokenType}; +use std::iter::Peekable; +use std::vec::IntoIter; + +#[derive(Debug)] +pub struct Parser { + tok_stream: Peekable>, +} +impl Parser { + pub fn new(tok_stream: IntoIter) -> Self { + Self { + tok_stream: tok_stream.peekable(), + } + } +} + +#[derive(Debug, PartialEq)] +pub struct Tree { + pub l_child: Option, + pub r_child: Option, +} + +impl Tree { + pub fn default() -> Self { + Self { + l_child: None, + r_child: None, + } + } + + pub fn new(l_child: Option, r_child: Option) -> Self { + Self { l_child, r_child } + } +} +#[derive(Debug, PartialEq)] +pub struct Node { + pub value: Token, + pub branch: Box, +} + +impl Node { + pub fn new(tok_val: Token, branch: Box) -> Self { + Self { + value: tok_val, + branch, + } + } +} + +impl Parser { + pub fn parse_expression(&mut self) -> Option { + if let Some(peeked_token) = self.tok_stream.peek() { + println!("parse_expression() called! {:?}", peeked_token); + match peeked_token { + Token { + tok_type: TokenType::OPERATION, + .. + } => { + return self.parse_operation(); + } + Token { + tok_type: TokenType::REGISTER, + .. + } => { + println!("unexpected placement of register!"); + return None; + } + Token { + tok_type: TokenType::EOF, + .. + } => { + return None; + } + _ => { + self.tok_stream.next(); + return self.parse_expression(); + } + } + } else { + return None; + } + } + pub fn parse_operation(&mut self) -> Option { + let mut l_child: Node; + if let Some(peeked_token) = self.tok_stream.peek() { + l_child = Node::new(peeked_token.clone(), Box::new(Tree::default())); + } else { + return None; + } + self.tok_stream.next(); + if let Some(peeked_token) = self.tok_stream.peek() { + match peeked_token { + Token { + tok_type: TokenType::REGISTER, + .. + } => { + l_child.branch.l_child = self.parse_operand(); + l_child.branch.r_child = self.parse_operand(); + return Some(l_child); + } + _ => { + return Some(l_child); + } + } + } else { + return Some(l_child); + } + } + pub fn parse_operand(&mut self) -> Option { + let mut l_child: Node; + if let Some(peeked_token) = self.tok_stream.peek() { + match peeked_token { + Token { + tok_type: TokenType::REGISTER, + .. + } => { + let token_buffer = peeked_token.clone(); + self.tok_stream.next(); + return Some(Node::new(token_buffer, Box::new(Tree::default()))); + } + Token { + tok_type: TokenType::COMMA_DELIM, + .. + } => { + self.tok_stream.next(); + return self.parse_operand(); + } + Token { + tok_type: TokenType::IMM_VALUE, + .. + } => { + let token_buffer = peeked_token.clone(); + self.tok_stream.next(); + return Some(Node::new(token_buffer, Box::new(Tree::default()))); + } + _ => { + return None; + } + } + } else { + return None; + } + } +} diff --git a/src/lsp/lsp85/src/frontend/token.rs b/src/lsp/lsp85/src/frontend/token.rs new file mode 100644 index 0000000..17d1f56 --- /dev/null +++ b/src/lsp/lsp85/src/frontend/token.rs @@ -0,0 +1,51 @@ +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct Location { + pub row: usize, + pub col: usize, +} +impl Location { + pub fn new(row: usize, col: usize) -> Self { + Self { row, col } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Token { + pub tok_literal: String, + pub tok_type: TokenType, + pub location: Location, + pub offset: usize, // -ve char offset +} + +impl Token { + pub fn new( + offset: usize, + tok_type: TokenType, + location: Location, + tok_literal: String, + ) -> Self { + Self { + tok_literal, + tok_type, + location, + offset, + } + } +} + +// ADD A,B +// +// INSTRUCTION +// OPERATION REGISTER COMMA_DELIM REGISTER +// +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum TokenType { + OPERATION, + IMM_VALUE, + REGISTER, + COMMA_DELIM, + BOL, + EOL, + EOF, + ILLEGAL, +} diff --git a/src/lsp/lsp85/src/frontend/utils/files.rs b/src/lsp/lsp85/src/frontend/utils/files.rs new file mode 100644 index 0000000..3d7f2a8 --- /dev/null +++ b/src/lsp/lsp85/src/frontend/utils/files.rs @@ -0,0 +1,13 @@ +use std::fs::File; +use std::io::BufRead; +use std::io::BufReader; +use std::io::Lines; +use std::io::Read; +use std::iter::Enumerate; + +pub fn get_source_buffer(f_name: &'static str) -> Option>>> { + let file = File::open(f_name).ok()?; + let buffer = BufReader::new(file); + let lines = buffer.lines().enumerate(); + return Some(lines); +} diff --git a/src/lsp/lsp85/src/frontend/utils/mod.rs b/src/lsp/lsp85/src/frontend/utils/mod.rs new file mode 100644 index 0000000..d3ab969 --- /dev/null +++ b/src/lsp/lsp85/src/frontend/utils/mod.rs @@ -0,0 +1 @@ +pub mod files; diff --git a/src/lsp/lsp85/src/lib.rs b/src/lsp/lsp85/src/lib.rs new file mode 100644 index 0000000..dc77a29 --- /dev/null +++ b/src/lsp/lsp85/src/lib.rs @@ -0,0 +1,2 @@ +#[macro_use] +pub mod server; diff --git a/src/lsp/lsp85/src/main.rs b/src/lsp/lsp85/src/main.rs new file mode 100644 index 0000000..dd2d0a3 --- /dev/null +++ b/src/lsp/lsp85/src/main.rs @@ -0,0 +1,112 @@ +mod frontend; +mod server; + +// use frontend::lexer::Lexer; +// use frontend::parser::{Parser,Node}; +// use frontend::token::{Token, TokenType,Location}; +// use frontend::utils::files::get_source_buffer; + +use lsp_server::{ExtractError, Message, Notification, Request, RequestId, Response}; +use lsp_types::{ + CompletionItem, CompletionResponse, + request::{Completion, HoverRequest}, +}; +use server::bindings::{wasm_completion_handler, wasm_hover_handler}; +use server::{handlers, lsp85, routers}; +use std::error::Error; + +pub fn main() -> Result<(), Box> { + let lsp = lsp85::build() + .stdio() + .enable_hover() + .enable_completion() + .initialize(); + + let lsp = match lsp { + Ok(lsp) => lsp, + Err(e) => { + eprintln!("init failed: {:?}", e); + return Err(e); + } + }; + + let conn = match lsp.conn.as_ref() { + Some(conn) => conn, + None => { + eprintln!("no conn"); + return Err("no conn".into()); + } + }; + + for msg in &conn.receiver { + eprintln!("Message incoming: {:?}", msg); + match msg { + Message::Request(req) => { + let down = match conn.handle_shutdown(&req) { + Ok(true) => true, + Ok(false) => false, + Err(e) => { + eprintln!("error: {:?}", e); + false + } + }; + if down { + eprintln!("shutting down!"); + return Ok(()); + } + eprintln!("got request: {:?}", req); + + lsp_router!(req,lsp,{ + Completion=>handlers::completion_handler, + HoverRequest=>handlers::hover_handler, + }); + } + Message::Response(rs) => { + eprintln!("response: {:?}", rs); + } + Message::Notification(n) => { + match &n { + Notification { method, .. } + if *method == String::from("textDocument/didSave") => + { + eprintln!("File saved!"); + } + e => { + eprintln!("unimplemented {:?}", e); + } + } + eprintln!("notification: {:?}", n); + } + } + } + + if let Some(io_threads) = lsp.io_threads { + if let Err(e) = io_threads.join() { + eprintln!("Error joining IO threads: {:?}", e); + } + } + + Ok(()) +} +// if let Some(source) = get_source_buffer("test_value.asm") { +// // buffered reading +// let mut ast_list: Vec> = vec![]; +// for (line_no, read_buf) in source { +// if let Ok(read_buf) = read_buf { +// let mut l = Lexer::new(read_buf, line_no); +// let mut tokns_buf: Vec = vec![]; +// tokns_buf.push(Token::new(0,TokenType::BOL,Location::new(0,0),String::from("BOL"))); + +// for tok in l { +// tokns_buf.push(tok); +// } +// // println!("{:?}", tokns_buf); + +// let mut p = Parser::new(tokns_buf.into_iter()); +// ast_list.push(p.parse_expression()); +// } else { +// println!("Error reading!"); +// } +// println!("{:?}",ast_list); +// } +// } diff --git a/src/lsp/lsp85/src/server/bindings.rs b/src/lsp/lsp85/src/server/bindings.rs new file mode 100644 index 0000000..5e472bc --- /dev/null +++ b/src/lsp/lsp85/src/server/bindings.rs @@ -0,0 +1,272 @@ +use crate::server::handlers; +use lsp_server::{ExtractError, Message, Notification, Request, RequestId, Response}; +use lsp_types::CompletionItemKind; +use lsp_types::request::{Completion, HoverRequest}; +use lsp_types::{ + CompletionItem, CompletionParams, CompletionResponse, Documentation, HoverParams, + MarkupContent, MarkupKind, +}; +use wasm_bindgen::prelude::*; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub fn wasm_request_handler(req: JsValue) -> Result { + let msg = serde_wasm_bindgen::from_value(req) + .map_err(|e| JsValue::from_str(&format!("Invalid request: {:?}", e)))?; + match msg { + Message::Request(req) => { + // eprintln!("got request: {:?}", req); + wasm_lsp_router!(req,{ + Completion=>wasm_completion_handler, + HoverRequest=>wasm_hover_handler, + }); + return Ok(format!("Request handled and routed!").into()); + } + Message::Response(rs) => { + // eprintln!("response: {:?}", rs); + return Ok(format!("Response: {:?}", rs).into()); + } + Message::Notification(n) => { + match &n { + Notification { method, .. } if *method == String::from("textDocument/didSave") => { + return Ok(format!("File saved!").into()); + } + e => { + return Err(format!("Error in saving file!").into()); + } + } + return Ok(format!("notification: {:?}", n).into()); + } + } +} + +pub fn wasm_completion_handler(id: &RequestId, params: &CompletionParams) -> Result { + // eprintln!("got completion request #{}: {:?}", id, params); + let responses = vec![ + CompletionItem { + label: "MOV".to_string(), + detail: Some("MOV - Move data between registers".to_string()), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: "`MOV` instruction **copies** the content of the **source register** into **destination register**.".to_string(), + })), + kind: Some(CompletionItemKind::KEYWORD), + ..Default::default() + }, + CompletionItem { + label: "MVI".to_string(), + detail: Some("MVI - Move immediate data".to_string()), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: "The **8-bit data** is stored in the **destination register** of **memory**.".to_string(), + })), + kind: Some(CompletionItemKind::KEYWORD), + ..Default::default() + }, + CompletionItem { + label: "LDA".to_string(), + detail: Some("LDA - Load accumulator direct".to_string()), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: "The contents of a **memory location**, specified by a **16-bit address** in the operand, are copied to the **accumulator**.".to_string(), + })), + kind: Some(CompletionItemKind::KEYWORD), + ..Default::default() + }, + CompletionItem { + label: "LDAX".to_string(), + detail: Some("LDAX - Load accumulator indirect".to_string()), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: "The contents of the **designated register pair** point to a **memory location**. This instruction **copies** the contents of that memory location into the **accumulator**.".to_string(), + })), + kind: Some(CompletionItemKind::KEYWORD), + ..Default::default() + }, + CompletionItem { + label: "LXI".to_string(), + detail: Some("LXI - Load register pair immediate".to_string()), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: "The instruction **loads 16-bit data** in the **register pair** designated in the operand.".to_string(), + })), + kind: Some(CompletionItemKind::KEYWORD), + ..Default::default() + }, + CompletionItem { + label: "LHLD".to_string(), + detail: Some("LHLD - Load H and L registers direct".to_string()), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: "The instruction **copies** the contents of the **memory location** pointed out by the **16-bit address** into **register L** and copies the contents of the **next memory location** into **register H**. The contents of the **source memory** are not altered.".to_string(), + })), + kind: Some(CompletionItemKind::KEYWORD), + ..Default::default() + }, + CompletionItem { + label: "SUB".to_string(), + detail: Some("SUB - Subtract".to_string()), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: "**Subtract** instruction".to_string(), + })), + kind: Some(CompletionItemKind::KEYWORD), + ..Default::default() + }, + CompletionItem { + label: "ADD".to_string(), + detail: Some("ADD - Add".to_string()), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: "**Add** values".to_string(), + })), + kind: Some(CompletionItemKind::KEYWORD), + ..Default::default() + }, + CompletionItem { + label: "STAX".to_string(), + detail: Some("STAX - Store accumulator indirect".to_string()), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: "Stores the contents of the **accumulator** into the **memory location** pointed to by the **designated register pair**.".to_string(), + })), + kind: Some(CompletionItemKind::KEYWORD), + ..Default::default() + }, + + CompletionItem { + label: "PUSH".to_string(), + detail: Some("PUSH - Push register pair to stack".to_string()), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: "The contents of the specified **register pair** are **pushed onto the stack**, decrementing the **stack pointer** by 2.".to_string(), + })), + kind: Some(CompletionItemKind::KEYWORD), + ..Default::default() + }, + + CompletionItem { + label: "POP".to_string(), + detail: Some("POP - Pop register pair from stack".to_string()), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: "Two bytes from the **stack** are **popped** and loaded into the specified **register pair**, incrementing the **stack pointer** by 2.".to_string(), + })), + kind: Some(CompletionItemKind::KEYWORD), + ..Default::default() + }, + + CompletionItem { + label: "INR".to_string(), + detail: Some("INR - Increment register".to_string()), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: "Increments the contents of the specified **register** by **1**. Flags are affected except Carry.".to_string(), + })), + kind: Some(CompletionItemKind::KEYWORD), + ..Default::default() + }, + + CompletionItem { + label: "DCR".to_string(), + detail: Some("DCR - Decrement register".to_string()), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: "Decrements the contents of the specified **register** by **1**. Flags are affected except Carry.".to_string(), + })), + kind: Some(CompletionItemKind::KEYWORD), + ..Default::default() + }, + + CompletionItem { + label: "DAD".to_string(), + detail: Some("DAD - Double add register pair".to_string()), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: "Adds the contents of the specified **register pair** to the **HL pair**. Only the **Carry flag** is affected.".to_string(), + })), + kind: Some(CompletionItemKind::KEYWORD), + ..Default::default() + }, + + // --- JMP Variants --- + CompletionItem { + label: "JMP".to_string(), + detail: Some("JMP - Unconditional jump".to_string()), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: "Program execution **jumps** to the specified **16-bit address** unconditionally.".to_string(), + })), + kind: Some(CompletionItemKind::KEYWORD), + ..Default::default() + }, + + CompletionItem { + label: "JC".to_string(), + detail: Some("JC - Jump if carry".to_string()), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: "Jumps to the given address **if the Carry flag = 1**.".to_string(), + })), + kind: Some(CompletionItemKind::KEYWORD), + ..Default::default() + }, + + CompletionItem { + label: "JNC".to_string(), + detail: Some("JNC - Jump if no carry".to_string()), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: "Jumps to the given address **if the Carry flag = 0**.".to_string(), + })), + kind: Some(CompletionItemKind::KEYWORD), + ..Default::default() + }, + + CompletionItem { + label: "JZ".to_string(), + detail: Some("JZ - Jump if zero".to_string()), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: "Jumps to the given address **if the Zero flag = 1**.".to_string(), + })), + kind: Some(CompletionItemKind::KEYWORD), + ..Default::default() + }, + + CompletionItem { + label: "JNZ".to_string(), + detail: Some("JNZ - Jump if not zero".to_string()), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: "Jumps to the given address **if the Zero flag = 0**.".to_string(), + })), + kind: Some(CompletionItemKind::KEYWORD), + ..Default::default() + }, + ]; + + let result = CompletionResponse::Array(responses); + let result = match serde_json::to_string(&result) { + Ok(result) => result, + Err(e) => "[ERROR] failed to convert JSON-2-String".to_string().into(), + }; + return Ok(serde_wasm_bindgen::to_value(&result)?); +} + +pub fn wasm_hover_handler(id: &RequestId, params: &HoverParams) -> Result { + // eprintln!("hovr request {}: {:?}", id, params); + + let hover_result = lsp_types::Hover { + contents: lsp_types::HoverContents::Scalar(lsp_types::MarkedString::String( + "dummy hover info".to_string(), + )), + range: None, + }; + + let result = match serde_json::to_string(&hover_result) { + Ok(result) => result, + Err(_) => return Err("[ERROR] failed to convert JSON-2-String".to_string().into()), + }; + return Ok(serde_wasm_bindgen::to_value(&result)?); +} diff --git a/src/lsp/lsp85/src/server/handlers.rs b/src/lsp/lsp85/src/server/handlers.rs new file mode 100644 index 0000000..ef70827 --- /dev/null +++ b/src/lsp/lsp85/src/server/handlers.rs @@ -0,0 +1,245 @@ +use lsp_server::{ExtractError, Message, Notification, Request, RequestId, Response}; +use lsp_types::CompletionItemKind; +use lsp_types::{ + CompletionItem, CompletionParams, CompletionResponse, Documentation, HoverParams, + MarkupContent, MarkupKind, +}; + +pub fn completion_handler( + id: &RequestId, + params: CompletionParams, +) -> Result { + eprintln!("got completion request #{}: {:?}", id, params); + let responses = vec![ + CompletionItem { + label: "MOV".to_string(), + detail: Some("MOV - Move data between registers".to_string()), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: "`MOV` instruction **copies** the content of the **source register** into **destination register**.".to_string(), + })), + kind: Some(CompletionItemKind::KEYWORD), + ..Default::default() + }, + CompletionItem { + label: "MVI".to_string(), + detail: Some("MVI - Move immediate data".to_string()), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: "The **8-bit data** is stored in the **destination register** of **memory**.".to_string(), + })), + kind: Some(CompletionItemKind::KEYWORD), + ..Default::default() + }, + CompletionItem { + label: "LDA".to_string(), + detail: Some("LDA - Load accumulator direct".to_string()), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: "The contents of a **memory location**, specified by a **16-bit address** in the operand, are copied to the **accumulator**.".to_string(), + })), + kind: Some(CompletionItemKind::KEYWORD), + ..Default::default() + }, + CompletionItem { + label: "LDAX".to_string(), + detail: Some("LDAX - Load accumulator indirect".to_string()), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: "The contents of the **designated register pair** point to a **memory location**. This instruction **copies** the contents of that memory location into the **accumulator**.".to_string(), + })), + kind: Some(CompletionItemKind::KEYWORD), + ..Default::default() + }, + CompletionItem { + label: "LXI".to_string(), + detail: Some("LXI - Load register pair immediate".to_string()), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: "The instruction **loads 16-bit data** in the **register pair** designated in the operand.".to_string(), + })), + kind: Some(CompletionItemKind::KEYWORD), + ..Default::default() + }, + CompletionItem { + label: "LHLD".to_string(), + detail: Some("LHLD - Load H and L registers direct".to_string()), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: "The instruction **copies** the contents of the **memory location** pointed out by the **16-bit address** into **register L** and copies the contents of the **next memory location** into **register H**. The contents of the **source memory** are not altered.".to_string(), + })), + kind: Some(CompletionItemKind::KEYWORD), + ..Default::default() + }, + CompletionItem { + label: "SUB".to_string(), + detail: Some("SUB - Subtract".to_string()), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: "**Subtract** instruction".to_string(), + })), + kind: Some(CompletionItemKind::KEYWORD), + ..Default::default() + }, + CompletionItem { + label: "ADD".to_string(), + detail: Some("ADD - Add".to_string()), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: "**Add** values".to_string(), + })), + kind: Some(CompletionItemKind::KEYWORD), + ..Default::default() + }, + CompletionItem { + label: "STAX".to_string(), + detail: Some("STAX - Store accumulator indirect".to_string()), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: "Stores the contents of the **accumulator** into the **memory location** pointed to by the **designated register pair**.".to_string(), + })), + kind: Some(CompletionItemKind::KEYWORD), + ..Default::default() + }, + + CompletionItem { + label: "PUSH".to_string(), + detail: Some("PUSH - Push register pair to stack".to_string()), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: "The contents of the specified **register pair** are **pushed onto the stack**, decrementing the **stack pointer** by 2.".to_string(), + })), + kind: Some(CompletionItemKind::KEYWORD), + ..Default::default() + }, + + CompletionItem { + label: "POP".to_string(), + detail: Some("POP - Pop register pair from stack".to_string()), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: "Two bytes from the **stack** are **popped** and loaded into the specified **register pair**, incrementing the **stack pointer** by 2.".to_string(), + })), + kind: Some(CompletionItemKind::KEYWORD), + ..Default::default() + }, + + CompletionItem { + label: "INR".to_string(), + detail: Some("INR - Increment register".to_string()), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: "Increments the contents of the specified **register** by **1**. Flags are affected except Carry.".to_string(), + })), + kind: Some(CompletionItemKind::KEYWORD), + ..Default::default() + }, + + CompletionItem { + label: "DCR".to_string(), + detail: Some("DCR - Decrement register".to_string()), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: "Decrements the contents of the specified **register** by **1**. Flags are affected except Carry.".to_string(), + })), + kind: Some(CompletionItemKind::KEYWORD), + ..Default::default() + }, + + CompletionItem { + label: "DAD".to_string(), + detail: Some("DAD - Double add register pair".to_string()), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: "Adds the contents of the specified **register pair** to the **HL pair**. Only the **Carry flag** is affected.".to_string(), + })), + kind: Some(CompletionItemKind::KEYWORD), + ..Default::default() + }, + + // --- JMP Variants --- + CompletionItem { + label: "JMP".to_string(), + detail: Some("JMP - Unconditional jump".to_string()), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: "Program execution **jumps** to the specified **16-bit address** unconditionally.".to_string(), + })), + kind: Some(CompletionItemKind::KEYWORD), + ..Default::default() + }, + + CompletionItem { + label: "JC".to_string(), + detail: Some("JC - Jump if carry".to_string()), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: "Jumps to the given address **if the Carry flag = 1**.".to_string(), + })), + kind: Some(CompletionItemKind::KEYWORD), + ..Default::default() + }, + + CompletionItem { + label: "JNC".to_string(), + detail: Some("JNC - Jump if no carry".to_string()), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: "Jumps to the given address **if the Carry flag = 0**.".to_string(), + })), + kind: Some(CompletionItemKind::KEYWORD), + ..Default::default() + }, + + CompletionItem { + label: "JZ".to_string(), + detail: Some("JZ - Jump if zero".to_string()), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: "Jumps to the given address **if the Zero flag = 1**.".to_string(), + })), + kind: Some(CompletionItemKind::KEYWORD), + ..Default::default() + }, + + CompletionItem { + label: "JNZ".to_string(), + detail: Some("JNZ - Jump if not zero".to_string()), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: "Jumps to the given address **if the Zero flag = 0**.".to_string(), + })), + kind: Some(CompletionItemKind::KEYWORD), + ..Default::default() + }, + ]; + + let result = CompletionResponse::Array(responses); + let result = match serde_json::to_value(&result) { + Ok(result) => return Ok(result), + Err(e) => return Err(e), + }; +} + +pub fn hover_handler( + id: &RequestId, + params: HoverParams, +) -> Result { + eprintln!("hovr request {}: {:?}", id, params); + + let hover_result = lsp_types::Hover { + contents: lsp_types::HoverContents::Scalar(lsp_types::MarkedString::String( + "dummy hover info".to_string(), + )), + range: None, + }; + + let result = match serde_json::to_value(&hover_result) { + Ok(result) => { + return Ok(result); + } + Err(e) => { + return Err(e); + } + }; +} diff --git a/src/lsp/lsp85/src/server/mod.rs b/src/lsp/lsp85/src/server/mod.rs new file mode 100644 index 0000000..87dd49d --- /dev/null +++ b/src/lsp/lsp85/src/server/mod.rs @@ -0,0 +1,99 @@ +#[macro_use] +pub mod routers; +pub mod bindings; +pub mod handlers; + +use lsp_server::{Connection, IoThreads, Message, Notification, Request, RequestId}; +use lsp_types::{ + ClientCapabilities, CompletionOptions, HoverProviderCapability, InitializeParams, + ServerCapabilities, +}; +use std::error::Error; +use std::rc::Rc; + +pub struct lsp85 { + id: Option, + pub conn: Option, + pub io_threads: Option, + client_cap: Option, + server_cap: Option, +} + +// builder methods +impl lsp85 { + pub fn build() -> Self { + return Self { + id: None, + conn: None, + io_threads: None, + client_cap: None, + server_cap: Some(ServerCapabilities::default()), + }; + } + + pub fn stdio(mut self) -> Self { + let (conn, io_threads) = Connection::stdio(); + + self.conn = Some(conn); + self.io_threads = Some(io_threads); + self.populate_client_cap(); + + self + } + + // expects self.connection defined before hand + fn populate_client_cap(&mut self) { + match self + .conn + .as_ref() + .expect("[ERROR] Connection not initialized!") + .initialize_start() + { + Ok((id, params)) => { + self.id = Some(id); + + let init_params: InitializeParams = serde_json::from_value(params) + .expect("[[ERROR] Failed to parse initialization params!"); + self.client_cap = Some(init_params.capabilities); + } + Err(e) => { + eprintln!("[ERROR] Failed to initialize LSP"); + } + } + } + pub fn enable_completion(mut self) -> Self { + self.server_cap + .as_mut() + .expect("[ERROR] Expected existing server_cap!") + .completion_provider = Some(CompletionOptions::default()); + self + } + + pub fn enable_hover(mut self) -> Self { + self.server_cap + .as_mut() + .expect("[ERROR] Expected existing server_cap!") + .hover_provider = Some(HoverProviderCapability::Simple(true)); + self + } + + // expects self.connection and self.id + pub fn initialize(self) -> Result> { + let initialize_data = serde_json::json!({ + "capabilities": self.server_cap, + "serverInfo": { + "name":"lsp85", + "version":"0.1", + } + }); + + self.conn + .as_ref() + .expect("[ERROR] Expected populated connection!") + .initialize_finish( + self.id.as_ref().expect("Expected populated id!").clone(), + initialize_data, + )?; + Ok(self) + } +} diff --git a/src/lsp/lsp85/src/server/routers.rs b/src/lsp/lsp85/src/server/routers.rs new file mode 100644 index 0000000..f328813 --- /dev/null +++ b/src/lsp/lsp85/src/server/routers.rs @@ -0,0 +1,65 @@ +use lsp_server::{ExtractError, Request, RequestId}; +use serde::de::DeserializeOwned; + +pub fn cast(req: Request) -> Result<(RequestId, R::Params), ExtractError> +where + R: lsp_types::request::Request, + R::Params: serde::de::DeserializeOwned, +{ + req.extract(R::METHOD) +} + +#[macro_export] +macro_rules! wasm_lsp_router { + ( + $req:ident, { + $( $method:ty => $handler:path ),* $(,)? + } + ) => {{ + use crate::server::routers::cast; + +$( + let $req = match cast::<$method>($req) { + Ok((id, params)) => { + let resp = Response { + result: Some( + serde_wasm_bindgen::from_value( $handler( &id ,¶ms).expect("[ERROR] Failure in communication!"))?) , + id, + error: None, + }; + return Ok(serde_wasm_bindgen::to_value(&resp)?); + }, + Err(err @ ExtractError::JsonError { .. }) => panic!("{:?}", err), + Err(ExtractError::MethodMismatch(req)) => req, + }; +)* + }}; +} + +#[macro_export] +macro_rules! lsp_router { + ( + $req:ident, $state:ident, { + $( $method:ty => $handler:path ),* $(,)? + } + ) => {{ + use crate::server::routers::cast; + +$( + let $req = match cast::<$method>($req) { + Ok((id, params)) => { + let resp = Response { + result: Some($handler(&id,params).expect("[ERROR] Expected non-empty response")), + id, + error: None, + }; + eprintln!("{:?}",resp); + $state.conn.as_ref().expect("[ERROR] Expected valid connection!").sender.send(Message::Response(resp)); + continue; + }, + Err(err @ ExtractError::JsonError { .. }) => panic!("{:?}", err), + Err(ExtractError::MethodMismatch(req)) => req, + }; +)* + }}; +}