Skip to content

Commit

Permalink
feat: Lsp init
Browse files Browse the repository at this point in the history
  • Loading branch information
k3yss committed Dec 18, 2024
1 parent 56527c6 commit 601fac6
Show file tree
Hide file tree
Showing 6 changed files with 288 additions and 3 deletions.
3 changes: 3 additions & 0 deletions devenv/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Expand Down
17 changes: 16 additions & 1 deletion devenv/src/devenv.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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()
Expand Down
2 changes: 2 additions & 0 deletions devenv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
182 changes: 182 additions & 0 deletions devenv/src/lsp.rs
Original file line number Diff line number Diff line change
@@ -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<String, String>,
pub completion_json: Value,
}
impl Backend {
fn parse_line(&self, line: &str) -> (Vec<String>, 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<String> {
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<InitializeResult> {
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<Option<Value>> {
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<Option<CompletionResponse>> {
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,
})))
}
}
33 changes: 31 additions & 2 deletions devenv/src/main.rs
Original file line number Diff line number Diff line change
@@ -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<()> {
Expand Down Expand Up @@ -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(), &[]);
Expand Down Expand Up @@ -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,
Expand Down
54 changes: 54 additions & 0 deletions devenv/src/utils.rs
Original file line number Diff line number Diff line change
@@ -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<String, Value>, 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<String> = 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;
}

0 comments on commit 601fac6

Please sign in to comment.