diff --git a/devenv/src/cli.rs b/devenv/src/cli.rs index a9e1b5ffa..ebc44ae59 100644 --- a/devenv/src/cli.rs +++ b/devenv/src/cli.rs @@ -277,6 +277,9 @@ pub enum Commands { #[command(about = "Print the version of devenv.")] Version {}, + #[command(about = "Start devenv LSP")] + Lsp {}, + #[clap(hide = true)] Assemble, diff --git a/devenv/src/devenv.rs b/devenv/src/devenv.rs index 464dd2bf4..8562021d7 100644 --- a/devenv/src/devenv.rs +++ b/devenv/src/devenv.rs @@ -1,12 +1,14 @@ -use super::{cli, cnix, config, log, tasks}; +use super::{cli, cnix, config, log, lsp, tasks}; use clap::crate_version; use cli_table::Table; use cli_table::{print_stderr, WithTitle}; +use dashmap::DashMap; use include_dir::{include_dir, Dir}; use miette::{bail, Result}; use nix::sys::signal; use nix::unistd::Pid; use serde::Deserialize; +use serde_json::Value; use sha2::Digest; use std::collections::HashMap; use std::io::Write; @@ -15,6 +17,7 @@ use std::{ fs, path::{Path, PathBuf}, }; +use tower_lsp::{LspService, Server}; use tracing::{debug, error, info, info_span, warn, Instrument}; // templates @@ -394,6 +397,18 @@ impl Devenv { .await } + pub async fn lsp(&mut self, completion_json: &Value) -> Result<()> { + let (stdin, stdout) = (tokio::io::stdin(), tokio::io::stdout()); + info!("Inside the tokio main async lsp"); + let (service, socket) = LspService::new(|client| lsp::Backend { + client, + document_map: DashMap::new(), + completion_json: completion_json.clone(), + }); + Server::new(stdin, stdout, socket).serve(service).await; + Ok(()) + } + pub fn repl(&mut self) -> Result<()> { self.assemble(false)?; self.nix.repl() diff --git a/devenv/src/lib.rs b/devenv/src/lib.rs index 5a894696d..96b38c602 100644 --- a/devenv/src/lib.rs +++ b/devenv/src/lib.rs @@ -3,6 +3,8 @@ pub(crate) mod cnix; pub mod config; mod devenv; pub mod log; +pub mod lsp; +pub mod utils; pub use cli::{default_system, GlobalOptions}; pub use devenv::{Devenv, DevenvOptions}; diff --git a/devenv/src/lsp.rs b/devenv/src/lsp.rs new file mode 100644 index 000000000..00c9f11b2 --- /dev/null +++ b/devenv/src/lsp.rs @@ -0,0 +1,182 @@ +use dashmap::DashMap; +use regex::Regex; +use serde_json::Value; +use std::ops::Deref; +use tower_lsp::jsonrpc::Result; +use tower_lsp::lsp_types::*; +use tower_lsp::{Client, LanguageServer}; +use tracing::{debug, info}; +#[derive(Debug)] +pub struct Backend { + pub client: Client, + // document store in memory + pub document_map: DashMap, + pub completion_json: Value, +} +impl Backend { + fn parse_line(&self, line: &str) -> (Vec, String) { + let parts: Vec<&str> = line.split('.').collect(); + let partial_key = parts.last().unwrap_or(&"").to_string(); + let path = parts[..parts.len() - 1] + .iter() + .map(|&s| s.to_string()) + .collect(); + (path, partial_key) + } + fn search_json(&self, path: &[String], partial_key: &str) -> Vec { + let mut current = &self.completion_json; + for key in path { + if let Some(value) = current.get(key) { + current = value; + } else { + return Vec::new(); + } + } + match current { + Value::Object(map) => map + .keys() + .filter(|k| k.starts_with(partial_key)) + .cloned() + .collect(), + _ => Vec::new(), + } + } +} +#[tower_lsp::async_trait] +impl LanguageServer for Backend { + async fn initialize(&self, _: InitializeParams) -> Result { + Ok(InitializeResult { + server_info: None, + capabilities: ServerCapabilities { + text_document_sync: Some(TextDocumentSyncCapability::Kind( + TextDocumentSyncKind::FULL, + )), + completion_provider: Some(CompletionOptions { + resolve_provider: Some(false), + trigger_characters: Some(vec![".".to_string()]), + work_done_progress_options: Default::default(), + all_commit_characters: None, + ..Default::default() + }), + execute_command_provider: Some(ExecuteCommandOptions { + commands: vec!["dummy.do_something".to_string()], + work_done_progress_options: Default::default(), + }), + workspace: Some(WorkspaceServerCapabilities { + workspace_folders: Some(WorkspaceFoldersServerCapabilities { + supported: Some(true), + change_notifications: Some(OneOf::Left(true)), + }), + file_operations: None, + }), + ..ServerCapabilities::default() + }, + ..Default::default() + }) + } + async fn initialized(&self, _: InitializedParams) { + self.client + .log_message(MessageType::INFO, "devenv lsp is now initialized!") + .await; + } + async fn shutdown(&self) -> Result<()> { + Ok(()) + } + async fn did_change_workspace_folders(&self, _: DidChangeWorkspaceFoldersParams) { + self.client + .log_message(MessageType::INFO, "workspace folders changed!") + .await; + } + async fn did_change_configuration(&self, _: DidChangeConfigurationParams) { + self.client + .log_message(MessageType::INFO, "configuration changed!") + .await; + } + async fn did_change_watched_files(&self, _: DidChangeWatchedFilesParams) { + self.client + .log_message(MessageType::INFO, "watched files have changed!") + .await; + } + async fn execute_command(&self, _: ExecuteCommandParams) -> Result> { + self.client + .log_message(MessageType::INFO, "command executed!") + .await; + match self.client.apply_edit(WorkspaceEdit::default()).await { + Ok(res) if res.applied => self.client.log_message(MessageType::INFO, "applied").await, + Ok(_) => self.client.log_message(MessageType::INFO, "rejected").await, + Err(err) => self.client.log_message(MessageType::ERROR, err).await, + } + Ok(None) + } + async fn did_open(&self, _: DidOpenTextDocumentParams) { + self.client + .log_message(MessageType::INFO, "file opened!") + .await; + info!("textDocument/DidOpen"); + } + async fn did_change(&self, params: DidChangeTextDocumentParams) { + // info!("textDocument/DidChange, params: {:?}", params); + self.document_map.insert( + params.text_document.uri.to_string(), + params.content_changes[0].text.clone(), + ); + self.client + .log_message(MessageType::INFO, "file changed!") + .await; + } + async fn did_save(&self, _: DidSaveTextDocumentParams) { + info!("textDocument/DidSave"); + self.client + .log_message(MessageType::INFO, "file saved!") + .await; + } + async fn did_close(&self, _: DidCloseTextDocumentParams) { + info!("textDocument/DidClose"); + self.client + .log_message(MessageType::INFO, "file closed!") + .await; + } + async fn completion(&self, params: CompletionParams) -> Result> { + info!("textDocument/Completion"); + let uri = params.text_document_position.text_document.uri; + let file_content = match self.document_map.get(uri.as_str()) { + Some(content) => { + debug!("Text document content via DashMap: {:?}", content.deref()); + content.clone() + } + None => { + info!("No content found for the given URI"); + String::new() + } + }; + let position = params.text_document_position.position; + let line = position.line as usize; + let character = position.character as usize; + let line_content = file_content.lines().nth(line).unwrap_or_default(); + let line_until_cursor = &line_content[..character]; + // handling regex for getting the current word + let re = Regex::new(r".*\W(.*)").unwrap(); // Define the regex pattern + let current_word = re + .captures(line_until_cursor) + .and_then(|caps| caps.get(1)) + .map(|m| m.as_str()) + .unwrap_or(""); + debug!("Current line content {:?}", line_content); + debug!("Line until cursor: {:?}", line_until_cursor); + debug!("Current word {:?}", current_word); + // Parse the line to get the current path and partial key + let (path, partial_key) = self.parse_line(current_word); + // Search for completions in the JSON + let completions = self.search_json(&path, &partial_key); + info!("Probable completion items {:?}", completions); + // covert completions to CompletionItems format + let completion_items: Vec<_> = completions + .iter() + .map(|item| CompletionItem::new_simple(item.to_string(), "".to_string())) + .collect(); + Ok(Some(CompletionResponse::List(CompletionList { + is_incomplete: false, + items: completion_items, + }))) + } +} diff --git a/devenv/src/main.rs b/devenv/src/main.rs index 5746e7a69..f9b8aaae2 100644 --- a/devenv/src/main.rs +++ b/devenv/src/main.rs @@ -1,10 +1,13 @@ use clap::crate_version; use devenv::{ cli::{Cli, Commands, ContainerCommand, InputsCommand, ProcessesCommand, TasksCommand}, - config, log, Devenv, + config, log, utils, Devenv, }; use miette::Result; -use tracing::{info, warn}; +use std::fs::File; +use std::io::BufWriter; +use tracing::level_filters::LevelFilter; +use tracing::{debug, info, warn}; #[tokio::main] async fn main() -> Result<()> { @@ -35,6 +38,31 @@ async fn main() -> Result<()> { log::init_tracing(level, cli.global_options.log_format); + let file = + std::fs::File::create("/tmp/devenv-lsp.log").expect("Couldn't create devenv-lsp.log file"); + + let file = BufWriter::new(file); + let (non_blocking, _guard) = tracing_appender::non_blocking(file); + let subscriber = tracing_subscriber::fmt() + .with_max_level(LevelFilter::DEBUG) + .with_ansi(false) + .with_writer(non_blocking) + .finish(); + let _ = tracing::subscriber::set_global_default(subscriber); + let file = File::open("/home/k3ys/tmp/option.json").unwrap(); + let json: serde_json::Value = + serde_json::from_reader(file).expect("file should be proper JSON"); + // debug!("trimmed_json {:?}", trimmed_json); + let mut flatten_json = utils::flatten(json); + // debug!("flatten_json: {}", serde_json::to_string_pretty(&flatten_json).unwrap()); + let filter_keys = vec![ + String::from("declarations"), + String::from("loc"), + String::from("readOnly"), + ]; + let filter_keys_refs: Vec<&str> = filter_keys.iter().map(|s| s.as_str()).collect(); + let completion_json = utils::filter_json(&mut flatten_json, filter_keys_refs); + let mut config = config::Config::load()?; for input in cli.global_options.override_input.chunks_exact(2) { config.add_input(&input[0].clone(), &input[1].clone(), &[]); @@ -133,6 +161,7 @@ async fn main() -> Result<()> { Commands::Gc {} => devenv.gc(), Commands::Info {} => devenv.info().await, Commands::Repl {} => devenv.repl(), + Commands::Lsp {} => devenv.lsp(&completion_json).await, Commands::Build { attributes } => devenv.build(&attributes).await, Commands::Update { name } => devenv.update(&name).await, Commands::Up { process, detach } => devenv.up(process.as_deref(), &detach, &detach).await, diff --git a/devenv/src/utils.rs b/devenv/src/utils.rs new file mode 100644 index 000000000..d1760087c --- /dev/null +++ b/devenv/src/utils.rs @@ -0,0 +1,54 @@ +use serde_json::{Map, Value}; +use tracing::debug; +fn filter_attr(json_value: &mut Value, key: &str) { + match json_value { + Value::Object(ref mut map) => { + map.remove(key); + for val in map.values_mut() { + filter_attr(val, key); + } + } + Value::Array(ref mut arr) => { + for val in arr.iter_mut() { + filter_attr(val, key); + } + } + _ => {} + } +} +pub fn filter_json(json_value: &mut Value, keys: Vec<&str>) -> Value { + for key in keys { + filter_attr(json_value, key) + } + json_value.clone() +} +pub fn insert_nested_value(nested_map: &mut Map, loc: &[String], value: Value) { + let mut current = nested_map; + for (i, key) in loc.iter().enumerate() { + if i == loc.len() - 1 { + current.insert(key.clone(), value.clone()); + } else { + current = current + .entry(key.clone()) + .or_insert_with(|| Value::Object(Map::new())) + .as_object_mut() + .expect("Should be an object"); + } + } +} +pub fn flatten(json_value: Value) -> Value { + let mut nested_map = Map::new(); + if let Value::Object(flat_map) = json_value { + for (_, v) in flat_map { + if let Some(loc_array) = v.get("loc").and_then(|v| v.as_array()) { + let loc_vec: Vec = loc_array + .iter() + .filter_map(|v| v.as_str().map(String::from)) + .collect(); + insert_nested_value(&mut nested_map, &loc_vec, v); + } + } + } + let nested_json = Value::Object(nested_map); + return nested_json; +}