Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Cargo.toml support #690

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
build:
cd crates/taplo-cli && cargo build -F lsp -Fcargo_toml

run:
cd crates/taplo-cli && cargo run -F lsp -Fcargo_toml lsp tcp

install:
cd crates/taplo-cli && cargo install -F lsp -Fcargo_toml --path .
1 change: 1 addition & 0 deletions crates/taplo-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ lsp = ["async-ctrlc", "taplo-lsp"]
native-tls = ["taplo-common/native-tls", "taplo-lsp?/native-tls"]
rustls-tls = ["taplo-common/rustls-tls", "taplo-lsp?/rustls-tls"]
toml-test = []
cargo_toml = ["taplo-lsp?/cargo_toml"]

[dependencies]
taplo = { path = "../taplo", features = ["serde"] }
Expand Down
1 change: 1 addition & 0 deletions crates/taplo-lsp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ crate-type = ["cdylib", "rlib"]
default = ["rustls-tls"]
native-tls = ["taplo-common/native-tls"]
rustls-tls = ["taplo-common/rustls-tls"]
cargo_toml = []

[dependencies]
lsp-async-stub = { path = "../lsp-async-stub" }
Expand Down
6 changes: 6 additions & 0 deletions crates/taplo-lsp/src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,9 @@ pub(crate) use rename::*;

mod conversion;
pub(crate) use conversion::*;

mod code_action;
pub(crate) use code_action::*;

#[cfg(feature = "cargo_toml")]
pub(crate) mod cargo;
5 changes: 5 additions & 0 deletions crates/taplo-lsp/src/handlers/cargo.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mod completion;
pub(crate) use completion::*;

mod code_action;
pub(crate) use code_action::*;
70 changes: 70 additions & 0 deletions crates/taplo-lsp/src/handlers/cargo/code_action.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#![allow(unused)]
use std::{collections::HashMap, path::Path};

use lsp_async_stub::{rpc::Error, Context, Params};
use lsp_types::{
CodeAction, CodeActionKind, CodeActionOrCommand, CodeActionParams, CodeActionResponse,
Position, Range, TextEdit, WorkspaceEdit,
};
use taplo::dom::{Keys, Node};
use taplo_common::environment::Environment;

use crate::world::{DocumentState, World};

pub async fn code_action(
params: CodeActionParams,
path: Keys,
node: Node,
doc: &DocumentState,
) -> Result<Option<CodeActionResponse>, Error> {
let document_uri = &params.text_document.uri;
let position = params.range.start;

let mut dotted = path.dotted().split(".").peekable();
let location = dotted.next().unwrap_or_default();
if !["dependencies", "dev-dependencies", "build-dependencies"].contains(&location) {
return Ok(None);
}
let package = dotted.next().unwrap_or_default();

dbg!(&node);
dbg!(path.dotted());
let mut actions = Vec::new();

if dotted.peek().is_none() {
match &node {
Node::Str(s) => {
let version = s.value();
let range = node
.text_ranges(true)
.next()
.and_then(|r| doc.mapper.range(r));
let Some(range) = range else {
return Ok(None);
};
let start = Position::new(range.start.line as u32, range.start.character as u32);
let end = Position::new(range.end.line as u32, range.end.character as u32);
let range = Range::new(start, end);
let edit = TextEdit::new(range, format!("{{ version = \"{version}\" }}"));
let mut map = HashMap::new();
map.insert(document_uri.clone(), vec![edit]);
let action = CodeAction {
title: "Expand dependency specification".to_string(),
kind: Some(CodeActionKind::QUICKFIX),
edit: Some(WorkspaceEdit {
changes: Some(map),
..WorkspaceEdit::default()
}),
..CodeAction::default()
};
actions.push(CodeActionOrCommand::CodeAction(action));
}
_ => return Ok(None),
}
}
if actions.is_empty() {
Ok(None)
} else {
Ok(Some(actions))
}
}
179 changes: 179 additions & 0 deletions crates/taplo-lsp/src/handlers/cargo/completion.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
use std::{
path::{Path, PathBuf},
sync::LazyLock,
};

use lsp_async_stub::rpc::Error;
use lsp_types::{
CompletionItem, CompletionItemKind, CompletionList, CompletionParams, CompletionResponse,
InsertTextFormat,
};
use taplo::dom::{node::TableKind, Keys, Node};

use crate::query::Query;

// we want completions for

static TOP_1000_RECENTLY_DOWNLOADED_CRATES: &str = include_str!("./top_1000_recent_crates.txt");

pub struct IndexCache {
pub static_crate_rank: Vec<PackageRef<'static>>,
}

#[derive(Debug, Clone)]
pub struct PackageRef<'a> {
pub name: &'a str,
}

impl IndexCache {
pub fn new() -> Self {
let static_crate_rank = TOP_1000_RECENTLY_DOWNLOADED_CRATES
.trim_end_matches('\n')
.split('\n')
.map(|n| PackageRef { name: n })
.collect();
Self { static_crate_rank }
}

pub fn completion_for_package(&self, query: &str) -> Vec<PackageRef<'_>> {
self.static_crate_rank
.iter()
.filter(|c| c.name.starts_with(query))
.cloned()
.collect()
}

pub fn versions_for_package(&self, _package: &str) -> Vec<&str> {
vec!["1.0", "2.0"]
}

pub fn features_for_package(&self, _package: &str) -> Vec<&str> {
vec!["uuid", "postgres", "serde"]
}
}

static INDEX_CACHE: LazyLock<IndexCache> = LazyLock::new(|| IndexCache::new());

fn crate_index_path(package: &str) -> PathBuf {
let l = package.len();
match l {
1 => Path::new(&package).to_owned(),
2 => Path::new("2").join(package).to_owned(),
3 => Path::new("3").join(&package[..1]).join(package).to_owned(),
_ => Path::new(&package[..2])
.join(&package[2..4])
.join(package)
.to_owned(),
}
}

pub fn complete_dependencies(
_params: CompletionParams,
query: Query,
path: Keys,
node: Node,
) -> Result<Option<CompletionResponse>, Error> {
let mut dotted = path.dotted().split(".").skip(1).peekable();
let package = dotted.next().unwrap_or_default();

if dotted.peek().is_none()
&& matches!(&node, Node::Table(t) if matches!(t.kind(), TableKind::Regular))
{
// package is in header, e.g. [dependencies.tokio]
let items = INDEX_CACHE
.completion_for_package(package)
.into_iter()
.take(1)
.map(|p| {
let name = p.name.to_string();
let completion = CompletionItem::new_simple(name.clone(), name.clone());
// completion.insert_text = Some(format!("{name}$0"));
// completion.insert_text_format = Some(InsertTextFormat::SNIPPET);
// completion.kind = Some(CompletionItemKind::MODULE);
completion
})
.collect();
return Ok(Some(CompletionResponse::List(CompletionList {
is_incomplete: true,
items,
})));
}

if dotted.peek().is_none() && matches!(&node, Node::Invalid(_)) {
// package is in a table. e.g.
// [dependencies]
// tokio = "1.0"
let items = INDEX_CACHE
.completion_for_package(package)
.into_iter()
.take(1)
.map(|p| {
let name = p.name.to_string();
let mut completion = CompletionItem::new_simple(name.clone(), name.clone());
completion.insert_text = Some(format!("{name} = \"$1\"$0"));
completion.insert_text_format = Some(InsertTextFormat::SNIPPET);
completion.kind = Some(CompletionItemKind::MODULE);
completion
})
.collect();
return Ok(Some(CompletionResponse::List(CompletionList {
is_incomplete: true,
items,
})));
}

let next = dotted.next().unwrap_or("version");
if query.in_inline_table() || !["version", "features", "optional"].contains(&next) {
// we are in an inline table, or we are not in a known key
let mut items = Vec::with_capacity(3);
let k = "version";
let mut item = CompletionItem::new_simple(k.into(), k.into());
item.insert_text = Some(format!("{k} = \"$1\"$0"));
item.insert_text_format = Some(InsertTextFormat::SNIPPET);
items.push(item);
let k = "features";
let mut item = CompletionItem::new_simple(k.into(), k.into());
item.insert_text = Some(format!("{k} = [\"$1\"]$0"));
item.insert_text_format = Some(InsertTextFormat::SNIPPET);
items.push(item);
let k = "optional";
let mut item = CompletionItem::new_simple(k.into(), k.into());
item.insert_text = Some(format!("{k} = true$0"));
item.insert_text_format = Some(InsertTextFormat::SNIPPET);
items.push(item);
return Ok(Some(CompletionResponse::Array(items)));
}

match next {
"version" => {
dbg!("version");
let versions = INDEX_CACHE.versions_for_package(package);
let completions = versions
.into_iter()
.map(|v| CompletionItem::new_simple(v.to_string(), v.to_string()))
.collect();
Ok(Some(CompletionResponse::Array(completions)))
}
"features" => {
dbg!("features");
let features = INDEX_CACHE.features_for_package(package);
let completions = features
.into_iter()
.map(|f| {
let completion = CompletionItem::new_simple(f.to_string(), f.to_string());
completion
})
.collect();
Ok(Some(CompletionResponse::Array(completions)))
}
"optional" => {
dbg!("optional");
let completions = vec![
CompletionItem::new_simple("true".into(), "true".into()),
CompletionItem::new_simple("false".into(), "false".into()),
];
Ok(Some(CompletionResponse::Array(completions)))
}
_ => Ok(None),
}
}
Loading
Loading